* fix(continue): preserve session workspace on in-place restore
In-place /continue retargets agent.log_path to the restored file, so the
reset bind `_bind_workspace(None)` ran during restore was persisting
session_ws_set(path, "") — erasing that session's own workspace mapping to
"" (read back as "explicitly off") before we read it, which also short-
circuited the log-scan fallback. The continued session never re-entered its
workspace.
Add a `persist` flag to `_bind_workspace`; the restore-time reset passes
persist=False so it only refreshes in-memory state. The session→workspace
map is now written solely by explicit /workspace, /workspace off, and a
successful restore.
* fix(tui_v3): support Home/End keys for line start/end
PTK delivers Home/End as raw VT sequences in KeyPress.data, which
_esc_repl swallowed (only arrows were decoded). Map \x1b[H/[F/[1~/[4~
and SS3 \x1bOH/\x1bOF to internal bytes 0x07/0x14, extend _ESC_RE to
match SS3 as whole sequences, and add jump-to-line-start/end handlers
in _keys. Home no longer collides with Ctrl+A select-all.
* feat(worldline): integrate checkpoint-tree rewind into tui_v2
- tuiapp_v2.py: /rewind durable inline picker + /worldline three-pane
checkpoint tree (tree UI inlined; colors follow the v2 theme) with a
per-node diff viewport; conv-only /rewind restores via the persistent store.
- worldline.py (new): UI-agnostic backend — RewindStore (persistent
checkpoint tree + content-addressed blobs), reconcile, restore_plan,
node_diff, native-log projection.
- continue_cmd.py: gated worldline support — list_sessions(rewind_root=),
continue_inplace/copy(allow_empty=); default-off, other UIs byte-identical.
- tui_v3.py: status line shows concrete model id (unrelated minor tweak).
* feat(worldline): /rewind reuses the restore-mode picker (conv/code/both)
After picking a turn, /rewind now opens the same RestoreModeScreen as
/worldline to choose conversation / code / both, then restores via the
persistent store — making /rewind a true lite view of /worldline (linear
list, no tree/diff) over the identical restore backend.
On macOS the screenshot path lacked a counterpart to win32's
ClientToScreen. Detecting a target inside a cropped GrabScreen(bbox)
image and feeding the in-image (px,py) straight to Click silently
mis-clicks because the crop-origin offset is dropped.
- add CropToScreen(bbox, x, y): in-crop coords -> absolute physical
coords (pure crop-origin add, no scaling)
- GrabScreen docstring: warn about the crop-origin trap and not to
hand-roll screencapture -R (it takes logical points)
- Click 0%-pixel-change warning now names the common causes
- ljqCtrl_sop.md: add macOS subsection mirroring the win32 trap note
In-place /continue retargets agent.log_path to the restored file, so the
reset bind `_bind_workspace(None)` ran during restore was persisting
session_ws_set(path, "") — erasing that session's own workspace mapping to
"" (read back as "explicitly off") before we read it, which also short-
circuited the log-scan fallback. The continued session never re-entered its
workspace.
Add a `persist` flag to `_bind_workspace`; the restore-time reset passes
persist=False so it only refreshes in-memory state. The session→workspace
map is now written solely by explicit /workspace, /workspace off, and a
successful restore.
Annotate config block to guide new GA instances: enumerate candidate
mykey.py variable names and experimentally probe which config works;
only print var/field names, model, apibase host/path, status codes and
error types (never full dict or apikey/token); include a config format
example. Also flag that apibase/endpoint may differ per provider/proxy
at the two request-building sites.
ESC cancel sent session/cancel RPC but never cleared the busy flag, leaving the UI stuck if the server-side cancelled event never arrived. Also clear busy on all sessions when the bridge closes so pending poll loops exit cleanly.
Co-authored-by: dilong888 <dilong888@users.noreply.github.com>
- macljqCtrl.py: macOS implementation of mouse/keyboard/screenshot/window
enumeration mirroring ljqCtrl API, plus AX accessibility control tree
(AXElements/AXFind/AXPress) as the macOS equivalent of UIA
- computer_use.md: add section 3 documenting the macOS platform branch
- .gitignore: whitelist memory/macljqCtrl.py
web driver init nudges the browser by opening a page when no session
appears; os.startfile raises AttributeError on Linux/macOS, killing
the retry loop on headless servers. webbrowser.open is stdlib and
cross-platform with the same effect.
The tool-cards change (#593) tightened _md_line_has_box_drawing to exempt
pure-horizontal '─' runs as markdown hr. But a SIMPLE-box Markdown table's
only box glyph is its header rule, so that exemption dropped whole tables
out of the passthrough copy path and into the wide-render aligner, which
maps selection cell-columns to character indices — misaligning copy of any
table with CJK (wide) cells.
Remove the pure-horizontal exemption so a table's header rule is detected
again and the table run stays passthrough. Card gutters (└─ / │ / └) keep
their own prefix-based exemptions, so tool cards are unaffected. The only
cost is a real markdown hr copying as a dash run — the pre-#593 behavior.
- Anchor file becomes temp/.active_project.<host pid>: dies with the GA process,
so closing GA auto-deactivates; re-activation requires user confirmation per SOP
- Multiple GA instances each keep their own active project, no cross-talk
- Startup sweep removes only legacy unsuffixed anchors and own-pid leftovers
(pid reuse); anchors of other pids are never touched — deliberately no
liveness probe, because os.kill(pid, 0) on Windows terminates the process
Activate a project with a file anchor (temp/.active_project); every turn
the agent_before hook appends an L1 context (rules + memory file pointer
+ wrap-up discipline) to the latest user message, while the full project
memory (L2) stays on disk for the model to read on demand.
Zero core changes: plugin + SOP + gitignore whitelist only.
Slash commands returned from _on_enter before _expand ran, so a command
argument carried the literal [Pasted text #N] / [File #N] / [Image #N]
placeholder instead of the real content — e.g. `/morphling <pasted
multi-line target>` reached the agent as the marker string, not the paste.
Expand placeholders at the top of the command branch (mirroring the normal
submit path) and clear the placeholder stores afterward. No-op for commands
without placeholders.
Ctrl+C / selection copy fell back to Textual's OSC 52 escape on Windows/Linux.
Some terminals (notably VSCode's integrated terminal) decode the OSC 52 base64
payload as latin-1 instead of UTF-8, so non-ASCII copied text lands in the
clipboard as mojibake (e.g. 需要 -> éè¦). Classic Windows conhost ignores
OSC 52 clipboard writes entirely, so copy was a silent no-op there (incl.
Windows Server).
Prefer the OS-native clipboard (_copy_to_clipboard: win32 CF_UNICODETEXT /
pbcopy / wl-copy / xclip), which writes real Unicode and bypasses the terminal.
Keep OSC 52 only as a fallback, and force it under SSH (SSH_CONNECTION /
SSH_TTY) where the native call would write the remote host's clipboard instead
of the user's local one.
- _term_write(): route OSC/control sequences through Textual's driver writer
queue instead of direct sys.__stdout__. A title/OSC write landing mid-frame
during streaming used to split Textual's own escape sequences, rendering as
flashing ANSI garbage; serializing on the same writer queue lands it cleanly.
- sidebar: wrap the session list in a VerticalScroll (#sidebar-scroll) so it
scrolls when overflowing (scrollbar-gutter reserved to avoid row reflow);
inner #sidebar Static keeps padding so the click hit-test math is unchanged.
Keyboard session-switch scrolls the active session into view.
Rename tracked email.py/lark.py to _email_example.py/_lark_example.py so
the poller (which skips _-prefixed files) no longer polls them on a fresh
clone. Activate by copying to email.py/lark.py (now gitignored). Also nudge
manager to reuse existing subagents on IM signals.
/continue list previews showed JSON debris (e.g. `{`, `" } ]} === Response ===`)
for some sessions. Two causes in _preview_from_file:
1. Models sometimes emit an unclosed <summary>, so the non-greedy DOTALL regex
pairs it with a far-away </summary> and captures === block headers / JSON
across rounds. Now only the latest <summary> is used, and it is rejected if
it looks like such a cross-segment capture (contains `=== `/`"role"` or
>200 chars) — falling through instead of digging for older summaries.
2. The fallback scanned for the first non-=== line, returning JSON structure
lines. It now uses the last real user prompt via a new _last_user(), which
scans Prompt blocks directly (so response-less/aborted sessions still
preview) and reuses _user_text() to skip tool_result continuations and all
_INJECT_MARKERS (WORKING MEMORY / SYSTEM TIPS / DANGER ...).
Scanned 261 local logs: garbage previews 26 -> 0.
* fix(tui): stop CRLF stdout rendering as blank lines (v2)
Windows child stdout is `\r\n`; the streaming path fed it straight to
`Text.from_ansi`, which treats `\r` as a carriage return and erases each
line's text, so a `[Stdout]` block rendered as a run of blank lines until
the turn finished (the done-state Markdown render strips `\r`). Normalise
`\r\n` -> `\n` before `from_ansi`; lone `\r` is kept so progress-bar
overwrites still work.
* fix(tui): stop leaking empty per-session task_dir signal folders
Both v2 and v3 eagerly `os.makedirs` the per-session `_intervene` /
`_keyinfo` signal dir, but the only writer (`_intervene`) already creates
it lazily — so every session that never used intervene left a stale empty
`temp/_tui_v2_<pid>_<id>` / `temp/_tui_v3_<pid>` behind (85+ accumulated).
`consume_file` tolerates a missing dir, so the eager create is redundant.
- Set only the task_dir *path*; let the writer create it on first inject.
- Remove empty dirs on session close (v2 `_cmd_close`), on graceful exit
(v2 `on_unmount`, v3 `_cleanup`), and sweep stale ones left by crashes
at startup (v2 `on_mount`, v3 `main`). `os.rmdir` only removes empty
dirs, so a still-pending `_intervene` is never clobbered.
* fix(tui): repair /continue search box cursor & up-nav loop (v2)
The SearchablePicker swallowed Right while the search Input had focus
(always treating it as "select"), so the caret could never move right in
the query. And Up could leave the list but never return: from the search
box Down entered the list, yet Up at the top row was stuck.
- Right commits the highlight only when the caret is already at end of the
query; otherwise it moves the caret within the search text.
- Up on the first list row returns focus to the search box (scoped to
/continue — no-op for ChoiceLists outside a SearchablePicker), clearing
the highlight so the next Down re-enters at the first row, not the second.
- Up from the search box wraps around to the bottom of the list.
CommonStack (https://commonstack.ai) is an OAI-compatible gateway
aggregating Claude, GPT, Gemini, DeepSeek, MiniMax, Zhipu, xAI,
Moonshot, Qwen and Xiaomi models behind a single key.
Adds it to the configure_mykey wizard as a native_oai entry,
placed alongside other multi-provider relays (openrouter, crs, gmi).
The wizard's existing probe_models() flow fetches the live
/v1/models list at config time; model_choices keeps the same
2-item fallback shape as openrouter.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
/continue previously pushed plan_scan_baseline past every restored
message, so plan_state.is_active() saw an empty slice and hid the card —
even when the prior session's plan.md still had `[ ]` items the user
wanted to keep working on. `working['in_plan_mode']` isn't replayed by
the log restore either, so the card never came back.
Make the baseline conditional: default to 0 so the scanner walks the
full restored history; only push past it when the restored plan.md is
already all-done, suppressing the stale ✓ card. Resolved-path missing
or empty items naturally fall back to "no card".
/stop now also refills the input box with the just-sent user message,
so an interrupted send is one keystroke from edit-and-resend. Only
when the box is empty (don't clobber a half-typed follow-up). Agent
history is untouched — a resend duplicates the turn in LLM context;
/rewind 1 is the manual escape.
Behaviour matrix (/continue):
unfinished plan → baseline=0, scanner finds path, card resumes
completed plan → baseline=len(msgs), card stays hidden
plan.md deleted → resolve_path None, card stays hidden
fresh enter_plan_mode after continue → working flag re-arms, normal
/continue previously pushed plan_scan_baseline past every restored
message, so plan_state.is_active() saw an empty slice and hid the card —
even when the prior session's plan.md still had `[ ]` items the user
wanted to keep working on. `working['in_plan_mode']` isn't replayed by
the log restore either, so the card never came back.
Make the baseline conditional: default to 0 so the scanner walks the
full restored history; only push past it when the restored plan.md is
already all-done, suppressing the stale ✓ card. Resolved-path missing
or empty items naturally fall back to "no card".
/stop now also refills the input box with the just-sent user message,
so an interrupted send is one keystroke from edit-and-resend. Only
when the box is empty (don't clobber a half-typed follow-up). Agent
history is untouched — a resend duplicates the turn in LLM context;
/rewind 1 is the manual escape.
Behaviour matrix (/continue):
unfinished plan → baseline=0, scanner finds path, card resumes
completed plan → baseline=len(msgs), card stays hidden
plan.md deleted → resolve_path None, card stays hidden
fresh enter_plan_mode after continue → working flag re-arms, normal
* 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.
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.
- Add sys.path.append for llmcore's own directory as fallback
- Remove dead variable _imp_err
- Distinguish missing mykey vs import failure with clear error messages
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.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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>
`self.model` was pinned to `_sessions[0].model` at init; after failover the
topbar (and any other reader of `agent.get_llm_name(model=True)`) kept showing
the primary's model name even when `_cur_idx` had moved to a fallback session.
Convert to a property returning the current session's model. `_BROADCAST_ATTRS`
doesn't include `model`, and grep finds no external writes, so making it
read-only is safe.
Co-authored-by: d 🔹 <258577966+voidborne-d@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.
- Per-fold click-to-expand: assistant messages render as a sequence of
text/fold-header/fold-body widgets; FoldHeader is a clickable Static
that toggles m._toggled_folds and remounts just that message
- Ctrl+O is authoritative: clears all per-fold overrides so the state
is uniformly all-collapsed or all-expanded
- Streaming spinner: dedicated tiny Static (`m._spinner_widget`) after
segment widgets, driven by an independent 100ms timer. Bypasses
Markdown completely so unclosed code fences / paragraph whitespace
trimming can't eat it
- Hide `**LLM Running (Turn N) ...**` markers at render time only;
fold_turns still uses them for splitting
- Ctrl+B sidebar toggle: invalidate width-keyed cache and reuse the
resize debounce path so the chat reflows reliably after toggling
- #input min-width: 10 — guards Textual TextArea.render_lines against
`range() arg 3 must not be zero` when sidebar is shown on a very
narrow window and #main collapses to ≤ 0 cols
- Pasting non-image files (.docx/.pdf/.py …) was folded as `[Image #N]`
because clipboard helper returned any file path. Split into image vs
file by extension; non-images now fold as `[File #N]` and expand to
the path on submit, same flow as images.
- Backspace/Delete used to nibble placeholders one char at a time.
Override `action_delete_left/right`: when the caret sits flush against
a `[Pasted text #N +X lines]` / `[Image #N]` / `[File #N]` placeholder
on the matching side, remove the whole span and drop its dict entry.
- Ctrl+C inside the input was clearing the whole field. Root cause:
`screen.get_selected_text()` doesn't see TextArea's internal selection,
so the handler fell through to the input-reset branch. Fixed by
preferring the focused InputArea's `selected_text` before screen-level
drag selection.
- Input text/history/pastes were globally shared across agent sessions
(single InputArea widget). Moved the four state slots
(text/history/pastes/paste_counter) onto `AgentSession`, with a
`_swap_input_for_session` snapshot/restore on session switch.
- Fix 1: _patched_log function signature mismatch
- Location: plugins-langfuse_tracing.py
- Problem: monkey-patch function only accepts 2 parameters, but the original function has 3 parameters (including the default parameter log_path)
- Fix: add log_path=None parameter and pass it to the original function
- Fix 2: Agentmain module import failure
- Location: frontends-chatapp_common.py
- Problem: frontends directory cannot be imported into the agentmain of the upper directory
- Fix: Add the parent directory to sys.path at the beginning of the file
* fix(tui): debounce v2 layout reflow on resize
Same-size resize events are deduped, layout reflow is debounced to 80ms,
and message remount is deferred to the next frame so #messages.content_region
reports the post-resize width instead of the pre-resize one.
* feat(tui): rebind v2 keys (Ctrl+C dual, Ctrl+↑↓ sessions, Ctrl+O fold) + help overlay
- Ctrl+C is the single stop/quit key: stops a running task; otherwise
clears non-empty input; otherwise arms quit (press again within 2s to
exit). Removes Ctrl+S, Ctrl+Q, and InputArea's prior Ctrl+C
copy-or-clear binding. Any non-Ctrl+C key disarms the pending quit.
- Ctrl+O toggles turn fold (was Ctrl+F, which collides with terminal
app search shortcuts on macOS). Topbar gains a small "▾ fold"
indicator when fold mode is on.
- Ctrl+Up / Ctrl+Down switches sessions (was Ctrl+Left / Ctrl+Right)
to match the vertical session-list mental model.
- Esc is the single one-press cancel for ChoiceList — drops the prior
two-step "Left to leave, Left to cancel" flow. Esc also continues to
close the command palette and now closes the help overlay.
- Adds a Ctrl+? help overlay listing every keybinding. Bound on both
ctrl+? and ctrl+question_mark to survive terminal reporting quirks.
- Newline keeps Ctrl+J / Ctrl+Enter / Shift+Enter as cross-terminal
fallbacks (Ctrl+J is the portable one for macOS Terminal.app); drops
Alt+Enter.
- Bottom bar simplifies to four essentials + Ctrl+? and / hints, and
shows a transient "再按 Ctrl+C 退出" while quit is armed.
* fix(tui): switch help shortcut from Ctrl+? to Ctrl+/
Ctrl+? (= Ctrl+Shift+/) is unreliable across terminals — many emulators
either swallow it or report it as something Textual can't parse. Ctrl+/
is the conventional VS Code / command-line help shortcut and ships with
the legacy Ctrl+_ fallback (ASCII 0x1F is what older terminals send for
Ctrl+/). All three forms are bound (ctrl+slash, ctrl+/, ctrl+underscore)
so the binding survives terminal-reporting differences.
Bottom hint bar collapses to a single left-aligned line carrying the
command-palette and help hints alongside the action keys, since the
prior right column repeated information the left column already had room
for. The help overlay header reads "快捷键帮助" instead of "快捷键".
* fix(tui): ctrl+c dual-press with non-empty input, centered help modal
- Ctrl+C with non-empty input now clears AND arms quit; a second Ctrl+C
within 2s exits. Previously typing in the input let one Ctrl+C exit
silently after the clear, which was easy to trigger by accident.
- Help is now a ModalScreen pushed over the main screen so opening it
no longer shifts the input box. Centered with a lighter background
(#21262d on #30363d border) to read as a lifted panel.
- Esc-cancelling a ChoiceList removes the prompt cleanly with no
"已取消" output — the canceled prompt simply disappears from the
chat, matching expectations for in-flight tool prompts.
* fix(tui): keep Ctrl+C copy semantics and update choice hints
When there is selected text, Ctrl+C now copies the selection and returns
without triggering stop, input clear, or quit-arming. This preserves the
TUI selection/copy path after Ctrl+C became the global stop/quit key.
Choice prompts now advertise the current Esc-cancel behavior instead of
the removed Left-arrow flow, and stale comments/docs mentioning the old
left-arrow return path were cleaned up.
PR #360 disabled Textual mouse support on darwin (run(mouse=False)) to
keep terminal-native selection working. Side effect: ?1006 SGR mouse was
never enabled, while macOS Terminal.app / iTerm2 default ?1007 (alternate
scroll mode) translated wheel events into Up/Down keys — which then
triggered the newly added input-history navigation.
Re-enable mouse globally and explicitly disable ?1007 on mount so wheel
events flow through the SGR channel without spawning duplicate arrow
keys. Keyboard Up/Down history navigation is unaffected. The ?1007
disable is a no-op on Linux / Windows Terminal where it defaults off.
When an assistant message already has a mounted body widget, streaming updates only refreshed the widget content. Scroll the messages container after those in-place updates so both streaming chunks and final responses remain visible at the bottom.
Handle paste events directly for both short and multi-line input instead of mixing TextArea's default paste path with the custom placeholder path. This keeps single-line paste from being inserted twice while preserving the existing multi-line placeholder behavior.
Keep Telegram streaming overflow messages tied to the primary live message, reuse them across edits, and delete stale overflow replies when streamed content shrinks.
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.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the model only outputs tool calls with no meaningful text,
combined_content may contain only whitespace text blocks (e.g. '\n').
Strict API proxies reject this with HTTP 400 'empty content' error.
Filter out such blocks before sending to the API.
Added instructions for setting up a virtual environment and installing dependencies.
error: No virtual environment found; run `uv venv` to create an environment, or pass `--system` to install into a non-virtual environment
The previous implementation called send_typing once with an empty
typing_ticket, which had no effect. The iLink API requires a valid
typing_ticket obtained from getconfig first.
Changes:
- Add get_typing_ticket() method to fetch ticket via getconfig API
- Replace single send_typing call with continuous typing loop
- Typing refreshes every 2 seconds until agent finishes processing
- Stops cleanly via threading.Event when message handling completes
Co-authored-by: succ985 <succ985@users.noreply.github.com>
Replace fixed 5s sleep with the same backoff pattern already used in
dcapp.py: 5 -> 10 -> 20 -> ... -> 300s, resetting to 5s when a session
survived >=60s. Uses time.monotonic() to avoid clock-jump skew.
Switch to inc_out=True mode so queue items are incremental deltas
instead of cumulative text. The old cumulative approach broke when
agentmain's post-processing (</summary>\n\n insertion) shifted offsets,
causing the bridge's delta calculation to produce overlapping or
truncated content.
Now "next" items are forwarded directly, and "done" is only used as
content when no streaming occurred (error fallback).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- os.set_inheritable(_stdout_fd, False) on both Windows and Unix
- Unix path now uses os.dup() + os.fdopen() consistent with Windows
- Prevents subprocess.Popen children from inheriting the ACP JSON-RPC
file descriptor, which could corrupt the protocol stream
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Stdio-based JSON-RPC bridge implementing the Agent Client Protocol,
allowing codeg to connect to GenericAgent as a live ACP agent.
Handles Windows binary stdout/stdin, redirects stray prints to stderr,
and converts GenericAgent's cumulative text queue to ACP delta streaming.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
sop_index.md is a user-specific file that doesn't exist in the shared repo.
Replace it with L1 Insight index which is auto-injected into context each turn.
* feat(wecom): upgrade wecom bot from text-only to full media + progress streaming
- Add image/file message handling with encrypted media download
- Add media upload & send (auto image/file type detection, fallback to text on failure)
- Add real-time per-turn progress streaming via _turn_end_hooks
- Add send_done with automatic file attachment extraction from agent replies
- Add terminal CLI: help/status/stop/exit for runtime control
- Add run stats tracking (received/completed counts)
- Refactor message parsing into shared _accept() method (dedup + auth + register)
- Harden None-safety on body.get(from) and dedup ID fallback
- Move state from module globals to instance attributes
* refactor(wecomapp): parameterize agent & client, remove dead code
- Remove module-level agent creation; inject via constructor
- Make WSClient injectable in start() for testability
- Clean up stale comments and version tag
- Add inline doc for split_limit
assets/tools_schema.json declares file_write.mode default as 'append',
but assets/tools_schema_cn.json and ga.py both default to 'overwrite':
# ga.py:369
mode = args.get("mode", "overwrite")
Since the i18n switch in 7cadbd7 loads the English schema for en_*
locales, an LLM that trusts the schema default and omits the mode
parameter will silently overwrite instead of append. Align the English
schema with the Chinese schema and the Python handler so all three
agree on 'overwrite'.
Only use a proxy for tgapp when mykeys['proxy'] is explicitly configured, avoiding the dead default local proxy and restoring Telegram polling startup.
Closes#175
subprocess.run was already patched with CREATE_NO_WINDOW, but Popen and
os.startfile were unprotected. Agent code could open visible GUI windows
via subprocess.Popen(['notepad.exe']) or os.startfile().
The proxy was hardcoded to http://127.0.0.1:2082 which breaks for users
without a local proxy (e.g. international users with direct access).
Now defaults to None; users who need a proxy can set it in mykey.py.
- Add /llm command to list available models
- Add /llm [n] command to switch to specific model
- Improve /status command to show current LLM info with emoji indicators
- Update /help text to include /llm command
- Unify command parsing logic using parts and op variables
This brings fsapp.py (Feishu/Lark) frontend in line with other chat frontends (QQ, DingTalk, WeCom) that already support all slash commands.
- Add scripts/install-macos-app.sh for one-click macOS app installation
- Uses bundled assets/images/logo.jpg as app icon
- AppleScript prompts for project folder on first run
- Updates README with macOS Desktop App installation option
- Supports both interactive and --auto non-interactive modes
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
When the user runs '/continue N' in stapp, the agent's in-memory context
is restored, but the UI previously showed only a single '✅ restored' line
— all prior chat bubbles were missing.
This change parses the target session log and reconstructs the
user/assistant message pairs into st.session_state.messages, so reopening
a session feels like the conversation was never interrupted.
* continue_cmd.py: add extract_ui_messages(path)
- parses model_responses log into [{role, content}, ...]
- groups multi-turn LLM calls (prompts whose text starts with the
'### [WORKING MEMORY]' header) into a single assistant bubble,
inserting the existing '**LLM Running (Turn N) ...**' marker so
fold_turns() renders them as collapsible segments.
- two small helpers (_user_text / _assistant_text) keep parsing local.
* stapp.py: in the /continue branch, resolve the target log path BEFORE
calling handle_frontend_command (which snapshots the current log and
would otherwise shift list_sessions indices), then replace
session_state.messages with the reconstructed history on success.
Falls back to the previous behavior for bare /continue or failure.
Co-authored-by: wjl2023 <wjl2023@users.noreply.github.com>
- llmcore: prefix all error outputs with !!! marker
- ga: detect !!!Error: [SSL: in tail of content as incomplete response and retry
- add len(content)>100 guard to avoid false positives on short responses
Rework the Feishu frontend so each user turn renders as a single
collapsible task card that patches itself in place, replacing the
dq-based streaming path that produced many fragmented messages.
- One _TaskCard per turn; hook reacts to summary / exit_reason events
from the agent loop and patches the same card.
- Each step is a foldable panel: header shows the summary, expanding
reveals three sections (auto-hidden when empty):
* Thinking - from response.thinking (separate field, not content)
* Tool Calls - tool name + truncated JSON args
* Output - response.content, with protocol tags stripped so
the header summary is not duplicated inside
- Final reply rendered as a schema 2.0 markdown card for consistency.
- Code-review pass per code_review_principles.md:
* _TaskCard owns only stateful card lifecycle (start/step/done/fail)
* Pure formatting extracted to module-level _build_step_detail and
_fmt_tool_call (no more reaching into card._private from the hook)
* Hook is a ~10-line dispatcher
* Flattened a 4-level nested lambda into a named function
Anthropic's extended thinking streaming protocol emits two delta types
for a single thinking block: `thinking_delta` (the textual reasoning)
and `signature_delta` (a base64 HMAC tag appended at the end of the
block). Both must be accumulated into the same `content_block`.
Current code only handles `thinking_delta`, so `signature_delta` events
are silently dropped. When the assistant's reply (with thinking) is
echoed back on the next turn, Anthropic's server validates the
signature and rejects the request with 400:
"Invalid `signature` in `thinking` block"
Downstream effects observed in production (via sub2api relay logs):
- Every request with history triggers a 400 signature error
- The relay strips thinking blocks and retries, which changes the
cache prefix and invalidates prompt caching, forcing a full rebuild
of cache_creation_tokens (~20k-30k per affected request)
- Measured in a 5h window: 5/25 requests suffered cache invalidation,
accounting for 53.5% of total spend that was otherwise avoidable
Fix:
1. Initialize `current_block` with an empty `signature` field when a
thinking block starts, so the dict shape matches Anthropic's spec
(`{type, thinking, signature}`).
2. Handle `signature_delta` events by appending `delta.signature` to
`current_block["signature"]`. Using `+=` (rather than assignment)
mirrors how `thinking_delta` is accumulated and is robust against
future chunked signatures.
No behavior change for clients that disable extended thinking, or for
upstreams that don't emit `signature_delta`. For `tool_use` threads
that require valid thinking signatures to preserve reasoning context,
this fix is required — the previous behavior silently corrupted them.
Verification:
- Before fix: upstream returns 400 + retry; cache_creation_tokens
spike to ~25k on every 4th-5th request in a conversation
- After fix: upstream accepts the first attempt; cache_read_tokens
dominate, cache_creation_tokens only appear on the first request
of a fresh 5m prompt-cache window
- Replace 3 old QR codes (group 5/6/7) with 4 new ones (group 6/8/9/10)
- Update both English and Chinese sections in README.md
- Remove obsolete wechat_group5.jpg and wechat_group7.jpg
- Add wechat_group8.jpg, wechat_group9.jpg, wechat_group10.jpg
compress_session.py Phase 4 builds to_del from both processed files
and every skipped file. Phase 1 marks files younger than 2h as
'recent(<2h)' (line 175) so their still-active writer is not
interrupted, but Phase 4 then deletes them anyway.
Filter the skipped-loop by reason so 'recent' files are preserved.
Processed files and other skip reasons (dup, compression error)
still proceed through deletion as before.
- plan_sop: improved planning workflow
- verify_sop: rewritten inspired by Claude Code verification agent design
- Added two failure mode anchoring (verification avoidance & seduced by first 80%)
- Anti-rationalization checklist
- BEFORE PASS/FAIL guardrails
- Compressed from 104 to 65 lines while increasing coverage
Replace full-redraw approach with frozen/live slot pattern:
- Completed segments are frozen into individual st.empty() slots
- Only the last (active) segment is re-rendered on each tick
- Reduces flicker and improves performance for long multi-turn conversations
- agent_loop: next_prompt_patcher -> turn_end_callback with full context
- agent_loop: exit logic unified (break + callback), no early return
- ga: summary extraction moved from tool_after_callback to turn_end_callback
- ga: _turn_end_hooks support for external subscribers
- stapp: desktop pet button with HTTP status push
- keychain: XOR-masked secret storage with SecretStr
- gitignore: whitelist keychain.py
Root cause: stream loop emitted a bare st.empty() heartbeat per tick, accumulating
dozens of empty slots under chat_message. On the next rerun the history replay
path had no such slots, so Streamlit's incremental DOM diff misaligned and left
the previous render of the last text block as a grayed-out ghost.
Fix: unify streaming and history replay under the same shell
(slot = st.empty(); with slot.container(): render_segments(...)). Heartbeat now
re-enters the same slot each tick; when response is unchanged Streamlit's diff
is a no-op (no flicker), but the container() call still lets StopException
propagate so abort keeps working. Simplified render_segments from 5 params
(placeholders/rendered_cache/force_text) to 2 (segments, suffix).
Net: +19 -20 lines. Verified: no ghost, abort interrupts mid-stream, per-turn
fold-collapse preserved.
- stapp.py: add st.empty() after render_segments to force Streamlit StopException check on every iteration (incl. heartbeat)
- agentmain.py: fix nround type check, fix timeout comment
- Remove position:sticky from hoist criteria (sticky is scroll-following, not popup)
- Add cover < 1.0 upper bound (real dialogs do not exceed viewport area)
- tools state now held by Session (NativeClaudeSession.tools)
- MixinSession.__setattr__ broadcasts tools/system to all sub-sessions
- NativeToolClient no longer duplicates tools storage
- fix: use type(s) is instead of isinstance to avoid catching NativeOAISession subclass
- Refactored autonomous_operation_sop.md with clearer path warnings and workflow
- Added helper.py: complete_task() auto-prepends R-number + date to history
- Updated scheduled_task_sop.md and web_setup_sop.md
- Temperature auto-clamping for MiniMax models: (0, 1] range enforcement
- <think> tag handling for MiniMax M2.7 reasoning output (alongside existing <thinking> support)
- MiniMax configuration example in mykey_template.py
- Updated README.md and GETTING_STARTED.md with MiniMax provider docs
- 19 unit tests + 6 integration tests (3 live tests with MINIMAX_API_KEY)
MiniMax models (M2.7, M2.7-highspeed, M2.5, M2.5-highspeed) are accessed via
the standard OpenAI-compatible interface at https://api.minimax.io/v1, using
the existing LLMSession with an 'oai'-prefixed config key.
- Extract shared _openai_stream() from LLMSession and NativeOAISession
- Add Responses API compatibility: system->developer role, flat tools format, no temperature
- Improve error handling: read body before raise_for_status in stream mode
- Add retry mechanism to NativeOAISession (was missing)
- Add api_mode, connect_timeout, read_timeout config to NativeOAISession
- Moonshot API (like Kimi) only accepts temperature=1.0
- Added 'moonshot' detection alongside 'kimi' in both raw_ask methods
- Prevents 400 errors when using moonshot models
GenericAgent Desktop — Windows Portable (self-contained)
No Python install, no internet for dependencies, no source checkout — everything is bundled.
Requirements
- Windows 10/11 x64
- Microsoft Edge WebView2 runtime (usually preinstalled on Win11; you are prompted if missing)
Usage
1. Extract anywhere (a path without special characters is recommended).
2. Double-click GenericAgent.exe.
3. The first launch verifies and completes the bundled Python runtime offline with a
progress UI, then opens the main window.
4. Subsequent launches start instantly.
Notes
- To actually chat, configure a model / API key inside the app.
- If Windows Defender Firewall blocks the `127.0.0.1` / `localhost` loopback connection, the app may fail to start or reach its local service. Allow `GenericAgent.exe` and `runtime\python\python.exe` through the firewall, then restart the desktop app.
- This portable build is not Windows code-signed, so SmartScreen may warn. Verify the ZIP against the published SHA256SUMS-windows.txt before running it.
- You may move the whole folder; relaunching refreshes its saved paths. To force a new
preparation pass, delete runtime\.prepared and relaunch.
- runtime\ holds the bundled runtime and source; no need to touch it.
PYTHONDONTWRITEBYTECODE=1 "$PY" -c 'import aiohttp, fastapi, importlib.util, pydantic, uvicorn, websockets; assert all(importlib.util.find_spec(name) is None for name in ("setuptools", "wheel", "pkg_resources"))'
三个平台产物来自同一提交,并在公开发布前统一验证。All three platform artifacts come from one commit and are validated before publication.
## 安装方法 / Installation
- Windows x64:下载 `GenericAgent-Desktop-Windows-Portable.zip`,解压后双击 `GenericAgent.exe`。卸载时运行 `uninstall.bat`,再删除解压目录。
Windows x64: extract `GenericAgent-Desktop-Windows-Portable.zip`, then launch `GenericAgent.exe`. Run `uninstall.bat` before deleting the extracted directory.
- Linux x86_64:下载并解压 `GenericAgent-Desktop-Linux-Portable.tar.gz`,执行 `chmod +x GenericAgent.AppImage` 后运行它。卸载时运行 `uninstall.sh`,再删除解压目录。
Linux x86_64: extract `GenericAgent-Desktop-Linux-Portable.tar.gz`, run `chmod +x GenericAgent.AppImage`, then launch it. Run `uninstall.sh` before deleting the extracted directory.
- macOS Apple silicon (arm64):打开 `GenericAgent-Desktop-macOS-aarch64.dmg`,把 `GenericAgent.app` 拖入 Applications。
macOS Apple silicon (arm64): open `GenericAgent-Desktop-macOS-aarch64.dmg` and drag `GenericAgent.app` to Applications.
## 签名状态 / Signing status
Windows 便携版尚未使用代码签名证书,SmartScreen 可能显示警告;运行前请核对 `SHA256SUMS-windows.txt`。
The Windows portable build is not code-signed, so SmartScreen may warn; verify `SHA256SUMS-windows.txt` before running it.
macOS 应用仅使用 ad-hoc 签名,未使用 Apple Developer ID,也未 notarize。首次打开可能需要右键选择“打开”或在“隐私与安全性”中允许。
The macOS app is ad-hoc signed only. It is neither Developer ID signed nor notarized; first launch may require Open from the context menu or approval in Privacy & Security.
SHA-256 清单与各产物一同发布。SHA-256 manifests are included with the artifacts.
EOF
sed -i 's/^ //' "$NOTES_FILE"
if gh release view "$TAG_NAME" >/dev/null 2>&1; then
echo "A release already exists for $TAG_NAME; refusing to overwrite it" >&2
exit 1
fi
ASSETS=()
for filename in "${EXPECTED[@]}"; do
ASSETS+=("$ASSET_DIR/$filename")
done
# Keep the release invisible while all assets upload; only the final edit publishes it.
1. Search first: no huge PS recursion/no dir traversal for file search, prefer Google for web search, check cwd first, no guessing paths
1. Search first: must use es for filename search (no PS recursion/no dir traversal), always prefer Google for web search (no duckduckgo etc), check cwd first, no guessing paths
2. Cross-verify: never trust summaries, verify numbers on detail pages
3. Encoding safety: use file_read not PS cat/type; read before modify; import memory modules directly (already in PATH, no fake prefixes)
4. Close the loop: confirm after physical simulation; request intervention after 3 failures
4. Close the loop: confirm after physical simulation; request intervention after 3 failures; complete Git workflow
5. Processes: never kill python unconditionally (kills self), use exact PID, no os.kill for liveness check
6. Windows: prefer win32gui title enumeration for GUI state
7. Web JS: use native setter + event chain for input, check disabled before click, mind quote escaping; if scan empty/incomplete wait then rescan, no conclusions from first scan
8. SOP: read SOPs not from memory, must use utils if available
8. SOP: read SOPs not from memory, must use utils if available | complex long-running/user mentions planning -> read plan_sop
You have full physical access: file I/O, script execution, browser JS injection, and system-level intervention. Never deflect with "can't do it" — don't speculate, use tools to probe.
Summarize and reply in user's language or follow user's prompt.
## Action Principles
Before each tool call, reason: current phase, whether the last result met expectations, and next strategy in reply text of each turn.
<summary> (<30 tokens) before reply text.
Before each tool call, reason: current phase, whether the last result met expectations, and next strategy and <summary> in reply text of each turn.
- Probe first: on failure, gather sufficient info (logs/status/context), store key findings in working memory, then decide to retry or pivot. Ask the user before irreversible operations.
- Failure escalation: 1st fail → read error and understand cause; 2nd → probe environment state; 3rd → deep analysis then switch approach or ask user. Never repeat an action without new information.
"description":"Code executor. Prefer python. Multi-call OK, use script param. Reply code block is executed if no script arg; prefer for single call to avoid escaping. No hardcoding bulk data",
"cwd":{"type":"string","description":"Working directory, defaults to cwd"},
@@ -53,20 +53,21 @@
}},
{"type":"function","function":{
"name":"update_working_checkpoint",
"description":"Short-term working notepad (auto-injected each turn). Call early/mid task only when there is non-obvious state worth keeping. When: (1) after reading SOP — store needs & constraints (skip tasks <5 steps); (2) before subtask switch / context flush; (3) after repeated failures — re-read SOP and store new findings; (4) on new task — replace notepad, drop old progress, keep still-valid constraints.\n\nDon't call: tasks <5 steps, greetings, or when the task is done (use start_long_term_update instead).",
"description":"Short-term working notepad, auto-injected each turn to prevent info loss in long tasks. Call during early/mid stages, not at end. When: (1) after reading SOP, store user needs & key constraints (skip for simple 1-2 step tasks); (2) before subtask switch or context flush; (3) after repeated failures, re-read SOP and must store new findings; (4) on new task, update content, clear old progress but keep valid constraints.\n\nDon't call: simple tasks (1-2 steps), task completed (use long-term memory tool)",
"parameters":{"type":"object","properties":{
"key_info":{"type":"string","description":"Replaces current notepad (<200 tokens). Incremental update: review existing, keep valid, add/remove/modify. Store: pitfalls, user requirements, key params/findings, file paths, progress, next steps. Don't store: ephemeral info, obvious context, old task info when user switched tasks. Prefer over-updating over losing key info"}}}
"key_info":{"type":"string","description":"Replaces current notepad (<200 tokens). Incremental update: review existing, keep valid, add/remove/modify. Store: pitfalls, user requirements, key params/findings, file paths, progress, next steps. Don't store: ephemeral info, obvious context, old task info when user switched tasks. Prefer over-updating over losing key info"},
"related_sop":{"type":"string","description":"Related SOP names, tips for further re-read"}}}
}},
{"type":"function","function":{
"name":"ask_user",
"description":"Interrupt task to ask user when needing decisions, extra info, or facing unresolvable blockers. Multiple questions should in one call",
"description":"Interrupt task to ask user when needing decisions, extra info, or facing unresolvable blockers",
"parameters":{"type":"object","properties":{
"question":{"type":"string","description":"For multiple questions, one per line (contain candidates)"},
"candidates":{"type":"array","items":{"type":"string"},"description":"Optional quick-select choices for a single question only. Leave empty for multi-questions"}}}
"question":{"type":"string","description":"Question for the user"},
"candidates":{"type":"array","items":{"type":"string"},"description":"Optional quick-select choices for the user"}}}
}},
{"type":"function","function":{
"name":"start_long_term_update",
"description":"After a long task completes, distill long-term memory. Call when discovering info worth remembering (env facts/user prefs/lessons learned). Skip if memory already updated or in autonomous flow. Must call when a task that took 15+ turns is completed.",
"description":"Start distilling long-term memory. Call when discovering info worth remembering (env facts/user prefs/lessons learned). Skip if memory already updated or in autonomous flow. Must call when a task that took 15+ turns is completed",
If it resolves to something unexpected, do not rely on the shortcut. Run GA from the install directory with `python launch.pyw` or `python frontends/tuiapp_v2.py`.
#### Windows Defender blocks local loopback
The desktop app talks to its local bridge over `127.0.0.1` / `localhost`. On some Windows machines, Windows Defender Firewall may block this loopback connection, making the desktop app look like it cannot start or cannot connect to the service.
If this happens, allow `GenericAgent.exe` and the bundled `python.exe` through Windows Defender Firewall, then restart the desktop app.
#### Windows TUI rendering issues
TUI rendering on Windows depends on terminal, font, and `textual` version.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import{gasq}from"./tauri-compat-D_GD_wMe.js";varx,P;functionQ(){if(P)returnx;P=1;vari=4,t=.001,e=1e-7,s=10,n=11,a=1/(n-1),c=typeofFloat32Array=="function";functionl(u,o){return1-3*o+3*u}functiong(u,o){return3*o-6*u}functionT(u){return3*u}functionr(u,o,f){return((l(o,f)*u+g(o,f))*u+T(o))*u}functionh(u,o,f){return3*l(o,f)*u*u+2*g(o,f)*u+T(o)}functionC(u,o,f,p,y){varm,_,F=0;do_=o+(f-o)/2,m=r(_,p,y)-u,m>0?f=_:o=_;while(Math.abs(m)>e&&++F<s);return_}functionb(u,o,f,p){for(vary=0;y<i;++y){varm=h(o,f,p);if(m===0)returno;var_=r(o,f,p)-u;o-=_/m}returno}functiond(u){returnu}returnx=function(o,f,p,y){if(!(0<=o&&o<=1&&0<=p&&p<=1))thrownewError("bezier x values must be in [0, 1] range");if(o===f&&p===y)returnd;for(varm=c?newFloat32Array(n):newArray(n),_=0;_<n;++_)m[_]=r(_*a,o,p);functionF(v){for(varS=0,j=1,R=n-1;j!==R&&m[j]<=v;++j)S+=a;--j;varU=(v-m[j])/(m[j+1]-m[j]),A=S+U*a,k=h(A,o,p);returnk>=t?b(v,A,o,p):k===0?A:C(v,S,S+a,o,p)}returnfunction(S){returnS===0?0:S===1?1:r(F(S),f,y)}},x}varK=Q();constz=q(K);functionB(i,t,e){returnMath.min(Math.max(i,t),e)}functionW(i){constt=/\(([^)]+)\)/.exec(i);returnt?t[1].split(",").map(e=>parseFloat(e)):[]}functionV(){leti=arguments.length>0&&arguments[0]!==void0?arguments[0]:1,t=arguments.length>1&&arguments[1]!==void0?arguments[1]:.5;conste=B(i,1,10),s=B(t,.1,2);returnn=>n===0||n===1?n:-e*Math.pow(2,10*(n-1))*Math.sin((n-1-s/(Math.PI*2)*Math.asin(1/e))*(Math.PI*2)/s)}constN=(()=>{consti=["Quad","Cubic","Quart","Quint","Sine","Expo","Circ","Back","Elastic"],t={In:[[.55,.085,.68,.53],[.55,.055,.675,.19],[.895,.03,.685,.22],[.755,.05,.855,.06],[.47,0,.745,.715],[.95,.05,.795,.035],[.6,.04,.98,.335],[.6,-.28,.735,.045],V],Out:[[.25,.46,.45,.94],[.215,.61,.355,1],[.165,.84,.44,1],[.23,1,.32,1],[.39,.575,.565,1],[.19,1,.22,1],[.075,.82,.165,1],[.175,.885,.32,1.275],(s,n)=>a=>1-V(s,n)(1-a)],InOut:[[.455,.03,.515,.955],[.645,.045,.355,1],[.77,0,.175,1],[.86,0,.07,1],[.445,.05,.55,.95],[1,0,0,1],[.785,.135,.15,.86],[.68,-.55,.265,1.55],(s,n)=>a=>a<.5?V(s,n)(a*2)/2:1-V(s,n)(a*-2+2)/2]},e={linear:[.25,.25,.75,.75]};for(constsofObject.keys(t))t[s].forEach((n,a)=>{e["ease"+s+i[a]]=n});returne})();functionD(i){if(typeofi=="function")returni;!i||typeofi!="string"?i="linear":i=i.trim();lett=i.split("(")[0];conste=W(i);lets;returnt==="cubic-bezier"||t==="cubicBezier"?z(...e.length?e:N.linear):((!t||typeoft!="string"||typeoft=="string"&&N[t]==null)&&(t="linear"),s=N[t],typeofs=="function"?s(...e):e.length?z(...e):z(...s))}classZ{constructor(){this._eventMap=newMap}on(t,e){returnt&&typeofe=="function"&&(this._eventMap.has(t)||this._eventMap.set(t,[]),this._eventMap.get(t).push(e)),this}once(t,e){vars=this;if(t&&typeofe=="function"){constn=function(){e(...arguments),s.off(t,n)};this.on(t,n)}}off(t,e){if(t)if(typeofe=="function"){consts=this._eventMap.get(t);if(Array.isArray(s)&&s.length){letn=-1;for(;(n=s.findIndex(a=>a===e))>-1;)s.splice(n,1)}}elsee==null&&this._eventMap.delete(t);returnthis}emit(t){for(vare=arguments.length,s=newArray(e>1?e-1:0),n=1;n<e;n++)s[n-1]=arguments[n];returnthis._eventMap.has(t)?(this._eventMap.get(t).forEach(a=>a(...s)),!0):!1}}functionM(i){return!!(i&&typeofi=="object"&&(i.duration>0||typeofi.easing=="string"||typeofi.easing=="function"))}functionH(i,t,e,s,n){for(constaofObject.keys(t)){constc=t[a],l=typeofc=="number"?c:c.val;if(!(typeofc=="object"&&c.done)){if(M(c)&&s&&n&&c.duration){if(c.duration+s<=n||l!==i[a])return!1}elseif(typeofe[a]=="number"&&e[a]!==0)return!1;if(i[a]!==l)return!1}}return!0}functionE(i){constt={};for(consteini)Object.prototype.hasOwnProperty.call(i,e)&&(t[e]=typeofi[e]=="number"?i[e]:i[e].val);returnt}constI=[0,0];functionL(i,t,e,s,n,a,c){constl=-n*(t-s),g=-a*e,T=l+g,r=e+T*i,h=t+r*i;returnMath.abs(r)<c&&Math.abs(h-s)<c?(I[0]=s,I[1]=0,I):(I[0]=h,I[1]=r,I)}functionJ(i){constt={},e=i&&Object.keys(i)||[];for(constsofe)t[s]=0;returnt}const$={default:{tension:170,friction:26}},Y=Object.assign(Object.assign({},$.default),{precision:.01});functionG(i){lett=arguments.length>1&&arguments[1]!==void0?arguments[1]:{};if(M(t)){consts=D(t.easing),n=typeoft.duration=="number"&&t.duration>0?t.duration:1e3;t=Object.assign(Object.assign({},t),{easing:s,duration:n})}lete=Object.assign(Object.assign(Object.assign({},Y),t),{done:!1});if(i&&typeofi=="object"&&"val"ini){if(M(i)){consts=D(i.easing),n=typeofi.duration=="number"&&i.duration>0?i.duration:parseInt(t.duration)||1e3;i=Object.assign(Object.assign({},i),{easing:s,duration:n})}e=Object.assign(Object.assign({},e),i)}elsee=Object.assign(Object.assign({},e),{val:i});returne}constw=()=>Date.now(),O=1e3/60;classttextendsZ{constructor(){lett=arguments.length>0&&arguments[0]!==void0?arguments[0]:{},e=arguments.length>1&&arguments[1]!==void0?arguments[1]:{};super(),this._props=Object.assign({},t),this._config=Object.assign({},e),this.initStates()}_wrapConfig(t,e){e=e&&typeofe=="object"?Line truncated
Loaded 100 of 252 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.