Synced from monorepo
Synced from monorepo Changes: - Shell: accept target response id on rewind execute - Shell: stamp response id on chat user message chunks - Worktree: optional rebuild and stale git registration cleanup in auto-GC - Worktree: kind-aware auto-GC TTLs and config knobs - Worktree: macOS process CWD scan and Unix PID liveness for GC guards - Worktree: automatic throttled GC on startup (Linux age-based; non-Linux dead-only) - Pager: add `[ui].combine_queued_prompts` to batch queued follow-ups - Shell: stop overwriting user skills - Tools: read markdown in `skills/` directories untruncated - `/usage` shows per-session token and dollar usage in the TUI - Security: prompt on environment-dumping `ps` variants - Security: always-safe `kubectl` no longer runs arbitrary kubeconfig credential plugins without permission - Tools: make scheduler deletion durable - Shell: add relocation storage primitives - Shell: give side model calls their own conversation ids - Fix five workflow-runtime bugs (budget, pause, cancel, reconnect) - Security: peel `env -S` / `--split-string` operands in the Bash permission gate (managed deny/ask) - Pager: expose doctor in the TUI - Security: block unauthorized RCE via abused safe commands - Pager idle watcher cue: "1 subagent still running" instead of "watching · 1 subagent" - Security: block `rg --pre` arbitrary code execution in auto-mode - Voice: diagnose silent-mic failures (macOS permission) and add doctor/terminal-setup Voice section - App builder deployer: `allow_forking` and `show_built_with_grok` - Pager: stop stacking duplicate "Worked for" markers on parked turns - Shell: support `max` as a distinct reasoning effort tier - Tools: serialize background `/loop` fires on the whole work unit - Shell: add working-directory relocation state primitives - Proto: `ClientToolResult` and `ChatConfig` client-side tools - Shell: model providers - Chat: select App Builder product on the Build path - Shell: attach author identity to feedback when the deployment opts in - Doctor: fix for SSH wrap setup - Workflow authoring skills: create-workflow and import-claude-workflow docs - Add read-only grok doctor - Sandbox: apply Landlock without a controlling TTY - Pager: recover image paste over grok wrap on headless remotes - Pager: make actions screen-mode aware - Shell: resume sessions when the working directory moves - Pager: centralize terminal diagnostics - Workspace: gate inline shell file access - Pager: centralize terminal probes - Pager: edit minimal prompts in an external editor - Pager: standardize backgrounding on Ctrl+B - Shell: recap rides the parent turn's prompt cache - Tools: add scheduler lifecycle version clock Source-Revision: 0f4d7c91b8b2b408333f6de1e8a76cb8eaa71899
This commit is contained in:
parent
a881e6703f
commit
3af4d5d398
556 changed files with 56609 additions and 21892 deletions
|
|
@ -32,7 +32,7 @@ use crate::theme::Theme;
|
|||
pub(crate) const SPINNER_DIVISOR: u64 = 4;
|
||||
|
||||
/// Show each monitor-pulse frame for this many animation ticks — twice the
|
||||
/// [`SPINNER_DIVISOR`] dwell (~3.75 fps). The idle "watching" cue should
|
||||
/// [`SPINNER_DIVISOR`] dwell (~3.75 fps). The idle still-running cue should
|
||||
/// breathe calmly rather than read like the active turn spinner, so its
|
||||
/// `○ ◎ ◉ ◎` cycle runs at roughly half the speed (~1.07s per loop).
|
||||
pub(crate) const MONITOR_PULSE_DIVISOR: u64 = 8;
|
||||
|
|
@ -81,7 +81,7 @@ pub struct TurnStatusOutput {
|
|||
/// `Some(_)` to [`render_turn_status`] renders the buttons; passing `None`
|
||||
/// marks a keyboard-only host (minimal mode has no mouse capture) and suppresses
|
||||
/// both — that host cancels the turn via `Ctrl+C` and sends to background via
|
||||
/// `Ctrl+G` instead.
|
||||
/// `Ctrl+B` instead.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
pub struct MouseButtons {
|
||||
/// Whether the mouse is over the `[stop]` cancel button.
|
||||
|
|
@ -93,7 +93,7 @@ pub struct MouseButtons {
|
|||
/// Counts of idle-surviving "watcher" work — background jobs that can wake
|
||||
/// the agent for a new turn while it sits idle (commands and monitors on
|
||||
/// completion/events, `/loop` tasks on a timer, background subagents on
|
||||
/// finish). They share one persistent "watching" cue above the prompt.
|
||||
/// finish). They share one persistent still-running cue above the prompt.
|
||||
/// Broader than the tasks-pane `Watchers` group (monitors + loops only).
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
|
||||
pub struct Watchers {
|
||||
|
|
@ -107,59 +107,63 @@ pub struct Watchers {
|
|||
/// subagent is a background one — a foreground subagent would keep the
|
||||
/// parent in `TurnRunning`.
|
||||
pub subagents: usize,
|
||||
pub workflows: usize,
|
||||
}
|
||||
|
||||
impl Watchers {
|
||||
/// Total watcher count across all kinds.
|
||||
pub fn total(self) -> usize {
|
||||
self.commands + self.monitors + self.loops + self.subagents
|
||||
self.commands + self.monitors + self.loops + self.subagents + self.workflows
|
||||
}
|
||||
|
||||
/// Awaitable in-flight work — the kinds a blocking `wait_tasks` /
|
||||
/// `get_task_output` wait can resolve on (commands, monitors, subagents;
|
||||
/// scheduled `/loop` tasks are timers, not awaitable work).
|
||||
/// scheduled `/loop` tasks and workflows are not task waits).
|
||||
pub fn awaitable_work(self) -> usize {
|
||||
self.commands + self.monitors + self.subagents
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the "watching · …" label for the idle watcher cue, listing only the
|
||||
/// non-zero kinds with correct singular/plural nouns — e.g.
|
||||
/// `"watching · 1 command · 2 monitors · 1 loop · 1 subagent"`. Assumes
|
||||
/// `watchers.total() > 0`.
|
||||
fn watching_label(watchers: Watchers) -> String {
|
||||
/// Format a counts-first `"… still running"` cue from `(count, noun)` pairs,
|
||||
/// listing only the non-zero kinds (plain-`s` plurals) — e.g.
|
||||
/// `"1 command · 2 monitors still running"`. `None` when every count is
|
||||
/// zero. Single owner of the format mechanics so the agent view's idle cue
|
||||
/// and the dashboard's background-work label cannot drift.
|
||||
pub(crate) fn format_still_running<'a>(
|
||||
kinds: impl IntoIterator<Item = (usize, &'a str)>,
|
||||
) -> Option<String> {
|
||||
use std::fmt::Write as _;
|
||||
let mut label = String::with_capacity(32);
|
||||
label.push_str("watching");
|
||||
if watchers.commands > 0 {
|
||||
let noun = if watchers.commands == 1 {
|
||||
"command"
|
||||
} else {
|
||||
"commands"
|
||||
};
|
||||
let _ = write!(label, " \u{00b7} {} {noun}", watchers.commands);
|
||||
let mut label = String::with_capacity(48);
|
||||
for (count, noun) in kinds {
|
||||
if count == 0 {
|
||||
continue;
|
||||
}
|
||||
if !label.is_empty() {
|
||||
label.push_str(" \u{00b7} ");
|
||||
}
|
||||
let plural = if count == 1 { "" } else { "s" };
|
||||
let _ = write!(label, "{count} {noun}{plural}");
|
||||
}
|
||||
if watchers.monitors > 0 {
|
||||
let noun = if watchers.monitors == 1 {
|
||||
"monitor"
|
||||
} else {
|
||||
"monitors"
|
||||
};
|
||||
let _ = write!(label, " \u{00b7} {} {noun}", watchers.monitors);
|
||||
if label.is_empty() {
|
||||
return None;
|
||||
}
|
||||
if watchers.loops > 0 {
|
||||
let noun = if watchers.loops == 1 { "loop" } else { "loops" };
|
||||
let _ = write!(label, " \u{00b7} {} {noun}", watchers.loops);
|
||||
}
|
||||
if watchers.subagents > 0 {
|
||||
let noun = if watchers.subagents == 1 {
|
||||
"subagent"
|
||||
} else {
|
||||
"subagents"
|
||||
};
|
||||
let _ = write!(label, " \u{00b7} {} {noun}", watchers.subagents);
|
||||
}
|
||||
label
|
||||
label.push_str(" still running");
|
||||
Some(label)
|
||||
}
|
||||
|
||||
/// The idle watcher cue's label — e.g.
|
||||
/// `"1 command · 2 monitors · 1 loop · 1 subagent still running"`. Leads
|
||||
/// with the counts (not an ambient "watching") so a glance under a
|
||||
/// "Worked for X" marker still reads as unfinished work. `None` when no
|
||||
/// watchers are live.
|
||||
fn still_running_label(watchers: Watchers) -> Option<String> {
|
||||
format_still_running([
|
||||
(watchers.commands, "command"),
|
||||
(watchers.monitors, "monitor"),
|
||||
(watchers.loops, "loop"),
|
||||
(watchers.subagents, "subagent"),
|
||||
(watchers.workflows, "workflow"),
|
||||
])
|
||||
}
|
||||
|
||||
/// Whether the turn is blocked in a wait the shell aborts as soon as the
|
||||
|
|
@ -199,7 +203,7 @@ pub fn is_sendable_wait(activity: &Option<TurnActivity>) -> bool {
|
|||
/// - `total_tokens`: Total tokens used (context window usage), shown as `⇣Nk`.
|
||||
/// - `parked`: the turn is parked on a sendable wait and renders the stopped
|
||||
/// look (`AgentView::renders_parked`). The running-turn chrome is suppressed;
|
||||
/// only the "watching · …" cue renders (the parked turn is by definition
|
||||
/// only the "… still running" cue renders (the parked turn is by definition
|
||||
/// waiting on background work, so the cue explains the idle-looking chrome).
|
||||
/// - `flat_background`: when `true`, right-side timer/buttons use a transparent
|
||||
/// (`Color::Reset`) background instead of `theme.bg_base`, so the row blends
|
||||
|
|
@ -274,13 +278,15 @@ pub fn render_turn_status(
|
|||
return TurnStatusOutput::default();
|
||||
}
|
||||
|
||||
// Idle or parked with watchers: persistent watching cue (not scrollback
|
||||
// — it must never scroll away). Lower priority than the starting-session
|
||||
// and drain-blocked cues above.
|
||||
if (state.is_idle() || parked) && watchers.total() > 0 {
|
||||
// Idle or parked with watchers: persistent still-running cue (not
|
||||
// scrollback — it must never scroll away). Lower priority than the
|
||||
// starting-session and drain-blocked cues above.
|
||||
if (state.is_idle() || parked)
|
||||
&& let Some(cue) = still_running_label(watchers)
|
||||
{
|
||||
// Pulsing concentric circle (○ ◎ ◉ ◎) on a calm ambient cadence:
|
||||
// the agent is idle, so this "watching" breath runs slower than the
|
||||
// active turn spinner (see MONITOR_PULSE_DIVISOR).
|
||||
// the agent is idle, so this breath runs slower than the active
|
||||
// turn spinner (see MONITOR_PULSE_DIVISOR).
|
||||
let frames = crate::glyphs::monitor_icon_frames();
|
||||
let frame_idx = (tick / MONITOR_PULSE_DIVISOR) as usize % frames.len();
|
||||
let spans = vec![
|
||||
|
|
@ -288,7 +294,7 @@ pub fn render_turn_status(
|
|||
format!("{} ", frames[frame_idx]),
|
||||
Style::default().fg(theme.accent_system),
|
||||
),
|
||||
Span::styled(watching_label(watchers), Style::default().fg(theme.gray)),
|
||||
Span::styled(cue, Style::default().fg(theme.gray)),
|
||||
];
|
||||
buf.set_line(area.x, area.y, &Line::from(spans), area.width);
|
||||
return TurnStatusOutput::default();
|
||||
|
|
@ -758,7 +764,7 @@ fn render_starting_session(
|
|||
///
|
||||
/// A parked turn (`parked` — the stopped look while blocked on a sendable
|
||||
/// wait) suppresses the running-turn chrome entirely: the row shows only when
|
||||
/// watchers exist, rendering the "watching · …" cue.
|
||||
/// watchers exist, rendering the "… still running" cue.
|
||||
///
|
||||
/// Real MCP progress (`total > 0`) renders as a compact chip in the top status
|
||||
/// bar instead, so it does not affect this row.
|
||||
|
|
@ -1004,7 +1010,7 @@ mod tests {
|
|||
#[test]
|
||||
fn should_show_when_watchers_running() {
|
||||
// Idle but a watcher (command, monitor, loop, or subagent) is still
|
||||
// running → row stays visible so the persistent "watching · …" cue
|
||||
// running → row stays visible so the persistent "… still running" cue
|
||||
// can show.
|
||||
for watchers in [
|
||||
Watchers {
|
||||
|
|
@ -1039,7 +1045,7 @@ mod tests {
|
|||
#[test]
|
||||
fn should_show_parked_only_with_watchers() {
|
||||
// Parked (turn running but rendering the stopped look): the row shows
|
||||
// only to carry the "watching · …" cue — never the running chrome.
|
||||
// only to carry the "… still running" cue — never the running chrome.
|
||||
assert!(should_show(
|
||||
&AgentState::TurnRunning,
|
||||
false,
|
||||
|
|
@ -1149,7 +1155,12 @@ mod tests {
|
|||
/// Invoke `render_turn_status` for an idle agent with the given watcher
|
||||
/// counts at animation tick `tick`.
|
||||
fn render_idle_with_watchers_at_tick(watchers: Watchers, tick: u64) -> String {
|
||||
let area = Rect::new(0, 0, 60, 1);
|
||||
render_idle_with_watchers_in_width(watchers, tick, 72)
|
||||
}
|
||||
|
||||
/// [`render_idle_with_watchers_at_tick`] with an explicit row width.
|
||||
fn render_idle_with_watchers_in_width(watchers: Watchers, tick: u64, width: u16) -> String {
|
||||
let area = Rect::new(0, 0, width, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
render_turn_status(
|
||||
&mut buf,
|
||||
|
|
@ -1179,7 +1190,7 @@ mod tests {
|
|||
/// Invoke `render_turn_status` for a PARKED running turn (the stopped
|
||||
/// look) with the given watcher counts.
|
||||
fn render_parked_with_watchers(watchers: Watchers) -> String {
|
||||
let area = Rect::new(0, 0, 60, 1);
|
||||
let area = Rect::new(0, 0, 72, 1);
|
||||
let mut buf = Buffer::empty(area);
|
||||
render_turn_status(
|
||||
&mut buf,
|
||||
|
|
@ -1231,11 +1242,11 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn idle_with_monitors_renders_watching_line() {
|
||||
fn idle_with_monitors_renders_still_running_cue() {
|
||||
let text = render_idle_with_monitors(2);
|
||||
assert!(
|
||||
text.contains("watching") && text.contains("2 monitors"),
|
||||
"idle with monitors must render the watching cue, got: {text:?}"
|
||||
text.contains("2 monitors still running"),
|
||||
"idle with monitors must render the still-running cue, got: {text:?}"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1243,7 +1254,7 @@ mod tests {
|
|||
fn idle_with_one_monitor_uses_singular() {
|
||||
let text = render_idle_with_monitors(1);
|
||||
assert!(
|
||||
text.contains("watching \u{00b7} 1 monitor") && !text.contains("monitors"),
|
||||
text.contains("1 monitor still running") && !text.contains("monitors"),
|
||||
"single monitor must use the singular noun, got: {text:?}"
|
||||
);
|
||||
}
|
||||
|
|
@ -1258,14 +1269,14 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn idle_with_loops_renders_watching_line() {
|
||||
fn idle_with_loops_renders_still_running_cue() {
|
||||
let text = render_idle_with_watchers(Watchers {
|
||||
loops: 2,
|
||||
..Watchers::default()
|
||||
});
|
||||
assert!(
|
||||
text.contains("watching") && text.contains("2 loops"),
|
||||
"idle with loops must render the watching cue, got: {text:?}"
|
||||
text.contains("2 loops still running"),
|
||||
"idle with loops must render the still-running cue, got: {text:?}"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1276,20 +1287,20 @@ mod tests {
|
|||
..Watchers::default()
|
||||
});
|
||||
assert!(
|
||||
text.contains("watching \u{00b7} 1 loop") && !text.contains("loops"),
|
||||
text.contains("1 loop still running") && !text.contains("loops"),
|
||||
"single loop must use the singular noun, got: {text:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idle_with_subagents_renders_watching_line() {
|
||||
fn idle_with_subagents_renders_still_running_cue() {
|
||||
let text = render_idle_with_watchers(Watchers {
|
||||
subagents: 2,
|
||||
..Watchers::default()
|
||||
});
|
||||
assert!(
|
||||
text.contains("watching") && text.contains("2 subagents"),
|
||||
"idle with subagents must render the watching cue, got: {text:?}"
|
||||
text.contains("2 subagents still running"),
|
||||
"idle with subagents must render the still-running cue, got: {text:?}"
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -1300,11 +1311,20 @@ mod tests {
|
|||
..Watchers::default()
|
||||
});
|
||||
assert!(
|
||||
text.contains("watching \u{00b7} 1 subagent") && !text.contains("subagents"),
|
||||
text.contains("1 subagent still running") && !text.contains("subagents"),
|
||||
"single subagent must use the singular noun, got: {text:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idle_with_one_workflow_counts_run_once() {
|
||||
let text = render_idle_with_watchers(Watchers {
|
||||
workflows: 1,
|
||||
..Watchers::default()
|
||||
});
|
||||
assert!(text.contains("1 workflow still running"), "got: {text:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idle_with_monitors_and_loops_lists_both() {
|
||||
// Both watcher kinds present → one cue lists monitors then loops,
|
||||
|
|
@ -1315,7 +1335,7 @@ mod tests {
|
|||
..Watchers::default()
|
||||
});
|
||||
assert!(
|
||||
text.contains("watching \u{00b7} 1 monitor \u{00b7} 2 loops"),
|
||||
text.contains("1 monitor \u{00b7} 2 loops still running"),
|
||||
"both kinds must be listed in one cue, got: {text:?}"
|
||||
);
|
||||
}
|
||||
|
|
@ -1329,17 +1349,37 @@ mod tests {
|
|||
monitors: 2,
|
||||
loops: 1,
|
||||
subagents: 3,
|
||||
workflows: 0,
|
||||
});
|
||||
assert!(
|
||||
text.contains(
|
||||
"watching \u{00b7} 1 command \u{00b7} 2 monitors \u{00b7} 1 loop \u{00b7} 3 subagents"
|
||||
"1 command \u{00b7} 2 monitors \u{00b7} 1 loop \u{00b7} 3 subagents still running"
|
||||
),
|
||||
"all kinds must be listed in one cue, got: {text:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idle_with_commands_renders_watching_line() {
|
||||
fn narrow_area_clips_cue_tail_keeping_counts() {
|
||||
// 40 cols with three kinds: the row tail-clips with no ellipsis, so
|
||||
// the leading counts survive and the trailing suffix is what gets
|
||||
// cut. Pins the narrow-pane tradeoff of leading with the counts; a
|
||||
// smarter compact fallback would be a behavior change.
|
||||
let watchers = Watchers {
|
||||
commands: 1,
|
||||
monitors: 2,
|
||||
loops: 1,
|
||||
..Watchers::default()
|
||||
};
|
||||
let text = render_idle_with_watchers_in_width(watchers, 0, 40);
|
||||
assert!(
|
||||
text.contains("1 command \u{00b7} 2 monitors \u{00b7} 1 loop"),
|
||||
"the counts must survive the clip, got: {text:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn idle_with_commands_renders_still_running_cue() {
|
||||
// Plain background commands (non-monitor bg tasks) count as watchers:
|
||||
// they wake the agent with a task-completed turn, so the cue must show.
|
||||
let text = render_idle_with_watchers(Watchers {
|
||||
|
|
@ -1347,22 +1387,22 @@ mod tests {
|
|||
..Watchers::default()
|
||||
});
|
||||
assert!(
|
||||
text.contains("watching \u{00b7} 2 commands"),
|
||||
"idle with bg commands must render the watching cue, got: {text:?}"
|
||||
text.contains("2 commands still running"),
|
||||
"idle with bg commands must render the still-running cue, got: {text:?}"
|
||||
);
|
||||
let text = render_idle_with_watchers(Watchers {
|
||||
commands: 1,
|
||||
..Watchers::default()
|
||||
});
|
||||
assert!(
|
||||
text.contains("watching \u{00b7} 1 command") && !text.contains("commands"),
|
||||
text.contains("1 command still running") && !text.contains("commands"),
|
||||
"single command must use the singular noun, got: {text:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parked_with_watchers_renders_watching_not_running_chrome() {
|
||||
// A parked running turn renders the watching cue — never the busy
|
||||
fn parked_with_watchers_renders_cue_not_running_chrome() {
|
||||
// A parked running turn renders the still-running cue — never the busy
|
||||
// spinner/timers/[stop] chrome (the wait aborts as soon as the user
|
||||
// types, so that chrome would lie).
|
||||
let text = render_parked_with_watchers(Watchers {
|
||||
|
|
@ -1370,8 +1410,8 @@ mod tests {
|
|||
..Watchers::default()
|
||||
});
|
||||
assert!(
|
||||
text.contains("watching \u{00b7} 2 commands"),
|
||||
"parked with bg work must render the watching cue, got: {text:?}"
|
||||
text.contains("2 commands still running"),
|
||||
"parked with bg work must render the still-running cue, got: {text:?}"
|
||||
);
|
||||
assert!(
|
||||
!text.contains("Waiting") && !text.contains("[stop]"),
|
||||
|
|
@ -1431,52 +1471,57 @@ mod tests {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn watching_label_lists_only_nonzero_kinds() {
|
||||
fn still_running_label_lists_only_nonzero_kinds() {
|
||||
assert_eq!(
|
||||
watching_label(Watchers {
|
||||
still_running_label(Watchers {
|
||||
commands: 2,
|
||||
..Watchers::default()
|
||||
}),
|
||||
"watching \u{00b7} 2 commands"
|
||||
Some("2 commands still running".into())
|
||||
);
|
||||
assert_eq!(
|
||||
watching_label(Watchers {
|
||||
still_running_label(Watchers {
|
||||
monitors: 2,
|
||||
..Watchers::default()
|
||||
}),
|
||||
"watching \u{00b7} 2 monitors"
|
||||
Some("2 monitors still running".into())
|
||||
);
|
||||
assert_eq!(
|
||||
watching_label(Watchers {
|
||||
still_running_label(Watchers {
|
||||
loops: 1,
|
||||
..Watchers::default()
|
||||
}),
|
||||
"watching \u{00b7} 1 loop"
|
||||
Some("1 loop still running".into())
|
||||
);
|
||||
assert_eq!(
|
||||
watching_label(Watchers {
|
||||
still_running_label(Watchers {
|
||||
subagents: 1,
|
||||
..Watchers::default()
|
||||
}),
|
||||
"watching \u{00b7} 1 subagent"
|
||||
Some("1 subagent still running".into())
|
||||
);
|
||||
assert_eq!(
|
||||
watching_label(Watchers {
|
||||
still_running_label(Watchers {
|
||||
monitors: 1,
|
||||
loops: 2,
|
||||
..Watchers::default()
|
||||
}),
|
||||
"watching \u{00b7} 1 monitor \u{00b7} 2 loops"
|
||||
Some("1 monitor \u{00b7} 2 loops still running".into())
|
||||
);
|
||||
assert_eq!(
|
||||
watching_label(Watchers {
|
||||
still_running_label(Watchers {
|
||||
commands: 1,
|
||||
monitors: 1,
|
||||
loops: 1,
|
||||
subagents: 2,
|
||||
workflows: 0,
|
||||
}),
|
||||
"watching \u{00b7} 1 command \u{00b7} 1 monitor \u{00b7} 1 loop \u{00b7} 2 subagents"
|
||||
Some(
|
||||
"1 command \u{00b7} 1 monitor \u{00b7} 1 loop \u{00b7} 2 subagents still running"
|
||||
.into()
|
||||
)
|
||||
);
|
||||
assert_eq!(still_running_label(Watchers::default()), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in a new issue