From fd3688d61985e695d04c92bf2a9d2eb2b85f44ff Mon Sep 17 00:00:00 2001 From: sanil-23 Date: Wed, 5 Aug 2026 00:20:17 +0530 Subject: [PATCH 1/2] refactor(ui): retire "harness" as a noun for anything you interact with MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- README.md | 12 +- docs/TERMINOLOGY.md | 101 ++++--- docs/workflows.md | 20 +- src/sdk/src/hub/handoff/types.rs | 2 +- src/sdk/src/sessions/types.rs | 2 +- src/sdk/src/ui/README.md | 2 +- src/sdk/src/ui/command/catalog.rs | 12 +- src/sdk/src/ui/command/mod.rs | 22 +- src/sdk/src/ui/command/tests.rs | 34 ++- src/sdk/src/ui/command/types.rs | 10 +- src/sdk/src/ui/decisions/README.md | 4 +- src/sdk/src/ui/decisions/fold.rs | 4 +- src/sdk/src/ui/decisions/mod.rs | 2 +- src/tui/examples/pty_load.rs | 4 +- src/tui/src/app_loop.rs | 6 +- .../src/event_loop/cmd_dispatch/handoff.rs | 4 +- src/tui/src/event_loop/cmd_dispatch/mod.rs | 2 +- src/tui/src/event_loop/mod.rs | 6 +- src/tui/src/event_loop/types.rs | 6 +- src/tui/src/ui/app/agent_control.rs | 18 +- src/tui/src/ui/app/agent_control_tests.rs | 4 +- src/tui/src/ui/app/changes/baseline.rs | 6 +- src/tui/src/ui/app/changes/baseline_tests.rs | 38 +-- src/tui/src/ui/app/changes/mod.rs | 14 +- src/tui/src/ui/app/changes/tests.rs | 2 +- src/tui/src/ui/app/changes/types.rs | 32 +-- src/tui/src/ui/app/commands/changes.rs | 2 +- src/tui/src/ui/app/commands/dispatch.rs | 8 +- src/tui/src/ui/app/harness_workspace.rs | 30 +-- src/tui/src/ui/app/harness_workspace_tests.rs | 14 +- src/tui/src/ui/app/hosts/edit.rs | 2 +- src/tui/src/ui/app/input/mouse.rs | 32 +-- src/tui/src/ui/app/input/paste.rs | 8 +- src/tui/src/ui/app/input/tests.rs | 2 +- src/tui/src/ui/app/keys/agents.rs | 6 +- src/tui/src/ui/app/keys/harness.rs | 67 ++--- src/tui/src/ui/app/keys/mod.rs | 12 +- src/tui/src/ui/app/keys/routing/add_host.rs | 4 +- src/tui/src/ui/app/keys/routing/mod.rs | 4 +- src/tui/src/ui/app/mod.rs | 6 +- src/tui/src/ui/app/overlays.rs | 2 +- src/tui/src/ui/app/overlays_tests.rs | 16 +- src/tui/src/ui/app/rail/mod.rs | 10 +- src/tui/src/ui/app/rail/resolve.rs | 4 +- src/tui/src/ui/app/rail/tests.rs | 12 +- src/tui/src/ui/app/render/agents/composer.rs | 2 +- src/tui/src/ui/app/render/agents/harness.rs | 12 +- src/tui/src/ui/app/render/agents/mod.rs | 14 +- .../app/render/agents/rail/attention_tests.rs | 37 ++- .../render/agents/rail/harness_line/layout.rs | 14 +- src/tui/src/ui/app/render/agents/rail/mod.rs | 24 +- src/tui/src/ui/app/render/agents/rail/rows.rs | 8 +- .../src/ui/app/render/agents/rail/state.rs | 4 +- .../render/agents/rail/status_line_tests.rs | 28 +- .../src/ui/app/render/agents/rail/tests.rs | 18 +- .../src/ui/app/render/agents/transcript.rs | 2 +- .../ui/app/render/agents/transcript_tests.rs | 4 +- src/tui/src/ui/app/render/agents/types.rs | 7 +- src/tui/src/ui/app/render/changes.rs | 2 +- src/tui/src/ui/app/render/mod.rs | 16 +- src/tui/src/ui/app/render/routing/add_host.rs | 4 +- .../ui/app/render/routing/harnesses/mod.rs | 2 +- .../{harness_modals.rs => session_modals.rs} | 38 +-- .../app/render/settings/appearance_usage.rs | 4 +- src/tui/src/ui/app/render/settings/help.rs | 14 +- .../src/ui/app/render/settings/status_line.rs | 12 +- src/tui/src/ui/app/render/tests.rs | 12 +- .../render/workflows/node_preview/kinds.rs | 2 +- ...{harness_control.rs => session_control.rs} | 255 +++++++++--------- ...trol_tests.rs => session_control_tests.rs} | 2 +- src/tui/src/ui/app/settings_edit/mod.rs | 2 +- src/tui/src/ui/app/state.rs | 42 +-- src/tui/src/ui/app/tests.rs | 42 ++- src/tui/src/ui/app/types.rs | 112 ++++---- src/tui/src/ui/harness_pane/mod.rs | 6 +- src/tui/src/ui/harness_pane/spawn.rs | 14 +- src/tui/src/ui/harness_pane/tests/mod.rs | 2 +- src/tui/src/ui/harness_pane/tests/origin.rs | 10 +- src/tui/src/ui/harness_pane/tests/session.rs | 16 +- src/tui/src/ui/harness_pane/types.rs | 19 +- src/tui/src/worker/app/tests/helpers/mod.rs | 4 +- src/tui/src/worker/executor/run.rs | 14 +- src/tui/src/worker/executor/tests.rs | 10 +- src/tui/src/worker/executor_tests/basic.rs | 4 +- src/tui/src/worker/executor_tests/live.rs | 8 +- src/tui/src/worker/executor_tests/sessions.rs | 10 +- src/tui/src/worker/pty/handle/control.rs | 12 +- src/tui/src/worker/pty/handle/lifecycle.rs | 6 +- src/tui/src/worker/pty/handle/types.rs | 2 +- src/tui/src/worker/pty/manager/session.rs | 10 +- src/tui/src/worker/pty/mod.rs | 2 +- src/tui/src/worker/pty/tests/attention.rs | 4 +- src/tui/src/worker/pty/tests/control.rs | 30 +-- src/tui/src/worker/pty/tests/mod.rs | 4 +- src/tui/src/worker/pty/types.rs | 32 +-- ...ness_pane.rs => e2e_local_session_pane.rs} | 16 +- src/tui/tests/e2e_screen_stream.rs | 4 +- ...ss_takeover.rs => e2e_session_takeover.rs} | 8 +- src/tui/tests/feature_commands.rs | 6 +- src/tui/tests/feature_demo_fleet.rs | 2 +- src/tui/tests/feature_paste/attached.rs | 16 +- src/tui/tests/feature_paste/picker.rs | 12 +- ..._control.rs => feature_session_control.rs} | 66 ++--- ..._handoff.rs => feature_session_handoff.rs} | 40 +-- src/tui/tests/feature_workers/fleet.rs | 4 +- src/tui/tests/feature_workers/list.rs | 4 +- src/tui/tests/feature_workers/routing.rs | 8 +- 107 files changed, 907 insertions(+), 843 deletions(-) rename src/tui/src/ui/app/render/{harness_modals.rs => session_modals.rs} (92%) rename src/tui/src/ui/app/{harness_control.rs => session_control.rs} (75%) rename src/tui/src/ui/app/{harness_control_tests.rs => session_control_tests.rs} (91%) rename src/tui/tests/{e2e_local_harness_pane.rs => e2e_local_session_pane.rs} (96%) rename src/tui/tests/{e2e_harness_takeover.rs => e2e_session_takeover.rs} (95%) rename src/tui/tests/{feature_harness_control.rs => feature_session_control.rs} (91%) rename src/tui/tests/{feature_harness_handoff.rs => feature_session_handoff.rs} (92%) diff --git a/README.md b/README.md index 1779f1f8f..b46b56a5d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ **One terminal. Every agent you have. Working at once.** -Claude Code, Codex, and OpenCode are remarkable at running one task deeply. Medulla is what runs a hundred of them. It decides what work to hand out, places each piece on a harness that can do it, streams back what every one of them is doing, and keeps a live picture of the whole operation in front of you. +Claude Code, Codex, and OpenCode are remarkable at running one task deeply. Medulla is what runs a hundred of them. It decides what work to hand out, places each piece on an agent that can do it, streams back what every one of them is doing, and keeps a live picture of the whole operation in front of you. Fleets with everyone. @@ -43,14 +43,14 @@ Prebuilt binaries ship for Linux (x86\_64, aarch64), macOS (Apple Silicon), and **Repositories it understands.** Point Medulla at your projects once. It writes a short profile for each and uses it to route work to the right place, rather than guessing from a directory name. -**Plans that actually run.** A workflow is a saved, multi-step plan whose steps each run as a real harness session — with parallel branches, and approval gates where a human has to say yes. Ask for one in plain words and an agent will build it for you. +**Plans that actually run.** A workflow is a saved, multi-step plan whose steps each run as a real agent session — with parallel branches, and approval gates where a human has to say yes. Ask for one in plain words and an agent will build it for you. **Small surface, low spend.** The bulk of your fleet's output never reaches the orchestrator's context. It reasons over a distilled, current picture, so what you pay orchestrator rates on stays small however much is running underneath. ## Documentation -**Routing › Harnesses** also manages named OpenRouter-backed presets that reuse -Claude Code or Codex as the coding harness. Presets select an OpenRouter model +**Routing › Harness Types** also manages named OpenRouter-backed presets that +reuse Claude Code or Codex as the coding CLI. Presets select an OpenRouter model and fleet host while referring to `OPENROUTER_API_KEY` by environment-variable name only; restart the local host after saving one. See [`config.example.toml`](config.example.toml) for the complete shape. @@ -60,7 +60,7 @@ Full documentation: **[tinyhumans.gitbook.io/medulla](https://tinyhumans.gitbook * [Workers and Sessions](https://tinyhumans.gitbook.io/medulla/features/workers-and-sessions) — capacity, threads, and what survives. * [Workflows](https://tinyhumans.gitbook.io/medulla/features/workflows) — authored multi-step plans and their runs. * [MEDULLA.md Workspace Profiles](https://tinyhumans.gitbook.io/medulla/features/workspace-profiles) — telling the orchestrator what a repo is. -* [Orchestrator Routing](https://tinyhumans.gitbook.io/medulla/features/routing) — cognitive tiers, harness selection, strategies. +* [Orchestrator Routing](https://tinyhumans.gitbook.io/medulla/features/routing) — cognitive tiers, harness-type selection, strategies. * [Token Efficiency and Budgets](https://tinyhumans.gitbook.io/medulla/features/token-efficiency) — small surfaces and enforced budgets. Building on Medulla, or running it yourself? Everything technical — the TUI in depth, the CLI, worker daemons, configuration, architecture, and the SDK — is in **[Developers](https://tinyhumans.gitbook.io/medulla/developers)**. @@ -73,7 +73,7 @@ Request access and tell us what you are orchestrating. ## Why an orchestrator -Ask a harness to coordinate other harnesses and you hit the same quiet failure mode everywhere: the orchestrator is just another model with a transcript, and every harness it manages writes into that transcript. Accuracy degrades well before the context window fills. An orchestrator that reads raw fleet traffic stops scaling at a handful of agents — long before it runs out of room, it stops being able to think. +Ask a coding agent to coordinate other coding agents and you hit the same quiet failure mode everywhere: the orchestrator is just another model with a transcript, and every agent it manages writes into that transcript. Accuracy degrades well before the context window fills. An orchestrator that reads raw fleet traffic stops scaling at a handful of agents — long before it runs out of room, it stops being able to think. Orchestration is becoming the dominant pattern in agentic systems, yet it has been running on architectures designed for chat. A chat model manages one thread. An orchestrator has to hold an operation in its head: agents in flight, work being decomposed and delegated, results streaming back, decisions made continuously. Medulla is built for that. diff --git a/docs/TERMINOLOGY.md b/docs/TERMINOLOGY.md index 08401f282..7d7694849 100644 --- a/docs/TERMINOLOGY.md +++ b/docs/TERMINOLOGY.md @@ -14,25 +14,35 @@ system works on its behalf. ## Agent -A connected worker that executes **tasks**. Agents live inside a **workspace** on -a **host**, are surfaced through a **harness**, and are listed in `agent_list`. -Each agent has a set of **tools**, an MCP server inventory, and a health snapshot +A **declared** working identity on a **host**: a `harness` type × **workspace** +directory, written down in `[fleet].agentDeclarations` and carrying an `agentId`, +an optional name, `roles`, and a workspace `strategy`. Agents are declared, never +discovered — an agent exists because somebody wrote it down, not because a +process happens to be running. One host runs as many agents as you declare. + +The orchestrator delegates **tasks** to agents and lists them in `agent_list`; +each agent has a set of **tools**, an MCP server inventory, and a health snapshot (consecutive-ok / consecutive-failed). An agent is **idle** when it has no running -tasks and **busy** otherwise. The orchestrator delegates to agents; a manager -_manages_ them. +**sessions** and **busy** otherwise. A manager _manages_ agents. ## Harness -A runtime environment adapter — the layer that boots, supervises, and -communicates with a coding assistant CLI. Medulla supports several harness kinds: +**A type, not a thing.** `harness` is the attribute on an agent that says which +coding-assistant CLI its sessions run — `claude`, `codex`, `opencode`, or a +custom preset. It is a value in a dropdown; it is never an entity in the model, +never a level in the containment chain, and never a noun in the UI (the thing an +operator interacts with is an **agent** or one of its **sessions**). -| Harness | Transport | -| ----------- | ------------------------------------------------------- | -| Claude Code | ACP (Agent Client Protocol) over stdio, or legacy JSONL | -| Codex | ACP over stdio | -| OpenCode | ACP over stdio | +In code it also names the runtime adapter that boots, supervises, and talks to +that CLI: -A harness surfaces a **status** (idle / running / stopped), a **task board** +| Harness type | Transport | +| ------------ | ------------------------------------------------------- | +| Claude Code | ACP (Agent Client Protocol) over stdio, or legacy JSONL | +| Codex | ACP over stdio | +| OpenCode | ACP over stdio | + +The adapter surfaces a **status** (idle / running / stopped), a **task board** (tracked tasks with status open → active → blocked → done / cancelled), and an **event stream** (instruction queued, cycle start/end, task-board changes). The public wire shapes live in the `harness_contract` module and are versioned @@ -40,22 +50,32 @@ independently of any implementation. ## Host -A machine in the fleet — a physical or virtual environment that runs one or more -**harnesses**. A host is declared (not probed) and carries resource metadata -(CPU, memory). It is the top of the containment chain: +A machine, local or remote — the environment the agents declared on it run in. A +host is declared (not probed) and carries resource metadata (CPU, memory). It is +the top of the containment chain: ``` -Host → Harness → Workspace → Agent +Host → Agent → Session ``` +The local host is always present; a remote host is added by tiny.place address +and contributes the agents declared over there. This tree is what both the Agents +tab and the Hosts tab render, and its union is what the hub advertises to the +backend — one projection, rendered twice. + +*(The legacy `[fleet]` capacity snapshot still carries an older +`Host → Harness → Workspace → Agent` chain in its own types. That describes +declared capacity, not the entity model above.)* + ## Workspace -A filesystem directory exposed by a **harness** on a **host**. A workspace is -where agents read, write, and run code. Each workspace can carry a `MEDULLA.md` +A filesystem directory an **agent** works in, declared as part of that agent +together with its `strategy`: `checkout` (every session of the agent shares the +directory, so they run serially — the v1 default) or `worktree` (a carved +per-session copy, so they run in parallel — a follow-up). A workspace is where +agents read, write, and run code. Each workspace can carry a `MEDULLA.md` **profile** — a short frontmatter + prose summary that tells the orchestrator -what the directory _is_ and how to route work over it. Workspaces are registered -in the fleet configuration; without a registration entry the orchestrator cannot -place work there. +what the directory _is_ and how to route work over it. ## Hub @@ -75,8 +95,23 @@ tool calls and agent delegation are internal to the cycle. ## Session -A conversation thread between the user and the orchestrator. Sessions come in -two orthogonal axes: +**An agent session** is one running instance of an **agent** — what a **task** +actually executes in, and the row under an agent on the Agents rail. It carries a +`sessionId`, its launch anchor and workspace context, and two facts that are +independent of each other: + +- **`origin`** — `orchestrator` (auto-created by a dispatch, labelled from its + task) or `user` (opened from the UI and named by the operator). Origin never + changes. +- **`owner`** — who may drive it right now. Ownership moves: `ctrl-g` takes a + session from the orchestrator, handing it back returns it, and dispatch skips + any session the operator holds. + +A task **is** an agent session; the two differ only by origin. Sessions are never +roster entries — only their control state rides the advert. + +The word also names the transport-level conversation the SDK keys by +`(conversation × provider)`. Those come in two orthogonal axes: - **Class:** `Bounded` (one turn — a single cycle) or `Unbound` (long-lived, spanning multiple cycles). @@ -131,7 +166,7 @@ orchestrator match tasks to agents by what they can reach, rather than guessing. Placing a **manager** at a specific **host** + **workspace**. A deployment is the concrete instantiation of the fleet's declared containment chain. The orchestrator selects a host and workspace from the fleet registry, spawns the -manager there, and the manager then picks a harness and begins delegating. Once +manager there, and the manager then picks an agent and begins delegating. Once placed, a deployment is fixed for the cycle — a manager cannot move to a different host or workspace. @@ -140,8 +175,8 @@ different host or workspace. A saved, multi-step **directed graph** definition, usually acyclic but allowed to contain bounded loops (see the `loop` node). Each step is a node — triggers, agent dispatches, transforms, code execution, HTTP requests, and -more. An `agent` node runs as a real **harness** session (Claude Code, Codex, or -OpenCode). Workflows are authored as JSON files, stored in layered directories +more. An `agent` node runs as a real **agent session** on the harness type it +names (Claude Code, Codex, or OpenCode). Workflows are authored as JSON files, stored in layered directories (personal + per-repository), run through the vendored `tinyflows` engine, and surfaced in the TUI's Workflows tab with a canvas, run overlay, and copilot. @@ -170,16 +205,16 @@ with `medulla init` and registered with `medulla workspace add`. ## Provider -A coding-assistant CLI that a **harness** wraps. The three supported providers -are `claude` (Claude Code), `codex` (OpenAI Codex), and `opencode`. A provider is -selected per-task; the daemon spawns the CLI as a subprocess and communicates -over ACP or legacy JSONL. +A coding-assistant CLI — the same axis as an agent's **harness** type, seen from +the process end. The three supported providers are `claude` (Claude Code), +`codex` (OpenAI Codex), and `opencode`. The daemon spawns the CLI as a subprocess +and communicates over ACP or legacy JSONL. ## Daemon A long-running background process (`medulla daemon --headless`) that listens for -inbound **task frames** from the **hub**, spawns **providers** through -**harnesses**, and streams results back. One daemon = one **workspace**; a fleet +inbound **task frames** from the **hub**, spawns **providers** through their +harness adapters, and streams results back. One daemon = one **workspace**; a fleet is N daemon processes, not one daemon with N directories. ## TUI diff --git a/docs/workflows.md b/docs/workflows.md index 22fc563dd..38f2e98ac 100644 --- a/docs/workflows.md +++ b/docs/workflows.md @@ -1,11 +1,11 @@ # Workflows -A Medulla task is one instruction handed to one harness. A **workflow** is a +A Medulla task is one instruction handed to one agent. A **workflow** is a saved, multi-step plan: a directed graph whose `agent` steps each run as a real -coding-harness session — Claude Code, Codex, or OpenCode — in the order and with -the parallelism the graph declares. The graph is usually acyclic, but it may -contain a **bounded loop**: a `loop` node repeats a section until its -`max_iterations` cap or its `condition` says stop. +agent session — on Claude Code, Codex, or OpenCode — in the order and with the +parallelism the graph declares. The graph is usually acyclic, but it may contain +a **bounded loop**: a `loop` node repeats a section until its `max_iterations` +cap or its `condition` says stop. The engine is [`tinyflows`](https://github.com/tinyhumansai/tinyflows), vendored under `vendor/tinyflows` (see [vendoring.md](vendoring.md)). Medulla supplies the @@ -160,7 +160,7 @@ is meaningless, or wrong, on another. Name both when you mean both. A harness that is not one of the three built-in CLIs is taken as a custom harness preset id — the ones this machine has configured are listed by -`workflow_host` and in the TUI's Routing → Harnesses screen. Whether the *worker* +`workflow_host` and in the TUI's Routing → Harness Types screen. Whether the *worker* that runs the step exposes that preset is only answered when it runs. `harness` must be written plainly, never as a `=`-expression. Which binary and @@ -384,7 +384,7 @@ Workflows is a top-level tab: a sidebar, a canvas, and a copilot. it never reached are dimmed, and the inspector shows the node's duration and any diagnostics. - **The copilot** (`c`) is a conversation that edits the graph. Ask for a change - in plain words; a real harness session makes it with the MCP tools below, and + in plain words; a real agent session makes it with the MCP tools below, and the graph is then re-read from the store so the transcript reports what actually changed rather than whatever the agent said it did. @@ -423,7 +423,7 @@ compatible: - A worker's capability probe now advertises `workflows` — the ids it has installed, with names, descriptions, and step counts. - A task frame may carry a `workflow` field. Naming one makes the worker run that - saved graph instead of handing the frame's `text` to a harness; the text + saved graph instead of handing the frame's `text` to an agent; the text becomes the trigger payload. The ack, the reply, the correlation, and the work-snapshot attachment are all the ordinary ones, so an orchestrator that knows nothing about workflows still sees a task it dispatched and a task that @@ -457,7 +457,7 @@ request that changes what this host holds. Everything is served from the same layered store the Workflows tab, the `medulla workflow` subcommand and the MCP tools read — a socket `get` and `medulla workflow get` are one implementation, so they cannot drift. `copilot` is -not a read: it is a whole authoring turn on this machine's own harness, with the +not a read: it is a whole authoring turn on this machine's own agent, with the `medulla-workflows` tools attached, and its result is derived from re-reading the store afterwards rather than from what the model said it did. @@ -486,7 +486,7 @@ Three properties are load-bearing rather than incidental: A run reports itself in the *existing* `harness_work` vocabulary — a `plan_update` naming every node, `todo_update` as steps settle, `subagent_start` per agent node, and a `run_result`. So a workflow renders through the same pane -that shows a harness's own todo list, with no rendering code of its own. +that shows an agent's own todo list, with no rendering code of its own. ## Configuration diff --git a/src/sdk/src/hub/handoff/types.rs b/src/sdk/src/hub/handoff/types.rs index 174f9538c..4cbfc4554 100644 --- a/src/sdk/src/hub/handoff/types.rs +++ b/src/sdk/src/hub/handoff/types.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; /// Who holds a harness, as the orchestrator is told. /// -/// The SDK-side spelling of the TUI's `HarnessControl`. Deliberately a second +/// The SDK-side spelling of the TUI's `SessionControl`. Deliberately a second /// type rather than `serde` on the first: that enum's contract is that it is /// process-local and never serialized, and it is the single gate on dispatch. /// Deriving `Serialize` onto it would quietly make it a wire type and put the diff --git a/src/sdk/src/sessions/types.rs b/src/sdk/src/sessions/types.rs index f059356e6..a9a90e886 100644 --- a/src/sdk/src/sessions/types.rs +++ b/src/sdk/src/sessions/types.rs @@ -75,7 +75,7 @@ impl fmt::Display for SessionClass { /// # Origin is not ownership /// /// This is deliberately **not** "who may drive it now". That is *control* -/// (`HarnessControl` in the app crate, `owner` in the spec), it moves at runtime, +/// (`SessionControl` in the app crate, `owner` in the spec), it moves at runtime, /// and the two answer different questions: /// /// | | Origin | Control / owner | diff --git a/src/sdk/src/ui/README.md b/src/sdk/src/ui/README.md index 7848594b3..3b61e7aa3 100644 --- a/src/sdk/src/ui/README.md +++ b/src/sdk/src/ui/README.md @@ -7,7 +7,7 @@ UI-facing data surface shared with the terminal app: `events` (the folded event - [`agents/`](./agents/) — Pure view-model fold: turn the flat event stream into one lane per cognitive tier plus one lane per connected roster agent / anonymous task / peer session, with a row model for the Agents list and pre-wrapped transcript lines. A port of the TS `deriveAgentLanes` / `agentRowModel` / `laneLines` essentials. - [`chat_store/`](./chat_store/) — On-disk chat persistence for the Chat tab's thread trees. - [`command/`](./command/) — Slash-command parsing, the command catalog, and the `/copy` transcript helper. -- [`decisions/`](./decisions/) — Prepared operator decisions derived from harness escalations and pending worker questions. The fold is UI-agnostic so terminal and future hosts share stable ids, ordering, deduplication, and answer routing. +- [`decisions/`](./decisions/) — Prepared operator decisions derived from agent escalations and pending worker questions. The fold is UI-agnostic so terminal and future hosts share stable ids, ordering, deduplication, and answer routing. - [`events/`](./events/) — The TUI event vocabulary: every library `CycleEvent` plus the host-sourced rows (cycle framing, conversation turns, agent/session status, effects). `TuiEvent` deserializes any JSON `{kind, ...}` shape, keeping unknown kinds as a passthrough so a newer backend never drops rows on an older TUI. - [`fleet/`](./fleet/) — Pure view-model for the Fleet view: turn the declared capacity (`Host → Harness → Workspace → Agent`) and the agent-template catalog into a flattened row model plus pre-wrapped detail lines. - [`harness/`](./harness/) — Read-only view-model helpers for the agent-harness contract: a compact task board rendering for a `HarnessStatus` payload, and a one-line budget note for an agent's `AgentBudgetMetadata` seat stamp. Pure formatting only — the `medulla-tui` crate turns the returned `Line`s / strings into ratatui spans. diff --git a/src/sdk/src/ui/command/catalog.rs b/src/sdk/src/ui/command/catalog.rs index 1cbd4b31e..6ee8f87e8 100644 --- a/src/sdk/src/ui/command/catalog.rs +++ b/src/sdk/src/ui/command/catalog.rs @@ -71,22 +71,22 @@ pub const COMMANDS: &[CommandSpec] = &[ description: "Pick up an earlier saved session", }, CommandSpec { - name: "harness", - aliases: &[], - args: "[provider] [path]", - description: "Start a harness the orchestrator will not touch", + name: "session", + aliases: &["harness"], + args: "[harness] [path]", + description: "Start a session the orchestrator will not touch", }, CommandSpec { name: "takecontrol", aliases: &["take"], args: "", - description: "Take this harness from the orchestrator", + description: "Take this session from the orchestrator", }, CommandSpec { name: "handoff", aliases: &["hand"], args: "[note]", - description: "Give this harness back, with a note on what to continue", + description: "Give this session back, with a note on what to continue", }, CommandSpec { name: "abort", diff --git a/src/sdk/src/ui/command/mod.rs b/src/sdk/src/ui/command/mod.rs index 9f6a88dbe..6d265ea63 100644 --- a/src/sdk/src/ui/command/mod.rs +++ b/src/sdk/src/ui/command/mod.rs @@ -47,7 +47,7 @@ pub fn parse(input: &str) -> Option { "quit" | "q" | "exit" => SlashCommand::Quit, "new" => SlashCommand::NewSession, "resume" => SlashCommand::Resume, - "harness" => parse_harness(arg), + "session" | "harness" => parse_session(arg), "takecontrol" | "take" => SlashCommand::TakeControl, // `arg`, not `flag`: a note is prose the orchestrator reads, so its // capitalisation is the operator's to choose. @@ -82,21 +82,21 @@ pub fn copy_text(events: &[EventEnvelope], scope: CopyScope) -> String { } } -/// Parse the argument tail of `/harness` into its provider and path. +/// Parse the argument tail of `/session` into its harness type and path. /// -/// The shapes are `/harness`, `/harness `, and -/// `/harness `. A named provider is validated here, against the -/// same [`HarnessProvider::from_wire`] the wire uses, because "claud" should say -/// so rather than silently starting the default harness — that failure is -/// invisible until the wrong CLI is already running in the operator's workspace. +/// The shapes are `/session`, `/session `, and +/// `/session `. A named harness type is validated here, against +/// the same [`HarnessProvider::from_wire`] the wire uses, because "claud" should +/// say so rather than silently starting the default CLI — that failure is +/// invisible until the wrong one is already running in the operator's workspace. /// /// The path is not validated: only the front end knows the active workspace, and /// a bad path produces a far better message at spawn time than at parse time. -fn parse_harness(arg: &str) -> SlashCommand { - const USAGE: &str = "Usage: /harness [claude|codex|opencode] [path]"; +fn parse_session(arg: &str) -> SlashCommand { + const USAGE: &str = "Usage: /session [claude|codex|opencode] [path]"; let arg = arg.trim(); if arg.is_empty() { - return SlashCommand::NewHarness { + return SlashCommand::StartSession { provider: None, path: None, }; @@ -109,7 +109,7 @@ fn parse_harness(arg: &str) -> SlashCommand { if HarnessProvider::from_wire(&provider).is_none() { return SlashCommand::BadUsage(USAGE); } - SlashCommand::NewHarness { + SlashCommand::StartSession { provider: Some(provider), path, } diff --git a/src/sdk/src/ui/command/tests.rs b/src/sdk/src/ui/command/tests.rs index 2d3dc430e..822936fc2 100644 --- a/src/sdk/src/ui/command/tests.rs +++ b/src/sdk/src/ui/command/tests.rs @@ -139,47 +139,55 @@ fn usage_renders_the_argument_hint_only_when_there_is_one() { } #[test] -fn harness_takes_an_optional_provider_and_path() { +fn session_takes_an_optional_harness_and_path() { // Bare: the front end opens its picker rather than guessing. assert_eq!( - parse("/harness"), - Some(SlashCommand::NewHarness { + parse("/session"), + Some(SlashCommand::StartSession { provider: None, path: None, }) ); assert_eq!( - parse("/harness codex"), - Some(SlashCommand::NewHarness { + parse("/session codex"), + Some(SlashCommand::StartSession { provider: Some("codex".to_string()), path: None, }) ); assert_eq!( - parse("/harness Claude ~/work/foo"), - Some(SlashCommand::NewHarness { + parse("/session Claude ~/work/foo"), + Some(SlashCommand::StartSession { provider: Some("claude".to_string()), path: Some("~/work/foo".to_string()), }), - "the provider is matched case-insensitively, the path is left alone" + "the harness type is matched case-insensitively, the path is left alone" + ); + assert_eq!( + parse("/harness"), + Some(SlashCommand::StartSession { + provider: None, + path: None, + }), + "the old spelling still works — a rename must not break muscle memory" ); } #[test] -fn an_unknown_harness_provider_is_a_usage_error_not_a_default() { - // Silently falling back to the default provider would start the wrong CLI +fn an_unknown_harness_type_is_a_usage_error_not_a_default() { + // Silently falling back to the default would start the wrong CLI // in the operator's workspace, and they would not find out until it did // something. assert_eq!( parse("/harness claud"), Some(SlashCommand::BadUsage( - "Usage: /harness [claude|codex|opencode] [path]" + "Usage: /session [claude|codex|opencode] [path]" )) ); assert_eq!( - parse("/harness ~/work/foo"), + parse("/session ~/work/foo"), Some(SlashCommand::BadUsage( - "Usage: /harness [claude|codex|opencode] [path]" + "Usage: /session [claude|codex|opencode] [path]" )), "a bare path is ambiguous with a provider name, so it is refused" ); diff --git a/src/sdk/src/ui/command/types.rs b/src/sdk/src/ui/command/types.rs index 87a9add92..0ed7b154f 100644 --- a/src/sdk/src/ui/command/types.rs +++ b/src/sdk/src/ui/command/types.rs @@ -22,22 +22,22 @@ pub enum SlashCommand { NewSession, /// `/resume` — open the saved-chat picker. Resume, - /// `/harness [provider] [path]` — start a harness the orchestrator will not + /// `/session [harness] [path]` — start a session the orchestrator will not /// dispatch into. /// /// Both arguments are optional: with neither, the front end opens its /// picker. Parsing does not validate the path — only the front end knows /// what the active workspace is, and a path that does not exist is a /// spawn-time error with a much better message than a parse-time one. - NewHarness { - /// The harness CLI to run, lowercased, when one was named. + StartSession { + /// The harness type to run, lowercased, when one was named. provider: Option, /// The working directory to start it in, when one was given. path: Option, }, - /// `/takecontrol` — take the selected harness from the orchestrator. + /// `/takecontrol` — take the selected session from the orchestrator. TakeControl, - /// `/handoff [note]` — give the selected harness back to the orchestrator, + /// `/handoff [note]` — give the selected session back to the orchestrator, /// optionally saying what you were in the middle of. HandOff { /// What the operator wants continued, in their words. The single most diff --git a/src/sdk/src/ui/decisions/README.md b/src/sdk/src/ui/decisions/README.md index 316ac20a7..a7c754e69 100644 --- a/src/sdk/src/ui/decisions/README.md +++ b/src/sdk/src/ui/decisions/README.md @@ -1,11 +1,11 @@ # Decisions -Prepared operator decisions derived from harness escalations and pending worker questions. The fold is UI-agnostic so terminal and future hosts share stable ids, ordering, deduplication, and answer routing. +Prepared operator decisions derived from agent escalations and pending worker questions. The fold is UI-agnostic so terminal and future hosts share stable ids, ordering, deduplication, and answer routing. ## Contents - [`fold.rs`](./fold.rs) — Deterministic folding of current harness/lane state into prepared decisions. -- [`mod.rs`](./mod.rs) — Prepared operator decisions derived from harness escalations and pending worker questions. The fold is UI-agnostic so terminal and future hosts share stable ids, ordering, deduplication, and answer routing. +- [`mod.rs`](./mod.rs) — Prepared operator decisions derived from agent escalations and pending worker questions. The fold is UI-agnostic so terminal and future hosts share stable ids, ordering, deduplication, and answer routing. - [`tests.rs`](./tests.rs) — Decision-fold tests for ordering, dedupe, and answered-item removal. - [`types.rs`](./types.rs) — Data shapes for the prepared-decision queue. diff --git a/src/sdk/src/ui/decisions/fold.rs b/src/sdk/src/ui/decisions/fold.rs index 58a4cdd70..95a21c7e4 100644 --- a/src/sdk/src/ui/decisions/fold.rs +++ b/src/sdk/src/ui/decisions/fold.rs @@ -39,7 +39,7 @@ fn task_excerpt(task: Option<&TrackedTask>) -> Option { .or_else(|| Some(task.title.clone())) } -/// Fold harness escalations and pending lane questions into one stable queue. +/// Fold agent escalations and pending lane questions into one stable queue. /// /// Questions are ordered by lane/task order before free-form escalations. /// Duplicate escalation strings collapse to one item. A question disappears as @@ -86,7 +86,7 @@ pub fn decision_items(status: Option<&HarnessStatus>, lanes: &[AgentLane]) -> Ve id: format!("escalation:{:016x}", stable_hash(message)), kind: DecisionKind::Escalation, question: message.to_string(), - lane_context: "harness escalation".into(), + lane_context: "agent escalation".into(), contract_excerpt: None, answer_target: None, }); diff --git a/src/sdk/src/ui/decisions/mod.rs b/src/sdk/src/ui/decisions/mod.rs index 317d7a117..ae5b7b899 100644 --- a/src/sdk/src/ui/decisions/mod.rs +++ b/src/sdk/src/ui/decisions/mod.rs @@ -1,4 +1,4 @@ -//! Prepared operator decisions derived from harness escalations and pending +//! Prepared operator decisions derived from agent escalations and pending //! worker questions. The fold is UI-agnostic so terminal and future hosts share //! stable ids, ordering, deduplication, and answer routing. diff --git a/src/tui/examples/pty_load.rs b/src/tui/examples/pty_load.rs index f3cd9a291..854dc4652 100644 --- a/src/tui/examples/pty_load.rs +++ b/src/tui/examples/pty_load.rs @@ -32,7 +32,7 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use medulla::protocol::HarnessProvider; -use medulla_tui::worker::pty::{HarnessControl, LaunchSpec, PtyManager}; +use medulla_tui::worker::pty::{LaunchSpec, PtyManager, SessionControl}; // ------------------------------------------------------------- allocator --- @@ -122,7 +122,7 @@ fn flooding(label: &str) -> LaunchSpec { // The orchestrator's own sessions, as a task frame opens them: this // measures the dispatch path, and an operator-held session is one // `claim_idle` skips entirely. - control: HarnessControl::Orchestrator, + control: SessionControl::Orchestrator, origin: medulla_tui::worker::pty::SessionOrigin::Orchestrator, name: None, mcp_grant_session: None, diff --git a/src/tui/src/app_loop.rs b/src/tui/src/app_loop.rs index 2db4279d5..66d0c9560 100644 --- a/src/tui/src/app_loop.rs +++ b/src/tui/src/app_loop.rs @@ -528,8 +528,8 @@ pub(crate) async fn run_tui(raw: &[String]) -> anyhow::Result<()> { (primary.workspace().to_string(), providers, presets) }); let started_hosts = std::sync::Arc::new(std::sync::Mutex::new(local_hosts)); - let local_harnesses = primary_defaults.map(|(workspace, providers, custom_harnesses)| { - medulla_tui::ui::harness_pane::LocalHarnesses { + let local_sessions = primary_defaults.map(|(workspace, providers, custom_harnesses)| { + medulla_tui::ui::harness_pane::LocalSessions { sessions: harness_sessions.clone(), runtimes: host_runtimes.clone(), hub_address: medulla::hub::DEFAULT_LOCAL_HUB_ADDRESS.to_string(), @@ -678,7 +678,7 @@ pub(crate) async fn run_tui(raw: &[String]) -> anyhow::Result<()> { // one host, so extras are served and dispatchable but not yet // reflected there — a UI gap, not a hosting one. host: primary_observation.clone(), - harnesses: local_harnesses.clone(), + local_sessions: local_sessions.clone(), }, ) .await; diff --git a/src/tui/src/event_loop/cmd_dispatch/handoff.rs b/src/tui/src/event_loop/cmd_dispatch/handoff.rs index 9b4f39f6f..3d39d09f1 100644 --- a/src/tui/src/event_loop/cmd_dispatch/handoff.rs +++ b/src/tui/src/event_loop/cmd_dispatch/handoff.rs @@ -23,7 +23,7 @@ pub(super) fn run_handoff_cmd( msg_tx: &tokio::sync::mpsc::UnboundedSender, ) -> Option> { match cmd { - Cmd::HandOffHarness(brief) => { + Cmd::HandOffSession(brief) => { let rt = runtime.clone(); let tx = msg_tx.clone(); tokio::spawn(async move { @@ -52,7 +52,7 @@ pub(super) fn run_handoff_cmd( }); None } - Cmd::HoldHarness { workspace, reason } => { + Cmd::HoldSession { workspace, reason } => { let rt = runtime.clone(); let tx = msg_tx.clone(); tokio::spawn(async move { diff --git a/src/tui/src/event_loop/cmd_dispatch/mod.rs b/src/tui/src/event_loop/cmd_dispatch/mod.rs index 09b3af4fd..40b5253d9 100644 --- a/src/tui/src/event_loop/cmd_dispatch/mod.rs +++ b/src/tui/src/event_loop/cmd_dispatch/mod.rs @@ -87,7 +87,7 @@ pub(super) fn run_cmd( | Cmd::SubmitFeedback { .. } => { unreachable!("feedback commands return before main dispatch") } - Cmd::HandOffHarness(_) | Cmd::HoldHarness { .. } => { + Cmd::HandOffSession(_) | Cmd::HoldSession { .. } => { unreachable!("handoff commands return before main dispatch") } Cmd::Submit(input) => { diff --git a/src/tui/src/event_loop/mod.rs b/src/tui/src/event_loop/mod.rs index 14c04080b..c4d8dcfa5 100644 --- a/src/tui/src/event_loop/mod.rs +++ b/src/tui/src/event_loop/mod.rs @@ -52,7 +52,7 @@ pub(crate) async fn run( onboarding_path, link_obs, host, - harnesses, + local_sessions, } = wiring; let mut app = App::new(runtime.clone(), loaded); app.set_config_path(config_path); @@ -64,8 +64,8 @@ pub(crate) async fn run( if let Some(host) = host { app.set_host_observation(host); } - if let Some(harnesses) = harnesses { - app.set_local_harnesses(harnesses); + if let Some(sessions) = local_sessions { + app.set_local_sessions(sessions); } if let Some(status) = startup_status { app.set_status(status); diff --git a/src/tui/src/event_loop/types.rs b/src/tui/src/event_loop/types.rs index 368e2b24a..e568e4690 100644 --- a/src/tui/src/event_loop/types.rs +++ b/src/tui/src/event_loop/types.rs @@ -135,12 +135,12 @@ pub(crate) struct SessionWiring { /// A read-only view of the host running on this device, when one is. `None` /// means this machine orchestrates but does not run the work itself. pub host: Option, - /// The live harness sessions this device is running, and the state machine + /// The live sessions this device is running, and the state machine /// that says which task each one serves. /// - /// `None` when this machine does not host: there are no local harnesses to + /// `None` when this machine does not host: there are no local sessions to /// show, and the Agents tab falls back to a remote worker's streamed screen /// or to the transcript. Shared with the host's executor — the sessions it /// opens are the ones rendered here. - pub harnesses: Option, + pub local_sessions: Option, } diff --git a/src/tui/src/ui/app/agent_control.rs b/src/tui/src/ui/app/agent_control.rs index b4a85cba2..406c25d97 100644 --- a/src/tui/src/ui/app/agent_control.rs +++ b/src/tui/src/ui/app/agent_control.rs @@ -7,7 +7,7 @@ //! nothing, inheriting the harness and the directory from the declaration rather //! than asking again. //! -//! Both reuse the picker in [`harness_control`](super::harness_control): picking +//! Both reuse the picker in [`session_control`](super::session_control): picking //! a CLI and a directory is the same two steps either way, and the intent it //! carries ([`PickerPurpose`]) is what decides which of these two ends it lands //! in. @@ -23,9 +23,7 @@ use medulla::runtime::{suggest_agent_id, AgentDeclaration, WorkspaceRef, Workspa use crate::ui::harness_pane::HarnessChoice; -use super::types::{ - tab_pos, App, HarnessPicker, HarnessPickerStep, PickerPurpose, Prompt, PromptKind, -}; +use super::types::{tab_pos, AgentPicker, AgentPickerStep, App, PickerPurpose, Prompt, PromptKind}; impl App { /// Open the create-agent flow: harness type, then workspace, then a name. @@ -34,7 +32,7 @@ impl App { /// nothing in it: an agent is declared *on a host*, and there is none here to /// declare it on. pub(in crate::ui::app) fn open_new_agent_picker(&mut self) { - let Some(harnesses) = self.harnesses.clone() else { + let Some(harnesses) = self.local_sessions.clone() else { self.set_status("This device is not hosting, so it has no agents to declare"); return; }; @@ -43,11 +41,11 @@ impl App { self.set_status("No harness CLIs found on this device"); return; } - self.harness_picker = Some(HarnessPicker { + self.agent_picker = Some(AgentPicker { purpose: PickerPurpose::DeclareAgent, choices, index: 0, - step: HarnessPickerStep::Harness, + step: AgentPickerStep::Harness, cwd: harnesses.workspace.clone(), workspace_query: String::new(), workspace_choices: Vec::new(), @@ -55,7 +53,7 @@ impl App { workspace_picked: false, managed: true, }); - self.set_status("New agent · pick a harness · Enter workspace · Esc cancel"); + self.set_status("New agent · pick a harness type · Enter workspace · Esc cancel"); } /// Ask what to call the agent about to be declared for this pair. @@ -132,7 +130,7 @@ impl App { /// slower. Silent when the directory is already declared, and when this /// device is not hosting. pub(in crate::ui::app) fn offer_agent_declaration(&mut self, harness: &str, workspace: &str) { - if self.harnesses.is_none() { + if self.local_sessions.is_none() { return; } let declared = self.local_agent_declarations().into_iter().any(|held| { @@ -197,7 +195,7 @@ impl App { name: &str, managed: bool, ) { - let Some(harnesses) = self.harnesses.clone() else { + let Some(harnesses) = self.local_sessions.clone() else { self.set_status("This device is not hosting, so it has no sessions to open"); return; }; diff --git a/src/tui/src/ui/app/agent_control_tests.rs b/src/tui/src/ui/app/agent_control_tests.rs index dc8d7db2e..653731692 100644 --- a/src/tui/src/ui/app/agent_control_tests.rs +++ b/src/tui/src/ui/app/agent_control_tests.rs @@ -89,7 +89,7 @@ fn the_new_agent_row_opens_the_picker_in_declare_mode() { assert!(app.on_new_agent_row()); app.open_new_agent_picker(); - let picker = app.harness_picker.as_ref().expect("the picker opened"); + let picker = app.agent_picker.as_ref().expect("the picker opened"); assert_eq!(picker.purpose, PickerPurpose::DeclareAgent); } @@ -139,7 +139,7 @@ fn a_new_session_under_an_agent_asks_for_a_name_first() { fn a_named_session_opens_in_the_agents_own_harness_and_workspace() { let sessions = PtyManager::new(); let mut app = hosting_app(); - app.set_local_harnesses(shell_harnesses(sessions.clone())); + app.set_local_sessions(shell_harnesses(sessions.clone())); app.loaded.config.fleet.agent_declarations = vec![AgentDeclaration::new("shell", "", "codex", "/")]; diff --git a/src/tui/src/ui/app/changes/baseline.rs b/src/tui/src/ui/app/changes/baseline.rs index 649cc8d35..fa27ee802 100644 --- a/src/tui/src/ui/app/changes/baseline.rs +++ b/src/tui/src/ui/app/changes/baseline.rs @@ -1,4 +1,4 @@ -//! Resolves immutable launch baselines for live harness sessions. +//! Resolves immutable launch baselines for live agent sessions. use std::path::Path; @@ -29,7 +29,7 @@ pub(super) fn launch_baseline( } } -/// Resolve the selected harness without silently substituting another +/// Resolve the selected session without silently substituting another /// repository. The newest eligible harness is only a default when no live /// preferred row exists. pub(super) fn select_harness_baseline( @@ -44,7 +44,7 @@ pub(super) fn select_harness_baseline( row.launch_commit.as_deref(), row.launch_checkout_identity.as_deref(), ) - .ok_or_else(|| format!("Selected harness {} is not in a Git repository", row.label))?; + .ok_or_else(|| format!("Selected session {} is not in a Git repository", row.label))?; return Ok(Some((row, commit))); } } diff --git a/src/tui/src/ui/app/changes/baseline_tests.rs b/src/tui/src/ui/app/changes/baseline_tests.rs index 374df1503..849a6db4a 100644 --- a/src/tui/src/ui/app/changes/baseline_tests.rs +++ b/src/tui/src/ui/app/changes/baseline_tests.rs @@ -56,7 +56,7 @@ fn choosing_a_harness_baseline_clears_comments_when_repository_changes() { "first repository", ); - state.choose_harness_baseline().expect("switch repository"); + state.choose_session_baseline().expect("switch repository"); assert_eq!(state.root.as_deref(), Some(second_root.as_path())); assert_eq!(state.baseline.as_deref(), Some(second_baseline.as_str())); @@ -70,7 +70,7 @@ fn following_a_new_launch_commit_in_the_same_repository_preserves_comments() { let first = output(directory.path(), &["rev-parse", "HEAD"]); let mut state = GitChangesState::default(); let identity = crate::worker::pty::checkout::capture(directory.path()).expect("identity"); - state.follow_harness(directory.path(), &first, &identity); + state.follow_session(directory.path(), &first, &identity); state .comments .upsert(Path::new("src/main.rs"), CommentAnchor::File, "keep this"); @@ -80,7 +80,7 @@ fn following_a_new_launch_commit_in_the_same_repository_preserves_comments() { ); let second = output(directory.path(), &["rev-parse", "HEAD"]); - state.follow_harness(directory.path(), &second, &identity); + state.follow_session(directory.path(), &second, &identity); assert_eq!(state.baseline.as_deref(), Some(second.as_str())); assert_eq!(state.comments.count_for(Path::new("src/main.rs")), 1); @@ -98,18 +98,18 @@ fn choosing_harness_launch_replaces_an_operator_selected_baseline() { let manual = output(directory.path(), &["rev-parse", "HEAD"]); let identity = crate::worker::pty::checkout::capture(directory.path()).expect("identity"); let mut state = GitChangesState::default(); - state.follow_harness(directory.path(), &launch, &identity); + state.follow_session(directory.path(), &launch, &identity); state .choose_baseline(&manual, BaselineSource::Manual) .expect("manual baseline"); // Opening a selected harness with `d` uses this explicit activation after // following it; an ordinary refresh deliberately preserves Manual mode. - state.follow_harness(directory.path(), &launch, &identity); - state.choose_harness_baseline().expect("harness baseline"); + state.follow_session(directory.path(), &launch, &identity); + state.choose_session_baseline().expect("harness baseline"); assert_eq!(state.baseline.as_deref(), Some(launch.as_str())); - assert_eq!(state.baseline_source, BaselineSource::HarnessLaunch); + assert_eq!(state.baseline_source, BaselineSource::SessionLaunch); } #[test] @@ -119,33 +119,33 @@ fn returning_from_a_non_git_harness_preserves_same_repository_comments() { let launch = output(directory.path(), &["rev-parse", "HEAD"]); let identity = crate::worker::pty::checkout::capture(directory.path()).expect("identity"); let mut state = GitChangesState::default(); - state.follow_harness(directory.path(), &launch, &identity); + state.follow_session(directory.path(), &launch, &identity); state .comments .upsert(Path::new("src/main.rs"), CommentAnchor::File, "keep this"); - state.clear_repository("selected harness is outside Git".to_owned()); - state.follow_harness(directory.path(), &launch, &identity); + state.clear_repository("selected session is outside Git".to_owned()); + state.follow_session(directory.path(), &launch, &identity); assert_eq!(state.comments.count_for(Path::new("src/main.rs")), 1); } #[test] -fn applying_harness_launch_revalidates_the_checkout_marker() { +fn applying_session_launch_revalidates_the_checkout_marker() { let directory = tempdir().expect("repository"); init_repo(directory.path()); let launch = output(directory.path(), &["rev-parse", "HEAD"]); let identity = crate::worker::pty::checkout::capture(directory.path()).expect("identity"); let mut state = GitChangesState::default(); - state.follow_harness(directory.path(), &launch, &identity); + state.follow_session(directory.path(), &launch, &identity); fs::remove_dir_all(directory.path().join(".git")).expect("remove checkout metadata"); git(directory.path(), &["init"]); let error = state - .choose_harness_baseline() + .choose_session_baseline() .expect_err("replacement checkout must be rejected"); - assert_eq!(error, "Harness Git checkout changed since launch"); + assert_eq!(error, "Session Git checkout changed since launch"); } #[test] @@ -251,12 +251,12 @@ fn a_valid_harness_recovers_after_a_non_git_selection_clears_manual_state() { ..GitChangesState::default() }; - state.clear_repository("selected harness is outside Git".to_owned()); + state.clear_repository("selected session is outside Git".to_owned()); let identity = crate::worker::pty::checkout::capture(directory.path()).expect("identity"); - state.follow_harness(directory.path(), &launch, &identity); + state.follow_session(directory.path(), &launch, &identity); state.refresh(); - assert_eq!(state.baseline_source, BaselineSource::HarnessLaunch); + assert_eq!(state.baseline_source, BaselineSource::SessionLaunch); assert_eq!(state.baseline.as_deref(), Some(launch.as_str())); assert_eq!(state.error, None); } @@ -268,7 +268,7 @@ fn row( launch_commit: Option, started_at: i64, ) -> crate::worker::pty::SessionRow { - use crate::worker::pty::{HarnessControl, PtyState, SessionRow}; + use crate::worker::pty::{PtyState, SessionControl, SessionRow}; SessionRow { id: id.to_owned(), label: label.to_owned(), @@ -288,7 +288,7 @@ fn row( last_output_at: started_at, last_error: None, busy: false, - control: HarnessControl::User, + control: SessionControl::User, origin: crate::worker::pty::SessionOrigin::User, name: None, attention: None, diff --git a/src/tui/src/ui/app/changes/mod.rs b/src/tui/src/ui/app/changes/mod.rs index a5c2b9421..cc16502ba 100644 --- a/src/tui/src/ui/app/changes/mod.rs +++ b/src/tui/src/ui/app/changes/mod.rs @@ -37,8 +37,8 @@ impl App { /// replaces an operator-selected commit or manual baseline: `d` means the /// immutable launch diff for the harness under the cursor. pub(super) fn open_selected_harness_changes(&mut self) -> Option { - let session = self.harness_pane_session.clone()?; - self.selected_harness_session = Some(session); + let session = self.pane_session.clone()?; + self.rail_session = Some(session); self.tab_index = TABS .iter() .position(|tab| *tab == "Changes") @@ -61,16 +61,16 @@ impl App { /// Reload Changes, optionally overriding an operator-selected baseline. fn refresh_changes_with_harness(&mut self, activate_harness: bool) { let preferred_id = self - .attached_harness() + .attached_session() .map(str::to_owned) - .or_else(|| self.selected_harness_session.clone()); - let selected = self.harnesses.as_ref().map(|harnesses| { + .or_else(|| self.rail_session.clone()); + let selected = self.local_sessions.as_ref().map(|harnesses| { select_harness_baseline(harnesses.sessions.rows(), preferred_id.as_deref()) }); match selected { Some(Ok(Some((row, commit)))) => { let root = row.launch_root.as_deref().unwrap_or(&row.cwd); - self.changes.follow_harness( + self.changes.follow_session( Path::new(root), &commit, row.launch_checkout_identity @@ -78,7 +78,7 @@ impl App { .expect("validated harness identity"), ); if activate_harness { - if let Err(error) = self.changes.choose_harness_baseline() { + if let Err(error) = self.changes.choose_session_baseline() { self.set_status(error); } else { self.set_status(self.changes.status_message()); diff --git a/src/tui/src/ui/app/changes/tests.rs b/src/tui/src/ui/app/changes/tests.rs index 17b2d7907..153ee2709 100644 --- a/src/tui/src/ui/app/changes/tests.rs +++ b/src/tui/src/ui/app/changes/tests.rs @@ -399,7 +399,7 @@ fn following_a_harness_uses_its_launch_commit_until_operator_selects_another() { .0; let identity = crate::worker::pty::checkout::capture(directory.path()).expect("identity"); - state.follow_harness(directory.path(), &launch, &identity); + state.follow_session(directory.path(), &launch, &identity); state.refresh(); assert_eq!(state.root.as_deref(), Some(expected_root.as_path())); diff --git a/src/tui/src/ui/app/changes/types.rs b/src/tui/src/ui/app/changes/types.rs index bb2020e07..0c0d74246 100644 --- a/src/tui/src/ui/app/changes/types.rs +++ b/src/tui/src/ui/app/changes/types.rs @@ -36,11 +36,11 @@ pub(crate) type LoadedChanges = (Vec, Vec, Vec); /// How the active comparison baseline was chosen. #[derive(Debug, Clone, PartialEq, Eq, Default)] pub(crate) enum BaselineSource { - /// The app-start snapshot used until a harness becomes available. + /// The app-start snapshot used until a session becomes available. #[default] AppLaunch, - /// The commit captured immediately before the selected harness was spawned. - HarnessLaunch, + /// The commit captured immediately before the selected session was spawned. + SessionLaunch, /// A commit chosen from repository history. Commit, /// A revision entered by the operator. @@ -148,7 +148,7 @@ impl GitChangesState { } } - /// Clear repository-backed content when the selected harness cannot be + /// Clear repository-backed content when the selected session cannot be /// reviewed, preventing stale changes from another repository remaining on /// screen beneath the error. pub(crate) fn clear_repository(&mut self, error: String) { @@ -172,10 +172,10 @@ impl GitChangesState { self.error = Some(error); } - /// Follow a harness's immutable launch snapshot while launch mode is active. + /// Follow a session's immutable launch snapshot while launch mode is active. /// Review comments survive baseline changes within the same repository and /// are cleared only when the repository root changes. - pub(crate) fn follow_harness( + pub(crate) fn follow_session( &mut self, cwd: &Path, launch_commit: &str, @@ -189,7 +189,7 @@ impl GitChangesState { self.harness_checkout_identity = Some(checkout_identity.to_owned()); if matches!( self.baseline_source, - BaselineSource::AppLaunch | BaselineSource::HarnessLaunch + BaselineSource::AppLaunch | BaselineSource::SessionLaunch ) && (self.root.as_ref() != Some(&root) || self.baseline.as_deref() != Some(launch_commit)) { @@ -200,7 +200,7 @@ impl GitChangesState { self.comments_root = Some(root.clone()); self.root = Some(root); self.baseline = Some(launch_commit.to_owned()); - self.baseline_source = BaselineSource::HarnessLaunch; + self.baseline_source = BaselineSource::SessionLaunch; self.selected = 0; self.cursor = 0; self.scroll = 0; @@ -228,22 +228,22 @@ impl GitChangesState { Ok(()) } - /// Return to the selected harness's repository and captured launch commit. - pub(crate) fn choose_harness_baseline(&mut self) -> Result<(), String> { + /// Return to the selected session's repository and captured launch commit. + pub(crate) fn choose_session_baseline(&mut self) -> Result<(), String> { let root = self .harness_root .clone() - .ok_or_else(|| "No harness Git repository is available".to_owned())?; + .ok_or_else(|| "No session Git repository is available".to_owned())?; let baseline = self .harness_baseline .clone() - .ok_or_else(|| "No harness launch snapshot is available".to_owned())?; + .ok_or_else(|| "No session launch snapshot is available".to_owned())?; let identity = self .harness_checkout_identity .as_deref() - .ok_or_else(|| "No harness checkout identity is available".to_owned())?; + .ok_or_else(|| "No session checkout identity is available".to_owned())?; if !crate::worker::pty::checkout::matches(&root, identity) { - return Err("Harness Git checkout changed since launch".to_owned()); + return Err("Session Git checkout changed since launch".to_owned()); } let comments_root = self.comments_root.as_ref().or(self.root.as_ref()); if comments_root != Some(&root) { @@ -252,7 +252,7 @@ impl GitChangesState { self.comments_root = Some(root.clone()); self.root = Some(root); self.baseline = Some(baseline); - self.baseline_source = BaselineSource::HarnessLaunch; + self.baseline_source = BaselineSource::SessionLaunch; self.picking_baseline = false; self.selected = 0; self.cursor = 0; @@ -265,7 +265,7 @@ impl GitChangesState { pub(crate) fn baseline_label(&self) -> String { let source = match self.baseline_source { BaselineSource::AppLaunch => "app launch", - BaselineSource::HarnessLaunch => "harness launch", + BaselineSource::SessionLaunch => "session launch", BaselineSource::Commit => "commit", BaselineSource::Manual => "manual", }; diff --git a/src/tui/src/ui/app/commands/changes.rs b/src/tui/src/ui/app/commands/changes.rs index ecbc8ec94..04e62ba08 100644 --- a/src/tui/src/ui/app/commands/changes.rs +++ b/src/tui/src/ui/app/commands/changes.rs @@ -18,7 +18,7 @@ impl App { pub(crate) fn apply_change_baseline_selection(&mut self) { let index = self.changes.baseline_index; if index == 0 { - match self.changes.choose_harness_baseline() { + match self.changes.choose_session_baseline() { Ok(()) => self.set_status(self.changes.status_message()), Err(error) => self.set_status(error), } diff --git a/src/tui/src/ui/app/commands/dispatch.rs b/src/tui/src/ui/app/commands/dispatch.rs index 1fac47f4d..99c534764 100644 --- a/src/tui/src/ui/app/commands/dispatch.rs +++ b/src/tui/src/ui/app/commands/dispatch.rs @@ -444,11 +444,11 @@ impl App { self.new_thread(); } SlashCommand::Resume => return Some(Cmd::ListChats), - SlashCommand::NewHarness { provider, path } => { - self.start_harness_command(provider.as_deref(), path.as_deref()); + SlashCommand::StartSession { provider, path } => { + self.start_session_command(provider.as_deref(), path.as_deref()); } - SlashCommand::TakeControl => self.take_harness_control(), - SlashCommand::HandOff { note } => self.hand_harness_back(note), + SlashCommand::TakeControl => self.take_session_control(), + SlashCommand::HandOff { note } => self.hand_session_back(note), SlashCommand::Abort => { self.runtime.abort(); self.set_status("Abort requested"); diff --git a/src/tui/src/ui/app/harness_workspace.rs b/src/tui/src/ui/app/harness_workspace.rs index 6621cf6ed..0a89b68c7 100644 --- a/src/tui/src/ui/app/harness_workspace.rs +++ b/src/tui/src/ui/app/harness_workspace.rs @@ -4,7 +4,7 @@ use std::collections::{BinaryHeap, HashSet}; use std::path::Path; -use super::types::{App, HarnessPickerStep, WorkspaceChoice}; +use super::types::{AgentPickerStep, App, WorkspaceChoice}; use crate::ui::composer::flatten_paste; const MAX_WORKSPACE_CHOICES: usize = 10; @@ -16,12 +16,12 @@ impl App { /// Advance the launcher to its workspace step and populate the first list. pub(super) fn open_harness_workspace_step(&mut self, edit_default: bool) { let default = self - .harness_picker + .agent_picker .as_ref() .map(|picker| picker.cwd.clone()) .unwrap_or_default(); - if let Some(picker) = &mut self.harness_picker { - picker.step = HarnessPickerStep::Workspace; + if let Some(picker) = &mut self.agent_picker { + picker.step = AgentPickerStep::Workspace; picker.workspace_query = if edit_default { default } else { String::new() }; picker.workspace_index = 0; picker.workspace_picked = false; @@ -44,13 +44,13 @@ impl App { /// what the query is: a single-line box with no caret, edited by the same /// `push`/`pop` that typing uses. A path copied with a trailing newline /// therefore lands as the path plus a space, which - /// [`resolve_workspace`](crate::ui::harness_pane::LocalHarnesses::resolve_workspace) + /// [`resolve_workspace`](crate::ui::harness_pane::LocalSessions::resolve_workspace) /// trims before it is used. pub(super) fn paste_into_harness_workspace(&mut self, text: &str) { - let Some(picker) = &mut self.harness_picker else { + let Some(picker) = &mut self.agent_picker else { return; }; - if picker.step != HarnessPickerStep::Workspace { + if picker.step != AgentPickerStep::Workspace { return; } picker.workspace_query.push_str(&flatten_paste(text)); @@ -63,12 +63,12 @@ impl App { /// Recompute cached completions after the query changes. pub(super) fn refresh_harness_workspace_choices(&mut self) { - let Some(picker) = &self.harness_picker else { + let Some(picker) = &self.agent_picker else { return; }; let query = picker.workspace_query.clone(); let choices = self.workspace_choices(&query); - if let Some(picker) = &mut self.harness_picker { + if let Some(picker) = &mut self.agent_picker { picker.workspace_choices = choices; picker.workspace_index = picker .workspace_index @@ -90,10 +90,10 @@ impl App { /// copied out of a file manager takes — filled the list with its own /// children, and Enter silently started the harness in the first of them /// rather than in the directory that was asked for. - pub(super) fn selected_harness_workspace(&self) -> Option { - let picker = self.harness_picker.as_ref()?; + pub(super) fn selected_picker_workspace(&self) -> Option { + let picker = self.agent_picker.as_ref()?; let resolved = self - .harnesses + .local_sessions .as_ref() .map(|harnesses| harnesses.resolve_workspace(&picker.workspace_query)); // Blank means "the default", which is what the completions already rank @@ -111,13 +111,13 @@ impl App { /// Make the highlighted completion the editable query. pub(super) fn complete_harness_workspace(&mut self) { - let selected = self.harness_picker.as_ref().and_then(|picker| { + let selected = self.agent_picker.as_ref().and_then(|picker| { picker .workspace_choices .get(picker.workspace_index) .map(|choice| choice.path.clone()) }); - if let (Some(picker), Some(selected)) = (&mut self.harness_picker, selected) { + if let (Some(picker), Some(selected)) = (&mut self.agent_picker, selected) { picker.workspace_query = selected; picker.workspace_index = 0; // Completing *is* entering it: the query now names the directory, @@ -147,7 +147,7 @@ impl App { /// Rank recent, configured, and filesystem-derived workspace suggestions. fn workspace_choices(&self, query: &str) -> Vec { - let Some(harnesses) = &self.harnesses else { + let Some(harnesses) = &self.local_sessions else { return Vec::new(); }; let base = Path::new(&harnesses.workspace); diff --git a/src/tui/src/ui/app/harness_workspace_tests.rs b/src/tui/src/ui/app/harness_workspace_tests.rs index 008b71b69..52bdc1b19 100644 --- a/src/tui/src/ui/app/harness_workspace_tests.rs +++ b/src/tui/src/ui/app/harness_workspace_tests.rs @@ -76,7 +76,7 @@ fn folder_completion_keeps_only_the_best_bounded_set() { /// An Agents-tab app with a picker parked on its workspace step, whose default /// workspace is `workspace`. fn picker_on_workspace_step(workspace: &std::path::Path) -> super::types::App { - use super::types::{App, HarnessPicker, HarnessPickerStep}; + use super::types::{AgentPicker, AgentPickerStep, App}; let mut loaded = medulla::config::LoadedConfig::defaults("medulla.tui.json".into()); loaded.config.link = Some(medulla::config::LinkConfig::default()); @@ -84,7 +84,7 @@ fn picker_on_workspace_step(workspace: &std::path::Path) -> super::types::App { std::sync::Arc::new(medulla::runtime::mock::MockRuntime::empty()), loaded, ); - app.set_local_harnesses(crate::ui::harness_pane::LocalHarnesses { + app.set_local_sessions(crate::ui::harness_pane::LocalSessions { hooks: medulla::harness_hooks::HooksConfig::default(), log: None, sessions: crate::worker::pty::PtyManager::new(), @@ -97,11 +97,11 @@ fn picker_on_workspace_step(workspace: &std::path::Path) -> super::types::App { router: None, attribution: true, }); - app.harness_picker = Some(HarnessPicker { + app.agent_picker = Some(AgentPicker { purpose: super::types::PickerPurpose::Spawn, choices: Vec::new(), index: 0, - step: HarnessPickerStep::Workspace, + step: AgentPickerStep::Workspace, cwd: workspace.to_string_lossy().into_owned(), workspace_query: String::new(), workspace_choices: Vec::new(), @@ -127,7 +127,7 @@ fn a_pasted_directory_starts_there_rather_than_in_its_first_child() { app.on_event(crossterm::event::Event::Paste(pasted)); assert!( - !app.harness_picker + !app.agent_picker .as_ref() .unwrap() .workspace_choices @@ -135,7 +135,7 @@ fn a_pasted_directory_starts_there_rather_than_in_its_first_child() { "the children are still offered as completions" ); assert_eq!( - app.selected_harness_workspace() + app.selected_picker_workspace() .map(std::path::PathBuf::from), Some(root.path().to_path_buf()), "but Enter starts in the directory that was actually pasted" @@ -160,7 +160,7 @@ fn arrowing_onto_a_completion_still_wins_over_the_typed_query() { )); assert_eq!( - app.selected_harness_workspace() + app.selected_picker_workspace() .map(std::path::PathBuf::from), Some(root.path().join("alpha")), "a deliberately chosen completion is still what Enter uses" diff --git a/src/tui/src/ui/app/hosts/edit.rs b/src/tui/src/ui/app/hosts/edit.rs index 7d505234c..13dc6976f 100644 --- a/src/tui/src/ui/app/hosts/edit.rs +++ b/src/tui/src/ui/app/hosts/edit.rs @@ -101,7 +101,7 @@ impl App { // agent, and inventing one with no harness would advertise a // placement that cannot run. self.set_status(format!( - "{} reports no harness, so its roles cannot be saved", + "{} reports no harness type, so its roles cannot be saved", agent.agent_id )); return false; diff --git a/src/tui/src/ui/app/input/mouse.rs b/src/tui/src/ui/app/input/mouse.rs index cd362aeb2..51d618d34 100644 --- a/src/tui/src/ui/app/input/mouse.rs +++ b/src/tui/src/ui/app/input/mouse.rs @@ -44,15 +44,15 @@ impl App { /// Handle scroll and left-click mouse events for the active tab. pub(in crate::ui::app) fn on_mouse(&mut self, m: crossterm::event::MouseEvent) -> Option { if self.kill_armed.take().is_some() { - self.set_status("Harness kill cancelled"); + self.set_status("Session kill cancelled"); } // A modal swallows the mouse, the same way it swallows the keyboard. // Pickers and the hand-back question are modal: a click that navigated // the rail behind one would leave an overlay describing a row nobody - // was pointing at. In particular, do not let a second harness click + // was pointing at. In particular, do not let a second session click // replace the session named by an already-visible hand-back prompt. if self.resume_picker.is_some() - || self.harness_picker.is_some() + || self.agent_picker.is_some() || self.handback_prompt.is_some() { return None; @@ -73,7 +73,7 @@ impl App { MouseEventKind::Down(MouseButton::Left) => { if let Some(session) = self.harness_focus.attached_to().map(str::to_string) { let inside_attached_pane = - self.hit_harness.as_ref().is_some_and(|(rect, id)| { + self.hit_session.as_ref().is_some_and(|(rect, id)| { id == &session && rect.contains((m.column, m.row).into()) }); if !inside_attached_pane { @@ -94,10 +94,10 @@ impl App { // focus as Ctrl-]. Settle the configured hand-back policy // before changing the selected tab or rail row; otherwise // an Ask prompt would refer to a pane already hidden. - if !self.begin_harness_release(&session) { + if !self.begin_session_release(&session) { return None; } - self.release_harness(); + self.release_session(); } } } @@ -142,7 +142,7 @@ impl App { let Some(session) = self.harness_focus.attached_to().map(str::to_string) else { return false; }; - let Some((rect, id)) = self.hit_harness.clone() else { + let Some((rect, id)) = self.hit_session.clone() else { return false; }; if id != session || !rect.contains((m.column, m.row).into()) { @@ -151,7 +151,7 @@ impl App { let Some((button, motion)) = pointer_report(m.kind) else { return false; }; - let Some(harnesses) = self.harnesses.clone() else { + let Some(harnesses) = self.local_sessions.clone() else { return false; }; if !harnesses.takes_mouse(&session) { @@ -176,9 +176,9 @@ impl App { // being attached: reading back through a harness's output is the most // common thing to want from one, and making it cost a chord first would // be the wrapper getting in the way. - if let Some((rect, session)) = self.hit_harness.clone() { + if let Some((rect, session)) = self.hit_session.clone() { if rect.contains((x, y).into()) { - if let Some(harnesses) = self.harnesses.clone() { + if let Some(harnesses) = self.local_sessions.clone() { // Pane-relative: the child believes its screen starts at its // own origin, and reporting our absolute position would put // the event somewhere else entirely on it. @@ -396,15 +396,15 @@ impl App { if let Some(session) = row.session_id() { // Point the prompt at the row that was clicked, // not at whatever the last render left behind. - // `harness_pane_session` is written during the + // `pane_session` is written during the // draw, and no draw happens between the cursor // move above and this call — so without this the // prompt would offer to hand over the previously // visible harness, and confirming it would // transfer control of one the operator never // pointed at. - self.harness_pane_session = Some(session.to_string()); - self.open_harness_enter_prompt(); + self.pane_session = Some(session.to_string()); + self.open_session_enter_prompt(); // Drop whatever task the previous row was // watching, exactly as the fall-through below // does for every other row. This branch returns @@ -424,12 +424,12 @@ impl App { // A click inside the embedded terminal means "type here", the same // as `Ctrl-]`. Checked after the rail so a click that changes rows // is a navigation, not an attach to whatever the last frame showed. - if let Some((rect, session)) = self.hit_harness.clone() { + if let Some((rect, session)) = self.hit_session.clone() { if rect.contains((x, y).into()) - && self.harness_pane_session.as_deref() == Some(session.as_str()) + && self.pane_session.as_deref() == Some(session.as_str()) && !self.harness_focus.is_attached_to(&session) { - self.attach_to_pane_harness(); + self.attach_to_pane_session(); } } } else if tab == "Settings" && self.settings_subpage() == "Context" { diff --git a/src/tui/src/ui/app/input/paste.rs b/src/tui/src/ui/app/input/paste.rs index 177d7dc21..4f2c5c4ae 100644 --- a/src/tui/src/ui/app/input/paste.rs +++ b/src/tui/src/ui/app/input/paste.rs @@ -34,7 +34,7 @@ impl App { /// keystrokes it happens to spell; /// 3. an open inline prompt, flattened to one line because that is all it /// can draw; - /// 4. the harness picker, whose workspace step is a path box and whose + /// 4. the session picker, whose workspace step is a path box and whose /// harness step is a list; /// 5. any remaining modal, before anything per-tab — one can be raised over /// a tab the operator has since moved to; @@ -57,7 +57,7 @@ impl App { // it out let a paste land in a composer while the question stayed armed // for whatever key came next. if self.kill_armed.take().is_some() { - self.set_status("Harness kill cancelled"); + self.set_status("Session kill cancelled"); return; } // The hand-back question is asked while still attached, so it outranks @@ -75,10 +75,10 @@ impl App { prompt.paste(text); return; } - // The harness picker is two overlays in one: a provider list with no + // The session picker is two overlays in one: a harness-type list with no // field, then a visible path box. Routed as one call so the distinction // stays with the picker rather than being re-derived here. - if self.harness_picker.is_some() { + if self.agent_picker.is_some() { self.paste_into_harness_workspace(text); return; } diff --git a/src/tui/src/ui/app/input/tests.rs b/src/tui/src/ui/app/input/tests.rs index 27b9cbe8a..4227d67fc 100644 --- a/src/tui/src/ui/app/input/tests.rs +++ b/src/tui/src/ui/app/input/tests.rs @@ -213,7 +213,7 @@ fn clicking_an_action_row_stops_a_task_stream_it_left_behind() { &|row: &RailRow| row.new_session_agent().is_some(), ] { let mut app = app_with_tasks("dev", 3); - app.set_local_harnesses(super::super::rail::tests::shell_harnesses( + app.set_local_sessions(super::super::rail::tests::shell_harnesses( crate::worker::pty::PtyManager::new(), )); app.loaded.config.fleet.agent_declarations = vec![medulla::runtime::AgentDeclaration::new( diff --git a/src/tui/src/ui/app/keys/agents.rs b/src/tui/src/ui/app/keys/agents.rs index 064d81188..46ebe58bb 100644 --- a/src/tui/src/ui/app/keys/agents.rs +++ b/src/tui/src/ui/app/keys/agents.rs @@ -37,7 +37,7 @@ impl App { /// [`agents_panes`](crate::ui::app::render) lays out from, so the keyboard /// and the screen cannot disagree about whether there is somewhere to type. pub fn agents_composer_shown(&self) -> bool { - self.on_orchestrator_lane() && self.harness_pane_session.is_none() + self.on_orchestrator_lane() && self.pane_session.is_none() } /// Whether the Agents rail currently holds the keyboard. @@ -94,14 +94,14 @@ impl App { // its diff rather than making the operator tab across and rely on // the Changes view to remember which of several harnesses they had // selected. When no harness is shown, `d` remains ordinary typing. - KeyCode::Char('d') if !ctrl && !alt && self.harness_pane_session.is_some() => { + KeyCode::Char('d') if !ctrl && !alt && self.pane_session.is_some() => { AgentsKey::Handled(self.open_selected_harness_changes()) } KeyCode::Char('K') => { if let Some(target) = self.kill_target() { self.arm_kill(target); } else { - self.set_status("Select a running harness task first"); + self.set_status("Select a running session first"); } AgentsKey::Handled(None) } diff --git a/src/tui/src/ui/app/keys/harness.rs b/src/tui/src/ui/app/keys/harness.rs index af3fce2bd..642301ca8 100644 --- a/src/tui/src/ui/app/keys/harness.rs +++ b/src/tui/src/ui/app/keys/harness.rs @@ -22,7 +22,7 @@ use crate::ui::harness_pane::{ HarnessFocus, FOCUS_CHORD_LABEL, }; use crate::worker::pty::launch::bracket_paste; -use crate::worker::pty::HarnessControl; +use crate::worker::pty::SessionControl; use super::super::types::{AgentsFocus, App}; @@ -31,13 +31,14 @@ impl App { /// /// Safe to call when nothing is attached; that is the common case on the /// render path, which calls this whenever the selection moves. - pub(crate) fn release_harness(&mut self) { + pub(crate) fn release_session(&mut self) { // The operator has been looking at and handling this pane. Consume any // completion bell observed while it was attached so detaching cannot // reveal a stale alert that the hidden rail deliberately suppressed. - if let (Some(session), Some(harnesses)) = - (self.harness_focus.attached_to(), self.harnesses.as_ref()) - { + if let (Some(session), Some(harnesses)) = ( + self.harness_focus.attached_to(), + self.local_sessions.as_ref(), + ) { harnesses.sessions.acknowledge(session); } self.harness_focus = HarnessFocus::Chrome; @@ -54,13 +55,13 @@ impl App { if let Some(session) = self.harness_focus.attached_to().map(str::to_string) { if is_focus_chord(key) { // Releasing the keyboard is also the moment to settle who holds - // the harness. `begin_harness_release` answers `false` when it + // the harness. `begin_session_release` answers `false` when it // opened a prompt about that, and the keyboard must stay put // until it is answered — moving it out from under the question // would leave the operator answering about a pane they can no // longer see the state of. - if self.begin_harness_release(&session) { - self.release_harness(); + if self.begin_session_release(&session) { + self.release_session(); // The keyboard has to land somewhere it can be seen. The // cursor is on a harness row, which draws no composer, so // the rail is the only half of the tab that can answer a @@ -68,7 +69,7 @@ impl App { // every key after a release look like a dead terminal. self.focus_agents_rail(); self.set_status(format!( - "Released the harness · {FOCUS_CHORD_LABEL} to type again" + "Released the session · {FOCUS_CHORD_LABEL} to type again" )); } return true; @@ -79,27 +80,27 @@ impl App { // *Attaching* is a chrome binding, not a mode, so it yields to whatever // overlay is on top of the chrome. The pane behind an open picker is // still drawn — and so still resolves a harness session — which is how - // Enter in the "start a harness" modal used to attach to the harness + // Enter in the "start a session" modal used to attach to the session // already selected underneath it instead of launching the chosen one. if self.overlay_owns_keys() { return false; } let enter_on_harness = key.code == KeyCode::Enter && key.modifiers == KeyModifiers::NONE - && self.harness_pane_session.is_some() + && self.pane_session.is_some() && self.agents_focus == AgentsFocus::Rail; // Enter asks first. It is a navigation key, and walking the rail onto a // managed harness must not silently take it away from the orchestrator; // the chord below is the deliberate spelling and still attaches outright. if enter_on_harness { - self.open_harness_enter_prompt(); + self.open_session_enter_prompt(); // Consumed either way: Enter reaches this branch only when the // visible pane resolved to a harness, so it must not submit a // hidden composer or return focus to one. return true; } if is_focus_chord(key) { - self.attach_to_pane_harness(); + self.attach_to_pane_session(); return true; } false @@ -115,17 +116,17 @@ impl App { /// terminal is what every other terminal on the machine means by "type /// here", and requiring a chord to do it made the embedded pane the one /// exception. - pub(in crate::ui::app) fn attach_to_pane_harness(&mut self) { - let Some(session) = self.harness_pane_session.clone() else { - self.set_status("No harness on this row — select a running task to type into one"); + pub(in crate::ui::app) fn attach_to_pane_session(&mut self) { + let Some(session) = self.pane_session.clone() else { + self.set_status("No session on this row — select a running one to type into"); return; }; let running = self - .harnesses + .local_sessions .as_ref() .is_some_and(|harnesses| harnesses.is_running(&session)); if !running { - self.set_status("That harness has exited — its last screen is all that is left"); + self.set_status("That session has exited — its last screen is all that is left"); return; } // Focusing in *is* taking over. Keyboard ownership without control is @@ -134,27 +135,27 @@ impl App { // in, and a harness serves one turn at a time, so the two prompts come // back as one confidently wrong answer rather than as an error. let took = self - .harnesses + .local_sessions .as_ref() .and_then(|harnesses| harnesses.control(&session)) - == Some(HarnessControl::Orchestrator); + == Some(SessionControl::Orchestrator); if took { - if let Some(harnesses) = self.harnesses.clone() { - harnesses.set_control(&session, HarnessControl::User); + if let Some(harnesses) = self.local_sessions.clone() { + harnesses.set_control(&session, SessionControl::User); } - self.harness_took_control = true; + self.took_control_by_attach = true; } // Attaching answers whatever the harness was blinking about: the // operator is now looking at the screen that was asking. A named prompt // that is still up returns on the next refresh, so nothing is lost by // clearing it here — and a rail that keeps blinking at the pane you are // already typing in is how an indicator becomes furniture. - if let Some(harnesses) = self.harnesses.as_ref() { + if let Some(harnesses) = self.local_sessions.as_ref() { harnesses.sessions.acknowledge(&session); } self.harness_focus = HarnessFocus::Attached(session); self.set_status(format!( - "Typing into the harness · you have control · {FOCUS_CHORD_LABEL} to release" + "Typing into the session · you have control · {FOCUS_CHORD_LABEL} to release" )); } @@ -188,7 +189,7 @@ impl App { /// line discipline is raw and reads carriage return as the end of a line. pub(in crate::ui::app) fn paste_into_harness(&mut self, session: &str, text: &str) { let bracketed = self - .harnesses + .local_sessions .as_ref() .and_then(|harnesses| harnesses.sessions.bracketed_paste(session)) .unwrap_or(false); @@ -203,8 +204,8 @@ impl App { /// Write already-encoded bytes to the attached harness, detaching if the /// child has stopped listening. fn write_to_harness(&mut self, session: &str, bytes: &[u8]) { - let Some(harnesses) = self.harnesses.clone() else { - self.release_harness(); + let Some(harnesses) = self.local_sessions.clone() else { + self.release_session(); return; }; if let Err(err) = harnesses.write(session, bytes) { @@ -212,12 +213,12 @@ impl App { // the session back on the way out: a dead harness left under user // control is a slot nothing can ever reclaim, and there is nobody // left to answer a hand-back prompt about it. - if self.harness_took_control { - harnesses.set_control(session, HarnessControl::Orchestrator); - self.harness_took_control = false; + if self.took_control_by_attach { + harnesses.set_control(session, SessionControl::Orchestrator); + self.took_control_by_attach = false; } - self.release_harness(); - self.set_status(format!("Harness stopped listening ({err})")); + self.release_session(); + self.set_status(format!("Session stopped listening ({err})")); return; } // Typing means "I am here now". A pane left scrolled back would keep diff --git a/src/tui/src/ui/app/keys/mod.rs b/src/tui/src/ui/app/keys/mod.rs index 1edb7c1e4..8524f49bb 100644 --- a/src/tui/src/ui/app/keys/mod.rs +++ b/src/tui/src/ui/app/keys/mod.rs @@ -48,10 +48,10 @@ impl App { // owns exactly one keypress: only a deliberate `y` proceeds. if let Some((worker, task_id)) = self.kill_armed.take() { if k.code == KeyCode::Char('y') && k.modifiers.is_empty() { - self.set_status(format!("Killing harness for {task_id}…")); + self.set_status(format!("Killing the session for {task_id}…")); return Some(Cmd::KillTask { worker, task_id }); } - self.set_status("Harness kill cancelled"); + self.set_status("Session kill cancelled"); return None; } @@ -94,11 +94,11 @@ impl App { } // The harness picker owns navigation while open. - if self.harness_picker.is_some() { + if self.agent_picker.is_some() { if ctrl && k.code == KeyCode::Char('c') { self.should_quit = true; } else { - self.handle_harness_picker_key(k); + self.handle_agent_picker_key(k); } return None; } @@ -199,14 +199,14 @@ impl App { KeyCode::Char('t') => { match self.selected_agent_id().filter(|_| tab == "Agents") { Some(agent_id) => self.open_new_session(&agent_id), - None => self.open_harness_picker(), + None => self.open_session_picker(), } return None; } // Grab or give: one chord for both directions, because the rail // row and the pane title both say which way it will go. KeyCode::Char('g') => { - self.toggle_harness_control(); + self.toggle_session_control(); return None; } // Walk the open threads. The bare arrows belong to the composer, diff --git a/src/tui/src/ui/app/keys/routing/add_host.rs b/src/tui/src/ui/app/keys/routing/add_host.rs index 911846397..8a69c6973 100644 --- a/src/tui/src/ui/app/keys/routing/add_host.rs +++ b/src/tui/src/ui/app/keys/routing/add_host.rs @@ -27,7 +27,7 @@ impl App { // the arrows belong to the live step. Letting them keep driving // the kind list meant confirming Remote and arrowing to Local // carried the confirmation across, so the next Enter skipped - // "Choose a harness" and asked for a directory for a harness + // "Choose a harness type" and asked for a directory for a harness type // nobody had picked. Esc is how you go back a step. match (self.add_host_selected_kind(), self.add_host_kind_chosen) { (AddHostKind::Local, true) => { @@ -74,7 +74,7 @@ impl App { // would mean the arrows never reached the harness list. AddHostKind::Local if !self.add_host_kind_chosen => { self.add_host_kind_chosen = true; - self.set_status("Choose a harness · Enter to set the directory"); + self.set_status("Choose a harness type · Enter to set the directory"); } AddHostKind::Local => { let providers = self.add_host_providers(); diff --git a/src/tui/src/ui/app/keys/routing/mod.rs b/src/tui/src/ui/app/keys/routing/mod.rs index c6fa59fe2..86d42c34a 100644 --- a/src/tui/src/ui/app/keys/routing/mod.rs +++ b/src/tui/src/ui/app/keys/routing/mod.rs @@ -201,7 +201,7 @@ impl App { match crate::ui::app::TABS.iter().position(|tab| *tab == "Agents") { Some(index) => { self.tab_index = index; - self.set_status(format!("New agent on {} · pick a harness", host.label)); + self.set_status(format!("New agent on {} · pick a harness type", host.label)); } None => self.set_status("Declare a new agent from the Agents tab"), } @@ -413,7 +413,7 @@ impl App { KeyCode::Char('r') => { self.reload_custom_harnesses(); self.refresh_credential_status_if_needed(); - self.set_status("Harnesses refreshed"); + self.set_status("Harness types refreshed"); RoutingKey::Handled(Some(Cmd::RefreshFleet)) } _ => RoutingKey::Unhandled, diff --git a/src/tui/src/ui/app/mod.rs b/src/tui/src/ui/app/mod.rs index 00dc9c71f..d7272e76d 100644 --- a/src/tui/src/ui/app/mod.rs +++ b/src/tui/src/ui/app/mod.rs @@ -23,9 +23,6 @@ mod credentials; mod custom_harnesses; mod decisions; mod feedback; -mod harness_control; -#[cfg(test)] -mod harness_control_tests; mod harness_workspace; #[cfg(test)] mod harness_workspace_tests; @@ -38,6 +35,9 @@ mod overlays_tests; mod rail; mod render; mod routing_options; +mod session_control; +#[cfg(test)] +mod session_control_tests; mod session_focus; #[cfg(test)] mod session_focus_tests; diff --git a/src/tui/src/ui/app/overlays.rs b/src/tui/src/ui/app/overlays.rs index 73f9aa9cf..c961cab73 100644 --- a/src/tui/src/ui/app/overlays.rs +++ b/src/tui/src/ui/app/overlays.rs @@ -31,7 +31,7 @@ impl App { [ (Overlay::Decisions, self.decision_open), (Overlay::TemplatePopup, self.template_popup_open()), - (Overlay::HarnessPicker, self.harness_picker.is_some()), + (Overlay::AgentPicker, self.agent_picker.is_some()), (Overlay::HandbackPrompt, self.handback_prompt.is_some()), (Overlay::InlinePrompt, self.prompt.is_some()), ( diff --git a/src/tui/src/ui/app/overlays_tests.rs b/src/tui/src/ui/app/overlays_tests.rs index ec405ad25..ccde16086 100644 --- a/src/tui/src/ui/app/overlays_tests.rs +++ b/src/tui/src/ui/app/overlays_tests.rs @@ -18,8 +18,8 @@ use medulla::config::LoadedConfig; use medulla::runtime::mock::MockRuntime; use super::types::{ - tab_pos, App, HandbackPrompt, HarnessPicker, HarnessPickerStep, Overlay, PromptKind, - ResumePicker, RP_TEMPLATES, + tab_pos, AgentPicker, AgentPickerStep, App, HandbackPrompt, Overlay, PromptKind, ResumePicker, + RP_TEMPLATES, }; use crate::ui::composer::{Draft, TextPrompt}; @@ -39,12 +39,12 @@ fn raise(app: &mut App, overlay: Overlay) { app.tab_index = tab_pos("Hosts"); app.routing_index = RP_TEMPLATES; } - Overlay::HarnessPicker => { - app.harness_picker = Some(HarnessPicker { + Overlay::AgentPicker => { + app.agent_picker = Some(AgentPicker { purpose: super::types::PickerPurpose::Spawn, choices: Vec::new(), index: 0, - step: HarnessPickerStep::Harness, + step: AgentPickerStep::Harness, cwd: "/".into(), workspace_query: String::new(), workspace_choices: Vec::new(), @@ -78,7 +78,7 @@ fn raise(app: &mut App, overlay: Overlay) { const EVERY_OVERLAY: [Overlay; 6] = [ Overlay::Decisions, Overlay::TemplatePopup, - Overlay::HarnessPicker, + Overlay::AgentPicker, Overlay::HandbackPrompt, Overlay::InlinePrompt, Overlay::ResumePicker, @@ -199,7 +199,7 @@ fn overlays_are_listed_back_to_front_in_the_order_the_render_paints_them() { // The list is iterated to paint, so its order is the stacking order: the // hand-back question is asked over the picker that may have opened it. let mut app = app(); - raise(&mut app, Overlay::HarnessPicker); + raise(&mut app, Overlay::AgentPicker); raise(&mut app, Overlay::HandbackPrompt); raise(&mut app, Overlay::Decisions); @@ -207,7 +207,7 @@ fn overlays_are_listed_back_to_front_in_the_order_the_render_paints_them() { app.visible_overlays(), vec![ Overlay::Decisions, - Overlay::HarnessPicker, + Overlay::AgentPicker, Overlay::HandbackPrompt ] ); diff --git a/src/tui/src/ui/app/rail/mod.rs b/src/tui/src/ui/app/rail/mod.rs index 3778c4e58..def8ab4e3 100644 --- a/src/tui/src/ui/app/rail/mod.rs +++ b/src/tui/src/ui/app/rail/mod.rs @@ -253,7 +253,7 @@ impl App { let mut rows: Vec = lane_rows.into_iter().map(RailRow::Lane).collect(); // A device that hosts nothing cannot declare an agent on itself, so the // action is absent there rather than present and refusing. - let hosting = self.harnesses.is_some(); + let hosting = self.local_sessions.is_some(); if hosting { rows.push(RailRow::NewAgent); } @@ -300,8 +300,8 @@ impl App { /// The attached session is excluded. Its prompt is on screen in front of /// the person the count is for, so counting it would ask them to go and /// look at what they are already looking at. - pub(in crate::ui) fn harnesses_waiting(&self) -> usize { - let Some(harnesses) = self.harnesses.as_ref() else { + pub(in crate::ui) fn sessions_waiting(&self) -> usize { + let Some(harnesses) = self.local_sessions.as_ref() else { return 0; }; Self::count_waiting(&harnesses.sessions.waiting_sessions(), &self.harness_focus) @@ -334,7 +334,7 @@ impl App { /// and a row that vanishes on failure is a row that hides the failure. They /// leave when the operator forgets them. pub(super) fn own_session_rows(&self) -> Vec { - let Some(harnesses) = self.harnesses.as_ref() else { + let Some(harnesses) = self.local_sessions.as_ref() else { return Vec::new(); }; let mut rows: Vec = harnesses @@ -342,7 +342,7 @@ impl App { .rows() .into_iter() .filter(|row| { - row.origin.is_user() || row.control == crate::worker::pty::HarnessControl::User + row.origin.is_user() || row.control == crate::worker::pty::SessionControl::User }) .collect(); rows.sort_by_key(|row| row.started_at); diff --git a/src/tui/src/ui/app/rail/resolve.rs b/src/tui/src/ui/app/rail/resolve.rs index 46fc816a6..87bb4598b 100644 --- a/src/tui/src/ui/app/rail/resolve.rs +++ b/src/tui/src/ui/app/rail/resolve.rs @@ -73,7 +73,7 @@ mod tests { use medulla::protocol::HarnessProvider; use medulla::runtime::WorkspaceRef; - use crate::worker::pty::{HarnessControl, PtyState, SessionOrigin}; + use crate::worker::pty::{PtyState, SessionControl, SessionOrigin}; fn session(provider: HarnessProvider, cwd: &str) -> SessionRow { SessionRow { @@ -93,7 +93,7 @@ mod tests { last_output_at: 1, last_error: None, busy: false, - control: HarnessControl::User, + control: SessionControl::User, origin: SessionOrigin::User, name: None, attention: None, diff --git a/src/tui/src/ui/app/rail/tests.rs b/src/tui/src/ui/app/rail/tests.rs index 9b2e976b3..99845476a 100644 --- a/src/tui/src/ui/app/rail/tests.rs +++ b/src/tui/src/ui/app/rail/tests.rs @@ -26,22 +26,22 @@ pub(in crate::ui::app) fn app() -> App { /// allowed to offer `+ New agent` and to list local sessions. pub(in crate::ui::app) fn hosting_app() -> App { let mut app = app(); - app.set_local_harnesses(shell_harnesses(PtyManager::new())); + app.set_local_sessions(shell_harnesses(PtyManager::new())); app } -/// A [`LocalHarnesses`](crate::ui::harness_pane::LocalHarnesses) whose "codex" +/// A [`LocalSessions`](crate::ui::harness_pane::LocalSessions) whose "codex" /// is `/bin/sh`, so opening one starts a real pty client and nothing else. pub(in crate::ui::app) fn shell_harnesses( sessions: PtyManager, -) -> crate::ui::harness_pane::LocalHarnesses { +) -> crate::ui::harness_pane::LocalSessions { let mut env = HashMap::new(); if let Ok(path) = std::env::var("PATH") { env.insert("PATH".to_string(), path); } env.insert("TERM".to_string(), "xterm-256color".to_string()); env.insert("TINYPLACE_CODEX_BIN".to_string(), "/bin/sh".to_string()); - crate::ui::harness_pane::LocalHarnesses { + crate::ui::harness_pane::LocalSessions { sessions, runtimes: Arc::new(std::sync::Mutex::new(Vec::new())), hub_address: "medulla-orchestrator".to_string(), @@ -155,7 +155,7 @@ fn a_dispatched_session_and_an_operator_session_are_the_same_row_type() { let mut app = hosting_app(); app.loaded.config.fleet.agent_declarations = vec![AgentDeclaration::new("shell", "", "codex", "/")]; - let harnesses = app.local_harnesses().expect("hosting").clone(); + let harnesses = app.local_sessions().expect("hosting").clone(); let choice = harnesses .choices() .into_iter() @@ -189,7 +189,7 @@ fn a_session_in_an_undeclared_directory_is_still_listed() { // that is running, costing tokens and invisible is exactly the failure the // old separate group existed to prevent. let app = hosting_app(); - let harnesses = app.local_harnesses().expect("hosting").clone(); + let harnesses = app.local_sessions().expect("hosting").clone(); let choice = harnesses .choices() .into_iter() diff --git a/src/tui/src/ui/app/render/agents/composer.rs b/src/tui/src/ui/app/render/agents/composer.rs index 0b2e9fadc..8d1731a8c 100644 --- a/src/tui/src/ui/app/render/agents/composer.rs +++ b/src/tui/src/ui/app/render/agents/composer.rs @@ -72,7 +72,7 @@ impl App { // that cannot send it leaves the rail unreachable, so the key that // always works is named right where the cursor is. let caption = if self.agents_rail_focused() { - "↑↓ walk agents · K kill harness · Enter or type to write".to_string() + "↑↓ walk agents · K kill session · Enter or type to write".to_string() } else { format!("› {target} · Esc to pick an agent") }; diff --git a/src/tui/src/ui/app/render/agents/harness.rs b/src/tui/src/ui/app/render/agents/harness.rs index b00a25e8c..b1006f415 100644 --- a/src/tui/src/ui/app/render/agents/harness.rs +++ b/src/tui/src/ui/app/render/agents/harness.rs @@ -36,8 +36,8 @@ impl App { /// `None` when this device does not host, when the work settled (the /// runtime drops the record then, so the pane stops claiming a screen for /// work that is over), or when the answer would be a guess. - pub(super) fn local_harness_session(&self, selection: &Selection) -> Option { - let harnesses = self.harnesses.as_ref()?; + pub(super) fn local_session(&self, selection: &Selection) -> Option { + let harnesses = self.local_sessions.as_ref()?; // An operator-started harness row *is* a session — it names one // directly rather than through a task, which is the whole reason it // needs its own rail group: nothing ever dispatched into it, so the @@ -68,7 +68,7 @@ impl App { /// pane is attached the harness's cursor is drawn too — an operator typing /// into a terminal with no cursor cannot tell where their text is going. pub(super) fn draw_local_harness(&mut self, f: &mut Frame, area: Rect, session_id: &str) { - let Some(harnesses) = self.harnesses.clone() else { + let Some(harnesses) = self.local_sessions.clone() else { return; }; let attached = self.harness_focus.is_attached_to(session_id); @@ -89,7 +89,7 @@ impl App { // Recorded before the paint so a wheel event landing between frames // still has somewhere to go. - self.hit_harness = Some((inner, session_id.to_string())); + self.hit_session = Some((inner, session_id.to_string())); harnesses.fit(session_id, inner.width, inner.height); let Some(snapshot) = harnesses.screen(session_id) else { return; @@ -120,7 +120,7 @@ impl App { /// the reason people avoid them. fn harness_title(&self, session_id: &str, attached: bool) -> String { let row = self - .harnesses + .local_sessions .as_ref() .and_then(|harnesses| harnesses.sessions.row(session_id)); // What it is waiting for, before what it is: an operator who opened this @@ -144,7 +144,7 @@ impl App { ), // A session that vanished between resolving and drawing. Rare, and // naming it beats a title that claims a provider we no longer know. - None => "harness".to_string(), + None => "session".to_string(), }; if attached { format!("{what} · typing here · {FOCUS_CHORD_LABEL} to release") diff --git a/src/tui/src/ui/app/render/agents/mod.rs b/src/tui/src/ui/app/render/agents/mod.rs index 06a895be1..87cd6c567 100644 --- a/src/tui/src/ui/app/render/agents/mod.rs +++ b/src/tui/src/ui/app/render/agents/mod.rs @@ -112,20 +112,20 @@ impl App { lane_index, task, on_orchestrator, - harness: None, + session: None, }; - selection.harness = self.local_harness_session(&selection); + selection.session = self.local_session(&selection); // Focus follows the pane, not the other way round. If the cursor moved // off the attached session — or that session ended — the keyboard comes // back to the chrome, because keys landing in a harness the operator is // no longer looking at is the worst failure this feature can have. if let Some(attached) = self.harness_focus.attached_to() { - if selection.harness.as_deref() != Some(attached) { - self.release_harness(); + if selection.session.as_deref() != Some(attached) { + self.release_session(); } } - self.harness_pane_session = selection.harness.clone(); - self.selected_harness_session = selection.harness.clone(); + self.pane_session = selection.session.clone(); + self.rail_session = selection.session.clone(); selection } @@ -180,7 +180,7 @@ impl App { // work panel goes for the same reason: the harness's own screen already // shows its todos and edits, and the columns are better spent on the // terminal than on our second-hand copy of it. - let embedded = selection.harness.is_some(); + let embedded = selection.session.is_some(); // The composer belongs to the orchestrator lane and nowhere else. That // lane *is* the conversation — typing into it is how work starts. Every // other row is something already running somewhere: an agent, a task, a diff --git a/src/tui/src/ui/app/render/agents/rail/attention_tests.rs b/src/tui/src/ui/app/render/agents/rail/attention_tests.rs index 2cc7d9930..f08dbf762 100644 --- a/src/tui/src/ui/app/render/agents/rail/attention_tests.rs +++ b/src/tui/src/ui/app/render/agents/rail/attention_tests.rs @@ -1,4 +1,4 @@ -//! Tests for attention cues in harness rows and lane state classification. +//! Tests for attention cues in session rows and lane state classification. use medulla::ui::agents::{AgentLane, AgentRole, TaskState, TaskStatus}; use ratatui::style::{Color, Modifier}; @@ -9,6 +9,7 @@ use super::rail_title; use super::state::{classify_lane, lane_waiting_session, task_waiting_session}; use super::status::HarnessVisualState; use super::tests::{app, harness_row, NOW}; +use crate::ui::app::rail::RailRow; fn lane() -> AgentLane { AgentLane { @@ -57,7 +58,7 @@ fn waiting_row(cwd: &str) -> SessionRow { #[test] fn a_harness_waiting_on_you_blinks_and_says_what_it_wants() { let app = app(); - let lines = app.own_harness_lines(&waiting_row("/workspace/medulla"), false, 48, NOW); + let lines = app.own_session_lines(&waiting_row("/workspace/medulla"), false, 48, NOW); assert!(lines[0].to_string().starts_with("⚠ codex"), "{}", lines[0]); let style = lines[0].spans[0].style; @@ -69,7 +70,7 @@ fn a_harness_waiting_on_you_blinks_and_says_what_it_wants() { #[test] fn a_selected_harness_waiting_on_you_stays_blinking_yellow() { let app = app(); - let lines = app.own_harness_lines(&waiting_row("/workspace/medulla"), true, 48, NOW); + let lines = app.own_session_lines(&waiting_row("/workspace/medulla"), true, 48, NOW); let style = lines[0].spans[0].style; assert_eq!(style.fg, Some(Color::Yellow)); @@ -85,7 +86,7 @@ fn a_long_attention_reason_wraps_without_losing_words() { row.attention = Some(HarnessAttention::new(AttentionKind::Dialog, reason, NOW)); let rendered = app - .own_harness_lines(&row, false, 36, NOW) + .own_session_lines(&row, false, 36, NOW) .iter() .skip(1) .map(|line| line.to_string().trim().to_string()) @@ -103,7 +104,7 @@ fn the_pane_you_are_typing_in_does_not_blink_at_you() { let row = waiting_row("/workspace/medulla"); app.harness_focus = crate::ui::harness_pane::HarnessFocus::Attached(row.id.clone()); - let lines = app.own_harness_lines(&row, false, 48, NOW); + let lines = app.own_session_lines(&row, false, 48, NOW); assert_eq!(lines.len(), 1, "no second line: {lines:?}"); assert!(!lines[0].spans[0] @@ -118,7 +119,7 @@ fn an_exited_harness_stops_asking_for_anything() { let mut row = waiting_row("/workspace/medulla"); row.state = PtyState::Exited { code: Some(0) }; - let lines = app.own_harness_lines(&row, false, 48, NOW); + let lines = app.own_session_lines(&row, false, 48, NOW); assert_eq!(lines.len(), 1); assert!(lines[0].to_string().starts_with("✓ codex"), "{}", lines[0]); @@ -174,12 +175,30 @@ fn task_attention_marks_only_the_exact_waiting_session() { } #[test] -fn rail_title_reports_the_attention_snapshot_count() { +fn rail_title_counts_the_agents_on_the_tree_not_the_lanes() { + // Two agents on the rail and one of them running a task. Counting lanes + // instead would report the agents that happen to have traffic — one — and + // read as "Agents · 1" on a machine that has two. let mut item = lane(); item.tasks = vec![task(TaskStatus::Running, 1)]; + let rows = vec![ + RailRow::Agent(agent_row("busy")), + RailRow::Agent(agent_row("idle")), + RailRow::NewAgent, + ]; assert_eq!( - rail_title(&[item], 2), - "Agents · 1 · 1 running · ⚠ 2 waiting on you" + rail_title(&rows, &[item], 2), + "Agents · 2 · 1 running · ⚠ 2 waiting on you" ); } + +/// A bare agent row, as the tree produces one for a declared agent. +fn agent_row(agent_id: &str) -> crate::ui::app::rail::AgentRailRow { + crate::ui::app::rail::AgentRailRow { + agent_id: agent_id.to_string(), + host_id: String::new(), + agent: None, + lane_index: None, + } +} diff --git a/src/tui/src/ui/app/render/agents/rail/harness_line/layout.rs b/src/tui/src/ui/app/render/agents/rail/harness_line/layout.rs index 4c6bc144a..80a83af20 100644 --- a/src/tui/src/ui/app/render/agents/rail/harness_line/layout.rs +++ b/src/tui/src/ui/app/render/agents/rail/harness_line/layout.rs @@ -26,7 +26,7 @@ use ratatui::style::Style; use ratatui::text::{Line as TLine, Span}; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; -use crate::worker::pty::{HarnessControl, PtyState, SessionRow}; +use crate::worker::pty::{PtyState, SessionControl, SessionRow}; use super::super::wrap::{short_home, wrap_path}; @@ -240,17 +240,17 @@ fn harness_text(row: &SessionRow, style: HarnessNameStyle) -> String { /// Who holds the session, in the operator's chosen spelling. /// -/// "unmanaged" rather than [`HarnessControl::as_str`]'s "you": this is the whole +/// "unmanaged" rather than [`SessionControl::as_str`]'s "you": this is the whole /// reason an operator-started row exists, and someone who hands one to the /// orchestrator needs to see that it took effect. -fn control_text(control: HarnessControl, style: ControlStyle) -> String { +fn control_text(control: SessionControl, style: ControlStyle) -> String { match (style, control) { - (ControlStyle::Text, HarnessControl::User) => "unmanaged", - (ControlStyle::Text, HarnessControl::Orchestrator) => "orchestrator", + (ControlStyle::Text, SessionControl::User) => "unmanaged", + (ControlStyle::Text, SessionControl::Orchestrator) => "orchestrator", // `⊘` reads as "dispatch does not enter here", which is exactly what an // operator-held session means; `⊙` is the orchestrator holding it. - (ControlStyle::Icon, HarnessControl::User) => "⊘", - (ControlStyle::Icon, HarnessControl::Orchestrator) => "⊙", + (ControlStyle::Icon, SessionControl::User) => "⊘", + (ControlStyle::Icon, SessionControl::Orchestrator) => "⊙", } .to_string() } diff --git a/src/tui/src/ui/app/render/agents/rail/mod.rs b/src/tui/src/ui/app/render/agents/rail/mod.rs index 437d0dc5b..b2245f94c 100644 --- a/src/tui/src/ui/app/render/agents/rail/mod.rs +++ b/src/tui/src/ui/app/render/agents/rail/mod.rs @@ -52,8 +52,15 @@ pub(in crate::ui::app) const RAIL_MAX_CONTENT: usize = 36; /// two lines still reads as one row rather than as two entries. const CONT_INDENT: usize = 5; -/// Build the rail title from the lane inventory and one attention snapshot. -fn rail_title(lanes: &[AgentLane], waiting: usize) -> String { +/// Build the rail title from the tree, the lane inventory, and one attention +/// snapshot. +/// +/// The count is of **agents** — the rows of the tree — not of lanes. A lane is +/// folded from traffic, so counting lanes said how many things had been +/// dispatched to, which is not what "Agents · 3" claims and drops to zero on a +/// machine with three declared agents and a quiet morning. Running tasks still +/// come from the lanes: that *is* a fact about traffic. +fn rail_title(rows: &[RailRow], lanes: &[AgentLane], waiting: usize) -> String { let running_tasks: usize = lanes .iter() .map(|lane| { @@ -63,7 +70,10 @@ fn rail_title(lanes: &[AgentLane], waiting: usize) -> String { .count() }) .sum(); - let agents = lanes.iter().filter(|lane| !lane.role.is_function()).count(); + let agents = rows + .iter() + .filter(|row| matches!(row, RailRow::Agent(_))) + .count(); let mut title = if running_tasks > 0 { format!("Agents · {agents} · {running_tasks} running") } else { @@ -127,7 +137,7 @@ impl App { // disagree with each other — and the render thread takes the sessions // lock once rather than once per lane per task. let waiting_sessions = self - .harnesses + .local_sessions .as_ref() .map(|h| h.sessions.waiting_sessions()) .unwrap_or_default(); @@ -135,7 +145,7 @@ impl App { // rather than on rows of their own — the same number the tab badge // carries, so the two can never disagree. let waiting = App::count_waiting(&waiting_sessions, &self.harness_focus); - let title = rail_title(&selection.lanes, waiting); + let title = rail_title(&selection.rows, &selection.lanes, waiting); // The border says which half the keyboard is driving. Without it, Esc // moving focus to the rail is invisible until the next arrow press. let block = crate::ui::widgets::panel(&self.theme, title, self.agents_rail_focused()); @@ -301,7 +311,7 @@ impl App { CONT_INDENT, )); } - lines.extend(self.own_harness_lines(local, active, width, now)); + lines.extend(self.own_session_lines(local, active, width, now)); lines } other => wrap_line( @@ -349,7 +359,7 @@ impl App { // session over several lines; kept total so measurement can call // either. (None, Some(local)) => self - .own_harness_lines(local, active, RAIL_MAX_CONTENT, now) + .own_session_lines(local, active, RAIL_MAX_CONTENT, now) .into_iter() .next() .unwrap_or_default(), diff --git a/src/tui/src/ui/app/render/agents/rail/rows.rs b/src/tui/src/ui/app/render/agents/rail/rows.rs index 735e2fc59..f6d025183 100644 --- a/src/tui/src/ui/app/render/agents/rail/rows.rs +++ b/src/tui/src/ui/app/render/agents/rail/rows.rs @@ -8,7 +8,7 @@ use unicode_width::UnicodeWidthChar; use crate::ui::agents::{AgentLane, AgentRole, AgentRow, TaskStatus}; use crate::ui::util::fmt_tokens; -use crate::worker::pty::{HarnessAttention, HarnessControl, SessionRow, ATTENTION_GLYPH}; +use crate::worker::pty::{HarnessAttention, SessionControl, SessionRow, ATTENTION_GLYPH}; use super::super::super::super::types::App; use super::super::super::color; @@ -28,7 +28,7 @@ impl App { /// PTY attention overrides the ordinary state glyph and adds a textual cue, /// while the operator's field placement and visibility choices remain in /// force for the status line itself. - pub(in crate::ui::app::render) fn own_harness_lines( + pub(in crate::ui::app::render) fn own_session_lines( &self, row: &SessionRow, active: bool, @@ -51,7 +51,7 @@ impl App { } } else if active { self.theme.selection() - } else if row.control == HarnessControl::User { + } else if row.control == SessionControl::User { Style::default().fg(color("cyan")) } else { Style::default() @@ -223,7 +223,7 @@ impl App { if !self.loaded.config.appearance.show_session_titles { return None; } - let harnesses = self.harnesses.as_ref()?; + let harnesses = self.local_sessions.as_ref()?; running_session_title(lane, |task_id| { let id = harnesses.session_for_task(task_id)?; harnesses diff --git a/src/tui/src/ui/app/render/agents/rail/state.rs b/src/tui/src/ui/app/render/agents/rail/state.rs index afb4ab2a4..4d9202228 100644 --- a/src/tui/src/ui/app/render/agents/rail/state.rs +++ b/src/tui/src/ui/app/render/agents/rail/state.rs @@ -126,7 +126,7 @@ impl App { item: &AgentLane, waiting_sessions: &HashSet, ) -> bool { - let Some(harnesses) = self.harnesses.as_ref() else { + let Some(harnesses) = self.local_sessions.as_ref() else { return false; }; lane_waiting_session( @@ -140,7 +140,7 @@ impl App { /// Whether the exact task row is backed by a waiting local harness. pub(super) fn task_attention(&self, task_id: &str, waiting_sessions: &HashSet) -> bool { - let Some(harnesses) = self.harnesses.as_ref() else { + let Some(harnesses) = self.local_sessions.as_ref() else { return false; }; task_waiting_session( diff --git a/src/tui/src/ui/app/render/agents/rail/status_line_tests.rs b/src/tui/src/ui/app/render/agents/rail/status_line_tests.rs index 2a4c16e6b..6f359d3da 100644 --- a/src/tui/src/ui/app/render/agents/rail/status_line_tests.rs +++ b/src/tui/src/ui/app/render/agents/rail/status_line_tests.rs @@ -21,7 +21,7 @@ fn a_field_moved_to_line_two_leaves_the_first_line_and_indents() { path: FieldPlacement::Line2, ..StatusLineConfig::default() }); - let lines = app.own_harness_lines(&harness_row("/workspace/medulla"), false, 48, NOW); + let lines = app.own_session_lines(&harness_row("/workspace/medulla"), false, 48, NOW); assert_eq!(lines.len(), 2); assert_eq!(lines[0].to_string(), "● codex · unmanaged · main"); @@ -34,7 +34,7 @@ fn a_renamed_thread_is_shown_on_its_own_default_line() { let mut row = harness_row("/workspace/medulla"); row.thread_name = Some("Ship the sidebar".into()); - let lines = app.own_harness_lines(&row, false, 48, NOW); + let lines = app.own_session_lines(&row, false, 48, NOW); assert_eq!(lines.len(), 2); assert_eq!( @@ -50,7 +50,7 @@ fn three_lines_are_available_and_an_unused_one_is_closed_up() { branch: FieldPlacement::Line3, ..StatusLineConfig::default() }); - let lines = app.own_harness_lines(&harness_row("/workspace/medulla"), false, 48, NOW); + let lines = app.own_session_lines(&harness_row("/workspace/medulla"), false, 48, NOW); assert_eq!(lines.len(), 2); assert_eq!( @@ -73,7 +73,7 @@ fn every_line_is_still_bounded_by_the_rail_width() { row.branch = Some("feat/a-very-long-branch-name-indeed".into()); for width in [0, 1, 4, 8, 12, 36] { - for line in app.own_harness_lines(&row, false, width, NOW) { + for line in app.own_session_lines(&row, false, width, NOW) { assert!(line.width() <= width, "width {width}: {line:?}"); } } @@ -94,7 +94,7 @@ fn wide_branch_and_path_glyphs_stay_within_their_cell_budget() { path_style, ..StatusLineConfig::default() }); - let lines = app.own_harness_lines(&row, false, 10, NOW); + let lines = app.own_session_lines(&row, false, 10, NOW); assert_eq!(lines.len(), 2); assert!( @@ -112,7 +112,7 @@ fn the_harness_name_and_control_state_have_compact_spellings() { harness_style: HarnessNameStyle::Long, ..StatusLineConfig::default() }); - assert!(long.own_harness_lines(&row, false, 48, NOW)[0] + assert!(long.own_session_lines(&row, false, 48, NOW)[0] .to_string() .starts_with("● Codex · unmanaged")); @@ -122,7 +122,7 @@ fn the_harness_name_and_control_state_have_compact_spellings() { ..StatusLineConfig::default() }); assert_eq!( - icons.own_harness_lines(&row, false, 48, NOW)[0].to_string(), + icons.own_session_lines(&row, false, 48, NOW)[0].to_string(), "● ◆ · ⊘ · main · /workspace/medulla" ); } @@ -139,7 +139,7 @@ fn the_path_style_chooses_how_much_of_the_directory_survives() { path_style: style, ..StatusLineConfig::default() }); - app.own_harness_lines(&row, false, 44, NOW)[0].to_string() + app.own_session_lines(&row, false, 44, NOW)[0].to_string() }; assert_eq!(with_style(PathStyle::Last), "medulla-public"); @@ -163,11 +163,11 @@ fn a_field_can_be_held_back_until_its_row_is_selected() { let row = harness_row("/workspace/medulla"); assert_eq!( - app.own_harness_lines(&row, false, 48, NOW)[0].to_string(), + app.own_session_lines(&row, false, 48, NOW)[0].to_string(), "● codex · unmanaged · main" ); assert_eq!( - app.own_harness_lines(&row, true, 48, NOW)[0].to_string(), + app.own_session_lines(&row, true, 48, NOW)[0].to_string(), "● codex · unmanaged · main · /workspace/medulla" ); } @@ -210,7 +210,7 @@ fn an_on_alert_field_appears_only_for_a_harness_that_needs_attention() { let healthy = harness_row("/workspace/medulla"); assert_eq!( - app.own_harness_lines(&healthy, false, 48, NOW)[0].to_string(), + app.own_session_lines(&healthy, false, 48, NOW)[0].to_string(), "● codex · unmanaged · main" ); @@ -218,7 +218,7 @@ fn an_on_alert_field_appears_only_for_a_harness_that_needs_attention() { let mut alerting = harness_row("/workspace/medulla"); alerting.state = state; assert!( - app.own_harness_lines(&alerting, false, 48, NOW)[0] + app.own_session_lines(&alerting, false, 48, NOW)[0] .to_string() .ends_with("/workspace/medulla"), "{state:?} should count as an alert" @@ -227,7 +227,7 @@ fn an_on_alert_field_appears_only_for_a_harness_that_needs_attention() { let mut errored = harness_row("/workspace/medulla"); errored.last_error = Some("spawn failed".into()); - assert!(app.own_harness_lines(&errored, false, 48, NOW)[0] + assert!(app.own_session_lines(&errored, false, 48, NOW)[0] .to_string() .ends_with("/workspace/medulla")); } @@ -242,7 +242,7 @@ fn hiding_every_field_still_leaves_one_selectable_line() { path: FieldPlacement::Hidden, ..StatusLineConfig::default() }); - let lines = app.own_harness_lines(&harness_row("/workspace/medulla"), false, 48, NOW); + let lines = app.own_session_lines(&harness_row("/workspace/medulla"), false, 48, NOW); assert_eq!(lines.len(), 1, "the row must still occupy a clickable line"); assert_eq!(lines[0].to_string(), ""); diff --git a/src/tui/src/ui/app/render/agents/rail/tests.rs b/src/tui/src/ui/app/render/agents/rail/tests.rs index 3c1a9ed5c..998c44817 100644 --- a/src/tui/src/ui/app/render/agents/rail/tests.rs +++ b/src/tui/src/ui/app/render/agents/rail/tests.rs @@ -12,7 +12,7 @@ use ratatui::text::{Line as TLine, Span}; use unicode_width::UnicodeWidthStr; use crate::ui::app::App; -use crate::worker::pty::{AttentionKind, HarnessAttention, HarnessControl, PtyState, SessionRow}; +use crate::worker::pty::{AttentionKind, HarnessAttention, PtyState, SessionControl, SessionRow}; use super::rows::{display_session_title, running_session_title}; use super::wrap::{flow_path, short_home, wrap_line, wrap_path}; @@ -38,7 +38,7 @@ fn attention_uses_the_configured_color_and_can_stay_solid() { 0, )); - let lines = app.own_harness_lines(&row, false, 48, NOW); + let lines = app.own_session_lines(&row, false, 48, NOW); let style = lines[0].spans[0].style; assert_eq!(style.fg, Some(Color::LightMagenta)); @@ -168,7 +168,7 @@ pub(super) fn harness_row(cwd: &str) -> SessionRow { last_output_at: 1, last_error: None, busy: false, - control: HarnessControl::User, + control: SessionControl::User, origin: crate::worker::pty::SessionOrigin::User, name: None, attention: None, @@ -186,7 +186,7 @@ fn viewport_keeps_all_three_lines_of_the_selected_harness_visible() { #[test] fn an_operator_harness_uses_one_compact_line_like_the_orchestrator() { let app = app(); - let lines = app.own_harness_lines(&harness_row("/workspace/medulla"), false, 48, NOW); + let lines = app.own_session_lines(&harness_row("/workspace/medulla"), false, 48, NOW); assert_eq!(lines.len(), 1, "a harness should consume one rail row"); assert_eq!( @@ -198,7 +198,7 @@ fn an_operator_harness_uses_one_compact_line_like_the_orchestrator() { #[test] fn a_long_harness_path_is_shortened_instead_of_adding_rows() { let app = app(); - let lines = app.own_harness_lines( + let lines = app.own_session_lines( &harness_row("/workspace/tinyhumans/products/medulla-public"), false, 36, @@ -217,7 +217,7 @@ fn a_long_harness_path_is_shortened_instead_of_adding_rows() { fn a_harness_prefix_never_exceeds_the_available_width() { let app = app(); for width in [0, 1, 4, 8] { - let line = &app.own_harness_lines(&harness_row("/workspace/medulla"), false, width, NOW)[0]; + let line = &app.own_session_lines(&harness_row("/workspace/medulla"), false, width, NOW)[0]; assert!(line.width() <= width, "width {width}: {line:?}"); } } @@ -229,14 +229,14 @@ fn harness_branch_and_path_can_be_hidden_independently() { app.loaded.config.appearance.show_harness_branch = false; assert_eq!( - app.own_harness_lines(&row, false, 48, NOW)[0].to_string(), + app.own_session_lines(&row, false, 48, NOW)[0].to_string(), "● codex · unmanaged · /workspace/medulla" ); app.loaded.config.appearance.show_harness_branch = true; app.loaded.config.appearance.show_harness_path = false; assert_eq!( - app.own_harness_lines(&row, false, 48, NOW)[0].to_string(), + app.own_session_lines(&row, false, 48, NOW)[0].to_string(), "● codex · unmanaged · main" ); } @@ -248,7 +248,7 @@ fn a_non_git_harness_omits_the_branch_without_a_placeholder() { row.branch = None; assert_eq!( - app.own_harness_lines(&row, false, 48, NOW)[0].to_string(), + app.own_session_lines(&row, false, 48, NOW)[0].to_string(), "● codex · unmanaged · /workspace/medulla" ); } diff --git a/src/tui/src/ui/app/render/agents/transcript.rs b/src/tui/src/ui/app/render/agents/transcript.rs index 13509e8b4..b3687363c 100644 --- a/src/tui/src/ui/app/render/agents/transcript.rs +++ b/src/tui/src/ui/app/render/agents/transcript.rs @@ -38,7 +38,7 @@ impl App { // // Resolved in `agents_selection`, not here: it decides the layout as // well as the contents, so the split has already been made for it. - if let Some(session_id) = selection.harness.clone() { + if let Some(session_id) = selection.session.clone() { self.draw_local_harness(f, area, &session_id); return; } diff --git a/src/tui/src/ui/app/render/agents/transcript_tests.rs b/src/tui/src/ui/app/render/agents/transcript_tests.rs index 2ebdfe39b..c84877702 100644 --- a/src/tui/src/ui/app/render/agents/transcript_tests.rs +++ b/src/tui/src/ui/app/render/agents/transcript_tests.rs @@ -53,7 +53,7 @@ fn descriptorless_lanes_still_show_their_pull_request_context() { lane_index: Some(0), task: None, on_orchestrator: false, - harness: None, + session: None, }; let mut terminal = Terminal::new(TestBackend::new(100, 12)).unwrap(); terminal @@ -133,7 +133,7 @@ fn pane_for(row: RailRow) -> String { lane_index: None, task: None, on_orchestrator: false, - harness: None, + session: None, }; let mut terminal = Terminal::new(TestBackend::new(90, 20)).unwrap(); terminal diff --git a/src/tui/src/ui/app/render/agents/types.rs b/src/tui/src/ui/app/render/agents/types.rs index 3ad832ec3..35aec3435 100644 --- a/src/tui/src/ui/app/render/agents/types.rs +++ b/src/tui/src/ui/app/render/agents/types.rs @@ -35,14 +35,13 @@ pub(super) struct Selection { /// Whether the pane is showing the operator's own conversation, which /// scrolls separately and renders the chat log rather than model calls. pub(super) on_orchestrator: bool, - /// The live local harness session this row resolves to, when it resolves to - /// one. + /// The live local session this row resolves to, when it resolves to one. /// /// Decided here rather than at draw time because it changes the *layout*, - /// not just the contents: a harness paints its own composer, so ours has no + /// not just the contents: a session paints its own composer, so ours has no /// rows and the work panel no columns. Resolving it after the split would /// mean laying out for a transcript and then drawing a terminal into it. - pub(super) harness: Option, + pub(super) session: Option, } impl Selection { diff --git a/src/tui/src/ui/app/render/changes.rs b/src/tui/src/ui/app/render/changes.rs index c380e6106..1f3dd5178 100644 --- a/src/tui/src/ui/app/render/changes.rs +++ b/src/tui/src/ui/app/render/changes.rs @@ -35,7 +35,7 @@ impl App { .as_deref() .map(|id| id.get(..7).unwrap_or(id)) .unwrap_or("unavailable"); - rows.push(ListItem::new(format!("Harness launch {launch}"))); + rows.push(ListItem::new(format!("Session launch {launch}"))); rows.extend( self.changes.recent_commits.iter().map(|commit| { ListItem::new(format!("{} {}", commit.short_id(), commit.subject)) diff --git a/src/tui/src/ui/app/render/mod.rs b/src/tui/src/ui/app/render/mod.rs index b32dbff73..eb961351b 100644 --- a/src/tui/src/ui/app/render/mod.rs +++ b/src/tui/src/ui/app/render/mod.rs @@ -23,12 +23,12 @@ mod changes; mod decisions; mod feedback; pub(super) mod graph; -mod harness_modals; mod overview; mod points; mod prompt; mod routing; mod selection; +mod session_modals; mod settings; mod status_line; mod template_modal; @@ -293,10 +293,10 @@ impl App { // `Ctrl-]` on the Settings tab must not attach to whatever the Agents // tab was showing several frames ago. `draw_agents_pane` fills it back // in when it resolves a session. - self.harness_pane_session = None; + self.pane_session = None; // Same reasoning as above: a stale rect would route the wheel into a // terminal that is no longer on screen. - self.hit_harness = None; + self.hit_session = None; self.hit_workflow_preview = None; // Focus follows the pane, not the other way round. `agents_selection` // (called only while drawing the Agents tab) is what notices the cursor @@ -306,7 +306,7 @@ impl App { // meant for whatever tab was now on screen — was typed into a harness // pane the operator could no longer see. if self.harness_focus.attached_to().is_some() && self.tab() != "Agents" { - self.release_harness(); + self.release_session(); } // The composer now lives inside the Agents pane, so the only things that // still claim a row of their own below the content are the inline prompt @@ -350,7 +350,7 @@ impl App { match overlay { Overlay::Decisions => self.draw_decisions(f, rows[2]), Overlay::TemplatePopup => self.draw_template_modal(f, rows[2]), - Overlay::HarnessPicker => self.draw_harness_picker(f, rows[2]), + Overlay::AgentPicker => self.draw_harness_picker(f, rows[2]), Overlay::HandbackPrompt => self.draw_handback_prompt(f, rows[2]), Overlay::InlinePrompt => self.draw_prompt(f, rows[3]), Overlay::ResumePicker => self.draw_resume(f, rows[3]), @@ -383,7 +383,7 @@ impl App { // and an operator reading Workflows or Settings is exactly the person // who does not know a pane has stopped. The count rides on the tab so // the signal survives leaving the tab that carries it. - let waiting = self.harnesses_waiting(); + let waiting = self.sessions_waiting(); // Badges are built *before* the width is measured, because they are part // of what has to fit: measuring the bare names and then rendering wider // labels overflows the bar on a terminal that was only just wide enough, @@ -466,7 +466,7 @@ impl App { f.render_widget( Paragraph::new(TLine::from(Span::styled( format!( - "Typing into the harness — every key goes to it · {} releases the keyboard", + "Typing into the session — every key goes to it · {} releases the keyboard", crate::ui::harness_pane::FOCUS_CHORD_LABEL ), Style::default().add_modifier(Modifier::BOLD), @@ -485,7 +485,7 @@ impl App { } else if workflows { "Tab views · ⏎ open · Esc back · ←→ follow edges · ↑↓ lanes · i inspect · c copilot · x run · d dry-run · r refresh" } else { - "Tab views · Esc/↑↓ rail · ⏎/^] harness · d harness diff · ⇧⏎ newline · ⌥X cancel · ⌥A answer · ^N thread · ^↑↓ switch · ^Y copy · ^X abort" + "Tab views · Esc/↑↓ rail · ⏎/^] session · d session diff · ⇧⏎ newline · ⌥X cancel · ⌥A answer · ^N thread · ^↑↓ switch · ^Y copy · ^X abort" }; f.render_widget( Paragraph::new(TLine::from(Span::styled( diff --git a/src/tui/src/ui/app/render/routing/add_host.rs b/src/tui/src/ui/app/render/routing/add_host.rs index 6730e64cf..61a425968 100644 --- a/src/tui/src/ui/app/render/routing/add_host.rs +++ b/src/tui/src/ui/app/render/routing/add_host.rs @@ -177,7 +177,7 @@ impl App { } else { StepState::Live }; - lines.push(header(2, "Which harness it runs", state)); + lines.push(header(2, "Which harness type it runs", state)); lines.push(TLine::from("")); let providers = self.add_host_providers(); for (index, provider) in providers.iter().enumerate() { @@ -233,7 +233,7 @@ impl App { "Enter continue · Esc start over · c copy the install line" } (false, AddHostKind::Local) => { - "↑↓ choose a harness · Enter continue · Esc start over" + "↑↓ choose a harness type · Enter continue · Esc start over" } }, dim, diff --git a/src/tui/src/ui/app/render/routing/harnesses/mod.rs b/src/tui/src/ui/app/render/routing/harnesses/mod.rs index 97ae09be3..bf9acabc0 100644 --- a/src/tui/src/ui/app/render/routing/harnesses/mod.rs +++ b/src/tui/src/ui/app/render/routing/harnesses/mod.rs @@ -167,7 +167,7 @@ impl App { f.render_widget( Paragraph::new(Text::from(lines)) .wrap(Wrap { trim: false }) - .block(self.panel("Harnesses")), + .block(self.panel("Harness Types")), area, ); } diff --git a/src/tui/src/ui/app/render/harness_modals.rs b/src/tui/src/ui/app/render/session_modals.rs similarity index 92% rename from src/tui/src/ui/app/render/harness_modals.rs rename to src/tui/src/ui/app/render/session_modals.rs index 4b912830b..01931a73b 100644 --- a/src/tui/src/ui/app/render/harness_modals.rs +++ b/src/tui/src/ui/app/render/session_modals.rs @@ -12,23 +12,23 @@ use ratatui::text::{Line as TLine, Span, Text}; use ratatui::widgets::{Block, BorderType, Borders, Clear, Paragraph}; use ratatui::Frame; -use super::super::types::{App, HarnessPickerStep}; +use super::super::types::{AgentPickerStep, App}; const HARNESS_TRAILER_LINES: usize = 3; impl App { - /// Draw the "start a harness" picker. + /// Draw the "start a session" picker. pub(super) fn draw_harness_picker(&mut self, f: &mut Frame, area: Rect) { - let Some(picker) = &self.harness_picker else { + let Some(picker) = &self.agent_picker else { return; }; let (rows, title) = match picker.step { - HarnessPickerStep::Harness => ( + AgentPickerStep::Harness => ( picker.choices.len(), - "Choose harness — ↑/↓ · Enter workspace · Esc cancel", + "Choose a harness type — ↑/↓ · Enter workspace · Esc cancel", ), - HarnessPickerStep::Decision => (2, "Choose control — ↑/↓ · Enter confirm · Esc back"), - HarnessPickerStep::Workspace => ( + AgentPickerStep::Decision => (2, "Choose control — ↑/↓ · Enter confirm · Esc back"), + AgentPickerStep::Workspace => ( picker.workspace_choices.len(), "Choose workspace — type to filter · Tab complete · Enter start · Esc back", ), @@ -53,7 +53,7 @@ impl App { let mut lines = match picker.step { - HarnessPickerStep::Harness => { + AgentPickerStep::Harness => { let capacity = (inner.height as usize).saturating_sub(HARNESS_TRAILER_LINES); let range = harness_choice_window(picker.choices.len(), picker.index, capacity); picker.choices[range.clone()] @@ -74,7 +74,7 @@ impl App { }) .collect() } - HarnessPickerStep::Decision => { + AgentPickerStep::Decision => { let selected = picker .choices .get(picker.index) @@ -133,15 +133,15 @@ impl App { )), ] } - HarnessPickerStep::Workspace => { - let selected_harness = picker + AgentPickerStep::Workspace => { + let selected_session = picker .choices .get(picker.index) .map(|choice| choice.display_name()) .unwrap_or("harness"); let mut lines = vec![ TLine::from(Span::styled( - format!(" {selected_harness}"), + format!(" {selected_session}"), Style::default().add_modifier(Modifier::BOLD), )), TLine::from(format!( @@ -186,7 +186,7 @@ impl App { lines } }; - if picker.step == HarnessPickerStep::Harness { + if picker.step == AgentPickerStep::Harness { lines.push(TLine::from("")); lines.push(TLine::from(Span::styled( " Next: choose a workspace", @@ -196,7 +196,7 @@ impl App { // Said here as well as in the status line, because it is the one fact // that makes this different from every other way to start a harness. // Skip on the Decision step — it already shows both options inline. - if picker.step != HarnessPickerStep::Decision { + if picker.step != AgentPickerStep::Decision { lines.push(TLine::from(Span::styled( " unmanaged · the orchestrator will not dispatch into it", Style::default().add_modifier(Modifier::DIM), @@ -212,9 +212,9 @@ impl App { }; let area = centered(area, 72, 12); let title = if prompt.is_takeover { - "Take control of this harness" + "Take control of this session" } else { - "You still have this harness" + "You still have this session" }; let block = Block::default() .borders(Borders::ALL) @@ -235,7 +235,7 @@ impl App { // the orchestrator. if prompt.is_takeover { let lines = vec![ - TLine::from("The orchestrator is using this harness."), + TLine::from("The orchestrator is using this session."), TLine::from("Take control to type into it."), TLine::from(""), TLine::from(Span::styled( @@ -251,9 +251,9 @@ impl App { // focused in may not know they are holding anything. The sentence says // which of the two happened rather than implying the second. let how = if prompt.took_control { - "You took this harness when you focused in." + "You took this session when you focused in." } else { - "You asked for this harness." + "You asked for this session." }; // The note line shows a caret only while it is being edited, so the // operator can tell at a glance whether `y` will answer or type. diff --git a/src/tui/src/ui/app/render/settings/appearance_usage.rs b/src/tui/src/ui/app/render/settings/appearance_usage.rs index 6050f6b69..644bf8c67 100644 --- a/src/tui/src/ui/app/render/settings/appearance_usage.rs +++ b/src/tui/src/ui/app/render/settings/appearance_usage.rs @@ -51,7 +51,7 @@ impl App { lines.push(TLine::from("")); lines.push(TLine::from(Span::styled("Attention cues", heading))); lines.push(TLine::from(Span::styled( - " How Medulla highlights a task or harness waiting for you.", + " How Medulla highlights a task or session waiting for you.", description, ))); lines.push(TLine::from("")); @@ -180,7 +180,7 @@ impl App { // The harness-row toggles used to sit here. Point at where they went // rather than leaving an operator to find the new page by accident. lines.push(TLine::from(Span::styled( - "harness rows are laid out on the Status line page", + "session rows are laid out on the Status line page", Style::default().add_modifier(Modifier::DIM), ))); let where_saved = match &self.config_path { diff --git a/src/tui/src/ui/app/render/settings/help.rs b/src/tui/src/ui/app/render/settings/help.rs index be0a7bf9e..ff4bfb136 100644 --- a/src/tui/src/ui/app/render/settings/help.rs +++ b/src/tui/src/ui/app/render/settings/help.rs @@ -36,7 +36,7 @@ impl App { TLine::from("↑↓ move between subpages · 1-9 jump straight to one"), TLine::from("Appearance: j / k pick an option · ←/→ or Enter change it (saved live)"), TLine::from( - "Status line: j / k pick a harness-row field · ←/→ or Enter cycle it (live preview)", + "Status line: j / k pick a session-row field · ←/→ or Enter cycle it (live preview)", ), TLine::from("Config: j / k pick a setting · ←/→ change · Enter toggle (saved to config.toml)"), TLine::from("Feedback: j / k browse · u/d vote · c comment · n feature · b bug · s sort · f filter"), @@ -45,15 +45,15 @@ impl App { TLine::from(" "), TLine::from("Ctrl-N new thread · Ctrl-↑↓ switch threads · Ctrl-C quit"), TLine::from(" "), - TLine::from(Span::styled("Harnesses", bold)), + TLine::from(Span::styled("Sessions", bold)), TLine::from(format!( - "{FOCUS_CHORD_LABEL} type into the selected harness (and take it from the orchestrator)" + "{FOCUS_CHORD_LABEL} type into the selected session (and take it from the orchestrator)" )), TLine::from( "Agents rail: Enter on + New agent declares one (harness type × workspace dir)", ), TLine::from( - "Ctrl-T opens a session of the selected agent · elsewhere it starts a loose harness", + "Ctrl-T opens a session of the selected agent · elsewhere it starts a loose session", ), TLine::from("Ctrl-G grabs the selected session or gives it back to the orchestrator"), TLine::from( @@ -61,11 +61,11 @@ impl App { ), TLine::from("Ctrl-O returns to the orchestrator (and, once there, releases the mouse)"), TLine::from(format!( - "From an empty composer Esc focuses the rail · from a harness {FOCUS_CHORD_LABEL} releases to it" + "From an empty composer Esc focuses the rail · from a session {FOCUS_CHORD_LABEL} releases to it" )), - TLine::from("On the rail ↑↓ select a running harness task · K then y kills it"), + TLine::from("On the rail ↑↓ select a running session · K then y kills it"), TLine::from(Span::styled( - "While you hold a harness the orchestrator will not dispatch into it", + "While you hold a session the orchestrator will not dispatch into it", dim, )), TLine::from(" "), diff --git a/src/tui/src/ui/app/render/settings/status_line.rs b/src/tui/src/ui/app/render/settings/status_line.rs index ef937c6c2..c4411b0db 100644 --- a/src/tui/src/ui/app/render/settings/status_line.rs +++ b/src/tui/src/ui/app/render/settings/status_line.rs @@ -5,7 +5,7 @@ //! about a row that is thirty-six columns wide and shares those columns between //! five fields, so "branch on line 2" is not a question anyone can answer in the //! abstract — they have to see what it does to the path. It renders through the -//! same [`own_harness_lines`](crate::ui::app::App::own_harness_lines) the rail +//! same [`own_session_lines`](crate::ui::app::App::own_session_lines) the rail //! itself uses, against sample sessions, so it cannot drift from the real row. use ratatui::layout::Rect; @@ -18,7 +18,7 @@ use unicode_width::UnicodeWidthStr; use medulla::protocol::HarnessProvider; use crate::ui::app::render::agents::RAIL_MAX_CONTENT; -use crate::worker::pty::{HarnessControl, PtyState, SessionRow}; +use crate::worker::pty::{PtyState, SessionControl, SessionRow}; use super::super::super::status_line::{STATUS_LINE_ROWS, STATUS_LINE_ROW_COUNT}; use super::super::super::types::App; @@ -126,7 +126,7 @@ impl App { } let row = sample(); for (offset, line) in self - .own_harness_lines(&row, *active, width, medulla::clock::now_millis()) + .own_session_lines(&row, *active, width, medulla::clock::now_millis()) .into_iter() .enumerate() { @@ -169,7 +169,7 @@ fn sample_selected() -> SessionRow { last_output_at: 0, last_error: None, busy: false, - control: HarnessControl::User, + control: SessionControl::User, origin: crate::worker::pty::SessionOrigin::User, name: None, attention: None, @@ -185,7 +185,7 @@ fn sample_orchestrator() -> SessionRow { state: PtyState::Exited { code: Some(0) }, cwd: "/tmp/scratch".into(), branch: None, - control: HarnessControl::Orchestrator, + control: SessionControl::Orchestrator, ..sample_selected() } } @@ -194,7 +194,7 @@ fn sample_orchestrator() -> SessionRow { fn sample_alerting() -> SessionRow { SessionRow { state: PtyState::Failed, - last_error: Some("harness exited unexpectedly".into()), + last_error: Some("session exited unexpectedly".into()), ..sample_selected() } } diff --git a/src/tui/src/ui/app/render/tests.rs b/src/tui/src/ui/app/render/tests.rs index f74010cae..b555c19db 100644 --- a/src/tui/src/ui/app/render/tests.rs +++ b/src/tui/src/ui/app/render/tests.rs @@ -32,18 +32,18 @@ fn compact_tab_labels_shorten_the_current_wide_destinations() { #[test] fn harness_choice_window_keeps_the_selection_visible() { assert_eq!( - super::harness_modals::harness_choice_window(20, 0, 13), + super::session_modals::harness_choice_window(20, 0, 13), 0..13 ); assert_eq!( - super::harness_modals::harness_choice_window(20, 10, 13), + super::session_modals::harness_choice_window(20, 10, 13), 4..17 ); assert_eq!( - super::harness_modals::harness_choice_window(20, 19, 13), + super::session_modals::harness_choice_window(20, 19, 13), 7..20 ); - assert_eq!(super::harness_modals::harness_choice_window(2, 1, 13), 0..2); + assert_eq!(super::session_modals::harness_choice_window(2, 1, 13), 0..2); } fn lane(role: AgentRole) -> AgentLane { @@ -366,7 +366,7 @@ fn a_huge_argument_payload_is_clipped_not_dumped() { #[test] fn leaving_the_agents_tab_takes_the_keyboard_back_from_an_attached_harness() { - // The bug this pins: `release_harness` was only reached from + // The bug this pins: `release_session` was only reached from // `agents_selection`, which runs only while the Agents tab is being drawn. // It notices the *cursor* moving off the attached session and has nothing to // say once the operator has left the tab altogether — so focus stayed @@ -391,7 +391,7 @@ fn leaving_the_agents_tab_takes_the_keyboard_back_from_an_attached_harness() { terminal.draw(|f| app.draw(f)).expect("draw"); assert_eq!( - app.attached_harness(), + app.attached_session(), None, "keys must not reach a harness the operator has navigated away from" ); diff --git a/src/tui/src/ui/app/render/workflows/node_preview/kinds.rs b/src/tui/src/ui/app/render/workflows/node_preview/kinds.rs index 52c96fb0a..2c92bd451 100644 --- a/src/tui/src/ui/app/render/workflows/node_preview/kinds.rs +++ b/src/tui/src/ui/app/render/workflows/node_preview/kinds.rs @@ -157,7 +157,7 @@ fn agent_lines(config: &Value, defaults: &AgentDefaults) -> Vec> { ), ]), Line::from(Span::styled( - "A fresh, bounded harness session is started for this step.", + "A fresh, bounded agent session is started for this step.", Style::default().add_modifier(Modifier::DIM), )), ]; diff --git a/src/tui/src/ui/app/harness_control.rs b/src/tui/src/ui/app/session_control.rs similarity index 75% rename from src/tui/src/ui/app/harness_control.rs rename to src/tui/src/ui/app/session_control.rs index 70be805a9..8a789ee6f 100644 --- a/src/tui/src/ui/app/harness_control.rs +++ b/src/tui/src/ui/app/session_control.rs @@ -1,8 +1,8 @@ -//! Starting harnesses the operator owns, and moving control between them and +//! Starting sessions the operator owns, and moving control between them and //! the orchestrator. //! -//! Two features that turn out to be one. "Unmanaged" is not a kind of harness — -//! it is a harness the operator holds, and dispatch skips anything the operator +//! Two features that turn out to be one. "Unmanaged" is not a kind of session — +//! it is a session the operator holds, and dispatch skips anything the operator //! holds. So spawning one, taking one over, and handing one back are three //! spellings of the same state change, and they live together here. //! @@ -14,36 +14,35 @@ //! [`Cmd`](super::types::Cmd) and travels off-thread. Control flips locally //! first and the brief follows: a handback gated on a socket round-trip would //! fail whenever the uplink is down, which is exactly when an operator most -//! wants to let go of a harness. +//! wants to let go of a session. use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; use medulla::protocol::HarnessProvider; use crate::ui::composer::Draft; use crate::ui::harness_pane::HarnessChoice; -use crate::worker::pty::HarnessControl; +use crate::worker::pty::SessionControl; use super::types::{ - tab_pos, App, Cmd, HandbackPolicy, HandbackPrompt, HarnessPicker, HarnessPickerStep, - PickerPurpose, + tab_pos, AgentPicker, AgentPickerStep, App, Cmd, HandbackPolicy, HandbackPrompt, PickerPurpose, }; impl App { - /// Open the "start a harness" picker, or spawn directly when the command - /// already named a provider. + /// Open the "start a session" picker, or spawn directly when the command + /// already named a harness type. /// - /// `/harness` with no provider opens the picker rather than guessing: + /// `/session` with no harness type opens the picker rather than guessing: /// starting the wrong CLI in the operator's workspace is not something they /// find out about until it has already done something. - pub(super) fn start_harness_command(&mut self, provider: Option<&str>, path: Option<&str>) { - let Some(harnesses) = self.harnesses.clone() else { - self.set_status("This device is not hosting, so it has no harnesses to start"); + pub(super) fn start_session_command(&mut self, provider: Option<&str>, path: Option<&str>) { + let Some(harnesses) = self.local_sessions.clone() else { + self.set_status("This device is not hosting, so it has no sessions to start"); return; }; match provider.and_then(HarnessProvider::from_wire) { Some(provider) => { let cwd = path.unwrap_or("").to_string(); - self.spawn_harness(HarnessChoice::native(provider), &cwd, false); + self.spawn_session(HarnessChoice::native(provider), &cwd, false); } None => { let choices = harnesses.choices(); @@ -51,11 +50,11 @@ impl App { self.set_status("No harness CLIs found on this device"); return; } - self.harness_picker = Some(HarnessPicker { + self.agent_picker = Some(AgentPicker { purpose: PickerPurpose::Spawn, choices, index: 0, - step: HarnessPickerStep::Harness, + step: AgentPickerStep::Harness, cwd: path .map(str::to_string) .unwrap_or_else(|| harnesses.workspace.clone()), @@ -70,18 +69,18 @@ impl App { } /// Open the picker from the keyboard shortcut. - pub(crate) fn open_harness_picker(&mut self) { - self.start_harness_command(None, None); + pub(crate) fn open_session_picker(&mut self) { + self.start_session_command(None, None); } - /// Start a harness the operator owns and move the cursor onto it. + /// Start a session the operator owns and move the cursor onto it. /// - /// Selecting the new row matters more than it sounds: a harness that + /// Selecting the new row matters more than it sounds: a session that /// appears somewhere below the fold, with the pane still showing whatever /// was selected before, reads as "nothing happened". - pub(super) fn spawn_harness(&mut self, choice: HarnessChoice, cwd: &str, managed: bool) { - let Some(harnesses) = self.harnesses.clone() else { - self.set_status("This device is not hosting, so it has no harnesses to start"); + pub(super) fn spawn_session(&mut self, choice: HarnessChoice, cwd: &str, managed: bool) { + let Some(harnesses) = self.local_sessions.clone() else { + self.set_status("This device is not hosting, so it has no sessions to start"); return; }; let skip = self.harness_skip_permissions; @@ -101,7 +100,7 @@ impl App { } // Hand back first, then say what happened. `hand_back_session` // sets its own status, so setting ours before it would show the - // operator "Handed back …" for a harness they just started — + // operator "Handed back …" for a session they just started — // losing the name, the managed/unmanaged confirmation, and any // workspace-remember error this message carries. if managed { @@ -121,63 +120,63 @@ impl App { } } - /// Take the selected harness from the orchestrator. - pub(crate) fn take_harness_control(&mut self) { - let Some((harnesses, session)) = self.selected_harness() else { + /// Take the selected session from the orchestrator. + pub(crate) fn take_session_control(&mut self) { + let Some((harnesses, session)) = self.selected_session() else { return; }; - if harnesses.control(&session) == Some(HarnessControl::User) { - self.set_status("You already have this harness"); + if harnesses.control(&session) == Some(SessionControl::User) { + self.set_status("You already have this session"); return; } - harnesses.set_control(&session, HarnessControl::User); + harnesses.set_control(&session, SessionControl::User); if let Some(cwd) = harnesses.sessions.row(&session).map(|row| row.cwd) { - self.pending_cmds.push_back(Cmd::HoldHarness { + self.pending_cmds.push_back(Cmd::HoldSession { workspace: cwd, reason: None, }); } - self.set_status("You have this harness · the orchestrator will not dispatch into it"); + self.set_status("You have this session · the orchestrator will not dispatch into it"); } - /// Give the selected harness back to the orchestrator, with an optional note. - pub(crate) fn hand_harness_back(&mut self, note: Option) { + /// Give the selected session back to the orchestrator, with an optional note. + pub(crate) fn hand_session_back(&mut self, note: Option) { let Some((harnesses, session)) = self.handoff_target() else { return; }; - if harnesses.control(&session) == Some(HarnessControl::Orchestrator) { - self.set_status("The orchestrator already has this harness"); + if harnesses.control(&session) == Some(SessionControl::Orchestrator) { + self.set_status("The orchestrator already has this session"); return; } self.hand_back_session(&session, note); } - /// Toggle who holds the selected harness — the `Ctrl-G` shortcut. + /// Toggle who holds the selected session — the `Ctrl-G` shortcut. /// /// One key for both directions because the rail row and the pane title both /// say which way it will go, so a single "grab or give" is less to remember /// than two chords that each do nothing half the time. - pub(crate) fn toggle_harness_control(&mut self) { - let Some((harnesses, session)) = self.selected_harness() else { + pub(crate) fn toggle_session_control(&mut self) { + let Some((harnesses, session)) = self.selected_session() else { return; }; match harnesses.control(&session) { - Some(HarnessControl::User) => self.hand_back_session(&session, None), - Some(HarnessControl::Orchestrator) => self.take_harness_control(), - None => self.set_status("That harness is gone"), + Some(SessionControl::User) => self.hand_back_session(&session, None), + Some(SessionControl::Orchestrator) => self.take_session_control(), + None => self.set_status("That session is gone"), } } - /// Open the take-control or hand-back prompt depending on who holds the harness. + /// Open the take-control or hand-back prompt depending on who holds the session. /// - /// Enter on a harness row used to attach immediately, which is a control + /// Enter on a session row used to attach immediately, which is a control /// change made by a navigation key: an operator walking the rail with the - /// arrows and pressing Enter to "look closer" took the harness out from + /// arrows and pressing Enter to "look closer" took the session out from /// under the orchestrator without being asked. The question is the same one /// either way — which side of the handover is this? — so it reuses the /// hand-back prompt with the sentence turned around. - pub(crate) fn open_harness_enter_prompt(&mut self) { - let Some((harnesses, session)) = self.selected_harness() else { + pub(crate) fn open_session_enter_prompt(&mut self) { + let Some((harnesses, session)) = self.selected_session() else { return; }; match harnesses.control(&session) { @@ -185,19 +184,19 @@ impl App { // whether to give it back. `took_control` is read, not assumed: // a hold can begin implicitly (focusing in under a `Never` handback // policy), and hardcoding `false` would claim an explicit decision - // the operator never made — the same field `begin_harness_release` + // the operator never made — the same field `begin_session_release` // resolves the same way. - Some(HarnessControl::User) => { + Some(SessionControl::User) => { self.handback_prompt = Some(HandbackPrompt { session, - took_control: self.harness_took_control, + took_control: self.took_control_by_attach, note: Draft::default(), editing_note: false, is_takeover: false, }); } // The orchestrator holds it: typing into it means taking it first. - Some(HarnessControl::Orchestrator) => { + Some(SessionControl::Orchestrator) => { self.handback_prompt = Some(HandbackPrompt { session, took_control: false, @@ -207,69 +206,69 @@ impl App { }); } None => { - self.set_status("That harness is gone"); + self.set_status("That session is gone"); } } } - /// The harness `/handoff` means, without depending on a render having run. + /// The session `/handoff` means, without depending on a render having run. /// - /// [`selected_harness`](Self::selected_harness) reads `harness_pane_session`, + /// [`selected_session`](Self::selected_session) reads `pane_session`, /// which is written inside the Agents pane's draw and cleared at the top of - /// every frame. So `/handoff` typed from any other tab reported "no harness + /// every frame. So `/handoff` typed from any other tab reported "no session /// on this row" while the operator was demonstrably holding one — and with a /// note argument that is worse, because they have just typed a sentence that /// is then thrown away. /// /// In order: the attached session (unambiguous — the keyboard is in it), the - /// harness the last frame resolved, then the single running harness the + /// session the last frame resolved, then the single running session the /// operator holds. Ambiguity is reported, never guessed: handing back the - /// wrong harness puts an agent into a workspace somebody is still using. - fn handoff_target(&mut self) -> Option<(crate::ui::harness_pane::LocalHarnesses, String)> { - let Some(harnesses) = self.harnesses.clone() else { - self.set_status("This device is not hosting, so it has no harnesses"); + /// wrong session puts an agent into a workspace somebody is still using. + fn handoff_target(&mut self) -> Option<(crate::ui::harness_pane::LocalSessions, String)> { + let Some(harnesses) = self.local_sessions.clone() else { + self.set_status("This device is not hosting, so it has no sessions"); return None; }; if let Some(session) = self.harness_focus.attached_to() { return Some((harnesses, session.to_string())); } - if let Some(session) = self.harness_pane_session.clone() { + if let Some(session) = self.pane_session.clone() { return Some((harnesses, session)); } let held: Vec = harnesses .sessions .rows() .into_iter() - .filter(|row| row.control == HarnessControl::User && row.state.is_running()) + .filter(|row| row.control == SessionControl::User && row.state.is_running()) .map(|row| row.id) .collect(); match held.len() { 0 => { - self.set_status("You are not holding any harness"); + self.set_status("You are not holding any session"); None } 1 => Some((harnesses, held[0].clone())), n => { self.set_status(format!( - "You hold {n} harnesses — select one in Agents and press Ctrl-G" + "You hold {n} sessions — select one in Agents and press Ctrl-G" )); None } } } - /// The harness the cursor is on, with the handle needed to act on it. + /// The session the cursor is on, with the handle needed to act on it. /// /// Refuses with a reason rather than silently doing nothing, for the same /// reason the attach chord does: an operator who pressed a key and saw no /// change cannot tell "wrong row" from "broken feature". - fn selected_harness(&mut self) -> Option<(crate::ui::harness_pane::LocalHarnesses, String)> { - let Some(harnesses) = self.harnesses.clone() else { - self.set_status("This device is not hosting, so it has no harnesses"); + fn selected_session(&mut self) -> Option<(crate::ui::harness_pane::LocalSessions, String)> { + let Some(harnesses) = self.local_sessions.clone() else { + self.set_status("This device is not hosting, so it has no sessions"); return None; }; - let Some(session) = self.harness_pane_session.clone() else { - self.set_status("No harness on this row — select one to hand it over"); + let Some(session) = self.pane_session.clone() else { + self.set_status("No session on this row — select one to hand it over"); return None; }; Some((harnesses, session)) @@ -281,12 +280,12 @@ impl App { /// `false` means a prompt is now open and the operator is still attached — /// releasing before they answer would move the keyboard out from under the /// question being asked about it. - pub(crate) fn begin_harness_release(&mut self, session: &str) -> bool { + pub(crate) fn begin_session_release(&mut self, session: &str) -> bool { let held = self - .harnesses + .local_sessions .as_ref() .and_then(|harnesses| harnesses.control(session)) - == Some(HarnessControl::User); + == Some(SessionControl::User); if !held { return true; } @@ -297,14 +296,14 @@ impl App { } HandbackPolicy::Never => { self.set_status( - "Released · you still hold this harness (/handoff to give it back)", + "Released · you still hold this session (/handoff to give it back)", ); true } HandbackPolicy::Ask => { self.handback_prompt = Some(HandbackPrompt { session: session.to_string(), - took_control: self.harness_took_control, + took_control: self.took_control_by_attach, note: Draft::default(), editing_note: false, is_takeover: false, @@ -349,13 +348,13 @@ impl App { /// /// The question itself owns the keyboard and holds no field — `y`, `n` and /// `E` are answers, not text — so a paste made while it is up belongs to - /// neither the harness behind it nor the composer, and is dropped. After `E` + /// neither the session behind it nor the composer, and is dropped. After `E` /// the note *is* a text input, and pasting what you were doing into the /// brief the orchestrator receives is exactly what the note is for. /// /// Flattened to one line and inserted at the caret, matching /// [`edit_handback_note`](Self::edit_handback_note): the note is drawn as a - /// single row, and `Enter` there hands the harness back rather than breaking + /// single row, and `Enter` there hands the session back rather than breaking /// the line. pub(super) fn paste_into_handback_note(&mut self, text: &str) { let Some(prompt) = self.handback_prompt.as_mut() else { @@ -374,37 +373,37 @@ impl App { /// Hand `session` back and queue its brief. Every handback path ends here. /// - /// The order matters. The transcript is read while the harness is still + /// The order matters. The transcript is read while the session is still /// ours; control flips next, so the operator gets an answer on the same /// keystroke; the brief is queued last and travels asynchronously. /// - /// That ordering means the orchestrator can dispatch into the harness before + /// That ordering means the orchestrator can dispatch into the session before /// it has read the brief, and that is the right trade. The brief is /// *context*, not permission — gating the flip on a socket round-trip would - /// make handing a harness back fail whenever the uplink is down, which is + /// make handing a session back fail whenever the uplink is down, which is /// exactly when an operator most wants to let go of one. pub(super) fn hand_back_session(&mut self, session: &str, note: Option) { - let Some(harnesses) = self.harnesses.clone() else { + let Some(harnesses) = self.local_sessions.clone() else { return; }; // Read the row first: a session that has already gone is not handed - // back, and flipping control on a corpse would advertise a harness that + // back, and flipping control on a corpse would advertise a session that // does not exist. let Some(row) = harnesses.sessions.row(session) else { - self.set_status("That harness is gone"); + self.set_status("That session is gone"); return; }; let lines = harnesses .sessions .tail_lines(session, medulla::hub::handoff::TRANSCRIPT_LINES); - harnesses.set_control(session, HarnessControl::Orchestrator); - self.harness_took_control = false; + harnesses.set_control(session, SessionControl::Orchestrator); + self.took_control_by_attach = false; let brief = medulla::hub::handoff::normalize( medulla::hub::HarnessHandoff { // Per handback *event*, not per session: a second handback of the - // same harness is new work, and reusing the id would have the + // same session is new work, and reusing the id would have the // orchestrator ignore it as something it already picked up. id: format!("{}-{}", row.id, medulla::clock::now_millis()), at: medulla::clock::now_millis(), @@ -422,44 +421,44 @@ impl App { &lines, ); self.pending_cmds - .push_back(Cmd::HandOffHarness(Box::new(brief))); + .push_back(Cmd::HandOffSession(Box::new(brief))); self.set_status("Handed back · sending the orchestrator your brief"); } } impl App { - /// Route a key while the "start a harness" picker is open. + /// Route a key while the "start a session" picker is open. /// - /// The first step chooses a registered harness. The second step owns text + /// The first step chooses a registered harness type. The second step owns text /// input directly so filtering and filesystem completion update as the /// operator types. - pub(super) fn handle_harness_picker_key(&mut self, event: KeyEvent) { + pub(super) fn handle_agent_picker_key(&mut self, event: KeyEvent) { let code = event.code; let step = self - .harness_picker + .agent_picker .as_ref() .map(|picker| picker.step) - .unwrap_or(HarnessPickerStep::Harness); - if step == HarnessPickerStep::Decision { + .unwrap_or(AgentPickerStep::Harness); + if step == AgentPickerStep::Decision { self.handle_harness_decision_key(event); return; } - if step == HarnessPickerStep::Workspace { + if step == AgentPickerStep::Workspace { self.handle_harness_workspace_key(event); return; } match code { KeyCode::Esc => { - self.harness_picker = None; + self.agent_picker = None; self.set_status("Cancelled"); } KeyCode::Up => { - if let Some(picker) = &mut self.harness_picker { + if let Some(picker) = &mut self.agent_picker { picker.index = picker.index.saturating_sub(1); } } KeyCode::Down => { - if let Some(picker) = &mut self.harness_picker { + if let Some(picker) = &mut self.agent_picker { picker.index = (picker.index + 1).min(picker.choices.len().saturating_sub(1)); } } @@ -478,7 +477,7 @@ impl App { fn handle_harness_decision_key(&mut self, event: KeyEvent) { match event.code { // One step back, not two. Decision is reached *after* the workspace - // is chosen, so returning to the harness list would discard a + // is chosen, so returning to the harness-type list would discard a // workspace the operator never changed and make them reselect both. // Reuses the forward entry point so the hint text and the completion // list are the same ones the step normally opens with. @@ -486,30 +485,30 @@ impl App { self.open_harness_workspace_step(false); } KeyCode::Up | KeyCode::Down => { - if let Some(picker) = &mut self.harness_picker { + if let Some(picker) = &mut self.agent_picker { picker.managed = !picker.managed; } } KeyCode::Enter => { - let Some(workspace) = self.selected_harness_workspace() else { + let Some(workspace) = self.selected_picker_workspace() else { self.set_status("Choose a workspace first"); return; }; let choice = self - .harness_picker + .agent_picker .as_ref() .and_then(|picker| picker.choices.get(picker.index).cloned()); let managed = self - .harness_picker + .agent_picker .as_ref() .map(|p| p.managed) .unwrap_or(false); let Some(choice) = choice else { - self.set_status("Choose a harness first"); + self.set_status("Choose a harness type first"); return; }; - self.harness_picker = None; - self.spawn_harness(choice, &workspace, managed); + self.agent_picker = None; + self.spawn_session(choice, &workspace, managed); } _ => {} } @@ -519,21 +518,21 @@ impl App { fn handle_harness_workspace_key(&mut self, event: KeyEvent) { match event.code { KeyCode::Esc | KeyCode::BackTab => { - if let Some(picker) = &mut self.harness_picker { - picker.step = HarnessPickerStep::Harness; + if let Some(picker) = &mut self.agent_picker { + picker.step = AgentPickerStep::Harness; } - self.set_status("Pick a harness · Enter workspace · Esc cancel"); + self.set_status("Pick a harness type · Enter workspace · Esc cancel"); } // Moving the cursor is the operator choosing a completion over // whatever they entered, however few rows there are to move across. KeyCode::Up => { - if let Some(picker) = &mut self.harness_picker { + if let Some(picker) = &mut self.agent_picker { picker.workspace_index = picker.workspace_index.saturating_sub(1); picker.workspace_picked = !picker.workspace_choices.is_empty(); } } KeyCode::Down => { - if let Some(picker) = &mut self.harness_picker { + if let Some(picker) = &mut self.agent_picker { picker.workspace_index = (picker.workspace_index + 1) .min(picker.workspace_choices.len().saturating_sub(1)); picker.workspace_picked = !picker.workspace_choices.is_empty(); @@ -541,7 +540,7 @@ impl App { } KeyCode::Tab => self.complete_harness_workspace(), KeyCode::Backspace => { - if let Some(picker) = &mut self.harness_picker { + if let Some(picker) = &mut self.agent_picker { picker.workspace_query.pop(); picker.workspace_index = 0; picker.workspace_picked = false; @@ -549,7 +548,7 @@ impl App { self.refresh_harness_workspace_choices(); } KeyCode::Char(character) if is_text_input(event.modifiers) => { - if let Some(picker) = &mut self.harness_picker { + if let Some(picker) = &mut self.agent_picker { picker.workspace_query.push(character); picker.workspace_index = 0; picker.workspace_picked = false; @@ -557,12 +556,12 @@ impl App { self.refresh_harness_workspace_choices(); } KeyCode::Enter => { - let Some(workspace) = self.selected_harness_workspace() else { + let Some(workspace) = self.selected_picker_workspace() else { self.set_status("Choose an existing directory"); return; }; let purpose = self - .harness_picker + .agent_picker .as_ref() .map(|picker| picker.purpose.clone()) .unwrap_or(PickerPurpose::Spawn); @@ -570,20 +569,20 @@ impl App { // nothing starts, so there is nobody to own it yet. if purpose == PickerPurpose::DeclareAgent { let harness = self - .harness_picker + .agent_picker .as_ref() .and_then(|picker| picker.choices.get(picker.index)) .map(|choice| choice.id().to_string()); let Some(harness) = harness else { - self.set_status("Choose a harness first"); + self.set_status("Choose a harness type first"); return; }; - self.harness_picker = None; + self.agent_picker = None; self.prompt_agent_name(&harness, &workspace); return; } - if let Some(picker) = &mut self.harness_picker { - picker.step = HarnessPickerStep::Decision; + if let Some(picker) = &mut self.agent_picker { + picker.step = AgentPickerStep::Decision; picker.managed = true; } } @@ -593,7 +592,7 @@ impl App { /// Route a key while the hand-back question is open. /// - /// Enter means yes, because handing back is the safe answer: a harness left + /// Enter means yes, because handing back is the safe answer: a session left /// under a user who has walked away is one the orchestrator can never use, /// and that failure is silent. pub(super) fn handle_handback_key(&mut self, code: KeyCode) { @@ -601,14 +600,14 @@ impl App { return; }; // The takeover direction has no note and nothing to release: the - // operator is not holding the harness yet, so the only two answers are + // operator is not holding the session yet, so the only two answers are // "take it and start typing" and "leave it alone". if prompt.is_takeover { match code { KeyCode::Char('y') | KeyCode::Char('Y') | KeyCode::Enter => { self.handback_prompt = None; - self.take_harness_control(); - self.attach_to_pane_harness(); + self.take_session_control(); + self.attach_to_pane_session(); } KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => { self.handback_prompt = None; @@ -628,7 +627,7 @@ impl App { let note = self.handback_note(); self.handback_prompt = None; self.hand_back_session(&session, note); - self.release_harness(); + self.release_session(); } // Back to the question, keeping what was typed: an operator who // pressed Escape meant "stop typing", not "discard my sentence". @@ -651,20 +650,20 @@ impl App { let note = self.handback_note(); self.handback_prompt = None; self.hand_back_session(&session, note); - self.release_harness(); + self.release_session(); } KeyCode::Char('n') | KeyCode::Char('N') => { self.handback_prompt = None; - self.release_harness(); + self.release_session(); self.set_status( - "Released · you still hold this harness (/handoff to give it back)", + "Released · you still hold this session (/handoff to give it back)", ); } // Esc is "I did not mean to leave", so it puts the operator back // where they were rather than picking one of the answers for them. KeyCode::Esc => { self.handback_prompt = None; - self.set_status("Still typing into the harness"); + self.set_status("Still typing into the session"); } _ => {} } diff --git a/src/tui/src/ui/app/harness_control_tests.rs b/src/tui/src/ui/app/session_control_tests.rs similarity index 91% rename from src/tui/src/ui/app/harness_control_tests.rs rename to src/tui/src/ui/app/session_control_tests.rs index 4b08bb0ba..5ba446c04 100644 --- a/src/tui/src/ui/app/harness_control_tests.rs +++ b/src/tui/src/ui/app/session_control_tests.rs @@ -2,7 +2,7 @@ use crossterm::event::KeyModifiers; -use super::harness_control::is_text_input; +use super::session_control::is_text_input; #[test] fn workspace_text_accepts_altgr_but_rejects_control_shortcuts() { diff --git a/src/tui/src/ui/app/settings_edit/mod.rs b/src/tui/src/ui/app/settings_edit/mod.rs index 6d328a806..dcef80d8f 100644 --- a/src/tui/src/ui/app/settings_edit/mod.rs +++ b/src/tui/src/ui/app/settings_edit/mod.rs @@ -102,7 +102,7 @@ impl App { // always set; there is no "auto" state to fall back to. optional: false, }, - help: "Worker harness processes Medulla may run at once.", + help: "Agent sessions Medulla may run at once.", }, ]); rows diff --git a/src/tui/src/ui/app/state.rs b/src/tui/src/ui/app/state.rs index d982cc66d..2dc0a21e8 100644 --- a/src/tui/src/ui/app/state.rs +++ b/src/tui/src/ui/app/state.rs @@ -138,7 +138,7 @@ impl App { hit_tabs: Vec::new(), hit_tabs_row: 0, hit_agents: None, - hit_harness: None, + hit_session: None, hit_threads: None, hit_started_sessions: None, hit_context: None, @@ -151,15 +151,15 @@ impl App { last_events_len: 0, link_obs: None, host_obs: None, - harnesses: None, + local_sessions: None, harness_focus: crate::ui::harness_pane::HarnessFocus::default(), - harness_pane_session: None, - selected_harness_session: None, - harness_picker: None, + pane_session: None, + rail_session: None, + agent_picker: None, handback_prompt: None, help_scroll: 0, handback_policy, - harness_took_control: false, + took_control_by_attach: false, pending_cmds: std::collections::VecDeque::new(), harness_skip_permissions, copy_capture: None, @@ -333,30 +333,30 @@ impl App { self.host_obs.as_ref() } - /// Attach the live harness sessions this device is running. + /// Attach the live sessions this device is running. /// /// Only called when this machine hosts: without a host nothing runs here, so /// there is no screen to render and no PTY to type into. - pub fn set_local_harnesses(&mut self, harnesses: crate::ui::harness_pane::LocalHarnesses) { - self.harnesses = Some(harnesses); + pub fn set_local_sessions(&mut self, sessions: crate::ui::harness_pane::LocalSessions) { + self.local_sessions = Some(sessions); } - /// The live harness sessions this device is running, if it hosts. - pub fn local_harnesses(&self) -> Option<&crate::ui::harness_pane::LocalHarnesses> { - self.harnesses.as_ref() + /// The live sessions this device is running, if it hosts. + pub fn local_sessions(&self) -> Option<&crate::ui::harness_pane::LocalSessions> { + self.local_sessions.as_ref() } - /// The harness session the last draw resolved for the rail cursor. + /// The session the last draw resolved for the rail cursor. /// /// Inspection seam: it is set during render, so a test that wants to act on - /// "the selected harness" has to be able to see when the cursor has reached + /// "the selected session" has to be able to see when the cursor has reached /// one rather than counting rows it does not control. - pub fn harness_pane_session_for_test(&self) -> Option<&str> { - self.harness_pane_session.as_deref() + pub fn pane_session_for_test(&self) -> Option<&str> { + self.pane_session.as_deref() } - /// The harness session currently receiving the operator's keystrokes. - pub fn attached_harness(&self) -> Option<&str> { + /// The session currently receiving the operator's keystrokes. + pub fn attached_session(&self) -> Option<&str> { self.harness_focus.attached_to() } @@ -392,10 +392,10 @@ impl App { self.status = s.into(); } - /// Show and arm the harness-kill confirmation as one invariant-preserving + /// Show and arm the session-kill confirmation as one invariant-preserving /// state transition. pub(super) fn arm_kill(&mut self, target: (String, String)) { - self.set_status("Kill this harness? y confirm · any other key cancels"); + self.set_status("Kill this session? y confirm · any other key cancels"); self.kill_armed = Some(target); } @@ -553,7 +553,7 @@ impl App { /// rail, which carries the `+ New session` action and the operator's own /// harness rows as well as the lanes. Indexing the shorter list with it /// reported a lane for rows that name none, and the composer's visibility - /// hangs off this answer — so a harness row claimed a text box that was + /// hangs off this answer — so a session row claimed a text box that was /// never drawn, and every keystroke went into it. pub fn on_orchestrator_lane(&self) -> bool { let lanes = self.lanes(); diff --git a/src/tui/src/ui/app/tests.rs b/src/tui/src/ui/app/tests.rs index dcfa29926..3efeab3fe 100644 --- a/src/tui/src/ui/app/tests.rs +++ b/src/tui/src/ui/app/tests.rs @@ -72,18 +72,15 @@ fn every_tab_renders() { #[test] fn drawing_an_intervening_tab_preserves_the_harness_selected_for_changes() { let mut a = app(); - a.selected_harness_session = Some("older-harness".to_owned()); - a.harness_pane_session = Some("older-harness".to_owned()); + a.rail_session = Some("older-harness".to_owned()); + a.pane_session = Some("older-harness".to_owned()); a.tab_index = tab("Workflows"); render(&mut a); + assert_eq!(a.pane_session, None, "hidden panes cannot receive keys"); assert_eq!( - a.harness_pane_session, None, - "hidden panes cannot receive keys" - ); - assert_eq!( - a.selected_harness_session.as_deref(), + a.rail_session.as_deref(), Some("older-harness"), "tab navigation must not discard the Changes repository selection" ); @@ -150,7 +147,7 @@ fn typing_inserts_into_draft() { #[test] fn enter_answers_the_harness_picker_not_the_harness_behind_it() { - use super::types::{HarnessPicker, HarnessPickerStep, WorkspaceChoice}; + use super::types::{AgentPicker, AgentPickerStep, WorkspaceChoice}; use crate::ui::harness_pane::HarnessChoice; let mut a = app(); @@ -159,14 +156,14 @@ fn enter_answers_the_harness_picker_not_the_harness_behind_it() { // it — the state the attach shortcut reads. Opening the picker on top of // that used to lose the very next Enter to the pane underneath, which // attached instead of advancing to the workspace step. - a.harness_pane_session = Some("already-running".to_string()); - a.harness_picker = Some(HarnessPicker { + a.pane_session = Some("already-running".to_string()); + a.agent_picker = Some(AgentPicker { purpose: super::types::PickerPurpose::Spawn, choices: vec![HarnessChoice::native( medulla::protocol::HarnessProvider::Claude, )], index: 0, - step: HarnessPickerStep::Harness, + step: AgentPickerStep::Harness, cwd: ".".into(), workspace_query: String::new(), workspace_choices: Vec::new(), @@ -180,17 +177,17 @@ fn enter_answers_the_harness_picker_not_the_harness_behind_it() { let cmd = a.on_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); assert!(cmd.is_none()); - assert_eq!(a.attached_harness(), None, "must not attach behind a modal"); + assert_eq!(a.attached_session(), None, "must not attach behind a modal"); assert_eq!( - a.harness_picker.as_ref().map(|picker| picker.step), - Some(HarnessPickerStep::Workspace), + a.agent_picker.as_ref().map(|picker| picker.step), + Some(AgentPickerStep::Workspace), "the picker should have advanced to its workspace step" ); // This app has no local harnesses, so nothing completes the empty query. // Stand a choice in for the completion pass, which is what the workspace // step's Enter reads. - if let Some(picker) = &mut a.harness_picker { + if let Some(picker) = &mut a.agent_picker { picker.workspace_choices = vec![WorkspaceChoice { path: ".".into(), source: "recent", @@ -203,8 +200,8 @@ fn enter_answers_the_harness_picker_not_the_harness_behind_it() { assert!(cmd.is_none()); assert_eq!( - a.harness_picker.as_ref().map(|picker| picker.step), - Some(HarnessPickerStep::Decision), + a.agent_picker.as_ref().map(|picker| picker.step), + Some(AgentPickerStep::Decision), "the picker should have advanced to its decision step" ); } @@ -219,7 +216,7 @@ fn enter_on_a_harness_asks_before_taking_it() { // so — the point being that Enter is consumed by the harness path rather // than returning to the composer or submitting a turn, and that it never // attaches on its own. - a.harness_pane_session = Some("just-exited".to_string()); + a.pane_session = Some("just-exited".to_string()); let cmd = a.on_key(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); @@ -227,7 +224,7 @@ fn enter_on_a_harness_asks_before_taking_it() { assert!(a.agents_rail_focused()); assert!(a.status().contains("not hosting"), "{}", a.status()); assert!(a.handback_prompt.is_none()); - assert_eq!(a.attached_harness(), None); + assert_eq!(a.attached_session(), None); } #[test] @@ -235,16 +232,13 @@ fn d_on_a_selected_harness_opens_its_changes_tab() { let mut a = app(); a.tab_index = tab("Agents"); a.focus_agents_rail(); - a.harness_pane_session = Some("selected-harness".to_owned()); + a.pane_session = Some("selected-harness".to_owned()); let cmd = a.on_key(KeyEvent::new(KeyCode::Char('d'), KeyModifiers::NONE)); assert!(cmd.is_none()); assert_eq!(a.tab(), "Changes"); - assert_eq!( - a.selected_harness_session.as_deref(), - Some("selected-harness") - ); + assert_eq!(a.rail_session.as_deref(), Some("selected-harness")); assert_eq!(a.draft.text, "", "the shortcut must not type into chat"); } diff --git a/src/tui/src/ui/app/types.rs b/src/tui/src/ui/app/types.rs index 022bcec57..926960208 100644 --- a/src/tui/src/ui/app/types.rs +++ b/src/tui/src/ui/app/types.rs @@ -61,7 +61,7 @@ pub const TABS: [&str; 6] = [ /// The Routing tab's left-nav pages. /// /// Ordered by the containment chain. `Hosts` is the machine level the operator -/// registers and steers by hand; `Harnesses` is the runtime level, which is +/// registers and steers by hand; `Harness Types` is the runtime level, which is /// where credentials live — a subscription or an API key is a property of the /// CLI runtime that spends it, not of the machine it happens to sit on; /// `Workspaces` is the folder level, which is what the orchestrator actually @@ -84,7 +84,7 @@ pub const TABS: [&str; 6] = [ /// its name back here and renumbering. pub const ROUTING_SUBPAGES: [&str; 5] = [ "Hosts", - "Harnesses", + "Harness Types", "Agent Templates", "Add Host", "Strategies", @@ -356,21 +356,21 @@ pub enum Cmd { /// The `(worker address, task id)` to start streaming, if any. start: Option<(String, String)>, }, - /// Kill the harness serving a watched task after UI confirmation. + /// Kill the session serving a watched task after UI confirmation. KillTask { - /// The worker address that owns the harness. + /// The worker address that owns the session. worker: String, - /// The dispatched task whose harness should be killed. + /// The dispatched task whose session should be killed. task_id: String, }, - /// Push a handoff brief for a harness the operator just gave back. + /// Push a handoff brief for a session the operator just gave back. /// /// Off the render thread because it does two things that must not block a /// frame: shells out to `git` for the branch, and awaits a socket emit. /// Arrives with `branch`/`project` unset — the dispatcher fills them. - HandOffHarness(Box), - /// Tell the orchestrator the operator has taken the harness in a workspace. - HoldHarness { + HandOffSession(Box), + /// Tell the orchestrator the operator has taken the session in a workspace. + HoldSession { /// The workspace being taken. workspace: String, /// Why, when the operator said. @@ -410,7 +410,7 @@ pub enum Cmd { /// Run an installed workflow on this machine. /// /// Off-thread like every other filesystem/process command: a workflow run - /// dispatches real harness sessions and takes minutes, so doing it on the + /// dispatches real agent sessions and takes minutes, so doing it on the /// render thread would freeze the app for the whole run. #[cfg(feature = "workflows")] RunWorkflow { @@ -423,7 +423,7 @@ pub enum Cmd { }, /// Ask the copilot to change or explain a workflow. /// - /// Off-thread for the same reason a run is: the turn starts a real harness + /// Off-thread for the same reason a run is: the turn starts a real agent /// session, and the pane it reports into has to keep repainting while it /// does. #[cfg(feature = "workflows")] @@ -532,7 +532,7 @@ pub(super) struct ResumePicker { /// An overlay the app can draw over the content pane. /// /// Ordered as they stack, back to front: the two that float over the content, -/// then the harness picker, then the question asked about a harness being +/// then the session picker, then the question asked about a session being /// released, and finally the two that claim a row of their own below it. /// /// Produced by [`App::visible_overlays`], which is the single source of truth @@ -543,9 +543,9 @@ pub(super) enum Overlay { Decisions, /// The agent-template detail popup. TemplatePopup, - /// The "start a harness" picker. - HarnessPicker, - /// The question asked when the operator lets go of a harness. + /// The "start a session" picker. + AgentPicker, + /// The question asked when the operator lets go of a session. HandbackPrompt, /// The shared single-line prompt (Workers add/edit, Agents answer). InlinePrompt, @@ -553,7 +553,7 @@ pub(super) enum Overlay { ResumePicker, } -/// What the harness/workspace picker is being used for. +/// What the harness-type/workspace picker is being used for. /// /// The same two steps — pick a CLI, pick a directory — answer both questions the /// Agents tab asks, and they differ only in what happens at the end. Declaring an @@ -562,14 +562,14 @@ pub(super) enum Overlay { /// one overlay rather than two that would drift apart. #[derive(Debug, Clone, PartialEq, Eq)] pub(super) enum PickerPurpose { - /// Start a session here and now, declaring nothing — the `/harness` path. + /// Start a session here and now, declaring nothing — the `/session` path. Spawn, /// Declare an agent: `harness × workspace`, named on the step after. DeclareAgent, } -/// The modal state for the harness/workspace picker overlay. -pub(super) struct HarnessPicker { +/// The modal state for the harness-type/workspace picker overlay. +pub(super) struct AgentPicker { /// What confirming the last step will do. pub(super) purpose: PickerPurpose, /// Installed providers and registered presets, in offer order. @@ -577,7 +577,7 @@ pub(super) struct HarnessPicker { /// The highlighted row. pub(super) index: usize, /// Which half of the two-step picker owns the keyboard. - pub(super) step: HarnessPickerStep, + pub(super) step: AgentPickerStep, /// Default directory used to seed the editable workspace query. pub(super) cwd: String, /// Inline fuzzy-completion text on the workspace step. @@ -592,16 +592,16 @@ pub(super) struct HarnessPicker { /// that offers a single completion leaves the cursor on row zero however /// deliberately it was moved there. Set by the arrows, cleared whenever the /// query changes, and read by - /// [`selected_harness_workspace`](App::selected_harness_workspace) to decide + /// [`selected_picker_workspace`](App::selected_picker_workspace) to decide /// whether an entered directory outranks the completions listed under it. pub(super) workspace_picked: bool, /// Whether to spawn managed (orchestrator can dispatch) or unmanaged. pub(super) managed: bool, } -/// Active stage of the manual harness launcher. +/// Active stage of the manual session launcher. #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(super) enum HarnessPickerStep { +pub(super) enum AgentPickerStep { /// Choose an installed CLI or registered preset. Harness, /// Choose managed or unmanaged control mode. @@ -619,13 +619,13 @@ pub(super) struct WorkspaceChoice { pub(super) source: &'static str, } -/// The "you still hold this harness" confirmation shown on release. +/// The "you still hold this session" confirmation shown on release. /// /// Modelled on an unsaved-changes prompt, and for the same reason: an operator -/// who took a harness over and walked away has left the orchestrator locked out +/// who took a session over and walked away has left the orchestrator locked out /// of it, and the moment they release the keyboard is the only moment they are /// certainly thinking about it. Silently handing it back would be worse — it -/// would resume dispatch into a harness mid-thought. +/// would resume dispatch into a session mid-thought. pub(super) struct HandbackPrompt { /// The session the operator is releasing. pub(super) session: String, @@ -636,7 +636,7 @@ pub(super) struct HandbackPrompt { /// What the operator wants continued, typed into the prompt. /// /// This is the moment they actually have the context — they are leaving the - /// harness *now* — so it is the one place worth asking. `/handoff ` + /// session *now* — so it is the one place worth asking. `/handoff ` /// exists for the operator who already knows; this is for the one who is /// only reminded by being asked. pub(super) note: crate::ui::composer::Draft, @@ -647,7 +647,7 @@ pub(super) struct HandbackPrompt { /// letter answer the question for them. pub(super) editing_note: bool, /// Which direction the question is about: `true` asks whether to take the - /// harness from the orchestrator, `false` whether to hand it back. + /// session from the orchestrator, `false` whether to hand it back. /// /// One prompt for both because they are the same decision seen from either /// side, and the answer is the same keystroke — but the sentence has to say @@ -656,7 +656,7 @@ pub(super) struct HandbackPrompt { pub(super) is_takeover: bool, } -/// What to do when the operator releases a harness they hold. +/// What to do when the operator releases a session they hold. #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] pub enum HandbackPolicy { /// Ask, every time. @@ -671,7 +671,7 @@ pub enum HandbackPolicy { impl HandbackPolicy { /// Parse the `[harness].handback` config value, falling back to /// [`Ask`](Self::Ask) for anything unrecognized — a typo in a config file - /// should not silently change who controls a harness. + /// should not silently change who controls a session. pub fn from_config(value: &str) -> Self { match value.trim().to_ascii_lowercase().as_str() { "always" => HandbackPolicy::Always, @@ -715,7 +715,7 @@ pub(super) enum PromptKind { /// and is the only kind that carries a name; a dispatched one is labelled /// from its task. Blank leaves it unnamed rather than inventing one. SessionName { - /// The agent whose harness and workspace the session inherits. + /// The agent whose harness type and workspace the session inherits. agent_id: String, /// Whether the orchestrator may dispatch into it — ownership at birth. managed: bool, @@ -850,7 +850,7 @@ pub struct App { pub snapshot: RuntimeSnapshot, /// The active top-level tab index (into [`TABS`]). pub tab_index: usize, - /// Git changes from the selected harness or operator-chosen commit. + /// Git changes from the selected session or operator-chosen commit. pub(super) changes: super::changes::GitChangesState, pub(super) draft: Draft, pub(super) history: Vec, @@ -890,7 +890,7 @@ pub struct App { pub(super) chat_scroll: usize, /// Selected row in the command peek, while it is open. pub(super) command_index: usize, - /// Installed harnesses offered by the Add Host wizard, detected once. + /// Installed harness types offered by the Add Host wizard, detected once. /// /// Detection reads the environment and stat-checks every provider binary on /// `PATH`. The wizard asked on every render frame *and* every keypress, so a @@ -911,7 +911,7 @@ pub struct App { pub(super) template_index: usize, /// OpenRouter-backed harness presets loaded from the active config. pub(super) custom_harnesses: Vec, - /// Selected row on the Routing Harnesses page. + /// Selected row on the Routing Harness Types page. pub(super) custom_harness_index: usize, /// Scroll offset inside the open agent-template popup. pub(super) template_scroll: usize, @@ -958,11 +958,11 @@ pub struct App { /// Which kind of host the Add Host page is offering — a cursor into /// [`AddHostKind::ALL`]. pub(super) add_host_kind: usize, - /// Which harness a new local host will run — a cursor into the detected + /// Which harness type a new local host will run — a cursor into the detected /// provider list. pub(super) add_host_harness: usize, /// Whether the kind picker has been answered, so the arrows move on to the - /// harness list rather than re-picking local versus remote. + /// harness-type list rather than re-picking local versus remote. pub(super) add_host_kind_chosen: bool, /// The active Routing subpage (index into [`ROUTING_SUBPAGES`]). pub(super) routing_index: usize, @@ -1056,10 +1056,10 @@ pub struct App { /// belongs to. A row may wrap onto several lines, so a click resolves /// through this map rather than by adding an offset to a first-row index. pub(super) hit_agents: Option<(Rect, Vec)>, - // Where the embedded harness screen landed, and whose it is. Recorded so a + // Where the embedded session screen landed, and whose it is. Recorded so a // wheel event can be routed to the terminal under the pointer and given // coordinates relative to *its* origin rather than the screen's. - pub(super) hit_harness: Option<(Rect, String)>, + pub(super) hit_session: Option<(Rect, String)>, /// The threads strip's hit box and its first visible row, for click-to-switch. pub(super) hit_threads: Option<(Rect, usize)>, /// Where the orchestrator's conversation drew, and the task each of its @@ -1110,42 +1110,42 @@ pub struct App { // move on the host's own schedule, and the snapshot is the *runtime's* // picture of the world — the host is a peer to it, not part of it. pub(super) host_obs: Option, - // The live harness sessions this device is running. `None` when this machine + // The live sessions this device is running. `None` when this machine // does not host, in which case the Agents tab has no local screen to show // and falls back to a remote worker's streamed one, or to the transcript. - pub(super) harnesses: Option, - // Which of the TUI and the selected harness owns the keyboard. Reset to + pub(super) local_sessions: Option, + // Which of the TUI and the selected session owns the keyboard. Reset to // `Chrome` whenever the attached session stops being the selected one, so - // the operator's keys can never land in a harness they are not looking at. + // the operator's keys can never land in a session they are not looking at. pub(super) harness_focus: crate::ui::harness_pane::HarnessFocus, - // The harness session the Agents pane resolved on the last draw, and the + // The session the Agents pane resolved on the last draw, and the // only one the attach chord can act on. Recorded during render because that // is where the rail cursor is turned into a selection; cleared at the top of // every draw so it can never name a pane that is no longer on screen. - pub(super) harness_pane_session: Option, - // The harness selected on the Agents rail, retained while another tab is - // visible. Unlike `harness_pane_session`, this is navigation state rather - // than a keyboard-routing capability: Changes uses it to keep following + pub(super) pane_session: Option, + // The session selected on the Agents rail, retained while another tab is + // visible. Unlike `pane_session`, this is navigation state rather than a + // keyboard-routing capability: Changes uses it to keep following // the repository the operator selected after an intervening tab draw. - pub(super) selected_harness_session: Option, - /// The "start a harness" picker, while it is open. - pub(super) harness_picker: Option, - /// The "you still hold this harness" confirmation, while it is open. + pub(super) rail_session: Option, + /// The "start a session" picker, while it is open. + pub(super) agent_picker: Option, + /// The "you still hold this session" confirmation, while it is open. pub(super) handback_prompt: Option, /// How far the Help page is scrolled, in lines. pub(super) help_scroll: u16, - /// What releasing a held harness does, from `[harness].handback`. + /// What releasing a held session does, from `[harness].handback`. pub(super) handback_policy: HandbackPolicy, - /// Whether attaching is what took control of the current harness. + /// Whether attaching is what took control of the current session. /// /// Distinguishes "you picked this up by focusing in" from "you asked for it /// with /takecontrol", which the release prompt words differently: the /// second was a decision, and re-asking about it as though it were an /// accident is how a confirmation becomes noise. - pub(super) harness_took_control: bool, + pub(super) took_control_by_attach: bool, /// Commands raised by synchronous input handlers, drained by the event loop. /// - /// The key and mouse handlers that move harness control cannot return a + /// The key and mouse handlers that move session control cannot return a /// [`Cmd`] — `handle_handback_key` returns `()`, `handle_harness_key` /// returns `bool`, and the mouse path returns nothing — and threading an /// `Option` back through all three would be a wide, test-breaking @@ -1153,7 +1153,7 @@ pub struct App { /// it right after the event that produced it. Commands run in submission /// order. pub(super) pending_cmds: std::collections::VecDeque, - /// Whether operator-started harnesses launch with the permission-bypass + /// Whether operator-started sessions launch with the permission-bypass /// flag, from `[harness].skipPermissions`. pub(super) harness_skip_permissions: bool, } diff --git a/src/tui/src/ui/harness_pane/mod.rs b/src/tui/src/ui/harness_pane/mod.rs index d0548d36a..f64215324 100644 --- a/src/tui/src/ui/harness_pane/mod.rs +++ b/src/tui/src/ui/harness_pane/mod.rs @@ -13,7 +13,7 @@ //! shows them, and an *attached* screen lets the operator answer them. //! //! Responsibilities: -//! - [`LocalHarnesses`] — resolving "what is the cursor on" to a live session; +//! - [`LocalSessions`] — resolving "what is the cursor on" to a live session; //! - [`HarnessFocus`] — which of the TUI and the harness owns the keyboard; //! - [`keys`] — encoding a crossterm key back into the bytes a terminal sends; //! - [`spawn`] — starting a harness the orchestrator will not dispatch into, @@ -33,7 +33,7 @@ mod types; #[cfg(test)] mod tests; -pub use types::{HarnessChoice, HarnessFocus, LocalHarnesses}; +pub use types::{HarnessChoice, HarnessFocus, LocalSessions}; /// How the focus chord is written in hints and titles. /// @@ -47,7 +47,7 @@ pub use types::{HarnessChoice, HarnessFocus, LocalHarnesses}; /// terminals do not deliver this key the way it is written. pub const FOCUS_CHORD_LABEL: &str = "Ctrl-]"; -impl LocalHarnesses { +impl LocalSessions { /// The live session serving `task_id`, if one is. /// /// `None` once the task settles: the runtime drops the record then, so a diff --git a/src/tui/src/ui/harness_pane/spawn.rs b/src/tui/src/ui/harness_pane/spawn.rs index beae80c72..ea9a0f80c 100644 --- a/src/tui/src/ui/harness_pane/spawn.rs +++ b/src/tui/src/ui/harness_pane/spawn.rs @@ -9,25 +9,25 @@ //! //! [`claim_idle`]: crate::worker::pty::PtyManager::claim_idle -use crate::worker::pty::{HarnessControl, LaunchSpec, SessionOrigin}; +use crate::worker::pty::{LaunchSpec, SessionControl, SessionOrigin}; -use super::{HarnessChoice, LocalHarnesses}; +use super::{HarnessChoice, LocalSessions}; -impl LocalHarnesses { +impl LocalSessions { /// Who currently holds `session_id`. - pub fn control(&self, session_id: &str) -> Option { + pub fn control(&self, session_id: &str) -> Option { self.sessions.control(session_id) } /// Hand `session_id` to `control`; `false` when no such session exists. - pub fn set_control(&self, session_id: &str, control: HarnessControl) -> bool { + pub fn set_control(&self, session_id: &str, control: SessionControl) -> bool { self.sessions.set_control(session_id, control) } /// Start a harness the operator owns, returning its session id. /// /// `cwd` is where the child runs; an empty string means the host's - /// workspace. The session opens [`HarnessControl::User`]-held, which is the + /// workspace. The session opens [`SessionControl::User`]-held, which is the /// whole of "unmanaged" — dispatch skips it until it is handed over. /// /// # Errors @@ -103,7 +103,7 @@ impl LocalHarnesses { label: format!("you:{}", choice.id()), model, session_id: None, - control: HarnessControl::User, + control: SessionControl::User, // A person asked for this one, so it is theirs by origin as well as // by control — and it stays user-originated even after they hand it // to the orchestrator, which is the case the two fields exist to diff --git a/src/tui/src/ui/harness_pane/tests/mod.rs b/src/tui/src/ui/harness_pane/tests/mod.rs index acd145c2b..d318a5635 100644 --- a/src/tui/src/ui/harness_pane/tests/mod.rs +++ b/src/tui/src/ui/harness_pane/tests/mod.rs @@ -2,7 +2,7 @@ //! repo's 500-line ceiling: this module covers key encoding, mouse-wheel //! encoding, and focus; [`buttons`] covers click/drag/release encoding and the //! per-mode gate on it; [`session`] drives a real child on a real -//! pseudo-terminal to cover the session-facing half of [`super::LocalHarnesses`]. +//! pseudo-terminal to cover the session-facing half of [`super::LocalSessions`]. //! //! The encoder is where a mistake is invisible until an operator is sitting in //! front of a harness that ignores their arrow keys, so every family it emits is diff --git a/src/tui/src/ui/harness_pane/tests/origin.rs b/src/tui/src/ui/harness_pane/tests/origin.rs index 5d29f0a00..8b10e7ca4 100644 --- a/src/tui/src/ui/harness_pane/tests/origin.rs +++ b/src/tui/src/ui/harness_pane/tests/origin.rs @@ -12,13 +12,13 @@ use std::collections::HashMap; use medulla::protocol::HarnessProvider; -use crate::worker::pty::{HarnessControl, PtyManager}; +use crate::worker::pty::{PtyManager, SessionControl}; use super::session::harnesses; -/// A [`LocalHarnesses`](super::super::LocalHarnesses) whose "codex" is +/// A [`LocalSessions`](super::super::LocalSessions) whose "codex" is /// `/bin/sh`, so opening one starts a real pty client and nothing else. -fn shell_harnesses(sessions: PtyManager) -> super::super::LocalHarnesses { +fn shell_harnesses(sessions: PtyManager) -> super::super::LocalSessions { let mut harnesses = harnesses(sessions); let mut env = HashMap::new(); if let Ok(path) = std::env::var("PATH") { @@ -31,7 +31,7 @@ fn shell_harnesses(sessions: PtyManager) -> super::super::LocalHarnesses { } /// The picker's codex entry. -fn codex(harnesses: &super::super::LocalHarnesses) -> super::super::HarnessChoice { +fn codex(harnesses: &super::super::LocalSessions) -> super::super::HarnessChoice { harnesses .choices() .into_iter() @@ -51,7 +51,7 @@ fn a_session_the_operator_opens_is_user_originated() { let row = sessions.row(&id).expect("the session exists"); assert!(row.origin.is_user(), "a person asked for this one"); - assert_eq!(row.control, HarnessControl::User, "and holds it"); + assert_eq!(row.control, SessionControl::User, "and holds it"); assert_eq!(row.name, None, "the picker has no name prompt yet"); sessions.close(&id); diff --git a/src/tui/src/ui/harness_pane/tests/session.rs b/src/tui/src/ui/harness_pane/tests/session.rs index f73001f27..32747ddda 100644 --- a/src/tui/src/ui/harness_pane/tests/session.rs +++ b/src/tui/src/ui/harness_pane/tests/session.rs @@ -1,4 +1,4 @@ -//! Tests for the session-facing half of [`LocalHarnesses`], against a real +//! Tests for the session-facing half of [`LocalSessions`], against a real //! child on a real pseudo-terminal. //! //! `/bin/sh` stands in for a coding agent: it is a genuine pty client with a @@ -14,10 +14,10 @@ use std::time::{Duration, Instant}; use medulla::protocol::HarnessProvider; -use crate::worker::pty::{HarnessControl, LaunchSpec, PtyManager}; +use crate::worker::pty::{LaunchSpec, PtyManager, SessionControl}; use super::super::HarnessChoice; -use super::super::LocalHarnesses; +use super::super::LocalSessions; /// A spec that runs `sh -c