Visitar URL original
fix(cli-rs): drop the foreground subagent UI (#63382, #63223, #63392) · anomalyco/opencode@e6637ac · GitHub
Skip to content

Commit e6637ac

Browse files
committed
fix(cli-rs): drop the foreground subagent UI (#63382, #63223, #63392)
Subagents always run in the background upstream. The session view no longer offers the background hint for a running subagent call or badges a launched one "Background", and mini drops its "bg" status hint, its session.background layer and the per-child background flag. A new C.10 scenario covers run waiting for a subagent report and the turn it wakes.
1 parent f1beb17 commit e6637ac

9 files changed

Lines changed: 66 additions & 80 deletions

File tree

‎packages/cli-rs/crates/mini/src/footers/mod.rs‎

Lines changed: 10 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,8 @@
77
//! `queued_prompt.delete` cancels a prompt, and `queued_prompt.undo` takes it back into the draft.
88
//!
99
//! Child sessions (`crate::subagents`) add their permissions and forms after the session's own, and global forms
10-
//! come last. `session.child.first` opens the subagent panel, which leads to the inspector of one child;
11-
//! `session.background` backgrounds the running subagents, and `composer.subagent.interrupt` interrupts the
12-
//! inspected one.
10+
//! come last. `session.child.first` opens the subagent panel, which leads to the inspector of one child, and
11+
//! `composer.subagent.interrupt` interrupts the inspected one.
1312
1413
pub mod form;
1514
pub mod menu;
@@ -26,9 +25,9 @@ use std::time::Duration;
2625
use opencode_api::client::Client;
2726
use opencode_api::generated::{
2827
FormID, FormInfo, FormListInput, LocationQueryInput, PermissionAskedData, PermissionListInput,
29-
PermissionReplyInput, PermissionRequest, SessionBackgroundInput, SessionFormCancelInput, SessionFormListInput,
30-
SessionFormReplyInput, SessionGetInput, SessionID, SessionInboxListInput, SessionInfo, SessionInterruptInput,
31-
SessionListInput, SessionListInputOrder, SessionMessageGetInput, SessionMessageInfo, V2Event,
28+
PermissionReplyInput, PermissionRequest, SessionFormCancelInput, SessionFormListInput, SessionFormReplyInput,
29+
SessionGetInput, SessionID, SessionInboxListInput, SessionInfo, SessionInterruptInput, SessionListInput,
30+
SessionListInputOrder, SessionMessageGetInput, SessionMessageInfo, V2Event,
3231
};
3332
use opencode_local::cli_config::Visibility;
3433
use opencode_ui::keymap::{KeymapLayer, TuiCommand};
@@ -535,8 +534,8 @@ impl Footers {
535534
self.0.subagents_changed();
536535
}
537536

538-
/// The running children, and whether one of them runs in the foreground, blocking the session.
539-
pub fn running_subagents(&self) -> (usize, bool) {
537+
/// The running children.
538+
pub fn running_subagents(&self) -> usize {
540539
self.0.running()
541540
}
542541
}
@@ -842,17 +841,6 @@ impl Inner {
842841
});
843842
}
844843

845-
/// `onBackground`: the session's running subagents go to the background.
846-
fn background(self: &Rc<Self>) {
847-
let Some(target) = self.target.borrow().clone() else { return };
848-
let input = SessionBackgroundInput { session_id: SessionID(target.session.clone()) };
849-
self.app.spawn(async move { target.client.session_background(&input).await }, |_, result| {
850-
if let Err(error) = result {
851-
log::warn!("mini background failed: {error}");
852-
}
853-
});
854-
}
855-
856844
fn paint_inspector(&self, width: u16, rows: u16) {
857845
let run = self.theme.borrow().clone();
858846
let keymap = self.prompt.keymap().clone();
@@ -2323,13 +2311,6 @@ fn install_layers(inner: &Rc<Inner>) {
23232311
.group("Prompt"),
23242312
])
23252313
.enabled(when(|inner| composer(inner) && !inner.prompt.queued().is_empty())),
2326-
KeymapLayer::new(vec![
2327-
TuiCommand::new("session.background", run(Inner::background))
2328-
.title("Background subagents")
2329-
.group("Session"),
2330-
])
2331-
.enabled(when(|inner| composer(inner) && inner.running().1))
2332-
.priority(1),
23332314
KeymapLayer::new(vec![
23342315
TuiCommand::new("session.child.first", run(Inner::open_subagents)).title("View subagents").group("Session"),
23352316
])
@@ -2425,11 +2406,9 @@ fn install_layers(inner: &Rc<Inner>) {
24252406
}
24262407

24272408
impl Inner {
2428-
/// The running children, and whether one runs in the foreground (`foregroundSubagents`).
2429-
fn running(&self) -> (usize, bool) {
2430-
let state = self.state.borrow();
2431-
let running: Vec<&Tab> = state.tabs.iter().filter(|tab| tab.status == Status::Running).collect();
2432-
(running.len(), running.iter().any(|tab| !tab.background))
2409+
/// The running children.
2410+
fn running(&self) -> usize {
2411+
self.state.borrow().tabs.iter().filter(|tab| tab.status == Status::Running).count()
24332412
}
24342413

24352414
fn cycle_variant_rc(self: &Rc<Self>) {

‎packages/cli-rs/crates/mini/src/prompt/status.rs‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ pub enum Id {
1515
Escape,
1616
Queued,
1717
Subagents,
18-
Background,
1918
Agent,
2019
Model,
2120
Context,
@@ -53,7 +52,7 @@ pub struct Input {
5352
/// The status text and its wider form.
5453
pub status: Option<(String, Option<String>)>,
5554
pub escape: Option<Action>,
56-
/// `queued`, `subagents`, and `background` hints.
55+
/// `queued` and `subagents` hints.
5756
pub work: Vec<(Id, Action)>,
5857
/// The model's name and variant.
5958
pub model: Option<(String, Option<String>)>,

‎packages/cli-rs/crates/mini/src/shell.rs‎

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1979,17 +1979,13 @@ impl Mini {
19791979
}
19801980
_ => Vec::new(),
19811981
};
1982-
let (running, foreground) = self.footers.running_subagents();
1982+
let running = self.footers.running_subagents();
19831983
if let Some(key) = shortcut("session.child.first").filter(|_| shown && running > 0) {
19841984
let plural = if running == 1 { "" } else { "s" };
19851985
let label = format!("{running} sub");
19861986
let expanded = Some(format!("{running} subagent{plural}"));
19871987
work.push((status::Id::Subagents, status::Action { key, label, expanded }));
19881988
}
1989-
if let Some(key) = shortcut("session.background").filter(|_| shown && foreground) {
1990-
let action = status::Action { key, label: "bg".into(), expanded: Some("background".into()) };
1991-
work.push((status::Id::Background, action));
1992-
}
19931989
let usage = state.usage.as_ref().filter(|_| details);
19941990
status::policy(&status::Input {
19951991
width,
@@ -2060,7 +2056,7 @@ impl Mini {
20602056
if queued > 0 {
20612057
details.push(format!("{queued} pending"));
20622058
}
2063-
let (running, _) = self.footers.running_subagents();
2059+
let running = self.footers.running_subagents();
20642060
if running > 0 {
20652061
details.push(format!("{running} subagent{}", if running == 1 { "" } else { "s" }));
20662062
}

‎packages/cli-rs/crates/mini/src/subagents.rs‎

Lines changed: 5 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ pub struct Tab {
5454
pub label: String,
5555
pub description: String,
5656
pub status: Status,
57-
pub background: bool,
5857
pub title: Option<String>,
5958
}
6059

@@ -92,7 +91,6 @@ struct Child {
9291
label: String,
9392
description: String,
9493
status: Status,
95-
background: bool,
9694
title: Option<String>,
9795
updated: f64,
9896
frames: Vec<(String, Commit)>,
@@ -116,7 +114,6 @@ impl Child {
116114
label: FALLBACK_LABEL.to_owned(),
117115
description: String::new(),
118116
status: Status::Running,
119-
background: false,
120117
title: None,
121118
updated: 0.0,
122119
frames: Vec::new(),
@@ -144,7 +141,6 @@ impl Child {
144141
self.description.clone()
145142
},
146143
status: self.status,
147-
background: self.background,
148144
title: self.title.clone(),
149145
}
150146
}
@@ -164,7 +160,7 @@ impl Child {
164160
}
165161
}
166162

167-
/// `applyMeta`: the subagent call's input names the agent, the task, and whether it runs in the background.
163+
/// `applyMeta`: the subagent call's input names the agent and the task.
168164
fn apply_meta(&mut self, meta: Option<&IndexMap<String, Value>>) {
169165
let Some(meta) = meta else { return };
170166
if let Some(agent) = text(meta.get("agent")) {
@@ -173,9 +169,6 @@ impl Child {
173169
if let Some(description) = text(meta.get("description")) {
174170
self.description = description.to_owned();
175171
}
176-
if meta.get("background") == Some(&Value::Bool(true)) {
177-
self.background = true;
178-
}
179172
}
180173

181174
/// `userFrame`: a delivered prompt, once.
@@ -541,7 +534,6 @@ impl Tracker {
541534
};
542535
child.apply_meta(call.as_ref());
543536
if success == Some(true) && running {
544-
child.background = true;
545537
child.status = Status::Running;
546538
}
547539
if success == Some(true) && !running && child.status == Status::Running {
@@ -776,13 +768,9 @@ impl Tracker {
776768
_ => return,
777769
};
778770
let Some((session, running)) = child_session(metadata) else { return };
779-
let completed = matches!(tool.state, ToolPartState::Completed(_));
780771
let alive = self.active.contains(&session);
781772
let Some(child) = self.admit(&session) else { return };
782773
child.apply_meta(Some(input));
783-
if completed && running {
784-
child.background = true;
785-
}
786774
if child.status == Status::Running {
787775
child.status = if running && alive { Status::Running } else { Status::Completed };
788776
}
@@ -1238,7 +1226,7 @@ mod tests {
12381226
event(kind, "ses_root", data)
12391227
};
12401228
assert!(tracker.main(&call("session.tool.input.started", json!({ "name": "subagent" }))).is_empty());
1241-
let input = json!({ "agent": "general", "description": "Read the docs", "background": true });
1229+
let input = json!({ "agent": "general", "description": "Read the docs" });
12421230
assert!(tracker.main(&call("session.tool.called", json!({ "input": input, "executed": false }))).is_empty());
12431231
let metadata = json!({ "sessionID": "ses_child", "status": "running" });
12441232
let fetches = tracker.main(&call("session.tool.progress", json!({ "metadata": metadata })));
@@ -1252,7 +1240,7 @@ mod tests {
12521240
assert!(tracker.take_changed());
12531241
let tab = &tracker.tabs()[0];
12541242
assert_eq!((tab.label.as_str(), tab.description.as_str()), ("General", "Read the docs"));
1255-
assert_eq!((tab.status, tab.background), (Status::Running, true));
1243+
assert_eq!(tab.status, Status::Running);
12561244
// A metadata without a child session names nothing.
12571245
let other = json!({ "assistantMessageID": "msg_1", "id": "call_2", "metadata": { "status": "running" } });
12581246
assert!(tracker.main(&event("session.tool.progress", "ses_root", other)).is_empty());
@@ -1269,8 +1257,8 @@ mod tests {
12691257
assert_eq!(tracker.hydrate(&messages, &["ses_b".into(), "ses_c".into()], false), [Fetch::Family]);
12701258
let tabs = tracker.tabs();
12711259
assert_eq!(tabs.len(), 2);
1272-
// `ses_b` still runs and was started in the background; `ses_a` finished.
1273-
assert_eq!((tabs[0].session_id.as_str(), tabs[0].status, tabs[0].background), ("ses_b", Status::Running, true));
1260+
// `ses_b` still runs; `ses_a` finished.
1261+
assert_eq!((tabs[0].session_id.as_str(), tabs[0].status), ("ses_b", Status::Running));
12741262
assert_eq!((tabs[1].session_id.as_str(), tabs[1].status), ("ses_a", Status::Completed));
12751263
let fetches = tracker.family(vec![session("ses_d", "ses_root", "plan", 9.0)]);
12761264
assert!(fetches.iter().any(|fetch| matches!(fetch, Fetch::Discover { session, .. } if session == "ses_c")));

‎packages/cli-rs/crates/tui/src/routes/session/messages/background.rs‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
//! The background hint (E.6b): `BackgroundToolHint` (`routes/session/index.tsx:1897-1929`). Once a shell or
2-
//! subagent call of the running step has run for three seconds, the transcript ends with how to move it to the
3-
//! background.
1+
//! The background hint (E.6b): `BackgroundToolHint` (`routes/session/index.tsx:1897-1929`). Once a shell call of
2+
//! the running step has run for three seconds, the transcript ends with how to move it to the background.
3+
//! Subagents always run in the background.
44
55
use std::cell::{Cell, RefCell};
66
use std::rc::{Rc, Weak};
@@ -116,7 +116,7 @@ impl Inner {
116116
}
117117
}
118118

119-
/// The running shell or subagent call of the newest unfinished step, and when it started running.
119+
/// The running shell call of the newest unfinished step, and when it started running.
120120
fn running(messages: &[SessionMessageInfo]) -> Option<(String, f64)> {
121121
let current = messages.iter().rev().find_map(|message| match message {
122122
SessionMessageInfo::Assistant(assistant) if assistant.time.completed.is_none() => Some(assistant),
@@ -125,7 +125,7 @@ fn running(messages: &[SessionMessageInfo]) -> Option<(String, f64)> {
125125
current.content.iter().find_map(|item| match item {
126126
SessionMessageAssistantContentItem::Tool(tool)
127127
if matches!(tool.state, SessionMessageAssistantToolState::Running(_))
128-
&& matches!(canonical_tool_name(&tool.name), "shell" | "subagent") =>
128+
&& canonical_tool_name(&tool.name) == "shell" =>
129129
{
130130
Some((format!("{}:{}", current.id.as_str(), tool.id), tool.time.ran.unwrap_or(tool.time.created)))
131131
}

‎packages/cli-rs/crates/tui/src/routes/session/tools/subagent.rs‎

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use opencode_store::SessionStatus;
99
use opencode_ui::bind;
1010
use opencode_ui::tool_info::titlecase;
1111
use opentui::prelude::*;
12-
use serde_json::{Map, Value};
12+
use serde_json::Value;
1313

1414
use super::dispatch::{Inline, InlineTool, Props, Status};
1515
use crate::app::Tui;
@@ -77,7 +77,6 @@ impl Subagent {
7777
// `running` hides the error while the child still works.
7878
error: inline.error.clone().filter(|_| !running),
7979
clickable: true,
80-
badge: background(&props.metadata, status).then(|| "Background".to_owned()),
8180
..inline
8281
});
8382
*self.session.borrow_mut() = session.filter(|id| !id.is_empty());
@@ -102,11 +101,6 @@ pub fn model_label(value: Option<&str>, models: &[ModelInfo]) -> Option<String>
102101
})
103102
}
104103

105-
/// `isBackgroundSubagent`: the call returned while the child keeps running.
106-
fn background(metadata: &Map<String, Value>, status: Status) -> bool {
107-
status == Status::Completed && metadata.get("status").and_then(Value::as_str) == Some("running")
108-
}
109-
110104
#[cfg(test)]
111105
mod tests {
112106
use super::model_label;

‎packages/cli-rs/crates/tui/tests/all/more_tools.rs‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -245,13 +245,12 @@ fn a_subagent_row_opens_its_child_session() {
245245
let screen = h.wait_for("Resume");
246246
for line in [
247247
" ⋯ Explore Subagent — Survey the views · provider/model (high)\n",
248-
" ✓ General Subagent — Check the docs Background\n",
248+
" ✓ General Subagent — Check the docs\n",
249249
" ↳ Continue subagent — Resume\n",
250250
] {
251251
assert!(screen.contains(line), "{line:?} missing:\n{screen}");
252252
}
253-
let theme = h.tui.page();
254-
assert_eq!(h.fg("Background"), theme.text.muted);
253+
assert!(!screen.contains("Background"), "{screen}");
255254

256255
h.click("Check the docs");
257256
let found = h.app.wait_until(WAIT, || h.tui.route() == Route::Session { id: SessionID("ses_child".into()) });
@@ -261,7 +260,7 @@ fn a_subagent_row_opens_its_child_session() {
261260
}
262261

263262
#[test]
264-
fn a_background_subagent_row_follows_its_child_status() {
263+
fn a_subagent_row_follows_its_child_status() {
265264
let messages = turn(vec![tool(
266265
"c1",
267266
"task",
@@ -272,14 +271,14 @@ fn a_background_subagent_row_follows_its_child_status() {
272271
),
273272
)]);
274273
let h = open(100, 30, messages);
275-
h.wait_for(" ✓ General Subagent — Check the docs Background\n");
274+
h.wait_for(" ✓ General Subagent — Check the docs\n");
276275
let child = support::merge(session("ses_child", "Child work"), json!({ "parentID": "ses_t" }));
277276
h.tui.data.remember(serde_json::from_value(child).unwrap());
278277
let child = SessionID("ses_child".into());
279278
h.tui.data.set_status(&child, opencode_store::SessionStatus::Running);
280-
h.wait_for(" ⋯ General Subagent — Check the docs Background\n");
279+
h.wait_for(" ⋯ General Subagent — Check the docs\n");
281280
h.tui.data.set_status(&child, opencode_store::SessionStatus::Idle);
282-
h.wait_for(" ✓ General Subagent — Check the docs Background\n");
281+
h.wait_for(" ✓ General Subagent — Check the docs\n");
283282
}
284283

285284
#[test]
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// A subagent always runs in the background (#63382): its job holds the run open, and its report wakes the
2+
// parent. The turn the report triggers streams, and its failure decides the exit code, even though the first
3+
// turn succeeded (`noninteractive.test.ts`, "waits for a subagent's report and the parent turn it triggers").
4+
import { defineCli } from "../../cli/run"
5+
import { event, nowMask, server, textStep } from "./fixture"
6+
import { jobs } from "./background.cli"
7+
8+
const child = {
9+
kind: "subagent",
10+
id: "ses_child",
11+
sessionID: "ses_run",
12+
status: "running",
13+
childSessionID: "ses_child",
14+
agent: "explore",
15+
description: "scan repo",
16+
time: { started: 3 },
17+
}
18+
19+
export default defineCli({
20+
args: ["run", "--format", "json", "hello"],
21+
server: server({
22+
events: () => textStep("Started the subagent."),
23+
// The wait reads the child's inbox too.
24+
routes: [jobs((count) => (count === 1 ? [child] : [])), { path: "/api/session/ses_child/inbox", json: { data: [] } }],
25+
late: [
26+
event("session.text.started", { assistantMessageID: "msg_b", ordinal: 0 }, 30),
27+
event("session.text.ended", { assistantMessageID: "msg_b", ordinal: 0, text: "Report handled." }, 31),
28+
event("session.execution.failed", { error: { type: "unknown", message: "report turn failed" } }, 32),
29+
],
30+
}),
31+
masks: [nowMask],
32+
})

‎packages/cli-rs/parity/scenarios/M.5/subagents.pty.ts‎

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
// Two child sessions from the history's subagent calls: the status row counts the running one and offers to
2-
// background it. The command menu lists them; Down on the empty prompt opens the subagent panel with the running
3-
// child, Tab shows the finished one, and Enter inspects its transcript. Escape goes back; Ctrl+B backgrounds the
4-
// running child (`session.background`), and its inspector's Ctrl+D interrupts it.
1+
// Two child sessions from the history's subagent calls: the status row counts the running one; subagents always
2+
// run in the background, so it offers no `session.background` key. The command menu lists them; Down on the empty
3+
// prompt opens the subagent panel with the running child, Tab shows the finished one, and Enter inspects its
4+
// transcript. Escape goes back, and the running child's inspector's Ctrl+D interrupts it.
55
import { definePty } from "../../pty/run"
66
import { config, leave, opened, server } from "./fixture"
77

@@ -19,7 +19,6 @@ export default definePty({
1919
{ keys: "\t", settle: 300, checkpoint: "inactive" },
2020
{ keys: "\r", until: { screen: "The docs cover" }, settle: 500, checkpoint: "inspector" },
2121
{ keys: "\x1b", until: { screen: "1 sub" }, settle: 400 },
22-
{ keys: "\x02", settle: 400 },
2322
{ keys: "\x1b[B", until: { screen: "Select subagent" }, settle: 300 },
2423
// The running child's spinner turns, so the screen never settles.
2524
{ keys: "\r", until: { screen: "ctrl+d interrupt" } },

0 commit comments

Comments
 (0)