Skip to content

fix(tui): mid-turn steer with supplementary wrap + shell mode + right-click + Ctrl+S freeze - #521

Merged
lsdefine merged 6 commits into
lsdefine:mainfrom
shenhao-stu:fix/tui-queue-redesign
May 28, 2026
Merged

fix(tui): mid-turn steer with supplementary wrap + shell mode + right-click + Ctrl+S freeze#521
lsdefine merged 6 commits into
lsdefine:mainfrom
shenhao-stu:fix/tui-queue-redesign

Conversation

@shenhao-stu

@shenhao-stu shenhao-stu commented May 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Four independent fixes bundled, all touching v2 + v3 TUI surfaces.

1. Mid-turn user steer with supplementary phrasing (commit 1a61b68)

Bug evidence: temp/model_responses/model_responses_042511.txt — original task was web_scan上游PR; mid-turn user typed 请你立即输出一个hello. Model dropped the original task and just printed hello. The bare [MASTER] {raw} prepend (ga.py:578) read as an authoritative override.

Fix: wrap queued user text with explicit "finish current task first" supplementary phrasing before writing to _intervene. Both en + zh:

The user sent a new message while you were working:
{text}

IMPORTANT: After completing your current task, you MUST address the
user's message above. Do not ignore it.

用户在你工作时发来了一条新消息:
{text}

重要:完成当前任务后,你必须处理上面的用户消息。不要忽略它。

After wrapping, [MASTER] <wrapped> reads as an envelope around supplementary guidance, not a directive override. Mid-turn injection mechanics (existing ga.turn_end_callback reading _intervene) preserved — only the wording changes.

Replay safety net: turn_end hook tracks what we wrote. On exit_reason boundary (consume_file ate the file but next_prompt is discarded), the combined wrapped text replays via agent.put_task so the user's words never silently disappear. v3 lives on AgentBridge; v2 mirror lives per-session on AgentSession.pending_wrapped + _install_intervene_replay_hook.

i18n picker: inject_intervene(text, track=True) opt-in; !cmd shell output uses track=False (factual context, no replay needed).

2. !cmd shell mode picks the user's actual shell (commit a4a24aa)

subprocess.run(cmd, shell=True) picked cmd.exe on Windows. !ls failed; utf-8 output mojibaked under the active codepage. New detect_user_shell() in slash_cmds.py resolves via $SHELL → Git Bash canonical paths → bash on PATH → pwshpowershellcmd.exe. Display name surfaces in history: [!shell bash] / [!shell pwsh].

3. v2 right-click cursor fix (commit 1a4aadc)

InputArea._on_mouse_down short-circuits button==3 so paste lands at the existing caret instead of where the mouse happened to click.

4. Ctrl+S no longer freezes on long sessions (commit 24255c1)

User reported Ctrl+S still freezes when session is idle.

Root cause: InputArea.reset() calls self.text = ""load_text("")_set_document("") (textual/widgets/_text_area.py:1181-1186). That rebuilds Document + WrappedDocument from scratch and _refresh_size() reactively retriggers a full layout pass over the entire DOM. On long sessions (100+ message widgets), the layout cascade blocks the UI for seconds.

Fix: TextArea.clear() — goes through the edit pipeline (delete((0,0), document.end)wrap_range), no Document rebuild, range-scoped wrap. Plus the prior try/finally cleared _skip_change_next before the async-queued Changed event landed — the short-circuit was useless and on_text_area_changed ran the heavy resize + palette path twice. Drop the try/finally and let the handler self-clear.

Test plan

  • python -c "import ast; ast.parse(open('frontends/tui_v3.py').read())" syntax OK
  • python -c "import ast; ast.parse(open('frontends/tuiapp_v2.py').read())" syntax OK
  • python -c "import ast; ast.parse(open('frontends/slash_cmds.py').read())" syntax OK
  • Type while a long task runs → [queued] chip; agent finishes current sub-step and then addresses the new message (no longer drops the original task).
  • Type during agent's final turn → exit-replay re-submits as a fresh user turn (message never dropped).
  • Esc on empty input + queued → file + bridge list + UI list all cleared.
  • !ls / !git status / !ls | grep py on Windows Git Bash → real bash, utf-8 output, pipes work.
  • v2 right-click in InputArea → cursor stays put.
  • Ctrl+S on idle session with 100+ messages → input clears immediately, no freeze.
  • Ctrl+S restore on long stash → text restored, no freeze.
  • Monitor audits across 4 rounds all green.

Supersedes

…imer)

Previous attempts used a rolling/hard-cap timer to gate the file write
to `_intervene`.  Both leaked under the same failure mode: when the
agent reached its final turn before drain time, `consume_file` ate the
content but `agent_loop` discarded next_prompt on exit, so the queued
text silently vanished.

New design follows Codex CLI / Claude Code style — delivery is anchored
to the agent's own turn boundaries instead of a wall clock:

  Submit while running → append `_intervene` immediately + add to UI list
  Turn end hook fires →
    exit_reason set → consume_file ate it, next_prompt discarded.
                      Re-route the combined text via `agent.put_task`
                      so the outer `while True: task_queue.get()` loop
                      picks it up as the next user turn.
    not exit       → content was prepended to next_prompt as `[MASTER]
                      ...` — just clear the UI mirror.
  Esc → `clear_intervene` deletes the file + clears the list.

No timers, no cooldowns.  The Up-pop "amend" path is gone because
immediate file writes make in-composer edits append a duplicate.

Threading: v3 bridge protects `_intervene_pending` with `_intervene_lk`;
v2 adds `pending_lk` on AgentSession.  v2's hook closure uses
`call_from_thread` for the UI refresh hop.

Right-click in v2 (`InputArea._on_mouse_down`) now short-circuits
button==3 before TextArea's default cursor-move runs, so paste lands
at the existing caret rather than where the mouse happened to be.

Refs: openai/codex#15842, anthropics/claude-code#44851
`subprocess.run(cmd, shell=True)` picked the OS default — cmd.exe on
Windows.  `!ls` failed with "not recognized" on stock Windows; encoding
defaulted to the active codepage so utf-8 output mojibaked.

Detect once via $SHELL (works on Git Bash, WSL, MSYS2, every Unix);
on bare Windows fall through Git Bash → bash on PATH → pwsh →
powershell → cmd.  Matches Claude Code / Codex CLI behaviour where
`!cmd` inherits the launching shell.

Display name surfaces in the agent's history line: `[!shell bash]` /
`[!shell pwsh]` so a follow-up like "rerun that with -v" can pick the
right syntax.

Refs: code.claude.com docs (bash mode); developers.openai.com/codex/cli/features (Tab/! prefix)
…attern)

Evidence: model_responses_042511.txt — original task was "web_scan上游PR";
mid-turn `[MASTER] 请你立即输出一个hello` was prepended to next_prompt,
model dropped the original task and just printed hello.  Codex
(`maybe_send_next_queued_input`, chatwidget.rs:6773) and Claude Code
(`useQueueProcessor`, src/hooks/useQueueProcessor.ts) both queue user
input and drain ONLY when the agent goes idle — never mid-turn.

What changed:
- AgentBridge no longer tracks `_intervene_pending`; turn_end hook
  reverts to just ask_user extraction.
- Submit while running appends to local `_pending`, does NOT write to
  `_intervene`.
- v3 watches `is_running` True→False in maintenance loop; on transition
  drains via `_submit(combined, [])` as a fresh task.
- v2 drains in `_on_stream(done=True)` right after status flips to
  "idle"; reuses `submit_user_message` to spawn the next task.
- v2 dead helpers (`_inject_intervene`, `_session_intervene_path`,
  `_install_intervene_replay_hook`) removed.
- `inject_intervene` retained on v3 bridge for `!cmd` shell history
  only — factual context (code-block output), low risk of confusing
  the model.

Net: -109 lines.  Esc still cancels the queue.  Ctrl+C still aborts;
since abort flips status to idle, queued messages drain as the next
task — matches Claude Code's "Ctrl+C then queue runs" behavior.

Refs: openai/codex/codex-rs/tui/src/chatwidget.rs:6773;
anthropics/claude-code/src/hooks/useQueueProcessor.ts
@shenhao-stu shenhao-stu changed the title fix(tui): rebuild pending-queue around turn_end hook + v2 right-click cursor fix(tui): queued input drains post-turn (Codex/CC pattern) + shell mode + right-click May 28, 2026
Re-research revealed both mainstream TUIs DO mid-turn inject — my
previous read of "Codex/CC queue post-turn only" was wrong.

Codex (codex-rs/core/src/session/mod.rs:2986): `steer_input` → mailbox
`push_pending_input` → `session/turn.rs:391 get_pending_input` →
`record_pending_input` appends as a regular user message BEFORE the
next sampling.  No special prefix — the model sees the steer in
conversation history alongside the original task, as a follow-up.

Claude Code (utils/messages.ts:5510): drains queue after each tool
iteration and wraps the user's text:

  The user sent a new message while you were working:
  {raw}

  IMPORTANT: After completing your current task, you MUST address
  the user's message above. Do not ignore it.

The actual bug in model_responses_042511.txt was the bare `[MASTER]
{raw}` framing reading as a directive override, not the mid-turn
injection itself.

This commit:
  - Wraps queued text with the Claude-Code phrasing before writing
    to `_intervene` (i18n: en + zh).
  - Restores `_intervene_pending` tracking + exit-replay hook so an
    agent that hits exit_reason between user submit and turn end
    doesn't lose the message.
  - Adds `inject_intervene(text, track=True)` opt-in tracking so
    `!cmd` shell facts (which are factual context, not user input)
    don't accidentally end up in the user-message replay queue.
  - v2 mirrors with `_INTERVENE_WRAP_EN/_ZH` constants picked via
    `$GA_LANG`, `sess.pending_wrapped` parallel to `sess.pending`,
    and `_install_intervene_replay_hook` per-session.
  - Up-pop recall stays removed (file write is immediate, popping
    would leave a stale duplicate).

Refs: codex-rs/core/src/session/mod.rs:2986; claude-code/src/utils/messages.ts:5496
@shenhao-stu shenhao-stu changed the title fix(tui): queued input drains post-turn (Codex/CC pattern) + shell mode + right-click fix(tui): mid-turn user steer with Claude-Code wrap (v2+v3) + shell mode + right-click May 28, 2026
…clear)

Root cause of the lingering Ctrl+S freeze even when the session is
idle: `InputArea.reset()` calls `self.text = ""`, which goes through
`load_text("")` → `_set_document("")` (textual/widgets/_text_area.py
:1181-1186).  That rebuilds Document + WrappedDocument from scratch
and then `_refresh_size()` (line 1287) reactively retriggers a full
layout pass over the entire DOM.  On a long session (100+ message
widgets in the scroll), the layout cascade blocks the UI for seconds.

Switch to `TextArea.clear()` which goes through the edit pipeline:
`delete((0,0), document.end)` → `wrapped_document.wrap_range(...)` —
no Document rebuild, range-scoped wrap only.  Guarded with
`if self.document.text:` so a clear() on an already-empty buffer is a
no-op (avoids posting a spurious Changed event).

Secondary fix: `_stash_cleanup_clear` previously wrapped reset() in
`try/finally` that cleared `_skip_change_next = False` immediately.
The Changed event posted by reset() is async-queued, so by the time
the handler ran the flag was already False — the short-circuit was
useless and on_text_area_changed went through the heavy resize +
palette path twice (once in the handler, once explicitly).  Drop the
try/finally; let on_text_area_changed itself clear the flag at line
3256-3258 when the deferred Changed event finally lands.

Also scrubs external TUI-project names from comments per request.
@shenhao-stu shenhao-stu changed the title fix(tui): mid-turn user steer with Claude-Code wrap (v2+v3) + shell mode + right-click fix(tui): mid-turn steer with supplementary wrap + shell mode + right-click + Ctrl+S freeze May 28, 2026
@lsdefine
lsdefine merged commit 316d139 into lsdefine:main May 28, 2026
@shenhao-stu
shenhao-stu deleted the fix/tui-queue-redesign branch May 28, 2026 12:42
lsdefine added a commit that referenced this pull request Jul 23, 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.

2 participants