From e14fc4712c14a9a752510127814e43bf15df1996 Mon Sep 17 00:00:00 2001 From: Hunter B Date: Wed, 3 Jun 2026 22:19:22 -0700 Subject: [PATCH] fix(tui): label pending input delivery modes Harvested from PR #2532 by @cyq1017. Pending input rows now distinguish steer-pending, rejected-steer, and queued-follow-up states, with continuation rows aligned under the delivery label. Refs #2054; leaves the broader cancel/edit affordance work open. Co-authored-by: cyq1017 <61975706+cyq1017@users.noreply.github.com> --- CHANGELOG.md | 6 +- crates/tui/src/tui/app.rs | 3 +- crates/tui/src/tui/ui.rs | 3 +- .../src/tui/widgets/pending_input_preview.rs | 82 ++++++++++++++++++- docs/V0_9_0_EXECUTION_MAP.md | 3 +- 5 files changed, 90 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0bf3a67..27afa409 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -68,10 +68,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 expandable transcript row by default, while running, failed, shell, patch, review, diff, and other risky tool cells remain visible. The setting `tool_collapse = "compact" | "expanded" | "calm"` controls the behavior. +- Pending-input preview rows now label delivery mode explicitly as steer + pending, rejected steer, or queued follow-up, with wrapped continuation rows + aligned under the label so busy-turn input state is easier to read (#2054). ### Community -Thanks to **@cyq1017** for the restore-listing implementation (#2513), +Thanks to **@cyq1017** for the restore-listing implementation (#2513) and +pending-input delivery-mode label work (#2532, #2054), **@wywsoor** for the broader macOS/iTerm rollback UX report (#2494), **@HUQIANTAO** for the `web_run` lock-splitting work (#2502) and turn-metadata prefix-cache stability work (#2517), **@xyuai** for canonical CodeWhale diff --git a/crates/tui/src/tui/app.rs b/crates/tui/src/tui/app.rs index f82a200e..136abf75 100644 --- a/crates/tui/src/tui/app.rs +++ b/crates/tui/src/tui/app.rs @@ -1548,7 +1548,8 @@ pub struct App { /// cancelled cleanly). Surfaced in the pending-input preview so the user /// knows the steer was deferred to end-of-turn. Today no engine path /// produces these; the field is scaffolding for a future signalling - /// channel and the bucket renders identically when populated. + /// channel and the bucket renders with a rejected-steer label when + /// populated. pub rejected_steers: VecDeque, /// Legacy resend flag for pending steer recovery. pub submit_pending_steers_after_interrupt: bool, diff --git a/crates/tui/src/tui/ui.rs b/crates/tui/src/tui/ui.rs index 882bf2b9..d1f3abc9 100644 --- a/crates/tui/src/tui/ui.rs +++ b/crates/tui/src/tui/ui.rs @@ -6491,7 +6491,8 @@ async fn handle_plan_choice( /// - `pending_steers` — typed during a running turn + Esc; held until the /// abort lands and gets resubmitted as a fresh merged turn. /// - `rejected_steers` — engine declined a mid-turn steer (scaffolding; -/// no engine path produces these yet but the bucket renders identically). +/// no engine path produces these yet but the bucket renders with a distinct +/// rejected-steer label). /// - `queued_messages` — Enter while busy (offline-mode FIFO); drained at /// end-of-turn. fn build_pending_input_preview(app: &App) -> PendingInputPreview { diff --git a/crates/tui/src/tui/widgets/pending_input_preview.rs b/crates/tui/src/tui/widgets/pending_input_preview.rs index cc3829db..78aabd0c 100644 --- a/crates/tui/src/tui/widgets/pending_input_preview.rs +++ b/crates/tui/src/tui/widgets/pending_input_preview.rs @@ -24,6 +24,9 @@ use crate::tui::widgets::Renderable; /// Per-item line cap before we collapse the rest into a `…` overflow row. const PREVIEW_LINE_LIMIT: usize = 3; +const PENDING_STEER_PREFIX: &str = " ↳ Steer pending: "; +const REJECTED_STEER_PREFIX: &str = " ↳ Rejected steer: "; +const QUEUED_MESSAGE_PREFIX: &str = " ↳ Queued follow-up: "; /// Description of the keybinding the hint line at the bottom should advertise /// for the "edit last queued message" action. @@ -109,14 +112,38 @@ impl PendingInputPreview { &mut lines, Line::from(vec![Span::raw("• "), Span::raw("Pending inputs")]), ); + let pending_steer_indent = continuation_indent(PENDING_STEER_PREFIX); for steer in &self.pending_steers { - push_truncated_item(&mut lines, steer, width, dim, " ↳ ", " "); + push_truncated_item( + &mut lines, + steer, + width, + dim, + PENDING_STEER_PREFIX, + &pending_steer_indent, + ); } + let rejected_steer_indent = continuation_indent(REJECTED_STEER_PREFIX); for steer in &self.rejected_steers { - push_truncated_item(&mut lines, steer, width, dim, " ↳ ", " "); + push_truncated_item( + &mut lines, + steer, + width, + dim, + REJECTED_STEER_PREFIX, + &rejected_steer_indent, + ); } + let queued_message_indent = continuation_indent(QUEUED_MESSAGE_PREFIX); for message in &self.queued_messages { - push_truncated_item(&mut lines, message, width, dim_italic, " ↳ ", " "); + push_truncated_item( + &mut lines, + message, + width, + dim_italic, + QUEUED_MESSAGE_PREFIX, + &queued_message_indent, + ); } if !self.queued_messages.is_empty() { lines.push(Line::from(vec![Span::styled( @@ -154,6 +181,10 @@ impl Renderable for PendingInputPreview { } } +fn continuation_indent(prefix: &str) -> String { + " ".repeat(display_width(prefix)) +} + fn push_section_header(lines: &mut Vec>, header: Line<'static>) { lines.push(header); } @@ -423,6 +454,51 @@ mod tests { assert!(rows.iter().any(|r| r.contains("↑"))); } + #[test] + fn pending_input_rows_label_each_delivery_mode() { + let mut preview = PendingInputPreview::new(); + preview.pending_steers.push("steer".to_string()); + preview.rejected_steers.push("rejected".to_string()); + preview.queued_messages.push("queued".to_string()); + + let rows = render_to_string(&preview, 80); + + assert!( + rows.iter().any(|row| row.contains("Steer pending: steer")), + "missing pending-steer label: {rows:?}" + ); + assert!( + rows.iter() + .any(|row| row.contains("Rejected steer: rejected")), + "missing rejected-steer label: {rows:?}" + ); + assert!( + rows.iter() + .any(|row| row.contains("Queued follow-up: queued")), + "missing queued-follow-up label: {rows:?}" + ); + } + + #[test] + fn wrapped_pending_input_aligns_continuation_under_label() { + let mut preview = PendingInputPreview::new(); + preview + .queued_messages + .push("alpha beta gamma delta epsilon zeta".to_string()); + + let rows = render_to_string(&preview, 34); + + assert!(rows[1].contains("Queued follow-up: alpha")); + assert!( + rows[2].starts_with(&continuation_indent(QUEUED_MESSAGE_PREFIX)), + "continuation should align under label: {rows:?}" + ); + assert!( + !rows[2].trim().is_empty(), + "continuation should keep wrapped text: {rows:?}" + ); + } + #[test] fn message_truncates_to_three_visible_lines() { let mut preview = PendingInputPreview::new(); diff --git a/docs/V0_9_0_EXECUTION_MAP.md b/docs/V0_9_0_EXECUTION_MAP.md index 3de17522..6a90463c 100644 --- a/docs/V0_9_0_EXECUTION_MAP.md +++ b/docs/V0_9_0_EXECUTION_MAP.md @@ -50,6 +50,7 @@ harvest/stewardship commits: | #2736 sub-agent model inheritance | Locally harvested with explicit-override and provider-shaping tests. | Tool-agent routing now inherits the parent runtime model instead of hard-coding `deepseek-v4-flash`, while explicit DeepSeek-style tool-agent overrides still win. The `reasoning_effort = off` fast lane is covered by strict OpenAI-like provider request-shaping tests. Credit @h3c-hexin; comment/close the original after the integration branch is public. | | #2737 configured `skills_dir` discovery | Locally harvested with explicit-config precedence. | The system prompt now unions workspace-discovered skills and configured `skills_dir` skills instead of treating the configured directory as a fallback. Explicit configured skills are inserted before global defaults so they are not lost behind a large global skill library. Credit @h3c-hexin; comment/close the original after the integration branch is public. | | #2738 dense tool-call transcript collapse | Locally harvested with expansion, cache-key, and safety fixes. | Successful read/search/list-style tool runs collapse by default once they cross the density threshold; failures, running cells, shell/exec, patch/write/edit/delete, diff preview, plan update, and review cells stay visible. Users can expand a group with Enter/Space/mouse and can set `tool_collapse = "compact" | "expanded" | "calm"`. Credit @idling11 and issue #2692; comment/close the original after the integration branch is public. | +| #2532 pending-input delivery-mode labels | Locally re-harvested for #2054. | Pending-input preview rows now label steer-pending, rejected-steer, and queued-follow-up delivery modes, and wrapped continuation rows align under the label. `cargo test -p codewhale-tui --bin codewhale-tui --locked pending_input_preview -- --nocapture` passed. Credit @cyq1017; #2054 remains open for cancel/edit-mode affordance clarity. | | #2636 project-context mtime cache | Defer direct merge; harvest only after cache key/signature is widened. | Must include constitution changes, auto-generated context deletion, canonical path equivalence, and overwrite detection before landing. | | #2634 HarmonyOS port | Locally harvested with additional Nix-chain clearance; keep credited and do not close until the integration branch is public. | User-supplied MatePad Edge demo (`https://bilibili.com/video/av116689597368905`) confirms real-device interest. Added env-driven OpenHarmony SDK setup, OHOS platform guards/fallbacks, self-update disablement, and OHOS target gating for Starlark execpolicy parsing plus PTY support so published OHOS builds do not pull `nix` 0.28 through `rustyline` or `portable-pty`. `cargo check --workspace --all-features --locked`, focused PTY/clipboard tests, and `cargo tree --locked -p codewhale-tui --target aarch64-unknown-linux-ohos -i nix@0.28.0` passed; full OHOS target check is blocked on this host because `OHOS_NATIVE_SDK`/target CC/sysroot are not configured and `ring` cannot find `assert.h`. | | #2687 append-only mode/approval prompt | Defer direct merge; draft has compile failures and Plan-mode prompt correctness risks. | Any future harvest must keep stable `message[0]` genuinely mode-agnostic, preserve mode/approval suffixes after capacity replans, and distinguish external overrides from persisted generated prompts. | @@ -74,7 +75,7 @@ v0.9 branch so the remaining Windows/manual checks are explicit. | Sub-agent timeout and trust model (#1806, #719) | Fixed or covered in current branch. | `heartbeat_timeout_secs` clamp/default test passed, and `agent_open_description_explains_fresh_vs_forked_context_and_trust_model` asserts that sub-agent results are self-reports. | | Sub-agent checkpoint/resume (#2029) | Still release-blocking. | Session projection/transcript handles exist, but no checkpoint/continue status or resume contract has landed. Needs a child checkpoint/timeout/resume test that preserves policy and completes. | | Live shell/session liveness (#1786) | Partially fixed, still release-blocking. | Shell containment and turn-liveness tests exist, but orphaned PID/session-load reaping and long-running shell LIVE-state recovery remain open. Needs stale PID reaping and live-state regression coverage. | -| Queued/live input feedback (#2054) | Partially covered; UX clarity still blocking. | `cargo test -p codewhale-tui --bin codewhale-tui --locked queued -- --nocapture` passed for queued-message recovery/editing, but pending rows still need clear delivery-mode labels and cancel/edit-mode clarity tests. | +| Queued/live input feedback (#2054) | Partially covered; UX clarity still blocking. | Queued-message recovery/editing and pending-input delivery-mode labels are covered by `queued` and `pending_input_preview` focused tests. Still needs cancel/edit-mode affordance clarity and a repro for accidentally entering queued-draft edit while a turn is loading. | | Prompt/UI calmness (#1191) | Defer or narrow. | No release-blocking regression evidence yet; keep as polish unless a current user-facing prompt/UI failure is identified. | ## PR Harvest Queue