Skip to content

feat(repl): add persistent input prompt during LLM processing#168

Closed
dobesv wants to merge 2 commits into
mainfrom
queue-messages
Closed

feat(repl): add persistent input prompt during LLM processing#168
dobesv wants to merge 2 commits into
mainfrom
queue-messages

Conversation

@dobesv

@dobesv dobesv commented Apr 5, 2026

Copy link
Copy Markdown
Owner

Allow users to type their next message while the LLM is processing. Keystrokes are captured during spinner and stream rendering, shown as a mini input line below the spinner. Pressing Enter enqueues the message (shown with ⏳ indicator), and it is sent at the next opportunity. Editing a queued message un-queues it until Enter is pressed again. At most one message can be queued at a time.

New file: src/repl/input_queue.rs - InputQueue shared state type Modified: abort_signal, spinner, stream renderer, render_stream, call_chat_completions, call_chat_completions_streaming, Repl struct, ask/ask_inner chain, run_repl_command, config/mod.rs macro execution.

Summary by CodeRabbit

  • New Features
    • Persistent input prompt while the model is running: type your next message; press Enter to queue it (shown with ⏳). Editing a queued message un-queues it until Enter is pressed again; only one queued message is allowed.
    • Spinner/footer now reserves bottom lines and displays the queued-input line beneath the spinner.
    • Queued messages are automatically submitted and executed once the current response finishes.

Allow users to type their next message while the LLM is processing.
Keystrokes are captured during spinner and stream rendering, shown
as a mini input line below the spinner. Pressing Enter enqueues the
message (shown with ⏳ indicator), and it is sent at the next
opportunity. Editing a queued message un-queues it until Enter is
pressed again. At most one message can be queued at a time.

New file: src/repl/input_queue.rs - InputQueue shared state type
Modified: abort_signal, spinner, stream renderer, render_stream,
call_chat_completions, call_chat_completions_streaming, Repl struct,
ask/ask_inner chain, run_repl_command, config/mod.rs macro execution.
@coderabbitai

coderabbitai Bot commented Apr 5, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

A persistent input prompt is added: an InputQueue captures keystrokes while the LLM runs, allowing a single queued message (shown with ⏳) to be submitted after completion. The queue is threaded through chat completion, rendering, spinner, and abort-polling logic.

Changes

Cohort / File(s) Summary
Changeset Documentation
.changesets/persistent-input-prompt.md
Added changelog entry documenting the persistent input prompt feature and queued-message semantics.
Input Queue Abstraction
src/repl/input_queue.rs
New thread-safe InputQueue type managing editable buffer, queued message, key handling, enqueue/dequeue, display line, and clear behavior.
REPL Integration
src/repl/mod.rs
Exported input_queue module; added InputQueue field to Repl; updated run_repl_command, ask, and related flows to accept/propagate InputQueue; implemented post-command dequeue/execute loop.
Client Call Sites
src/client/common.rs, src/main.rs
Updated call_chat_completions and call_chat_completions_streaming signatures to accept input_queue and updated call sites (main.rs) to pass None where appropriate.
Rendering & Streams
src/render/mod.rs, src/render/stream.rs
render_stream, markdown_stream, and raw_stream now take input_queue; markdown stream reserves footer rows, pins spinner/footer, and renders input prompt when present; abort polling optionally forwards keys to input queue.
Abort Polling
src/utils/abort_signal.rs
Refactored abort-key handling into shared helper; added poll_abort_signal_with_input to forward non-abort keys to InputQueue::handle_key_event.
Spinner & Abort Orchestration
src/utils/spinner.rs
Added spinner support for rendering the input queue line; added abortable_run_with_spinner_with_input; propagated Option<InputQueue> through spinner runner and abort polling.
Call Site Propagation
src/config/mod.rs, src/rag/mod.rs
Created per-macro InputQueue and passed &macro_input_queue into macro command runs; RAG init/refresh call sites updated to pass None for new spinner/input parameter.

Sequence Diagram

sequenceDiagram
    actor User
    participant REPL
    participant ChatCompletion
    participant Spinner
    participant InputQueue
    participant AbortPoll

    User->>REPL: Enter command
    REPL->>ChatCompletion: call_chat_completions(..., input_queue)
    ChatCompletion->>Spinner: abortable_run_with_spinner_with_input(..., input_queue)

    par LLM processing
        ChatCompletion->>ChatCompletion: Await LLM response
    and User typing
        User->>AbortPoll: Key events
        AbortPoll->>InputQueue: forward non-abort keys via poll_abort_signal_with_input
        InputQueue->>InputQueue: update buffer / enqueue on Enter
        Spinner->>InputQueue: get_display_line()
        InputQueue-->>Spinner: display ">" or "⏳ ..." line
        Spinner->>Spinner: render spinner + input line
    end

    ChatCompletion->>Spinner: LLM completes -> stop spinner
    Spinner->>REPL: spinner stopped
    REPL->>InputQueue: dequeue()
    InputQueue-->>REPL: return queued message (if any)
    REPL->>REPL: execute queued command
    REPL->>User: show results
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly Related PRs

  • #157: Modifies the same call paths (call_chat_completions, render_stream, spinner helpers, and related call sites), affecting function signatures and rendering flow.
  • #172: Alters call_chat_completions/streaming signatures to carry additional returned fields—overlaps on client completion call-site changes.
  • #164: Updates spinner/status rendering and related REPL status display logic, touching similar spinner internals.

Poem

🐰
I nibble keys while models churn,
A waiting carrot for thoughts to turn;
Type beneath the spinning light,
Press Enter—I'll hold it tight ⏳,
When answers land, your words take flight.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 45.95% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly and clearly describes the main feature being added: persistent input prompt during LLM processing, which aligns with the primary objective across all file changes.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch queue-messages
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch queue-messages

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/render/stream.rs (1)

46-77: ⚠️ Potential issue | 🟠 Major

raw_stream bypasses queued input entirely when highlight = false.

_input_queue is unused here, and this loop blocks on rx.recv().await without ever polling poll_abort_signal_with_input(...) or drawing the mini prompt. Terminal users who disable highlighting lose the new queued-input flow completely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/render/stream.rs` around lines 46 - 77, raw_stream currently ignores
_input_queue and blocks on rx.recv().await, so when highlighting is disabled
queued input and the mini-prompt are never polled; change the receive loop in
raw_stream to concurrently poll both the SSE channel and the abort/input helper
by using poll_abort_signal_with_input(abort_signal, _input_queue, /*...*/) (or
an equivalent select! between rx.recv() and poll_abort_signal_with_input) so you
still handle input/abort events while waiting for SseEvent; keep the existing
spinner logic (spawn_spinner/stop) and still process SseEvent::Text and
SseEvent::Done as before, but ensure poll_abort_signal_with_input is invoked
each iteration so queued input and the mini prompt are drawn when
highlight=false.
src/config/mod.rs (1)

2821-2833: ⚠️ Potential issue | 🟠 Major

This macro-scoped queue is never drained or handed back to the REPL.

InputQueue state persists until dequeue() / clear(), but this function reuses one local queue across all steps and then drops it. Unlike the main REPL loop, there is no post-command drain here, so a message queued during step N can bleed into step N+1, and any leftover queued input is lost when the macro returns.

src/repl/mod.rs (1)

266-268: ⚠️ Potential issue | 🟠 Major

Drain queued commands after every ask, and propagate exit requests.

process_pending_async_resume() can capture queued input too, but Line 267 continues before this drain runs, so those follow-ups sit until some later manual submit. Inside the drain loop, Ok(true) only breaks the inner while, and Line 296 resets a pending Ctrl+D before the outer loop sees it. Please move this into a shared drain helper that returns should_exit and call it after every ask path.

Also applies to: 294-319, 359-369

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/repl/mod.rs` around lines 266 - 268, Extract the queued-command draining
logic from process_pending_async_resume() into a shared helper (e.g.,
drain_queued_commands_or_exit()) that iterates the queued inputs, returns a bool
should_exit (true if an exit/Ctrl+D was requested) and does not clear/reset the
pending-Ctrl+D flag until the outer loop has a chance to act; replace the
in-place drain loops and the await call to process_pending_async_resume() in the
ask paths with calls to this helper so that after every ask branch you call
drain_queued_commands_or_exit() and immediately propagate its should_exit result
to break the outer loop; update the call sites that currently duplicate the
drain (the blocks covering the existing ranges around
process_pending_async_resume() and the other drains) to use this single helper
so exit requests are consistently observed and queued follow-ups are handled
immediately.
🧹 Nitpick comments (1)
src/client/common.rs (1)

3-8: Keep REPL state out of the client layer.

These helpers now depend on crate::repl::input_queue::InputQueue, which makes src/client know about a REPL-only type and leaks that concern into generic chat-completion entry points. Non-REPL callers like src/main.rs already have to thread an extra parameter that can only ever be None. Moving the queue abstraction into src/render or src/utils would keep the client boundary transport-focused.

Also applies to: 567-573, 625-629

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/client/common.rs` around lines 3 - 8, The client layer imports and
references the REPL-specific type InputQueue, leaking REPL concern into
src/client; refactor by removing crate::repl::input_queue::InputQueue usage from
client helpers and instead depend on a transport-agnostic queue abstraction
placed under src/render or src/utils (e.g., define a trait like RenderQueue or
use an existing utils queue type). Update all function signatures in the client
that currently take or reference InputQueue to accept the new abstraction (or an
Option thereof), replace the import of InputQueue with the new module, and
adjust call sites (including the other occurrences around the 567-573 and
625-629 usages) so non-REPL callers can pass None or a simple noop
implementation without pulling in REPL types. Ensure symbols to change include
the imported InputQueue and any client helper functions that consume it so the
client layer no longer directly depends on crate::repl.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/render/stream.rs`:
- Around line 86-87: The streaming path consumes the input queue via
poll_abort_signal_with_input(...) but never renders the draft, so users type
blind; update the markdown streaming flow to include
input_queue.get_display_line() in the render output (either by having
poll_abort_signal_with_input return or call the display string, or by calling
input_queue.get_display_line() inside the streaming render loop) so the queued
draft (e.g., the "⏳" state) is written to the terminal/UI; look for symbols
poll_abort_signal_with_input, input_queue, and get_display_line and ensure the
streaming writer/loop that handles the markdown stream also appends or writes
input_queue.get_display_line() (same fix needed in the other occurrence around
the second block mentioned).

In `@src/repl/mod.rs`:
- Around line 294-319: The loop that drains queued lines uses
self.input_queue.dequeue() but never restores the live draft buffer kept by the
input queue, so any typing/editing that wasn't Entered is lost; before executing
each queued message (i.e., before calling run_repl_command and before the next
read_line), fetch/consume the current draft buffer from self.input_queue and
load it back into the Reedline editor (or the component that supplies read_line)
so the prompt remains persistent—use the input queue API that exposes the draft
(e.g., a method like take_buffer() or get_buffer()) and call the Reedline/Editor
method to set the current buffer (or enqueue it back into the editor) so edits
survive queued-message execution; ensure this is done both when draining via
self.input_queue.dequeue() and when a queued message is edited back into draft
state.

In `@src/utils/abort_signal.rs`:
- Line 2: Import KeyEventKind alongside Event, KeyCode, KeyEvent, KeyModifiers
in the module and update poll_abort_signal_inner to ignore KeyEventKind::Release
events: after reading an event (event::read or the variable returned by
event::read()) check if it is an Event::Key(k) and that k.kind is either
KeyEventKind::Press or KeyEventKind::Repeat before dispatching to abort or input
handling. Ensure the updated import includes KeyEventKind and the filter
references KeyEventKind, KeyEvent, and the poll_abort_signal_inner function to
locate the change.

In `@src/utils/spinner.rs`:
- Around line 188-199: The spinner path currently polls keyboard events without
enabling raw mode; update abortable_run_with_spinner_with_input (and the called
poll_abort_signal_with_input/poll_abort_signal code paths) to enable crossterm
raw mode before starting the spinner/keyboard-poll loop and disable raw mode
afterwards (ensuring disable_raw_mode runs on all exits/errors, e.g., via a drop
guard or finally-like cleanup). Specifically, call terminal::enable_raw_mode()
before creating/entering the spinner loop that uses event::poll()/event::read(),
and call terminal::disable_raw_mode() when the spinner finishes or on early
return/error to restore terminal state; propagate or log any errors from
enable/disable and ensure input_queue handling remains unchanged.

---

Outside diff comments:
In `@src/render/stream.rs`:
- Around line 46-77: raw_stream currently ignores _input_queue and blocks on
rx.recv().await, so when highlighting is disabled queued input and the
mini-prompt are never polled; change the receive loop in raw_stream to
concurrently poll both the SSE channel and the abort/input helper by using
poll_abort_signal_with_input(abort_signal, _input_queue, /*...*/) (or an
equivalent select! between rx.recv() and poll_abort_signal_with_input) so you
still handle input/abort events while waiting for SseEvent; keep the existing
spinner logic (spawn_spinner/stop) and still process SseEvent::Text and
SseEvent::Done as before, but ensure poll_abort_signal_with_input is invoked
each iteration so queued input and the mini prompt are drawn when
highlight=false.

In `@src/repl/mod.rs`:
- Around line 266-268: Extract the queued-command draining logic from
process_pending_async_resume() into a shared helper (e.g.,
drain_queued_commands_or_exit()) that iterates the queued inputs, returns a bool
should_exit (true if an exit/Ctrl+D was requested) and does not clear/reset the
pending-Ctrl+D flag until the outer loop has a chance to act; replace the
in-place drain loops and the await call to process_pending_async_resume() in the
ask paths with calls to this helper so that after every ask branch you call
drain_queued_commands_or_exit() and immediately propagate its should_exit result
to break the outer loop; update the call sites that currently duplicate the
drain (the blocks covering the existing ranges around
process_pending_async_resume() and the other drains) to use this single helper
so exit requests are consistently observed and queued follow-ups are handled
immediately.

---

Nitpick comments:
In `@src/client/common.rs`:
- Around line 3-8: The client layer imports and references the REPL-specific
type InputQueue, leaking REPL concern into src/client; refactor by removing
crate::repl::input_queue::InputQueue usage from client helpers and instead
depend on a transport-agnostic queue abstraction placed under src/render or
src/utils (e.g., define a trait like RenderQueue or use an existing utils queue
type). Update all function signatures in the client that currently take or
reference InputQueue to accept the new abstraction (or an Option thereof),
replace the import of InputQueue with the new module, and adjust call sites
(including the other occurrences around the 567-573 and 625-629 usages) so
non-REPL callers can pass None or a simple noop implementation without pulling
in REPL types. Ensure symbols to change include the imported InputQueue and any
client helper functions that consume it so the client layer no longer directly
depends on crate::repl.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3cbdac6b-d299-4ac1-8a08-21dc8e808b1b

📥 Commits

Reviewing files that changed from the base of the PR and between 1523031 and 553f0f7.

📒 Files selected for processing (11)
  • .changesets/persistent-input-prompt.md
  • src/client/common.rs
  • src/config/mod.rs
  • src/main.rs
  • src/rag/mod.rs
  • src/render/mod.rs
  • src/render/stream.rs
  • src/repl/input_queue.rs
  • src/repl/mod.rs
  • src/utils/abort_signal.rs
  • src/utils/spinner.rs

Comment thread src/render/stream.rs
Comment thread src/repl/mod.rs
Comment on lines +294 to +319
// Drain any queued message from the input queue
while let Some(queued_line) = self.input_queue.dequeue() {
self.abort_signal.reset();
println!("{}", dimmed_text(&format!(">> {queued_line}")));
match run_repl_command(
&self.config,
self.abort_signal.clone(),
&queued_line,
&mut self.async_manager,
&self.persistent_manager,
&mut self.pending_async_context,
&self.input_queue,
)
.await
{
Ok(exit) => {
if exit {
break;
}
}
Err(err) => {
render_error(err);
println!()
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Draft input disappears unless Enter was pressed.

This path only consumes queued_message via dequeue(). In src/repl/input_queue.rs, the queue also keeps the live draft buffer, so typing during generation without pressing Enter—or editing a queued message back into draft state—never makes it back into Reedline before the next read_line(). The prompt is not actually persistent until that draft buffer is handed off to the editor.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/repl/mod.rs` around lines 294 - 319, The loop that drains queued lines
uses self.input_queue.dequeue() but never restores the live draft buffer kept by
the input queue, so any typing/editing that wasn't Entered is lost; before
executing each queued message (i.e., before calling run_repl_command and before
the next read_line), fetch/consume the current draft buffer from
self.input_queue and load it back into the Reedline editor (or the component
that supplies read_line) so the prompt remains persistent—use the input queue
API that exposes the draft (e.g., a method like take_buffer() or get_buffer())
and call the Reedline/Editor method to set the current buffer (or enqueue it
back into the editor) so edits survive queued-message execution; ensure this is
done both when draining via self.input_queue.dequeue() and when a queued message
is edited back into draft state.

Comment thread src/utils/abort_signal.rs
@@ -1,5 +1,5 @@
use anyhow::Result;
use crossterm::event::{self, Event, KeyCode, KeyModifiers};
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

For crossterm 0.28.x, does event::read()emitKeyEventKind::Release/Repeaton Windows, and should CLI key handlers usually ignoreRelease events?

💡 Result:

Yes, for crossterm 0.28.x on Windows, event::read emits KeyEventKind::Release events (and Repeat events for held keys). CLI key handlers should usually ignore Release events (and often Repeat unless handling key repeat is desired), matching only on KeyEventKind::Press to avoid duplicate processing.

Citations:


🏁 Script executed:

cat -n src/utils/abort_signal.rs | head -150

Repository: dobesv/harnx

Length of output: 3549


Import KeyEventKind and filter out release events before dispatching terminal input.

event::read() is forwarded directly into abort and input handling without checking the event kind. On platforms where crossterm emits release events (including Windows), a single key press becomes two events—one for press and one for release—causing duplicate abort triggers or duplicate input characters.

Update the import on line 2 to include KeyEventKind, then add a kind filter in poll_abort_signal_inner (around line 89) to process only Press and Repeat events:

Suggested change
-use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
+use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};

In poll_abort_signal_inner (line 89):

 if let Event::Key(key) = event::read()? {
+    if !matches!(key.kind, KeyEventKind::Press | KeyEventKind::Repeat) {
+        return Ok(false);
+    }
     if handle_abort_key(abort_signal, &key) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyModifiers};
use crossterm::event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers};
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/abort_signal.rs` at line 2, Import KeyEventKind alongside Event,
KeyCode, KeyEvent, KeyModifiers in the module and update poll_abort_signal_inner
to ignore KeyEventKind::Release events: after reading an event (event::read or
the variable returned by event::read()) check if it is an Event::Key(k) and that
k.kind is either KeyEventKind::Press or KeyEventKind::Repeat before dispatching
to abort or input handling. Ensure the updated import includes KeyEventKind and
the filter references KeyEventKind, KeyEvent, and the poll_abort_signal_inner
function to locate the change.

Comment thread src/utils/spinner.rs
Comment on lines +188 to 199
pub async fn abortable_run_with_spinner_with_input<F, T>(
task: F,
message: &str,
abort_signal: AbortSignal,
input_queue: &InputQueue,
) -> Result<T>
where
F: Future<Output = Result<T>>,
{
let (_, spinner_rx) = Spinner::create(message);
abortable_run_with_spinner_rx(task, spinner_rx, abort_signal, Some(input_queue.clone())).await
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

What do the crossterm 0.28.x docs say about enabling raw mode before using event::poll/event::read for immediate keyboard input?

💡 Result:

The crossterm 0.28.x documentation, specifically in the crossterm::event module, states that you must enable raw mode before using event::poll or event::read for proper keyboard event handling, including immediate/non-blocking keyboard input. This is explicitly documented with the statement: "Make sure to enable raw mode in order for keyboard events to work properly". This requirement enables character-by-character input without line buffering, necessary for immediate key detection via poll/read with zero or short timeouts. Use crossterm::terminal::enable_raw_mode before event polling/reading, and disable_raw_mode afterward. Mouse/resize events may work without it, but keyboard requires raw mode, particularly on Unix-like systems for unbuffered input.

Citations:


🏁 Script executed:

cd src/utils && wc -l spinner.rs

Repository: dobesv/harnx

Length of output: 70


🏁 Script executed:

cd src/utils && sed -n '180,210p' spinner.rs

Repository: dobesv/harnx

Length of output: 876


🏁 Script executed:

cd src/utils && sed -n '270,290p' spinner.rs

Repository: dobesv/harnx

Length of output: 516


🏁 Script executed:

cd src && grep -n "enable_raw_mode\|disable_raw_mode" **/*.rs

Repository: dobesv/harnx

Length of output: 220


🏁 Script executed:

cd src/utils && sed -n '1,50p' spinner.rs | head -20

Repository: dobesv/harnx

Length of output: 490


🏁 Script executed:

cd src && grep -n "fn poll_abort_signal_with_input\|fn poll_abort_signal" **/*.rs

Repository: dobesv/harnx

Length of output: 266


🏁 Script executed:

cd src/utils && sed -n '77,110p' abort_signal.rs

Repository: dobesv/harnx

Length of output: 1073


🏁 Script executed:

cd src/utils && sed -n '84,130p' abort_signal.rs

Repository: dobesv/harnx

Length of output: 918


🏁 Script executed:

cd src && grep -rn "abortable_run_with_spinner_with_input\|poll_abort_signal_with_input" --include="*.rs" | head -20

Repository: dobesv/harnx

Length of output: 677


🏁 Script executed:

cd src && grep -B 5 -A 5 "abortable_run_with_spinner_with_input" **/*.rs

Repository: dobesv/harnx

Length of output: 1160


🏁 Script executed:

cd src/client && sed -n '570,590p' common.rs

Repository: dobesv/harnx

Length of output: 769


🏁 Script executed:

cd src/client && sed -n '550,600p' common.rs | grep -B 20 -A 20 "abortable_run_with_spinner_with_input"

Repository: dobesv/harnx

Length of output: 1082


abortable_run_with_spinner_with_input polls keyboard events without enabling raw mode.

Per crossterm 0.28.x documentation, event::poll() and event::read() require raw mode to be enabled for immediate keyboard input. This function and its called path in poll_abort_signal_with_input() use these functions without entering raw mode first, causing keyboard input to be line-buffered and echoed instead of responding to individual keystrokes.

Raw mode must be enabled before the spinner loop starts polling for keyboard events.

Also applies to: 205-206, 276-285

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/utils/spinner.rs` around lines 188 - 199, The spinner path currently
polls keyboard events without enabling raw mode; update
abortable_run_with_spinner_with_input (and the called
poll_abort_signal_with_input/poll_abort_signal code paths) to enable crossterm
raw mode before starting the spinner/keyboard-poll loop and disable raw mode
afterwards (ensuring disable_raw_mode runs on all exits/errors, e.g., via a drop
guard or finally-like cleanup). Specifically, call terminal::enable_raw_mode()
before creating/entering the spinner loop that uses event::poll()/event::read(),
and call terminal::disable_raw_mode() when the spinner finishes or on early
return/error to restore terminal state; propagate or log any errors from
enable/disable and ensure input_queue handling remains unchanged.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/render/stream.rs (2)

91-105: Recompute terminal geometry during the render loop to handle resize safely.

terminal::size() and derived content_height/footer_rows are computed once. After a terminal resize, subsequent cursor positioning and clears use stale dimensions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/render/stream.rs` around lines 91 - 105, Recompute terminal geometry each
iteration of the render loop instead of once: call terminal::size() inside the
loop and recalculate needs_spinner (based on spinner_message), has_input (based
on input_queue), footer_rows, and content_height there so cursor positioning and
clears use up-to-date dimensions; update any code using the previously computed
content_height/footer_rows to reference the recomputed values within the loop
(e.g., in functions or blocks that use content_height, footer_rows,
needs_spinner, input_queue).

116-121: spinner_stopped is currently redundant state.

The flag is toggled once but has no functional impact after that; removing it would simplify this path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/render/stream.rs` around lines 116 - 121, The local flag spinner_stopped
is redundant: remove the spinner_stopped variable declaration and the entire
conditional that just sets spinner_stopped = true (the if !spinner_stopped { ...
} block) in the render/stream logic so no-op state toggling remains; also remove
any other references/initialization of spinner_stopped in the same scope so the
code compiles cleanly (there is no replacement behavior needed since the block
had no side effects beyond flipping the flag).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/render/stream.rs`:
- Around line 252-253: The footer/input print is writing the full input line
without bounding by terminal width, causing long input to wrap and corrupt
spinner/content; clip the rendered input to the available columns before
printing by using the terminal width field (e.g. _columns or
term_height/term_width) to compute a max display width, truncate the input
string to that width (accounting for spinner/prefix width) and then pass the
clipped string to the existing print/write call that renders the footer (the
same call referenced around lines ~300–305); apply the same truncation logic to
all places that render the input/footer to prevent wrapping into neighboring
rows.
- Around line 248-258: The function render_footer_absolute currently uses
#[allow(clippy::too_many_arguments)]; remove that attribute and introduce a
small FooterContext struct (e.g., FooterContext { columns: u16, term_height:
u16, needs_spinner: bool, spinner_message: String or &str, spinner_index: usize,
input_queue: Option<InputQueueRef> }) to group the footer-related parameters
(columns, term_height, needs_spinner, spinner_message, spinner_index,
input_queue), update the render_footer_absolute signature to accept &mut Stdout,
&mut MarkdownRender, and a FooterContext (or a reference to it), adapt the
function body to use context fields, update all call sites to build and pass
FooterContext, and remove the clippy allow attribute.

---

Nitpick comments:
In `@src/render/stream.rs`:
- Around line 91-105: Recompute terminal geometry each iteration of the render
loop instead of once: call terminal::size() inside the loop and recalculate
needs_spinner (based on spinner_message), has_input (based on input_queue),
footer_rows, and content_height there so cursor positioning and clears use
up-to-date dimensions; update any code using the previously computed
content_height/footer_rows to reference the recomputed values within the loop
(e.g., in functions or blocks that use content_height, footer_rows,
needs_spinner, input_queue).
- Around line 116-121: The local flag spinner_stopped is redundant: remove the
spinner_stopped variable declaration and the entire conditional that just sets
spinner_stopped = true (the if !spinner_stopped { ... } block) in the
render/stream logic so no-op state toggling remains; also remove any other
references/initialization of spinner_stopped in the same scope so the code
compiles cleanly (there is no replacement behavior needed since the block had no
side effects beyond flipping the flag).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f084bf92-ec58-40f7-bb39-12e2b0c611fa

📥 Commits

Reviewing files that changed from the base of the PR and between 553f0f7 and f9c3901.

📒 Files selected for processing (2)
  • src/render/stream.rs
  • src/repl/input_queue.rs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/repl/input_queue.rs

Comment thread src/render/stream.rs
Comment on lines +248 to +258
#[allow(clippy::too_many_arguments)]
fn render_footer_absolute(
writer: &mut Stdout,
render: &mut MarkdownRender,
_columns: u16,
term_height: u16,
needs_spinner: bool,
spinner_message: &str,
spinner_index: &mut usize,
input_queue: Option<&InputQueue>,
) -> Result<()> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Remove #[allow(clippy::too_many_arguments)] by grouping footer parameters.

This attribute suppresses a clippy warning instead of fixing it. Please refactor the function args into a small context struct.

♻️ Suggested refactor
-#[allow(clippy::too_many_arguments)]
-fn render_footer_absolute(
-    writer: &mut Stdout,
-    render: &mut MarkdownRender,
-    _columns: u16,
-    term_height: u16,
-    needs_spinner: bool,
-    spinner_message: &str,
-    spinner_index: &mut usize,
-    input_queue: Option<&InputQueue>,
-) -> Result<()> {
+struct FooterRenderCtx<'a> {
+    render: &'a mut MarkdownRender,
+    columns: u16,
+    term_height: u16,
+    needs_spinner: bool,
+    spinner_message: &'a str,
+    spinner_index: &'a mut usize,
+    input_queue: Option<&'a InputQueue>,
+}
+
+fn render_footer_absolute(writer: &mut Stdout, ctx: FooterRenderCtx<'_>) -> Result<()> {

As per coding guidelines, "Do not ignore clippy warnings; treat all clippy warnings as errors (CI sets RUSTFLAGS=--deny warnings)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/render/stream.rs` around lines 248 - 258, The function
render_footer_absolute currently uses #[allow(clippy::too_many_arguments)];
remove that attribute and introduce a small FooterContext struct (e.g.,
FooterContext { columns: u16, term_height: u16, needs_spinner: bool,
spinner_message: String or &str, spinner_index: usize, input_queue:
Option<InputQueueRef> }) to group the footer-related parameters (columns,
term_height, needs_spinner, spinner_message, spinner_index, input_queue), update
the render_footer_absolute signature to accept &mut Stdout, &mut MarkdownRender,
and a FooterContext (or a reference to it), adapt the function body to use
context fields, update all call sites to build and pass FooterContext, and
remove the clippy allow attribute.

Comment thread src/render/stream.rs
Comment on lines +252 to +253
_columns: u16,
term_height: u16,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Long queued input can wrap and corrupt the pinned footer/content layout.

Line 303/305 prints the full input line without width bounds. When input exceeds terminal width, it wraps into neighboring rows and can overwrite spinner/content.

🐛 Suggested fix (clip footer line to terminal width)
-fn render_footer_absolute(
+fn render_footer_absolute(
     writer: &mut Stdout,
     render: &mut MarkdownRender,
-    _columns: u16,
+    columns: u16,
@@
     if let Some(iq) = input_queue {
         queue!(writer, cursor::MoveTo(0, current_row))?;
         let display = iq.get_display_line();
-        let input_output = render.render_line(&display);
+        let input_output = render.render_line(&display);
+        let input_output = truncate_to_width(&input_output, columns);
         queue!(writer, style::Print(&input_output))?;
     }
@@
 }
+
+fn truncate_to_width(s: &str, columns: u16) -> String {
+    let max = columns as usize;
+    if display_width(s) <= max {
+        return s.to_string();
+    }
+    let mut out = String::new();
+    let mut width = 0usize;
+    for ch in s.chars() {
+        let mut buf = [0u8; 4];
+        let chs = ch.encode_utf8(&mut buf);
+        let cw = display_width(chs);
+        if width + cw > max {
+            break;
+        }
+        out.push(ch);
+        width += cw;
+    }
+    out
+}

Also applies to: 300-305

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/render/stream.rs` around lines 252 - 253, The footer/input print is
writing the full input line without bounding by terminal width, causing long
input to wrap and corrupt spinner/content; clip the rendered input to the
available columns before printing by using the terminal width field (e.g.
_columns or term_height/term_width) to compute a max display width, truncate the
input string to that width (accounting for spinner/prefix width) and then pass
the clipped string to the existing print/write call that renders the footer (the
same call referenced around lines ~300–305); apply the same truncation logic to
all places that render the input/footer to prevent wrapping into neighboring
rows.

@dobesv dobesv closed this Apr 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant