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.
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
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
`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)
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
`app.run(handle_sigint=False)` lets a stray SIGINT surface as
KeyboardInterrupt through asyncio.runners — catch it in run() so the
shell sees a clean exit. Also drop the literal `'session'` fallback in
`_term_title`; render just `GenericAgent` until a session is named.
`tuiapp_v2.py:27` imports `fmt_key`, `fmt_keys` from `keysym`, but the
module file was never tracked. PR #513 (tui v2/v3 fixes) is unusable on a
fresh checkout without this. Adds the cross-platform shortcut formatter
that turns binding strings into mac/Win-Linux key labels (e.g.
`"ctrl+b"` → `"⌃B"` / `"Ctrl+B"`), honoring `GA_KEYSYM_STYLE`.
Last commit wrapped both the `! cmd` echo AND the `└ output` rows in
the charcoal tile. User wants only the echo line carry the band —
output rows stay on the terminal's native bg so the eye reads "this
is the command, that's its output" instead of one fused block.
Previous commit's _TILE_SHELL forced a light fg across both the `└`
gutter and the command output, erasing the dim styling on the gutter
and overwriting any ANSI the shell command emitted into stdout.
Make the tile bg-only (`\x1b[48;2;65;60;65m`). Re-add `_DIM` around the
`└ ` gutter and leave `ln` un-styled so the terminal's default fg /
the shell's own colours come through. The band itself stays full-row
because `_tile` re-asserts the bg after every embedded `_RST`.
Per user screenshot 040748, the committed `! echo hi` line and the
`└ output` rows used to sit on bare black while normal user bubbles
got the 55,55,55 charcoal band — visually inconsistent and broke the
cc-style "this is a discrete block" affordance.
New `_TILE_SHELL` (65,60,65 bg, light fg) wraps both halves via
`_tile()`, so the pink `!` prompt and dim `└` glyph inherit the band
across the full row width. Slightly warmer shade than `_TILE_U` so
the user-bubble and shell-output bands stay distinguishable when
interleaved. Black-terminal only — matches user spec.
v2 Ctrl+S freeze: defer reset() + cleanup via call_after_refresh so the
keystroke handler returns immediately even when streaming has the
reactive queue saturated. Added _skip_change_next flag to short-circuit
on_input_area_changed during the deferred clear.
v3 terminal title: route _set_term_title via os.write(1,...) — sys.stdout
gets redirected to sb_agent.log during PTK app run, so OSC 0 escapes
were silently ending up in the log file. Default _session_name = 'session'
for v2 parity, and repaint title on running→idle transition.
v3 input row bg fill: _tile() now pads explicit bg-active spaces to
width — PTK's cell renderer doesn't honour \x1b[K (erase-to-EOL), so
the user bubble's charcoal band used to stop at text length.
v3 /emoji picker: arrow-key menu (mirrors /llm). Pet styles cleaned to
bear (default) + cat + dot + unicode. Bear calm tier restored to ʕ•ᴥ•ʔ.
• used for the calm tier, o for focused — matches user-requested mood
progression. Pet frame rate decoupled from spinner via self._spin // 5.
v3 ask_user "coderun freeze" fix: _tool_status no longer marks an
in-flight ask_user chip as ✓ ok when the stream contains the
"Waiting for your answer ..." marker. Bumped DoneEvent grace from 2 s
to 10 s when the marker is in the stream so a slow ask_user_queue.put
doesn't cause the picker to fall through to _finalize.
v3 /scheduler picker: added pre_checked kwarg to _show_menu so the
running set lands atomically. slash_cmds._match_service relaxed to
path-only matching so directly-launched (`python reflect/foo.py`)
reflect tasks are detected. Running rows highlighted in functional
green. Confirm card accepts ← as cancel (parity with Esc) — same on
v2 ChoiceList.
v3 keybindings: Esc Esc (within 800 ms) → /rewind (handled in
_handle_key so PTK's Esc interception path actually fires).
v3 pending input queue: agent.task_dir wired so ga.turn_end_callback
can consume `<task_dir>/_intervene`. New AgentBridge.inject_intervene
appends to the file (atomic vs read-modify-write), and _drain_pending
routes the combined message either through that hook (mid-turn) or via
put_task (idle). Cooldown 5 s, resets per new pending. ↑ recalls the
last queued message, Esc clears the queue. v2 has equivalent shape:
AgentSession.pending + per-session agent.task_dir + _inject_intervene
+ _poll_pending timer.
! shell magic: prefix `!` enters shell mode — pink border, prompt mark
swaps `❯` → `!`, body strips the leading `!` so the glyph appears
once. Enter runs the rest as `subprocess.run(shell=True, timeout=30)`,
echoes `! cmd` + `└ output` into scrollback, and appends to LLM
history (via _intervene when agent is running, direct backend.history
append when idle — sidesteps the iterator race).
/resume: forwarded as a literal to the agent so GA's _handle_slash_cmd
(agentmain.py:124) can expand it. Added to v3 _cmds() + v2 COMMANDS +
slash_cmds.COMMANDS + i18n + /help + Tips.
Turn marker regex fix: agent_loop.py:52 switches `**LLM Running
(Turn N) ...**` to the short `**Turn N ...**` when task_dir is set.
Extended all v3/v2 turn-marker regexes to match either form.
Cherry-picked upstream fixes:
- PR #466 (8ae3645): _collapse_choice early-return when the
on_select callback (e.g. /rewind) detaches the anchor widget.
- PR #461 (08f21e8): SelectableStatic.has_valid_selection_parent +
app-level _is_stale_selectable_mouse_event filter that swallows
mouse events on detached widgets.
Added installation instructions for Windows and Linux, including one-click install options and manual clone instructions. Updated configuration examples and GUI launch commands.
1. _TILE_U: switch tile bg/fg to truecolor (48;2;55;55;55 / 38;2;230;230;230)
so the file-tree tiles render legibly on Win Terminal dark themes
instead of a muddy 8-color block.
2. _ticker: tighten the redraw cadence 0.4s -> 0.1s so the spinner and
status line feel responsive on Windows.
3. terminal title: add _set_term_title() helper and emit OSC 0 on init /
session new / rename / quit so the tab title tracks the active session.
4. _drain: stream-aware grace window — when the pending stream carries a
pending ask_user ("Waiting for your answer"), extend the post-done poll
so the question card is not swallowed before the prompt appears.
5. /emoji command: split _PETS into _PETS_UNICODE / _PETS_ASCII behind a
module-level _pet_style toggle; add the /emoji dispatch + _cmd_emoji
handler so users on fonts without emoji can fall back to ASCII pets.
Local-only edits, no cross-module API change; AST parses clean.
Co-authored-by: Shen Hao <shenhao-stu@users.noreply.github.com>
* fix(tui): slash-cmd polish — /scheduler perf, v2 i18n typo, v3 bundle i18n
- slash_cmds: cache `running_services` list (≥1s TTL) and use a single
service-name preload + lru-cached lookup. /scheduler picker cold path
~2.1s → ~1.0s, warm path ~0ms. Cache is invalidated whenever
`start_service` succeeds so the picker reflects the new state on the
next open.
- tuiapp_v2: drop the stray space inside `/reload-keys` zh description
("重新加载 mykey.py" → "重新加载mykey.py").
- tui_v3: route the remaining slash_cmds-bundle rows (/autorun,
/morphling, /goal, /hive, /scheduler) through _t() the same way
/update already was, with matching en/zh entries in _I18N. Keeps the
v3 command palette fully bilingual.
* tui: settle spinner on abort + DRY-extract annotation helpers
Aborting a turn (Ctrl+C / /stop) left the live spinner ticking elapsed
until the LLM stream unwound in the background. Now we freeze a
`_stop_summary` snapshot the instant the user aborts and flip the line to
a settled "Stopping…", then "Stopped after Xm Ys" once done.
- ChatMessage._stop_summary: abort-time (elapsed, in, out) snapshot
- _mark_stopping(): freeze + repaint every in-flight assistant on /stop
- _stopping_annotation() / _done_annotation() stopped-state branch
- DRY: _fmt_tokens() single source for `↑ N · ↓ M`, _freeze_summary()
single source for the snapshot tuple (de-dups spinner/done/stopping
and the two capture paths)
- carries the branch's existing ask_user multi-choice "; ".join collapse
---------
Co-authored-by: Shen Hao <shenhao-stu@users.noreply.github.com>
The previous /update prompt always did `git pull` + auto-merge and kept
local edits without committing, which left users with upstream changes
sitting as staged-only changes and an unintended divergent local branch.
Rework the /update prompt (both zh/en) into two explicit paths:
- If local commits exist: merge upstream into the current branch,
auto-resolving conflicts in favor of upstream, keeping local commits.
- Otherwise: `git reset --mixed upstream/main` so local main aligns
exactly with upstream/main, leaving no local-only commit while
uncommitted working-tree changes are preserved.
Never push; never create a commit in the no-local-commit path. Also adds
a concise preview + summary step. Prompt kept short per user request.
Before: /scheduler picker always showed an empty selection, so reflect/*.py
and frontends/*app*.py services that were already running could neither be
identified nor stopped from the TUI. Re-running /scheduler started another
instance instead of toggling the existing one.
This change keeps discovery in the shared slash_cmds module and wires both
TUIs through the same helpers (parity rule from tui_frontend_sop):
* frontends/slash_cmds.py
- running_service_ids(): inspect hub.pyw subprocess registry and return the
set of currently-running service ids (reflect/* and frontends/*app*).
- stop_service(sid): graceful terminate via the same registry, idempotent.
- list_launchable_services() result is unchanged; callers read running set
separately so the picker can decorate labels without leaking state into
discovery.
* frontends/tuiapp_v2.py, frontends/tui_v3.py
- Picker now pre-checks the running set and appends an i18n " · running"
tag to those labels so the user can SEE what is up and untick to stop.
- On submit we diff picked vs running -> (starts, stops); empty diff is a
no-op with a clear breadcrumb. Confirm card lists both sides.
- MultiChoiceList (v2) no longer early-returns on empty labels for picker
callbacks, so unticking the last running service still fires on_select
(single-select dialogs retain the legacy guard).
- Full bilingual i18n: scheduler.running_tag / no_change / diff.start /
diff.stop added to both en/zh tables.
Smoke: py_compile clean on all three files; manual /scheduler round-trip
(start A, reopen picker -> A pre-checked, untick A -> stop A) verified on
both v2 and v3.
The /scheduler picker previously listed only reflect/*.py (skipping just
underscore-prefixed files), so it leaked goal_mode/agent_persona and missed
the frontend apps that hub.pyw's launcher shows. It also started tasks via
`python reflect/X.py` instead of the canonical `agentmain.py --reflect`.
- slash_cmds.py: add list_launchable_services() + start_service() that mirror
hub.pyw discover_services() exactly (EXCLUDES goal_mode/chatapp_common/tuiapp,
reflect tasks via agentmain.py --reflect, frontend *app*.py incl. streamlit).
- tuiapp_v2.py / tui_v3.py: /scheduler now shows the full hub.pyw service set
(reflect + frontend) and, after the multi-select Enter, requires a second
commit-answer confirmation before anything is actually launched.
Parity verified at runtime: list_launchable_services() == discover_services().
Hook the slash_cmds bundle into tui_v3: register the commands in the
palette, add the prompt-injection dispatch and the /scheduler launcher.
Extend the in-house _show_menu with an opt-in multi_select mode
(Space toggles, Enter submits) so /scheduler can start several reflect
tasks at once, plus the matching command tips.
Hook the slash_cmds bundle into tuiapp_v2: register the commands in the
COMMANDS palette and dispatch table, add the prompt-injection handlers
(/update /autorun /morphling /goal /hive) and the /scheduler picker
(multi-select reflect tasks via the existing multi_choice picker), plus
the matching command tips.
Introduce frontends/slash_cmds.py, a shared bundle backing the new
slash commands for both Textual (v2) and curses (v3) frontends.
Provides prompt builders for /update /autorun /morphling /goal /hive
(each injects a concise, imperative directive into the agent prompt)
and the reflect-task discovery/launch helpers used by /scheduler.
- Plan card (📋): per-session message-scan with `plan_scan_baseline` so
`/continue` of a finished session doesn't auto-resurrect old plan cards;
prefers `<summary>当前步骤:` over the long plan-item echo; placeholder
while enter_plan_mode is armed but plan.md hasn't been written yet.
- Done annotation (⠿ {Verb} for Xm Ys · ↑ N · ↓ M): post-turn elapsed
+ per-call token deltas, frozen at done so the line stays put across
re-mounts. Spinner annotation gated on cumulative deltas past stream-start
baselines — fixes `↓ 16` showing immediately on prompt submit.
- fold_turns regex anchored to line-start (`^` + MULTILINE) so embedded
backticks in tool output (e.g. `N|\`\`\`\`` gutter from file_read) don't
pair with later real fences. ~100x faster on long /continue sessions.
- /cost: per-session card + `/cost all`; `Context window` line now reads
GA's actual char cap via `cost_tracker.context_window_chars(backend)` =
`backend.context_win * 3` (the `trim_messages_history` trigger threshold
in llmcore.py), so the percentage reflects real budget and respects
`mykey.py` overrides. Unit changes from token to character. Dropped the
model-theoretical `_CTX_LIMITS` table (never actually constrained anything).
Subagent log scan via `cost_tracker.scan_subagent_logs` aggregates
`temp/*/stdout.log` since TUI start.
- /review: standalone in-session reviewer; reuses `review_cmd.handle`
to render the inline prompt and submits as a normal user task.
- llmcore: emit `[Output] tokens=N` for chat_completions / responses modes
(already emitted for `messages` SSE) so cost_tracker scopes output
tokens uniformly across all three API modes.
- review_inline_prompt.{txt,en.txt}: filled out the in-session review
discipline — Three Iron Rules, Q1-Q4 framing, severity examples,
8 false-positive rules expanded, 8 wording rules expanded, 8-item
self-check. ZH/EN both grow ~80 lines.
- memory/review_sop.md: rewrite 52 → 191 lines, 11 sections covering
quick-start, entry files, Three Iron Rules, 5-step workflow, severity,
output protocol, relationship to plan_sop / verify_sop. Style mirrors
plan_sop.md + autonomous_operation_sop.md.
Files: frontends/{tuiapp_v2,tuiapp,cost_tracker,plan_state}.py, llmcore.py,
memory/review_sop.md, memory/review_sop/review_inline_prompt.{txt,en.txt},
.gitignore (allowlist new SOP).
Co-authored-by: Shen Hao <shenhao-stu@users.noreply.github.com>
Mirrors Claude Code's /btw: ask the agent a quick question without
interrupting the in-flight main task.
- frontends/btw_cmd.py: snapshot backend.history under lock + deepcopy →
background-thread backend.raw_ask single-turn answer; never writes
backend.history; never touches task_queue. Reuses backend.raw_ask +
make_messages, no new LLM instance.
- frontends/chatapp_common.py: register /btw in HELP_COMMANDS; handle
/btw in AgentChatMixin (sync via asyncio.to_thread); install() at
module bottom so the slash hook is wired for any frontend that
imports chatapp_common.
- frontends/stapp.py: persist display_queue + partial_response in
st.session_state so the streaming bubble survives Streamlit reruns;
new render_main_stream() helper covers both new tasks and resume;
/btw branch uses plain st.rerun() (preserves dq) so the main task's
remaining output renders in a follow-up bubble; non-/btw prompts
explicitly call agent.abort() to keep the original "submit cancels"
UX. Strip <summary> meta tags from text segments and drop dangling
"LLM Running (Turn N) ..." markers in resumed partials to clean up
the rendered chat.