fix(tui): a command that moves to the background after 30 s says so plainly

When a foreground bash call outlived its wait and moved to the background,
the card copied the model's receipt ("Background task started: shell-42
... use task_shell_wait ...") into its live output. That told the user how
the model should poll, not what happened. Ctrl+B already showed a user
notice for the same transition.

- A running Exec result no longer seeds `live_output` with the model's
  receipt. The card keeps its live task identity, and output arrives from
  the job as before.
- The auto-background case pushes the same `ShellWaitReleased` toast as
  Ctrl+B ("... keep running in /jobs"), at Info level for 6 s. A call the
  model started with background:true gets no toast, because nothing moved.
- canonical_background_bash_keeps_live_task_identity_for_follow_up_actions
  now asserts the card has no copied receipt.

Evidence: cargo fmt --all -- --check is clean. The compile and the ui test
are pending a local test build and hosted CI on
codewhale-hq/Codewhale#6957.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01BsXVvReYca2UxzimQtj7Mj
This commit is contained in:
Claude committed 2026-10-11 12:53:47 +00:00
1 parent 8eb23adb03
commit d33dc38613
2 files changed
+17 -18

No files matched your search

+16 -13
View File
@@ -714,16 +714,15 @@ pub(super) fn handle_tool_call_complete(
// Preserve the execution/audit name while recovering the action-qualified
// semantic name from the registered call input. Active entries and
// already-flushed history use separate detail stores.
let semantic_name = app
.active_tool_details
.get(id)
.or_else(|| {
app.tool_cells
.get(id)
.and_then(|cell_index| app.tool_details_by_cell.get(cell_index))
})
let detail = app.active_tool_details.get(id).or_else(|| {
app.tool_cells
.get(id)
.and_then(|cell_index| app.tool_details_by_cell.get(cell_index))
});
let semantic_name = detail
.map_or(name, |detail| canonical_action_alias(name, &detail.input))
.to_string();
let explicit_background = detail.is_some_and(|detail| exec_is_background(&detail.input));
// Roll any child-LLM token usage the tool reports into the
// session-cost counter. Runs unconditionally so future tools that
@@ -809,6 +808,7 @@ pub(super) fn handle_tool_call_complete(
})
.flatten();
let mut workflow_panel_output: Option<String> = None;
let mut moved_to_jobs = false;
if let Some(cell) = app.cell_at_virtual_index_mut(cell_index) {
match cell {
@@ -874,11 +874,8 @@ pub(super) fn handle_tool_call_complete(
.as_deref()
.map(super::history::summarize_tool_output);
exec.live_output = None;
} else if status == ToolStatus::Running
&& exec.interaction.is_none()
&& !tool_result.content.is_empty()
{
exec.live_output = Some(tool_result.content.clone());
} else if status == ToolStatus::Running && exec.interaction.is_none() {
moved_to_jobs = !explicit_background;
}
} else if let Err(err) = result.as_ref()
&& exec.interaction.is_none()
@@ -999,6 +996,12 @@ pub(super) fn handle_tool_call_complete(
if let Some(output) = workflow_panel_output.as_deref() {
apply_workflow_output_to_panel(app, output);
}
if moved_to_jobs {
let notice = app
.tr(codewhale_localization::MessageId::ShellWaitReleased)
.replace("{jobs}", "/jobs");
app.push_status_toast(notice, crate::tui::app::StatusToastLevel::Info, Some(6_000));
}
// If the mutated cell lived inside the active group, bump the active-cell
// revision so the transcript cache re-renders the synthetic tail row.
+1 -5
View File
@@ -1435,11 +1435,7 @@ fn canonical_background_bash_keeps_live_task_identity_for_follow_up_actions() {
assert_eq!(exec.shell_task_id.as_deref(), Some("shell-42"));
assert_eq!(exec.command, "cargo test --workspace");
assert!(exec.output.is_none());
assert!(
exec.live_output
.as_deref()
.is_some_and(|output| { output.contains("Background task started: shell-42") })
);
assert!(exec.live_output.is_none());
}
#[test]