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: StringController 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: StringThis 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
impl ShellController
Sourcepub 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
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.
Sourcepub fn with_config(self, config: Option<Arc<ConfigState>>) -> Self
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.
Sourcepub(super) fn live_settings(&self) -> SettingsType
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.
Sourcepub fn set_conn_factory(
&self,
factory: Box<dyn Fn(&str, &HashMap<String, Value>) -> Result<Box<dyn ConnInterface>, String> + Send + Sync>,
)
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).
Sourcepub(super) fn try_lock_run(&self) -> bool
pub(super) fn try_lock_run(&self) -> bool
Try to acquire the run lock. Returns false if already running.
Sourcepub(super) fn unlock_run(&self)
pub(super) fn unlock_run(&self)
Release the run lock.
Sourcepub(super) fn set_status(inner: &mut ShellControllerInner, status: &str)
pub(super) fn set_status(inner: &mut ShellControllerInner, status: &str)
Update process status and increment version (must hold inner lock).
Sourcepub(super) fn get_status_snapshot(&self) -> BlockControllerRuntimeStatus
pub(super) fn get_status_snapshot(&self) -> BlockControllerRuntimeStatus
Get the runtime status (snapshot).
Sourcepub fn last_output_secs_ago(&self) -> Option<u64>
pub fn last_output_secs_ago(&self) -> Option<u64>
Seconds since last PTY output, or None if no output yet.
Sourcepub fn is_agent_pane(&self) -> bool
pub fn is_agent_pane(&self) -> bool
True if this pane is running an agent CLI.
Sourcepub(super) fn should_run_on_start(meta: &HashMap<String, Value>) -> bool
pub(super) fn should_run_on_start(meta: &HashMap<String, Value>) -> bool
Check block meta for whether to run on start.
Sourcepub(super) fn should_run_once(meta: &HashMap<String, Value>) -> bool
pub(super) fn should_run_once(meta: &HashMap<String, Value>) -> bool
Check block meta for run-once mode (used in full lifecycle integration).
Sourcepub(super) fn should_clear_on_start(meta: &HashMap<String, Value>) -> bool
pub(super) fn should_clear_on_start(meta: &HashMap<String, Value>) -> bool
Check block meta for clear-on-start (used in full lifecycle integration).
Sourcepub(super) fn should_close_on_exit(meta: &HashMap<String, Value>) -> bool
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.
Sourcepub(super) fn effective_close_on_exit(
meta: &HashMap<String, Value>,
controller_type: &str,
) -> bool
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.
Sourcepub(super) fn should_close_on_exit_force(meta: &HashMap<String, Value>) -> bool
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’sExitStatusdoesn’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 inlifecycle.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 ordinarycmd.exeexit — 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. Seedocs/specs/SPEC_TERM_EXIT_RESPAWN_LOOP_2026_09_15.md§10.
Sourcepub(super) fn close_on_exit_delay_ms(meta: &HashMap<String, Value>) -> u64
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.
Sourcepub(super) fn get_conn_name(meta: &HashMap<String, Value>) -> String
pub(super) fn get_conn_name(meta: &HashMap<String, Value>) -> String
Get the connection name from block meta.
Sourcepub(super) fn get_cmd_str(meta: &HashMap<String, Value>) -> String
pub(super) fn get_cmd_str(meta: &HashMap<String, Value>) -> String
Get the command string from block meta.
Sourcepub(super) fn get_cmd_args(meta: &HashMap<String, Value>) -> Vec<String>
pub(super) fn get_cmd_args(meta: &HashMap<String, Value>) -> Vec<String>
Get cmd:args array from block meta.
Sourcepub(super) fn is_interactive(meta: &HashMap<String, Value>) -> bool
pub(super) fn is_interactive(meta: &HashMap<String, Value>) -> bool
Check if cmd:interactive is set in block meta.
Sourcepub(super) fn publish_status(&self)
pub(super) fn publish_status(&self)
Publish current controller status via the MPS broker.
Source§impl ShellController
impl ShellController
Sourcefn apply_cmd_env(
&self,
c: &mut CommandBuilder,
block_meta: &HashMap<String, Value>,
)
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.
Sourcefn command_process(
&self,
cmd_str: &str,
cmd_args: &[String],
interactive: bool,
block_meta: &HashMap<String, Value>,
) -> Result<CommandBuilder, String>
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§impl ShellController
impl ShellController
Sourcepub(super) fn spawn_pty_size(&self, rt_opts: &Option<Value>) -> PtySize
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
impl Controller for ShellController
Source§fn start(
&self,
block_meta: HashMap<String, Value>,
rt_opts: Option<Value>,
force: bool,
) -> Result<(), String>
fn start( &self, block_meta: HashMap<String, Value>, rt_opts: Option<Value>, force: bool, ) -> Result<(), String>
force restarts even if already running.Source§fn stop(&self, _graceful: bool, new_status: &str) -> Result<(), String>
fn stop(&self, _graceful: bool, new_status: &str) -> Result<(), String>
graceful waits for process to exit; new_status is the target state.Source§fn stop_for_replace(&self, new_status: &str) -> Result<(), String>
fn stop_for_replace(&self, new_status: &str) -> Result<(), String>
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 moreSource§fn get_runtime_status(&self) -> BlockControllerRuntimeStatus
fn get_runtime_status(&self) -> BlockControllerRuntimeStatus
Source§fn send_input(
&self,
input: BlockInputUnion,
seq: Option<u64>,
) -> Result<(), String>
fn send_input( &self, input: BlockInputUnion, seq: Option<u64>, ) -> Result<(), String>
seq is the per-TermViewModel monotonic counter; None means fire-and-forget (no ordering).Source§fn controller_type(&self) -> &str
fn controller_type(&self) -> &str
Source§fn agent_id(&self) -> Option<String>
fn agent_id(&self) -> Option<String>
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>)
fn set_agent_id(&self, id: Option<String>)
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 shutdown(
&self,
_deadline: Instant,
) -> Pin<Box<dyn Future<Output = StopOutcome> + Send>>
fn shutdown( &self, _deadline: Instant, ) -> Pin<Box<dyn Future<Output = StopOutcome> + Send>>
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 moreSource§fn turn_provenance(&self) -> Option<TurnProvenance>
fn turn_provenance(&self) -> Option<TurnProvenance>
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>>
fn turn_tracker(&self) -> Option<Arc<TurnActivityTracker>>
None for
controllers that track no turns.Source§fn stable_agent_id(&self) -> Option<String>
fn stable_agent_id(&self) -> Option<String>
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>
fn stable_agent_uid(&self) -> Option<String>
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§
impl !Freeze for ShellController
impl !RefUnwindSafe for ShellController
impl !UnwindSafe for ShellController
impl Send for ShellController
impl Sync for ShellController
impl Unpin for ShellController
impl UnsafeUnpin for ShellController
Blanket Implementations§
Source§impl<T> BorrowMut<T> for Twhere
T: ?Sized,
impl<T> BorrowMut<T> for Twhere
T: ?Sized,
Source§fn borrow_mut(&mut self) -> &mut T
fn borrow_mut(&mut self) -> &mut T
§impl<T> Downcast for Twhere
T: Any,
impl<T> Downcast for Twhere
T: Any,
§fn into_any(self: Box<T>) -> Box<dyn Any>
fn into_any(self: Box<T>) -> Box<dyn Any>
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>
fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>
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)
fn as_any(&self) -> &(dyn Any + 'static)
&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)
fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)
&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
impl<T> DowncastSync for T
impl<A, B, T> HttpServerConnExec<A, B> for Twhere
B: Body,
§impl<T> Instrument for T
impl<T> Instrument for T
§fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
fn instrument(self, span: Span) -> Instrumented<Self> ⓘ
Source§impl<T> IntoEither for T
impl<T> IntoEither for T
Source§fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
fn into_either(self, into_left: bool) -> Either<Self, Self> ⓘ
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 moreSource§fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
fn into_either_with<F>(self, into_left: F) -> Either<Self, Self> ⓘ
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