Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0d1fb19
Session card: side chats and subagents ride a lanes card under it
jsgrrchg Oct 12, 2026
db27e97
Session card: drop the running pill from the Subagents heading
jsgrrchg Oct 12, 2026
5c319da
Session card: badge the toggle while side chats or subagents run
jsgrrchg Oct 12, 2026
dd4a57e
Fix side chat rename opening the file explorer
jsgrrchg Oct 12, 2026
23035c4
Session card: cap the lanes card at half the window's height
jsgrrchg Oct 12, 2026
7e0fa50
Scroll side chat rename fields into view
jsgrrchg Oct 12, 2026
07cdfc0
Session card: a click on a side chat only opens it
jsgrrchg Oct 12, 2026
56a37f7
Demo: a session fanning out 40 subagents for the lanes card
jsgrrchg Oct 12, 2026
e97183d
Merge remote-tracking branch 'upstream/main' into feat/session-card-l…
jsgrrchg Oct 13, 2026
c90ff64
Session card: side chats wear their harness mark
jsgrrchg Oct 13, 2026
a1f1b44
Session card: the lanes card scrolls with the floating rail
jsgrrchg Oct 14, 2026
a1071ca
Demo: let a second demo pick its own state and port
jsgrrchg Oct 14, 2026
cba4483
Merge remote-tracking branch 'upstream/main' into feat/session-card-l…
jsgrrchg Oct 14, 2026
54c2172
Session card: drop the side chats' track
jsgrrchg Oct 14, 2026
e701980
Session card: side chat starters move to the heading once branches exist
jsgrrchg Oct 14, 2026
693153f
Session card: subagent rows carry the session's harness mark
jsgrrchg Oct 14, 2026
0713428
Session card: drop the failed count from the finished fold
jsgrrchg Oct 14, 2026
b591baf
Session card: the branch row shows the checkout's uncommitted +/- totals
jsgrrchg Oct 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 177 additions & 1 deletion crates/harness/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,108 @@ pub struct MockHarness {
pub script: Vec<AgentEvent>,
}

/// The subagent count a `/fanout [N]` prompt asks for (40 when bare).
fn fanout_count(prompt: &str) -> Option<usize> {
let rest = prompt.trim().strip_prefix("/fanout")?;
if !rest.is_empty() && !rest.starts_with(char::is_whitespace) {
return None;
}
let count = rest.trim().parse().unwrap_or(40);
Some(count.clamp(1, 200))
}

fn fanout_id(i: usize) -> String {
format!("mock-fanout-{i}")
}

fn fanout_tag(i: usize, event: AgentEvent) -> AgentEvent {
AgentEvent::Subagent {
parent_tool_use_id: fanout_id(i),
event: Box::new(event),
}
}

/// The `/fanout` opening: a line of text, then `count` spawn chips in the
/// claude driver's shape, each seeded with its task like a Task prompt.
fn fanout_spawns(count: usize) -> Vec<AgentEvent> {
const VERBS: [&str; 5] = ["Audit", "Map", "Profile", "Verify", "Trace"];
const AREAS: [&str; 8] = [
"the fold path",
"the commit cadence",
"the sync client",
"the registry doc",
"the composer dock",
"the sidebar sort",
"the diff reconciler",
"the relay transport",
];
let mut events = vec![AgentEvent::TextDelta {
text: format!("\n### Fan-out\n\nSending {count} scouts across the workspace.\n\n"),
}];
for i in 0..count {
let description = format!(
"{} {}",
VERBS[i % VERBS.len()],
AREAS[(i / VERBS.len()) % AREAS.len()]
);
let prompt = format!("{description} and report anything that breaks its invariants.");
events.push(AgentEvent::ToolCall {
id: fanout_id(i),
call: zeron_proto::ToolCall::Unknown {
name: format!("Agent: {description}"),
input: Some(serde_json::json!({
"description": description,
"prompt": prompt,
})),
},
});
events.push(fanout_tag(i, AgentEvent::UserMessage { text: prompt }));
events.push(fanout_tag(
i,
AgentEvent::TextDelta {
text: "Reading the code paths involved.\n\n".into(),
},
));
}
events
}

/// The `i`th scout's report and settle; every seventh fails.
fn fanout_settle(i: usize) -> Vec<AgentEvent> {
let failed = i % 7 == 6;
vec![
fanout_tag(
i,
AgentEvent::TextDelta {
text: if failed {
"Could not reproduce the setup; giving up.".into()
} else {
"Invariants hold; nothing to report.".into()
},
},
),
AgentEvent::ToolResult {
id: fanout_id(i),
is_error: failed,
output: None,
diff: None,
},
fanout_tag(
i,
AgentEvent::Done {
status: if failed {
DoneStatus::Errored
} else {
DoneStatus::Completed
},
result: None,
error: None,
session_id: None,
},
),
]
}

/// The scripted question set for the `ZERON_MOCK_QUESTION` variant (exercises
/// the QuestionPanel end-to-end: single-select page, multi-select page).
fn question_script() -> Vec<UserInputQuestion> {
Expand Down Expand Up @@ -100,7 +202,7 @@ impl Harness for MockHarness {

async fn run(
&self,
_request: RunRequest,
request: RunRequest,
controls: RunControls,
) -> Result<BoxStream<'static, Result<AgentEvent, HarnessError>>, HarnessError> {
// Optional pacing knob for demos/manual testing: `ZERON_MOCK_DELAY_MS`
Expand Down Expand Up @@ -166,6 +268,49 @@ impl Harness for MockHarness {
return Ok(stream.boxed());
}

// Dev/testing knob: a `/fanout N` prompt spawns N subagents at once,
// then settles them one by one, oldest first (every seventh fails) —
// the only data-side way to fill the session card's subagent lane,
// scroll it, and watch it drain into its finished fold. They settle
// `ZERON_MOCK_DELAY_MS` apart, 2.5s by default.
if let Some(count) = fanout_count(&request.prompt) {
let settle = if delay_ms == 0 {
std::time::Duration::from_millis(2500)
} else {
delay
};
let (tx, rx) = tokio::sync::mpsc::unbounded_channel::<AgentEvent>();
tokio::spawn(async move {
for event in fanout_spawns(count) {
if tx.send(event).is_err() {
return;
}
tokio::time::sleep(std::time::Duration::from_millis(40)).await;
}
for i in 0..count {
tokio::time::sleep(settle).await;
for event in fanout_settle(i) {
if tx.send(event).is_err() {
return;
}
}
}
let _ = tx.send(AgentEvent::TextDelta {
text: format!("All {count} scouts reported back."),
});
let _ = tx.send(AgentEvent::Done {
status: DoneStatus::Completed,
result: None,
error: None,
session_id: None,
});
});
let stream = futures::stream::unfold(rx, |mut rx| async move {
rx.recv().await.map(|event| (Ok(event), rx))
});
return Ok(stream.boxed());
}

// Dev/testing knob: `ZERON_MOCK_REPEAT=N` loops the script body N times
// before the final Done — long single-reply streams for frame-cost /
// smoothness measurement (the terminal `Done` is emitted exactly once,
Expand Down Expand Up @@ -650,3 +795,34 @@ impl Harness for MockHarness {
.boxed())
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn fanout_reads_its_count_from_the_prompt() {
assert_eq!(fanout_count("/fanout"), Some(40));
assert_eq!(fanout_count(" /fanout 12 "), Some(12));
assert_eq!(fanout_count("/fanout 9000"), Some(200));
assert_eq!(fanout_count("/fanouts"), None);
assert_eq!(fanout_count("Walk me through the pipeline"), None);
}

#[test]
fn fanout_spawns_every_scout_and_every_seventh_fails() {
let spawns = fanout_spawns(40)
.into_iter()
.filter(|event| matches!(event, AgentEvent::ToolCall { .. }))
.count();
assert_eq!(spawns, 40);
let failed = (0..40)
.filter(|&i| {
fanout_settle(i)
.iter()
.any(|event| matches!(event, AgentEvent::ToolResult { is_error: true, .. }))
})
.count();
assert_eq!(failed, 5);
}
}
30 changes: 0 additions & 30 deletions crates/ui/src/files/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ pub mod mutations;
pub mod preview;
mod rename;
pub mod search;
mod sections;
mod subagents;
#[cfg(test)]
pub(crate) mod test_support;
pub mod tree;
Expand Down Expand Up @@ -254,24 +252,6 @@ pub enum FilesEvent {
ShowAllFilesChanged(bool),
CloseReady,
CloseCancelled,
/// A footer row: open this subagent's transcript in the right pane.
OpenSubagent {
doc_id: String,
title: String,
frozen: bool,
},
/// A footer row: open this side chat (by id) in the right pane.
OpenChildChat(String),
/// A footer row's double-click: edit this side chat's title in place.
RenameChildChat(String),
ChildChatContextMenu {
chat_id: String,
position: Point<Pixels>,
},
/// The Chats header's "+": start a fresh side chat of the active chat.
NewChildChat,
/// The Chats header's fork: fork the active chat into a side chat.
ForkChat,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
Expand Down Expand Up @@ -347,8 +327,6 @@ pub struct FilesSurface {
loads: HashMap<(String, Option<String>), Task<()>>,
error: Option<SharedString>,
started: bool,
/// The Subagents / Chats footer under the tree.
sections: sections::ExplorerSections,
_observe: Subscription,
_search_events: Subscription,
}
Expand Down Expand Up @@ -377,9 +355,6 @@ impl Render for FilesSurface {
self.render_explorer(&theme, cx).into_any_element()
};
let editor_context_menu = self.render_editor_context_menu(&theme, cx);
// The explorer docks its Subagents / Chats sections under the tree;
// an editor surface has no footer.
let sections = (!is_editor).then(|| self.render_sections(&theme, cx));
div()
.id(SharedString::from(format!(
"files-surface-{}",
Expand All @@ -399,7 +374,6 @@ impl Render for FilesSurface {
.as_ref()
.map(|message| crate::popover::error_row(&theme, message).into_any_element()),
)
.children(sections)
.children(editor_context_menu)
.children(self.render_tree_context_menu(&theme, cx))
.children(self.render_tree_delete(&theme, window, cx))
Expand Down Expand Up @@ -674,9 +648,6 @@ impl FilesSurface {
this.ensure_loaded(cx);
}
this.sync_active_markdown_comments(cx);
if !this.presentation.is_editor() {
this.refresh_sections(cx);
}
});
// The list state exposes no scroll handle, so the floating rail
// bridges scroll activity through the scroll handler (the
Expand Down Expand Up @@ -739,7 +710,6 @@ impl FilesSurface {
loads: HashMap::new(),
error: None,
started: false,
sections: sections::ExplorerSections::default(),
_observe: observe,
_search_events: search_events,
};
Expand Down
Loading
Loading