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.
Wire the existing /review handler into tgapp so Telegram users can invoke in-session code review, including help responses and streamed agent execution.
scrollback-first tui_v3 rounds out v2 parity across this session:
- /export: v2-aligned subcommands (clip/file/all/legacy); clip writes to the
real OS clipboard via existing copy() (pbcopy/clip.exe/wl-copy/xclip/xsel);
picker→file prefills an editable filename
- tool-call folding: per-turn fold ported from v2 fold_turns; Ctrl+O toggles;
incremental repaint folds turn N the moment turn N+1 marker streams in
- ask_user: focus-cycle model (↑↓ moves between candidates and the input box,
caret hidden on candidate focus); multi-select is checkbox-only with no
input box; [x]/[ ] ASCII checkboxes instead of Unicode ballot glyphs
- plan card: v2 _refresh_planbar port — 5-row budget, current-step line,
complete/placeholder states with grace periods, mtime-cached reads
- attachments: bracketed-paste, image/file placeholders, multi-line paste
aligned to v2 behaviour
- i18n: zh/en layer merged inline (was a separate tui_v3_i18n.py) so the
frontend ships as one drop-in module
pyproject: add prompt_toolkit + pillow to the `ui` extra.
macOS Terminal.app quantises truecolor to its nearest 256 slot (so
#5e6ad2 lavender lands on slot 62 #5f5fd7, visibly more blue) and
renders \x1b[2m as a heavy multiply instead of a gentle blend, producing
the "blue border + heavy shadow" against iTerm's identical code.
Branch on TERM_PROGRAM: iTerm keeps its truecolor accent and \x1b[2m
dim; Apple_Terminal switches to pinned 256-slot accent (105) and an
explicit mid-gray (244) for dim. Box border (146) was already 256-slot
so it carries unchanged.
Replace _history_lines (pre-rendered) with _blocks (source + kind) so
_repaint_screen can re-render every chip, banner, user panel at the
current width. \x1b[3J wipes scrollback on resize/Ctrl+L so the entire
conversation reflows, not just the visible viewport.
SIGWINCH delivery via self-pipe + select() — PEP 475 auto-retry on
os.read makes signal-driven InterruptedError unreliable under iTerm
split panes, so a dedicated pipe wakes the loop deterministically.
Streaming tracks a single _streaming_block whose source grows monotonically
through safe-position commits; mid-stream resize re-renders both the
committed source and the volatile live_tail.
Eight independent fixes accumulated since the last batch, all landing in
tuiapp_v2.py:
1. Code-block selection offset: _align_md_renders's is_centered heuristic
tripped on any code line indented >=4 spaces (Python's default), stripping
the indent from the source string so the visible selection and the copied
text disagreed. Add wide_lead >= 2*nt0_lead guard — only Rich's H1 centering
(wide_lead ~ width/2) crosses it, code lines never do.
2. Blockquote-continuation selection offset: Rich re-emits `▌ ` on every
wrapped visual line of a quote, but the wide-render carries the marker
only once. Strip the marker as a decoration prefix (counts as indent, not
content) and switch the wrap-accumulator to account for the per-break
eaten space — both pre-existing code-block wraps and blockquotes now map
correctly.
3. Streaming ANSI garbage: while a turn is streaming, the tail text segment
was rendered with `Text(content, style=C_FG)` instead of `Text.from_ansi`.
SGR codes from the model/tool output would render as literal `[31m`
glyphs and only resolve after `m.done` flipped or a resize forced a
remount via the Markdown path. Switch the two streaming fast paths to
Text.from_ansi — O(n) scan, no Markdown parse, colors honored live.
4. continue picker LIMIT removed: tuiapp's own `LIMIT = 20` truncated the
/continue picker even though list_sessions() always builds the full list.
Show everything; header now reads "选择要恢复的会话 (N 条)".
5. Long-paste rescue + user echo elision: Windows Terminal yanks window
focus when its >=5KiB paste-warning dialog pops, leaving focused=None
after the user confirms; Textual then routes the Paste event to the App
bubble instead of InputArea, and the paste appears to do nothing. Add a
GenericAgentTUI._on_paste forwarder that refocuses #input and replays the
event into InputArea._on_paste. Separately, on submit, elide user-side
display of very long pastes (head 10 / tail 5 / `⋯ 省略 N 行 ⋯`); the
agent still receives the full content via expand_placeholders.
6. mykey.py hot reload: load_llm_sessions() already had mtime-gated reload
plumbing, but it only ran on Agent.__init__ / next_llm / list_llms — so
editing mykey.py mid-session required restart. Two triggers added:
on_input_area_submitted now calls load_llm_sessions() before sending
(cheap mtime check; rebuilds on change), and a new /reload-keys command
forces a sweep across every session (resetting llmcore._mykey_mtime
per-iteration so the cache doesn't short-circuit later sessions).
7. Resize / sidebar toggle no longer jumps to bottom: _remount_current_session
used to unconditionally scroll_end after rebuilding. Capture scroll_y +
"was at bottom?" first; stick to bottom only if the user was already there,
otherwise restore the prior scroll_y so resizes don't yank the user away
from what they were reading.
8. Markdown table cells wrap instead of ellipsizing: Rich's Markdown
TableElement passes no `overflow` to Table.add_column, so the default
"ellipsis" hides long cell content with `…` on narrow widths. Patch
TableElement.__rich_console__ on import to pass overflow="fold".
When a single LLM provider goes down mid-task (auth, network, quota,
etc.), the conductor falls completely silent: MixinSession yields
errors as text chunks (not Python exceptions), so the existing
try/except guard in conductor_loop never triggers, and the conductor's
only way to talk to the user (LLM-decided POST /chat tool call) is dead.
Add a tiny watchdog: monitor_conductor_queue now returns the conductor
agent's final done text, and conductor_loop scans the tail-1000-char
window for "!!!Error:" — the prefix llmcore uses for unrecoverable
errors. Tail window (not whole text) avoids false positives from
earlier turns; using a character window (not last-line) matches
ga.do_no_tool's content[-100:] style and tolerates the decoration
(fence + "[Info] Final response to user.") that do_no_tool appends
after the error chunk when its own short window check misses. When
detected, push the matched error line as a system chat message so the
user knows what happened. A dedup check prevents repeated warnings
if multiple events fail in succession.
+16 -4 lines. Only frontends/conductor.py touched. No agent_loop /
llmcore changes; other frontends unaffected.
When setup window starts the bridge, the main window was already created
with URL http://127.0.0.1:14168/ but failed to load (bridge not running yet).
After bridge becomes ready, we now navigate the main window to reload the
bridge URL before showing it, preventing the white screen issue.
- 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>
The earlier split-around-code-spans helper was incomplete (no ~~~ fences,
no indented blocks, no multi-line inline code) and over-engineered. The
underlying CommonMark hazard is only the start-of-line HTML block that
swallows the next line — mid-line tags are inline HTML Rich already
renders as text. Anchoring the regex to line start (^ + MULTILINE,
optional <=3 leading spaces) handles the original swallow case while
leaving every code-region variant intact. One known gap: a <summary>
tag on the first column inside a fenced block still matches.
_META_TAG_RE blindly stripped the tags so inline-code samples like
'`<summary>x</summary>`' became bare empty backticks. Split the
text on fenced/inline code regions and only strip in the gaps.
Non-ga-default themes fight Rich's frozen accent expectations — code spans
on sel_bg, link colors, heading hues end up either washed out or invisible
on themes like tokyo-night / textual-light. Until each is hand-tuned,
collapse heading / list / code styles to fg + muted so body text stays
readable; inline code also drops its sel_bg background under ga-default
(was hiding text in some terminals).
- watch_theme no longer remounts the whole transcript: just clears render
caches and repaints chrome. Long-history switches go from multi-second
Markdown reparse to ~150ms. Already-mounted message text stays in the
old palette until the next natural reflow (fold toggle, resize, new
turn, session switch).
- _markdown_rich_theme gains a colored flag. Non-ga-default themes render
Markdown in plain fg with only structural emphasis (bold/italic/code
backdrop) until per-theme accents are tuned — avoids text blending
into the background on themes whose accent slots collide with sel_bg.
- Sidebar Q/S preview used C_DIM (0.35 bg/fg blend); under the active row's
SEL_BG the contrast collapses on tokyo-night. Switch to C_MUTED (0.55).
- Drop the 'theme: X' system message — the picker provides the visual
feedback already, the extra row is noise on every preview tick.
- Replace cycle-theme with ThemePicker modal: OptionList live-previews on
highlight, Enter commits + saves, Esc reverts to the prior theme.
- Persist UI settings in temp/tui_settings.json; restored on startup.
- Theme Markdown list bullets and ordered-list numbers via markdown.list /
markdown.item.bullet / markdown.item.number — previously hard-cyan.
Per-chunk _render_md on the growing tail segment is O(chunks x turn_len)
with two Markdown parses each call. During streaming, update the last
text widget with plain Text; on the terminal done chunk, render Markdown
once and swap, restoring code blocks, lists, and clean-copy source.
_user_text was misclassifying any prompt whose text block didn't start
with '### [WORKING MEMORY]' as a real user message — but auto-continue
prompts can interleave [SYSTEM TIPS], [System] regenerate triggers,
[DANGER] guard injections, and similar. Treat any prompt carrying a
tool_result block as auto-continue (the natural signal that this is
the next round of an in-flight LLM call), and extend the injection
marker list as a secondary guard for the rare tool_result-less
synthetic prompts.
extract_ui_messages now renders tool_use headers and tool_result fences
into the assistant content using the same string format that agent_loop
yields live, so fold_turns folds restored sessions identically to live
chat. Tool-only response rounds no longer drop the user message, and
Turn 1 also carries a marker so the first turn folds like the rest.
The two CJK-aware drop-ins (divide_line for Rich/textual.content, and
compute_wrap_offsets for textual._wrap/_wrapped_document) shared
identical greedy-pack-then-fold core logic, written twice. Extract into
_fold_chunk_cells so each call site only handles interface shape (Rich
words vs Textual chunk regex, tab handling).
Also extend the patch to cover textual._wrap.compute_wrap_offsets and
textual.document._wrapped_document — the TextArea / Static-via-Content
paths use compute_wrap_offsets, not divide_line, so the prior patch
missed input-box and user-bubble wrap. Drop try/except defensiveness
around the Textual patch (we target latest Textual; let real breakage
surface), split _CJK_WRAP_RE into commented sub-ranges, prune WHAT
comments.
Verified: non-CJK output remains byte-equivalent to upstream across
both divide_line and compute_wrap_offsets fixture sets; live render at
width 60 packs the first line to ~98% utilization on input box, USER
bubble, and AGENT bubble.
_render_md now produces two renders: the existing narrow ANSI roundtrip
for display (mouse selection still anchors via segment.style.meta) and a
wide render at width=10000 that contains only structural \n. An alignment
walk maps each visual line back to a position in the wide source, so
SelectableStatic.get_selection extracts source text without wrap-induced
newlines.
The aligner handles three layouts:
- 1:1 (no wrap): narrow line maps directly to wide line
- N:1 wrap: hanging indent on continuation lines is stripped, and the
whitespace eaten at wrap points (e.g. spaces between English words) is
recovered from the wide line
- centered single-line (Markdown headers): wide and narrow have different
leading-pad widths; source uses the lstripped content, and narrow's pad
becomes per-line indent that selection clamps over
Rich's divide_line treats a Chinese run as one indivisible word and
bumps it whole to the next line when it doesn't fit the remaining
space, wasting cells like 'AI ↩ 助手...'. Patch both rich._wrap and
textual.content so Text.wrap (assistant Markdown) and Visual.to_strips
(user messages) pack leading CJK chars into the current line's
remainder, then fold the rest at full width.
Non-CJK words keep stock behavior; code-block content remains
untouched because the patch only adjusts wrap break positions.
These are agent-internal metadata: <summary> already feeds the sidebar
preview and the fold title, <thinking> is meant to be hidden. They were
leaking into the chat body, and worse, CommonMark parses
'<summary>X</summary>\n<body>' (no blank line between) as a single HTML
block that swallows the following body line — so the model's actual
reply disappeared from view whenever it wrote the summary tag immediately
before its answer with only a single newline.
Strip both tags in _render_md before passing to the Markdown parser.
The clock chip was github-dark's brighter success green (#56d364),
which read as more vibrant than the rest of the muted palette and
clashed with C_GREEN. Use the same #7ec27e the sidebar uses for the
active session marker, so all 'positive accent' greens in the chrome
sit at one saturation level.
The five topbar chips (session name / model / effort / tasks / clock)
were hardcoded github-dark hex values introduced in the upstream
topbar redesign, so they stayed cyan/amber/lavender on every theme.
Add five chip_* slots to _DEFAULT_PALETTE and derive them for
built-in themes from primary / secondary / warning / accent / success,
keeping five distinguishable hues across themes. watch_theme syncs
the C_CHIP_* globals on switch.
Also move the '▾ fold' indicator from the LEFT column to the RIGHT,
just before the clock. The left column packs identity + session +
running status pill into a ratio=1 ellipsis column, so a long status
('running 2m 15s') was eating the trailing fold glyph as '...'.
Adds Ctrl+T theme cycling across six palettes: a custom ga-default
matching the existing github-dark scheme plus Textual built-ins
(nord, gruvbox, dracula, tokyo-night, textual-light). Themes flow
through Textual's native register_theme + get_css_variables, so CSS
references $ga-bg / $ga-fg / etc. that resolve from the active theme;
ga-default uses our exact palette while built-ins derive the missing
slots (border/dim/muted/border_hi) from resolved bg/fg via a blend.
watch_theme keeps the C_* module globals (Rich Text colors) and the
role-color dict in sync with the active theme, invalidates message
caches, and remounts the current session. Rich Markdown rendering
also passes a per-theme RichTheme so code blocks, links, headings,
and quotes follow the active palette instead of Rich's frozen
defaults. text-muted / text-disabled are deliberately not inherited
from Textual: those resolve to 'auto NN%', a Textual-only syntax
Rich Text rejects.
Adds to frontends/tuiapp_v2.py:
- /rename <name>: rename current session; collision-checked across in-memory
sessions and the persistent registry. Topbar and sidebar refresh in place.
- /continue <name>: resume by stored name in addition to the existing index
form (/continue 1). The picker (/continue with no arg) labels each entry
with its stored name when available.
- /cost [all]: per-session token usage (input / output / cache_create /
cache_read), elapsed time, request count, and context-window %-left. The
"all" form lists every tracked thread, including ended sessions.
- ask_user picker upgrades: free-text escape hatch on every list, Esc-back
navigation, single/multi-select switch from a [多选]/[multi] question hint,
candidate sanitizer that handles JSON-envelope debris.
- Runtime tip footer (`└ Tip: ...`) that rotates short usage hints.
- Topbar status chip showing the active model + idle/busy state.
- Cross-platform launcher hardening: auto-install of rich/textual on first
run, and a hint when running under git-bash/mintty.
New modules:
- frontends/session_names.py: JSON sidecar at temp/model_responses/
session_names.json keyed by log-file basename; touched only by /rename and
/continue <name>. continue_cmd is not modified.
- frontends/cost_tracker.py: per-thread TokenStats, captured via
monkey-patches on llmcore._record_usage and llmcore.print (Anthropic SSE's
final output count only arrives via the latter). Idempotent install().
No core files touched.