Skip to main content

ShellController

Struct ShellController 

Source
pub struct ShellController {
    pub(super) controller_type: String,
    pub(super) tab_id: String,
    pub(super) block_id: String,
    pub(super) run_lock: Arc<AtomicBool>,
    pub(super) inner: Arc<Mutex<ShellControllerInner>>,
    pub(super) conn_factory: Mutex<Option<Box<dyn Fn(&str, &HashMap<String, Value>) -> Result<Box<dyn ConnInterface>, String> + Send + Sync>>>,
    pub(super) broker: Option<Arc<Broker>>,
    pub(super) event_bus: Option<Arc<EventBus>>,
    pub(super) mstore: Option<Arc<Store>>,
    pub(super) filestore: Option<Arc<FileStore>>,
    pub(super) auth_key: String,
    pub(super) config: Option<Arc<ConfigState>>,
}
Expand description

ShellController manages one shell or command block.

Fields§

§controller_type: String

Controller type: “shell” or “cmd”.

§tab_id: String§block_id: String§run_lock: Arc<AtomicBool>

Prevents concurrent run() calls.

§inner: Arc<Mutex<ShellControllerInner>>

Protected inner state.

§conn_factory: Mutex<Option<Box<dyn Fn(&str, &HashMap<String, Value>) -> Result<Box<dyn ConnInterface>, String> + Send + Sync>>>

Optional factory for creating ConnInterface (for testing).

§broker: Option<Arc<Broker>>

MPS broker for publishing events (blockfile, controllerstatus).

§event_bus: Option<Arc<EventBus>>

Event bus (unused for now, reserved for future event routing).

§mstore: Option<Arc<Store>>

AgentMux object store — used to seed cmd:cwd on shell spawn.

§filestore: Option<Arc<FileStore>>

FileStore write-through target for PTY output persistence (SPEC_TERMINAL_SCROLLBACK_PERSISTENCE_2026_07_23.md §2.1) — lets handle_append_block_file’s “term” writes survive a reconnect, mirroring PersistentSubprocessController/SubprocessController’s existing filestore wiring.

§auth_key: String

This instance’s API auth key, re-injected into the spawned PTY as AGENTMUX_AUTH_KEY (see lifecycle.rs) so muxsh/muxspect/ muxopen/muxlog, run by a human from a plain Terminal-widget pane, can authenticate the same way an agent pane’s process already does (PersistentSpawnConfig/SubprocessSpawnConfig’s own env_vars). Can’t be read back from the process env at spawn time the way AGENTMUX_LOCAL_URL is — config.rs deliberately scrubs it from this process’s own environment right after startup (PR #801) — so it has to be threaded in explicitly instead.

§config: Option<Arc<ConfigState>>

The live, file-watched config (bootstrap/background.rs’s config_watcher, kept current by config_watcher_fs and the setconfig handler), read at each spawn for the global cmd:env defaults. None in tests and mock contexts, which then get no global defaults. Set via with_config.

Implementations§

Source§

impl ShellController

Source

pub fn new( controller_type: String, tab_id: String, block_id: String, broker: Option<Arc<Broker>>, event_bus: Option<Arc<EventBus>>, mstore: Option<Arc<Store>>, filestore: Option<Arc<FileStore>>, auth_key: String, ) -> Self

Create a new ShellController.

Source

pub fn with_config(self, config: Option<Arc<ConfigState>>) -> Self

Attach the live config handle the spawn reads global cmd:env defaults from. Builder-style, like PersistentSubprocessController:: with_identity_stores.

Source

pub(super) fn live_settings(&self) -> SettingsType

Current global settings. Read from the live config on every call, not captured at construction, so a settings.json edit reaches the next spawn of an existing pane too. Defaults when no config is attached.

Source

pub fn set_conn_factory( &self, factory: Box<dyn Fn(&str, &HashMap<String, Value>) -> Result<Box<dyn ConnInterface>, String> + Send + Sync>, )

Set a custom ConnInterface factory (for testing).

Source

pub(super) fn try_lock_run(&self) -> bool

Try to acquire the run lock. Returns false if already running.

Source

pub(super) fn unlock_run(&self)

Release the run lock.

Source

pub(super) fn set_status(inner: &mut ShellControllerInner, status: &str)

Update process status and increment version (must hold inner lock).

Source

pub(super) fn get_status_snapshot(&self) -> BlockControllerRuntimeStatus

Get the runtime status (snapshot).

Source

pub fn last_output_secs_ago(&self) -> Option<u64>

Seconds since last PTY output, or None if no output yet.

Source

pub fn is_agent_pane(&self) -> bool

True if this pane is running an agent CLI.

Source

pub(super) fn should_run_on_start(meta: &HashMap<String, Value>) -> bool

Check block meta for whether to run on start.

Source

pub(super) fn should_run_once(meta: &HashMap<String, Value>) -> bool

Check block meta for run-once mode (used in full lifecycle integration).

Source

pub(super) fn should_clear_on_start(meta: &HashMap<String, Value>) -> bool

Check block meta for clear-on-start (used in full lifecycle integration).

Source

pub(super) fn should_close_on_exit(meta: &HashMap<String, Value>) -> bool

Check block meta for close-on-exit, as literally set — no default inference. Use effective_close_on_exit at call sites that need the controller-type-aware default; this raw getter exists so that default logic has a single, testable “was it explicitly set” primitive to build on.

Source

pub(super) fn effective_close_on_exit( meta: &HashMap<String, Value>, controller_type: &str, ) -> bool

Whether this block should close its pane when the underlying process exits — should_close_on_exit layered with a controller-type-aware default for when cmd:closeonexit isn’t set at all. An explicit value in meta always wins either way; this only decides the unset case.

Defaults to true for BLOCK_CONTROLLER_SHELL (the interactive PTY both the Terminal widget and an agent’s composer-drawer shell use) — matches the ordinary expectation that typing exit closes the pane, the default behavior of most terminal apps. Defaults to false for BLOCK_CONTROLLER_CMD (one-shot, non-interactive command execution, e.g. a build command) — that pane exists specifically to show its output/exit code after the command finishes, and auto-closing it by default would hide exactly the information it’s for. See docs/specs/SPEC_TERM_EXIT_RESPAWN_LOOP_2026_09_15.md §10.

Source

pub(super) fn should_close_on_exit_force(meta: &HashMap<String, Value>) -> bool

Check block meta for force close-on-exit. Not currently read by any call site — kept as a meta getter (parsed, tested, and available for a future distinction) rather than deleted outright, because two attempts at giving it a real meaning during this PR both turned out unworkable, not because the getter itself is wrong:

  • Exit-code-based (“close even on a non-zero exit”): wait()’s exit-code capture is itself lossy — portable-pty’s ExitStatus doesn’t expose a raw code on all platforms, so success/failure is all that’s reliably known (see the wait/cleanup task’s own comment in lifecycle.rs) — not enough to build a principled “force” distinction on.
  • Flusher-drain-timeout-based (“close even if a background descendant might still be producing output”): tried and reverted in this same PR after live-testing on Windows showed FLUSHER_DRAIN_TIMEOUT (10s) gets hit on essentially every ordinary cmd.exe exit — ConPTY’s own console-host process routinely outlives the direct child by design — which made gating close-on-exit on it fire almost never, defeating the whole feature on the one platform this was tested on. See docs/specs/SPEC_TERM_EXIT_RESPAWN_LOOP_2026_09_15.md §10.
Source

pub(super) fn close_on_exit_delay_ms(meta: &HashMap<String, Value>) -> u64

Get the close-on-exit delay in ms — how long to leave the pane showing its final output before actually closing it.

Defaults to 0 (close immediately). The knob predates any caller and its original 2000 was never exercised; when close-on-exit was first wired up (§10) that 2s was kept as-is, and the reporter’s first working retest asked for it to go — a shell you just typed exit into has already shown you whatever it was going to show, so the pause reads as lag, not as a chance to read the output. Still honored when set explicitly via cmd:closeonexitdelay, for a pane that genuinely wants a beat before vanishing. See docs/specs/SPEC_TERM_EXIT_RESPAWN_LOOP_2026_09_15.md §13.

Source

pub(super) fn get_conn_name(meta: &HashMap<String, Value>) -> String

Get the connection name from block meta.

Source

pub(super) fn get_cmd_str(meta: &HashMap<String, Value>) -> String

Get the command string from block meta.

Source

pub(super) fn get_cmd_args(meta: &HashMap<String, Value>) -> Vec<String>

Get cmd:args array from block meta.

Source

pub(super) fn is_interactive(meta: &HashMap<String, Value>) -> bool

Check if cmd:interactive is set in block meta.

Source

pub(super) fn publish_status(&self)

Publish current controller status via the MPS broker.

Source§

impl ShellController

Source

fn apply_cmd_env( &self, c: &mut CommandBuilder, block_meta: &HashMap<String, Value>, )

Apply cmd:env to an interactive shell’s command: the live global settings, then the block’s own metadata, then the identity strip.

Source

fn command_process( &self, cmd_str: &str, cmd_args: &[String], interactive: bool, block_meta: &HashMap<String, Value>, ) -> Result<CommandBuilder, String>

The process a cmd pane runs: cmd itself when it has cmd:args or cmd:interactive, otherwise cmd through the platform shell. Either way with the pane’s cmd:env, as an interactive shell gets it: a pane running a provider CLI’s login needs the account’s config directory.

Spawned directly, an npm .cmd shim on Windows is resolved to what it runs: the shim itself hangs under ConPTY (resolve_cli_spawn_target). One that can’t be read is refused, rather than started into a pane that would never respond.

Source

fn refuse_start(&self, message: String) -> Result<(), String>

Ends a start that was refused before anything ran: the pane is done, with no exit code, and the controller can be started again.

Source§

impl ShellController

Source

pub(super) fn spawn_pty_size(&self, rt_opts: &Option<Value>) -> PtySize

The size start() opens the PTY at, recorded for a paired device that watches the terminal (blockcontroller::term_size); resizes are recorded as they are sent (blockcontroller::send_input).

Trait Implementations§

Source§

impl Controller for ShellController

Source§

fn start( &self, block_meta: HashMap<String, Value>, rt_opts: Option<Value>, force: bool, ) -> Result<(), String>

Start the controller. May spawn background tasks. force restarts even if already running.
Source§

fn stop(&self, _graceful: bool, new_status: &str) -> Result<(), String>

Stop the controller. graceful waits for process to exit; new_status is the target state.
Source§

fn stop_for_replace(&self, new_status: &str) -> Result<(), String>

Stop this controller because it’s being REPLACED by a new one for the same block (session restart / resync’s needs_replace path), NOT because the block itself is being closed. Must terminate this controller’s own CLI process so it doesn’t linger, but — unlike stop() — must NOT reach any declared-background descendant the process may have spawned (e.g. a task dev instance launched via run_in_background: true); those must survive the replace. See docs/specs/SPEC_BACKGROUND_TASK_TEARDOWN_SURVIVAL_2026_08_20.md. Read more
Source§

fn get_runtime_status(&self) -> BlockControllerRuntimeStatus

Get the current runtime status.
Source§

fn send_input( &self, input: BlockInputUnion, seq: Option<u64>, ) -> Result<(), String>

Send input (terminal data, signal, or resize) to the controller. seq is the per-TermViewModel monotonic counter; None means fire-and-forget (no ordering).
Source§

fn controller_type(&self) -> &str

Get the controller type (e.g., “shell”, “cmd”).
Source§

fn block_id(&self) -> &str

Get the block ID.
Source§

fn agent_id(&self) -> Option<String>

This block’s own live, spawn-time-captured jekt/muxbus identity, if it has one — an independent source of truth for the recipient- identity check in ReactiveHandler::inject_message_inner, deliberately NOT derived from ReactiveHandler’s own name_to_blocks/agent_info maps (checking a registry against itself would be a tautology and catch nothing). Default None for controller types that aren’t jekt-addressable at all (e.g. plain terminals) — only ShellController and PersistentSubprocessController currently override this, mirroring the two paths that already resolve a jekt-registration identity at spawn time (resolve_agent_id_for_jekt, muxbus_agent_id_from_env).
Source§

fn set_agent_id(&self, id: Option<String>)

Refresh this block’s own captured jekt/muxbus identity (see agent_id’s doc comment). Called whenever ReactiveHandler::register_agent/register_agent_with_nonce (re-)registers THIS block’s block_id under a (possibly different) agent_id, so the two independently-written copies never drift apart (#2697: agent_id() was captured once at spawn and never refreshed, while the registry’s name bindings get re-keyed on every register_agent call — e.g. handle_reactive_register’s frontend-initiated HTTP path — causing a legitimately renamed or reconfigured agent’s own messages to be falsely rejected as an identity mismatch). Default no-op for controller types that don’t override agent_id either.
Source§

fn as_any(&self) -> &dyn Any

Downcast support for concrete controller types.
Source§

fn shutdown( &self, _deadline: Instant, ) -> Pin<Box<dyn Future<Output = StopOutcome> + Send>>

Stop gracefully because the block is being CLOSED: end any turn in progress, ask the process to exit, force-kill at deadline. The returned future resolves once the process has actually exited (or there was none). Does NOT drop the block’s process tracker — the caller does that after this resolves, so nothing the agent started outlives it. See docs/specs/SPEC_AGENT_PANE_CLOSE_GRACEFUL_SHUTDOWN_2026_09_18.md §9.3. Read more
Source§

fn turn_provenance(&self) -> Option<TurnProvenance>

What started this agent’s current turn (SPEC_AGENT_SELF_QUIT §6.3), if the controller tracks it. None = unknown, which never counts as the user — the safe answer for controllers that can’t tell (ACP, App Server, subprocess: both origins reach them through one method).
Source§

fn turn_tracker(&self) -> Option<Arc<TurnActivityTracker>>

This controller’s turn tracker, for the turn ledger’s bookkeeping from outside the controller (a held message’s join hint). None for controllers that track no turns.
Source§

fn stable_agent_id(&self) -> Option<String>

This block’s STABLE jekt identity — AGENTMUX_AGENT_ID as captured once at spawn, never refreshed. Distinct from agent_id, which tracks the LIVE, renameable display name and is deliberately refreshed every turn (see that method’s doc comment on #2697). AGENTMUX_AGENT_ID is embedded verbatim in PR-body tags specifically because it does NOT change if the agent is later renamed — but ReactiveHandler’s registry only tracks one key per block, and every turn’s agent_id() refresh evicts whatever this block was previously registered under. ReactiveHandler::inject_message_inner’s recipient-identity check (#2695) uses this alongside agent_id() so a jekt addressed to the stable ID is validated correctly even after the primary registry key has moved on to the live display name. See INCIDENT_2026_09_09_JEKT_STABLE_ID_ALIAS.md. Default None, mirroring agent_id’s default — only PersistentSubprocessController currently overrides this.
Source§

fn stable_agent_uid(&self) -> Option<String>

Identity M2: this block’s UID (db_agents.id) as carried in its spawn env (AGENTMUX_AGENT_UID, set by build_persistent_spawn_env since M1a), captured once at spawn like [stable_agent_id]. Backs the registry’s UID confirmer: consulted only for targets that resolved by UID, and None — a registered-but-not-yet-spawned controller, or a block with no row — is “unverifiable”, not a mismatch. Default None; only PersistentSubprocessController overrides this.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
§

impl<T> Downcast for T
where T: Any,

§

fn into_any(self: Box<T>) -> Box<dyn Any>

Convert Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.
§

fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>

Convert Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be further downcast into Rc<ConcreteType> where ConcreteType implements Trait.
§

fn as_any(&self) -> &(dyn Any + 'static)

Convert &Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &Any’s vtable from &Trait’s.
§

fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)

Convert &mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &mut Any’s vtable from &mut Trait’s.
§

impl<T> DowncastSync for T
where T: Any + Send + Sync,

§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Send + Sync> ⓘ

Convert Arc<Trait> (where Trait: Downcast) to Arc<Any>. Arc<Any> can then be further downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

§

impl<A, B, T> HttpServerConnExec<A, B> for T
where B: Body,

§

impl<T> Instrument for T

§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided [Span], returning an Instrumented wrapper. Read more
§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> IntoEither for T

Source§

fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ

Converts self into a Left variant of Either<Self, Self> if into_left is true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
Source§

fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
where F: FnOnce(&Self) -> bool,

Converts self into a Left variant of Either<Self, Self> if into_left(&self) returns true. Converts self into a Right variant of Either<Self, Self> otherwise. Read more
§

impl<T> PolicyExt for T
where T: ?Sized,

§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] only if self and other return Action::Follow. Read more
§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns [Action::Follow] if either self or other returns Action::Follow. Read more
Source§

impl<T> Same for T

Source§

type Output = T

Should always be Self
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

§

fn vzip(self) -> V

§

impl<T> WithSubscriber for T

§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a [WithDispatch] wrapper. Read more
§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a [WithDispatch] wrapper. Read more