feat(app): workspace-action MCP tools on the live bridge (F-mcp-orchestration, #194)#219
Merged
Merged
Conversation
…stration) The action rung after the read-only perception rung (#212): six mutating MCP tools — open_session, split_pane, focus_pane, rename_tab, close_pane, run_in_session — each a thin wrapper over an existing core::App event, so the orchestration surface never bypasses the state machine (#90's own constraint). core is untouched; all mutation is already expressed as events. The seam is app-side: the bridge grows an Act(Action) request and an ActionOutcome reply, and the shell adapter — which owns core::App and the one effect executor — resolves the stable handle, applies the event(s), performs the effects, and reports the resulting focused handle, so an agent gets act→observe in one round trip. An unknown handle or out-of-range tab is rejected before any state is touched (surfaced as invalid_params); every call stays tokio::timeout-bounded and apply-and-read (Q7). Handles are strings everywhere, matching list_sessions/snapshot. New shell/orchestrate.rs owns perform_action + one act_* per action, mirroring shell/launch.rs. 23 tests: perform_action behaviour and tool arg-mapping / rejection / bridge-failure shaping.
A pane handle from a non-active tab resolved (the session registry is workspace-global) but `Event::FocusPane` only searches the active tab and discards its result, so the follow-up focus-relative event acted on the *active* tab's focused pane while the tool reported success — close_pane killed the wrong terminal, split_pane split beside the wrong one, and focus_pane reported a focus move that never happened. Core grows `Workspace::reveal_pane_of` + `Event::RevealPane`: activate the owning tab, then focus the leaf, restoring the previous tab if the leaf turns out to be unreachable. `FocusPane` keeps its click-to-focus meaning — a click cannot land outside the active tab. The orchestration seam resolves through `tab_of` rather than the tab-blind registry, so a handle no open pane hosts is rejected instead of silently retargeting whatever holds focus. Also drops a bare issue number from a doc-comment (AGENTS.md).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
The action rung of the MCP control surface, after the read-only
perception rung (#212). Six mutating tools, each a thin wrapper over an
existing
core::Appevent — the orchestration surface never bypasses thestate machine (#90's own constraint):
Eventopen_session(project, kind)LaunchSession(via the shell's own launch path)split_pane(direction[, pane])[FocusPane +] SplitFocusedfocus_pane(session)FocusPanerename_tab(tab, title)RenameTabclose_pane([pane])CloseFocusedPane(lone pane → core collapses toclose_tab)run_in_session(session, text)TerminalInput(no wait)How
coreis untouched — all mutation is already expressed as events. The seamis app-side:
shell/bridge.rs—Request::Act(Action)+Reply::Acted(ActionOutcome { focused, error }).Actionhas the six variants; targets are the stablestring handle
list_sessions/snapshotalready report.shell.rs—Message::Bridgebranches: actions →perform_action(needs
&mut), the two read requests →respond(unchanged).shell/orchestrate.rs(new) —perform_action+ oneact_*per action,sharing
retarget/resolve/applied. It resolves the handle, applies theevent(s) through
App::apply, performs the effects via the one executor, andreports the resulting focused handle — so an agent gets act→observe in
one round trip. Mirrors
shell/launch.rs.mcp/handler.rs— the six#[tool]methods + arg DTOs. A rejectionbecomes
invalid_params; success returns{ focused_handle }.An unknown handle / out-of-range tab is rejected before any state is
touched;
runnever misfires into a wrong terminal. Every call staystokio::timeout-bounded and apply-and-read (Q7, from #192).Tests
23 new (all green):
perform_actionbehaviour (11) — open focuses a new pane; split targetsa pane and rejects an unknown one; focus/run reject an unknown handle without
touching state; run writes the bytes; rename relabels the tab; close kills a
lone-pane tab.
Action,defaults kind/direction, parses handles, relays a rejection as
invalid_params, reports the resulting focus, and surfaces a bridge failureas an error (not a hang).
Full suite green (234 app);
fmt·clippy·machete·deny·check-deps·check-arch·markdownlintall pass. ROADMAP entry ticked.Closes #194.