mirror of
https://github.com/codewhale-hq/Codewhale.git
synced 2026-11-08 17:10:50 -10:00
fix(tui): a shell card waiting on approval says so instead of "running (Ns)"
The engine emits ToolCallStarted before it asks for approval, so the shell card was created with started_at = now and rendered "running" with a braille spinner and a climbing "(Ns)" badge for the whole time the approval prompt sat open. Nothing had run. Observed in a real TUI against the cw7087 mock: "⣦ ▶ run running (84s) · echo later-01" above the approval card (CW/artifacts/cw6993-idle-20261018/ab-after-1-screen.txt). ExecCell gains awaiting_approval. When the approval prompt is raised (the Prompt disposition only; auto-approve and auto-deny paths never show a card), set_exec_card_awaiting_approval marks the card and clears started_at. note_human_decision_delivered, which every delivered human decision passes through (the modal, the web mirror, a timed-out denial), clears the mark and starts the clock, so the elapsed badge measures the command, not the wait. While marked, the header reads "waiting for approval" with the static low-motion glyph, and has_live_motion is false for that cell. The activity detail duration reads 0 while waiting for the same reason. Verified in a real TUI (debug build, mock provider, isolated HOME; CW/artifacts/approval-card-20261018/): - waiting, 12 s and 20 s: "● ▶ run waiting for approval · echo later-01" - after y: "✓ ▶ run done · echo later-01", next card waiting - after n: "✕ ▶ run failed · echo later-01", next card waiting Checks: - cargo test -p codewhale-tui --lib -- tui::history tui::tool_routing tui::transcript tui::active_cell approval exec_cell shell_card: 609 passed; 0 failed; 2 ignored - cargo clippy -p codewhale-tui --all-targets --all-features --locked with CI's flags: clean; cargo fmt --check: clean Known limits: only shell (Exec) cards carry the mark; other tool cards that need approval have no elapsed badge and still say "running". The label is English, like the neighbouring "running" and "failed" labels. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01NBKbhx19BRbZtvWK8rVKjz
This commit is contained in:
1 parent
eeb3f46471
commit
a83205733e
11 files changed
+58
-8
No files matched your search
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- A shell card whose command is waiting on your approval now says
|
||||
"waiting for approval" with a still mark, instead of "running (Ns)" with
|
||||
a spinner. Its elapsed time starts when you approve and the command
|
||||
actually runs.
|
||||
- `/color` gives a chat's composer box its own color. Bare `/color` cycles
|
||||
through red, orange, amber, green, cyan, blue, purple and pink; `/color
|
||||
<name>` picks one and `/color off` returns to the theme. The color is
|
||||
|
||||
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
- A shell card whose command is waiting on your approval now says
|
||||
"waiting for approval" with a still mark, instead of "running (Ns)" with
|
||||
a spinner. Its elapsed time starts when you approve and the command
|
||||
actually runs.
|
||||
- `/color` gives a chat's composer box its own color. Bare `/color` cycles
|
||||
through red, orange, amber, green, cyan, blue, purple and pink; `/color
|
||||
<name>` picks one and `/color off` returns to the theme. The color is
|
||||
|
||||
@@ -327,6 +327,7 @@ mod tests {
|
||||
started_at: Some(Instant::now()),
|
||||
duration_ms: None,
|
||||
stale_elapsed_since_output_ms: None,
|
||||
awaiting_approval: false,
|
||||
source: ExecSource::Assistant,
|
||||
description: None,
|
||||
interaction: None,
|
||||
|
||||
@@ -300,6 +300,7 @@ impl HistoryCell {
|
||||
{
|
||||
false
|
||||
}
|
||||
HistoryCell::Tool(ToolCell::Exec(exec)) if exec.awaiting_approval => false,
|
||||
HistoryCell::Tool(tool) => tool.is_running(),
|
||||
HistoryCell::User { .. }
|
||||
| HistoryCell::System { .. }
|
||||
@@ -1053,6 +1054,7 @@ pub struct ExecCell {
|
||||
pub started_at: Option<Instant>,
|
||||
pub duration_ms: Option<u64>,
|
||||
pub stale_elapsed_since_output_ms: Option<u64>,
|
||||
pub awaiting_approval: bool,
|
||||
pub source: ExecSource,
|
||||
pub interaction: Option<String>,
|
||||
/// Cached output summary — avoids re-parsing JSON every frame.
|
||||
@@ -1099,26 +1101,26 @@ impl ExecCell {
|
||||
.interaction
|
||||
.as_deref()
|
||||
.or(Some(command_summary.as_str()));
|
||||
let stale_status = self
|
||||
.stale_elapsed_since_output_ms
|
||||
.map(stale_shell_status_label);
|
||||
let held_status = if self.awaiting_approval && self.status == ToolStatus::Running {
|
||||
Some("waiting for approval".to_string())
|
||||
} else {
|
||||
self.stale_elapsed_since_output_ms
|
||||
.map(stale_shell_status_label)
|
||||
};
|
||||
let receipt = tool_receipt_label(
|
||||
crate::tui::widgets::tool_card::ToolFamily::Run,
|
||||
self.status,
|
||||
self.output.as_deref(),
|
||||
locale,
|
||||
);
|
||||
let status_text = stale_status
|
||||
.as_deref()
|
||||
.map(Cow::Borrowed)
|
||||
.unwrap_or(receipt);
|
||||
let status_text = held_status.as_deref().map(Cow::Borrowed).unwrap_or(receipt);
|
||||
lines.push(render_tool_header_with_summary(
|
||||
"Shell",
|
||||
header_summary,
|
||||
status_text.as_ref(),
|
||||
self.status,
|
||||
self.started_at,
|
||||
low_motion || stale_status.is_some(),
|
||||
low_motion || held_status.is_some(),
|
||||
));
|
||||
|
||||
if let Some(description) = self.description.as_deref() {
|
||||
|
||||
@@ -107,6 +107,7 @@ fn exec_tool(command: &str, status: ToolStatus) -> ExecCell {
|
||||
started_at: None,
|
||||
duration_ms: None,
|
||||
stale_elapsed_since_output_ms: None,
|
||||
awaiting_approval: false,
|
||||
source: ExecSource::Assistant,
|
||||
description: None,
|
||||
interaction: None,
|
||||
|
||||
@@ -112,6 +112,7 @@ pub(super) fn handle_tool_call_started(
|
||||
started_at: Some(Instant::now()),
|
||||
duration_ms: None,
|
||||
stale_elapsed_since_output_ms: None,
|
||||
awaiting_approval: false,
|
||||
source,
|
||||
description: None,
|
||||
interaction: Some(summary.clone()),
|
||||
@@ -150,6 +151,7 @@ pub(super) fn handle_tool_call_started(
|
||||
started_at: Some(Instant::now()),
|
||||
duration_ms: None,
|
||||
stale_elapsed_since_output_ms: None,
|
||||
awaiting_approval: false,
|
||||
source,
|
||||
description: input
|
||||
.get("description")
|
||||
@@ -327,6 +329,24 @@ fn push_active_tool_cell(
|
||||
app.mark_history_updated();
|
||||
}
|
||||
|
||||
pub(crate) fn set_exec_card_awaiting_approval(app: &mut App, tool_id: &str, awaiting: bool) {
|
||||
let Some(&index) = app.tool_cells.get(tool_id) else {
|
||||
return;
|
||||
};
|
||||
let is_change = matches!(
|
||||
app.cell_at_virtual_index(index),
|
||||
Some(HistoryCell::Tool(ToolCell::Exec(exec))) if exec.awaiting_approval != awaiting
|
||||
);
|
||||
if !is_change {
|
||||
return;
|
||||
}
|
||||
if let Some(HistoryCell::Tool(ToolCell::Exec(exec))) = app.cell_at_virtual_index_mut(index) {
|
||||
exec.awaiting_approval = awaiting;
|
||||
exec.started_at = (!awaiting).then(Instant::now);
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn register_tool_cell(
|
||||
app: &mut App,
|
||||
tool_id: &str,
|
||||
@@ -2771,6 +2791,7 @@ mod tests {
|
||||
started_at: None,
|
||||
duration_ms: Some(120),
|
||||
stale_elapsed_since_output_ms: None,
|
||||
awaiting_approval: false,
|
||||
source: ExecSource::Assistant,
|
||||
description: None,
|
||||
interaction: None,
|
||||
|
||||
@@ -197,6 +197,7 @@ fn exec_tool_cell_with_output(command: &str, output: String) -> HistoryCell {
|
||||
started_at: None,
|
||||
duration_ms: None,
|
||||
stale_elapsed_since_output_ms: None,
|
||||
awaiting_approval: false,
|
||||
source: ExecSource::Assistant,
|
||||
description: None,
|
||||
interaction: None,
|
||||
@@ -216,6 +217,7 @@ fn exec_tool_cell(command: &str) -> HistoryCell {
|
||||
started_at: None,
|
||||
duration_ms: None,
|
||||
stale_elapsed_since_output_ms: None,
|
||||
awaiting_approval: false,
|
||||
source: ExecSource::Assistant,
|
||||
description: None,
|
||||
interaction: None,
|
||||
|
||||
@@ -3232,6 +3232,7 @@ pub(crate) fn observe_human_request_settlement(app: &mut App, event: &EngineEven
|
||||
}
|
||||
|
||||
pub(crate) fn note_human_decision_delivered(app: &mut App, tool_id: &str) {
|
||||
crate::tui::tool_routing::set_exec_card_awaiting_approval(app, tool_id, false);
|
||||
if (app.is_loading || matches!(app.runtime_turn_status.as_deref(), Some("in_progress")))
|
||||
&& !app.suppress_stream_events_until_turn_complete
|
||||
&& !crate::tui::pending_requests::is_foreign_child_request(app, tool_id)
|
||||
|
||||
@@ -8595,6 +8595,7 @@ pub(super) async fn handle_approval_required_event(
|
||||
config.approval_default_selection(),
|
||||
config.approval_timeout(),
|
||||
);
|
||||
crate::tui::tool_routing::set_exec_card_awaiting_approval(app, &id, true);
|
||||
if let Some(agent_id) = crate::tui::pending_requests::child_agent_id(&id) {
|
||||
crate::tui::pending_requests::record(
|
||||
app,
|
||||
|
||||
@@ -6667,6 +6667,7 @@ fn running_exec_cell() -> HistoryCell {
|
||||
started_at: None,
|
||||
duration_ms: Some(20),
|
||||
stale_elapsed_since_output_ms: None,
|
||||
awaiting_approval: false,
|
||||
source: ExecSource::Assistant,
|
||||
description: None,
|
||||
interaction: None,
|
||||
@@ -10038,6 +10039,7 @@ fn refresh_shell_exec_live_output_finalizes_restored_missing_shell_job() {
|
||||
started_at: None,
|
||||
duration_ms: Some(1_234),
|
||||
stale_elapsed_since_output_ms: None,
|
||||
awaiting_approval: false,
|
||||
source: ExecSource::Assistant,
|
||||
description: None,
|
||||
interaction: None,
|
||||
@@ -10074,6 +10076,7 @@ fn shell_live_output_update_matches_exact_task_id_only() {
|
||||
started_at: None,
|
||||
duration_ms: Some(111),
|
||||
stale_elapsed_since_output_ms: None,
|
||||
awaiting_approval: false,
|
||||
source: ExecSource::Assistant,
|
||||
description: None,
|
||||
interaction: None,
|
||||
@@ -10090,6 +10093,7 @@ fn shell_live_output_update_matches_exact_task_id_only() {
|
||||
started_at: None,
|
||||
duration_ms: None,
|
||||
stale_elapsed_since_output_ms: None,
|
||||
awaiting_approval: false,
|
||||
source: ExecSource::Assistant,
|
||||
description: None,
|
||||
interaction: None,
|
||||
@@ -10186,6 +10190,7 @@ fn shell_live_output_update_marks_stale_running_job_static() {
|
||||
started_at: Some(Instant::now() - Duration::from_secs(90)),
|
||||
duration_ms: Some(90_000),
|
||||
stale_elapsed_since_output_ms: None,
|
||||
awaiting_approval: false,
|
||||
source: ExecSource::Assistant,
|
||||
description: None,
|
||||
interaction: None,
|
||||
@@ -10269,6 +10274,7 @@ fn shell_live_output_update_finalizes_background_exec_output() {
|
||||
started_at: None,
|
||||
duration_ms: None,
|
||||
stale_elapsed_since_output_ms: None,
|
||||
awaiting_approval: false,
|
||||
source: ExecSource::Assistant,
|
||||
description: None,
|
||||
interaction: None,
|
||||
@@ -10360,6 +10366,7 @@ fn shell_live_output_update_skips_finalized_exec_cell() {
|
||||
started_at: None,
|
||||
duration_ms: Some(10),
|
||||
stale_elapsed_since_output_ms: None,
|
||||
awaiting_approval: false,
|
||||
source: ExecSource::Assistant,
|
||||
description: None,
|
||||
interaction: None,
|
||||
@@ -21878,6 +21885,7 @@ fn spillover_pager_uses_session_artifact_for_specialized_bash_cell() {
|
||||
started_at: None,
|
||||
duration_ms: None,
|
||||
stale_elapsed_since_output_ms: None,
|
||||
awaiting_approval: false,
|
||||
source: ExecSource::Assistant,
|
||||
description: None,
|
||||
interaction: None,
|
||||
@@ -21945,6 +21953,7 @@ fn terminal_pause_has_live_owner_only_for_running_exec_cells() {
|
||||
started_at: Some(Instant::now()),
|
||||
duration_ms: None,
|
||||
stale_elapsed_since_output_ms: None,
|
||||
awaiting_approval: false,
|
||||
source: ExecSource::Assistant,
|
||||
description: None,
|
||||
interaction: Some("interactive".to_string()),
|
||||
@@ -26013,6 +26022,7 @@ fn turn_inspector_renders_overview_sections_for_active_turn() {
|
||||
started_at: None,
|
||||
duration_ms: Some(2400),
|
||||
stale_elapsed_since_output_ms: None,
|
||||
awaiting_approval: false,
|
||||
source: ExecSource::Assistant,
|
||||
description: None,
|
||||
interaction: None,
|
||||
@@ -26174,6 +26184,7 @@ fn turn_inspector_timeline_numbers_semantic_entries_and_checkpoint_actions() {
|
||||
started_at: None,
|
||||
duration_ms: Some(1250),
|
||||
stale_elapsed_since_output_ms: None,
|
||||
awaiting_approval: false,
|
||||
source: ExecSource::Assistant,
|
||||
description: None,
|
||||
interaction: None,
|
||||
@@ -26481,6 +26492,7 @@ fn turn_handoff_markdown_renders_compact_sections_for_active_turn() {
|
||||
started_at: None,
|
||||
duration_ms: Some(2400),
|
||||
stale_elapsed_since_output_ms: None,
|
||||
awaiting_approval: false,
|
||||
source: ExecSource::Assistant,
|
||||
description: None,
|
||||
interaction: None,
|
||||
|
||||
@@ -3909,6 +3909,7 @@ mod tests {
|
||||
started_at: None,
|
||||
duration_ms: None,
|
||||
stale_elapsed_since_output_ms: None,
|
||||
awaiting_approval: false,
|
||||
source: ExecSource::User,
|
||||
description: None,
|
||||
interaction: None,
|
||||
|
||||
Reference in new issue
Block a user