diff --git a/README.md b/README.md
index 8ab44455..2b42753d 100644
--- a/README.md
+++ b/README.md
@@ -351,9 +351,13 @@ Baselines across these dimensions include **Claude Code**, **OpenAI CodeX**, and
## 📅 Roadmap & News
+- **2026-05-23** — 🆕 **TUI v3 released** (`frontends/tui_v3.py`). Block-based scrollback with proper resize reflow, per-terminal color profile for cross-terminal parity, and feature parity with v2.
+- **2026-05-18** — 🆕 **Morphling mode**. Project-level skill absorption — extract goal + tests from any external repo, then decide per component: call, rewrite, or discard. See `memory/morphling_sop.md`.
+- **2026-05-17** — 🆕 **Goal Hive mode**. Multi-worker cooperative Goal mode — BBS-coordinated master/workers running long-horizon objectives in parallel. See `memory/goal_hive_sop.md`.
- **2026-05-15** — 🖥️ **Desktop GUI released**. One-line installs ship a ready-to-run desktop app (`frontends/GenericAgent.exe`). Developers launch via `python launch.pyw`.
- **2026-05-14** — 🆕 **Conductor sub-agent orchestration**. Spawn, supervise, and auto-clean parallel sub-agents; first-class delegation primitives complementing `/btw` side-questions.
- **2026-05-12** — 🆕 **TUI v2 released** (`frontends/tuiapp_v2.py`). Refined Textual frontend with image-paste folding, file paste, block-delete, Ctrl+C copy, history navigation, and `/llm` / `/export` / `/continue` pickers.
+- **2026-05-08** — 🆕 **Goal mode** (`reflect/goal_mode.py`). Time-budget-driven self-driven loop — "keep optimizing X for N hours" with no premature delivery.
- **2026-04-21** — 📄 [**Technical Report on arXiv**](https://arxiv.org/abs/2604.17091) — *GenericAgent: A Token-Efficient Self-Evolving LLM Agent via Contextual Information Density Maximization*.
- **2026-04-11** — Introduced **L4 session archive memory** and scheduler cron integration.
- **2026-03-23** — Personal WeChat supported as a bot frontend.
@@ -388,6 +392,7 @@ Thanks to the **LinuxDo** community for the support!
- [chilishark27/ga-manager](https://github.com/chilishark27/ga-manager)
- [wangjc683/galley](https://github.com/wangjc683/galley)
+- [FroStorM/A3Agent](https://github.com/FroStorM/A3Agent/tree/workbench)
---
@@ -720,9 +725,13 @@ GenericAgent 通过 **分层记忆 × 最小工具集 × 自主执行循环**
## 📅 路线图与最新动态
+- **2026-05-23** — 🆕 **TUI v3 正式发布**(`frontends/tui_v3.py`)。基于块的滚屏回看 + 正确的 resize 重排,每终端独立配色保证跨终端一致,并与 v2 达成功能对齐。
+- **2026-05-18** — 🆕 **Morphling 模式**。项目级能力吞噬 —— 从任意外部仓库抽取目标与测例后,对每个核心组件分别决定调用、重写或舍弃。详见 `memory/morphling_sop.md`。
+- **2026-05-17** — 🆕 **Goal Hive 模式**。多 worker 协作版 Goal —— Master/Worker 通过 BBS 协同推进长程目标。详见 `memory/goal_hive_sop.md`。
- **2026-05-15** — 🖥️ **桌面 GUI 发布**。一键安装会自带可直接运行的桌面端(`frontends/GenericAgent.exe`),开发者也可用 `python launch.pyw` 启动。
- **2026-05-14** — 🆕 **Conductor 子 Agent 编排**。派发、监督、自动清理并行子 Agent;与 `/btw` 旁路子 Agent 互补,提供一等公民级的任务委派原语。
- **2026-05-12** — 🆕 **TUI v2 正式发布**(`frontends/tuiapp_v2.py`)。重做视觉风格的 Textual 前端,支持图片粘贴折叠、文件粘贴、块删除、Ctrl+C 复制、历史导航,以及 `/llm` / `/export` / `/continue` 选择器。
+- **2026-05-08** — 🆕 **Goal 模式**(`reflect/goal_mode.py`)。时间预算驱动的自驱循环 —— "持续优化 X N 小时",预算没到不准提前交付。
- **2026-04-21** — 📄 [**技术报告已发布至 arXiv**](https://arxiv.org/abs/2604.17091) — *GenericAgent: A Token-Efficient Self-Evolving LLM Agent via Contextual Information Density Maximization*。
- **2026-04-11** — 引入 **L4 会话归档记忆**,并接入 scheduler cron 调度。
- **2026-03-23** — 支持个人微信接入作为 Bot 前端。
@@ -757,7 +766,7 @@ GenericAgent 通过 **分层记忆 × 最小工具集 × 自主执行循环**
- [chilishark27/ga-manager](https://github.com/chilishark27/ga-manager)
- [wangjc683/galley](https://github.com/wangjc683/galley)
-- https://github.com/FroStorM/A3Agent/tree/workbench
+- [FroStorM/A3Agent](https://github.com/FroStorM/A3Agent/tree/workbench)
---
diff --git a/docs/GETTING_STARTED.md b/docs/GETTING_STARTED.md
index 8d4525f1..eec10a58 100644
--- a/docs/GETTING_STARTED.md
+++ b/docs/GETTING_STARTED.md
@@ -48,9 +48,36 @@ python3 --version
### 下载项目
-1. 打开 [GitHub 仓库页面](https://github.com/lsdefine/GenericAgent)
-2. 点绿色 **Code** 按钮 → **Download ZIP**
-3. 解压到你喜欢的位置
+最方便的方式是 **一键安装**(自带隔离 Python 环境 + Git + 桌面端):
+
+**Windows PowerShell**
+
+```powershell
+powershell -ExecutionPolicy Bypass -c "irm http://fudankw.cn:9000/files/ga_install.ps1 | iex"
+```
+
+**Linux / macOS**
+
+```bash
+curl -fsSL http://fudankw.cn:9000/files/ga_install.sh | bash
+```
+
+或者手动 clone(开发者):
+
+```bash
+git clone https://github.com/lsdefine/GenericAgent.git
+cd GenericAgent
+uv venv && uv pip install -e ".[ui]"
+```
+
+也可以走最朴素的 ZIP:[GitHub 仓库页面](https://github.com/lsdefine/GenericAgent) → 点绿色 **Code** → **Download ZIP** → 解压到喜欢的位置。
+
+> 💡 **让 Claude / Codex 等 Agent 帮你装**:把下面这条 curl 丢给它,它会按官方指南替你完成安装:
+> ```bash
+> curl -fsSL https://raw.githubusercontent.com/lsdefine/GenericAgent/refs/heads/main/docs/installation_zh.md
+> ```
+>
+> 📖 平台差异、排障、升级流程见 [`docs/installation_zh.md`](installation_zh.md)。
### 创建配置文件
@@ -62,62 +89,55 @@ python3 --version
### 配置示例
-**最常见的用法:**
+**推荐首选:Claude 原生协议**:
```python
-# 变量名含 'oai' → 走 OpenAI 兼容格式 (/chat/completions)
-oai_config = {
- 'apikey': 'sk-你的密钥',
- 'apibase': 'http://你的API地址:端口',
- 'model': '模型名称',
+# 变量名同时含 'native' 和 'claude' → NativeClaudeSession(API 原生工具字段)
+native_claude_config = {
+ 'name': 'claude', # /llms 显示名 & mixin 引用名
+ 'apikey': 'sk-xxx', # sk-ant- 走 x-api-key;其它走 Bearer
+ 'apibase': 'https://api.anthropic.com', # 官方直连;反代渠道填对应地址
+ 'model': 'claude-opus-4-7', # [1m] 后缀触发 1M 上下文 beta
+ # 'fake_cc_system_prompt': True, # CC switch / 反代渠道必须置 True
}
```
-```python
-# 变量名含 'claude'(不含 'native')→ 走 Claude 兼容格式 (/messages)
-claude_config = {
- 'apikey': 'sk-你的密钥',
- 'apibase': 'http://你的API地址:端口',
- 'model': 'claude-sonnet-4-20250514',
-}
-```
+**也支持:OpenAI 原生协议**:
```python
-# MiniMax 使用 OpenAI 兼容格式,变量名含 'oai' 即可
-# 温度自动修正为 (0, 1],支持 M2.7 / M2.5 全系列,204K 上下文
-oai_minimax_config = {
- 'apikey': 'eyJh...',
- 'apibase': 'https://api.minimax.io/v1',
- 'model': 'MiniMax-M2.7',
+# 变量名同时含 'native' 和 'oai' → NativeOAISession
+native_oai_config = {
+ 'name': 'gpt', # /llms 显示名 & mixin 引用名
+ 'apikey': 'sk-xxx',
+ 'apibase': 'https://api.openai.com/v1', # 自动补 /v1/chat/completions
+ 'model': 'gpt-5.5',
}
```
-**使用标准工具调用格式(适合较弱模型):**
+**进阶:Mixin 故障转移**(多 session 自动切换,最稳的玩法):
```python
-# 变量名同时含 'native' 和 'claude' → Claude 标准工具调用格式
-native_claude_config = {
- 'apikey': 'sk-ant-你的密钥',
- 'apibase': 'https://api.anthropic.com',
- 'model': 'claude-sonnet-4-20250514',
+# llm_nos 按优先级排列;首项失败按指数退避切下一项
+mixin_config = {
+ 'llm_nos': ['claude', 'gpt'], # 与上面 native_* 的 name 字段对应
+ 'max_retries': 10,
+ 'base_delay': 0.5,
}
```
-> 💡 还支持 `native_oai_config`(OpenAI 标准工具调用)、`sider_cookie`(Sider)等,详见 `mykey_template.py` 中的注释。
+> 💡 完整字段说明(`thinking_type` / `reasoning_effort` / `context_win` / `proxy` / Zhipu / MiniMax / Kimi / OpenRouter 等渠道示例)见 `mykey_template.py` 顶部注释。
### 关键规则
-**变量命名决定接口格式**(不是模型名决定的):
+**变量命名决定 Session 类型**(不是模型名决定的):
-| 变量名包含 | 触发的 Session | 适用场景 |
-|-----------|---------------|---------|
-| `oai` | OpenAI 兼容 | 大多数 API 服务、OpenAI 官方 |
-| `claude`(不含 `native`) | Claude 兼容 | Claude API 服务 |
-| `native` + `claude` | Claude 标准工具调用 | 较弱模型推荐,工具调用更规范 |
-| `native` + `oai` | OpenAI 标准工具调用 | 较弱模型推荐,工具调用更规范 |
-
-> 例:用 Claude 模型,但 API 服务提供的是 OpenAI 兼容接口 → 变量名用 `oai_xxx`。
-> 例:用 MiniMax 模型 → 变量名用 `oai_minimax_config`,MiniMax 走 OpenAI 兼容接口。
+| 变量名包含 | 触发的 Session | 工具协议 | 适用场景 |
+|-----------|---------------|---------|---------|
+| `native` + `claude` | NativeClaudeSession | API 原生 tool 字段 | **推荐首选** — Claude 原生协议 |
+| `native` + `oai` | NativeOAISession | API 原生 tool 字段 | GPT/o 系列、OAI 兼容渠道 |
+| `mixin` | MixinSession | 多 session 故障转移 | 最稳;要求被引用 session 全为 native |
+| `claude`(不含 `native`) | ClaudeSession | 文本协议工具 | **deprecated**,后续版本可能移除 |
+| `oai`(不含 `native`) | LLMSession | 文本协议工具 | **deprecated**,后续版本可能移除 |
**`apibase` 填写规则**(会自动拼接端点路径):
@@ -167,13 +187,16 @@ Agent 会自己读代码、找出需要的包、全部装好。
### 升级到图形界面
-依赖装完后,就可以用 GUI 模式了:
+依赖装完后,可以选择适合你的前端:
-```bash
-python3 launch.pyw
-```
+| 前端 | 启动命令 | 说明 |
+|------|---------|------|
+| **桌面端** | 双击 `frontends/GenericAgent.exe`(Windows 一键安装自带) | 真原生窗口,零终端依赖 |
+| **TUI v3** | `python frontends/tui_v3.py` | 基于块的滚屏回看、resize 重排、每终端独立配色,跨终端体验一致 |
+| **TUI v2** | `python frontends/tuiapp_v2.py` | Textual 键盘驱动界面,图片粘贴折叠、`/llm`/`/export`/`/continue` 选择器 |
+| **Streamlit / 悬浮窗** | `python launch.pyw` | 浏览器中打开的 Streamlit UI,附带桌面悬浮窗 |
-启动后会出现一个桌面悬浮窗,直接在里面输入任务指令。
+> 💡 Windows 下推荐用 **Git Bash** 跑 TUI;PowerShell / cmd 对 Unicode 和键位支持较弱。仍异常时请直接告诉 Agent:「参考 Claude Code 在 Windows 终端的最佳配置帮我把 TUI 修一遍」。
### 可选:让 Agent 帮你做的事
@@ -244,6 +267,10 @@ Agent 会自动配好。如果你电脑上没有 Git,它也会帮你下载 por
| **Plan(规划)** | `查看你的代码,告诉我你的 plan 模式怎么启用` |
| **SubAgent(子代理)** | `查看你的代码,告诉我你的 subagent 模式怎么启用` |
| **自主探索** | `查看你的代码,告诉我你的自主探索模式怎么启用` |
+| **Goal** | `查看你的代码,告诉我 goal 模式怎么启用` |
+| **Goal Hive(多 worker 协作)** | `查看你的代码,告诉我 goal hive 模式怎么启用` |
+| **Conductor(多 subagent 编排)** | `查看你的代码,告诉我 conductor 模式怎么启用` |
+| **Morphling(吞噬外部项目)** | `查看你的代码,告诉我 morphling 模式怎么启用` |
> 💡 这就是 GenericAgent 的核心设计理念:**代码即文档**。Agent 能读懂自己的源码,所以任何功能你都可以直接问它。
@@ -268,4 +295,4 @@ GenericAgent 不预设技能,而是**靠使用进化**。每完成一个新任
> Agent 会自动 pull 最新代码并解读 commit log,告诉你新增了什么能力。
-> 更多细节请参阅 [README.md](../README.md) 或 [详细版图文教程](https://my.feishu.cn/wiki/CGrDw0T76iNFuskmwxdcWrpinPb)。
\ No newline at end of file
+> 更多细节请参阅 [README.md](../README.md) 或 [详细版图文教程](https://my.feishu.cn/wiki/CGrDw0T76iNFuskmwxdcWrpinPb)。
diff --git a/frontends/slash_cmds.py b/frontends/slash_cmds.py
index 46415073..f6298562 100644
--- a/frontends/slash_cmds.py
+++ b/frontends/slash_cmds.py
@@ -343,18 +343,20 @@ def start_service(name: str) -> tuple[bool, str]:
def _match_service(cmdline: list[str], svc: dict) -> bool:
"""Does this OS process belong to `svc`? Match on the trailing script
arg (`reflect/foo.py` for reflect tasks, `frontends/bar.py` for apps),
- which is invariant across `python` vs `pythonw` vs venv shims."""
+ which is invariant across `python` vs `pythonw` vs venv shims.
+
+ Reflect detection used to require BOTH `agentmain.py` AND the reflect
+ path in cmdline. That rejected tasks launched directly (`python
+ reflect/scheduler.py`) by launch.pyw, dev scripts, or by an earlier
+ TUI run that used a different launcher — they showed unticked in
+ /scheduler even when alive. Path-only match handles both styles; the
+ Python-process pre-filter in `running_services` keeps false positives
+ (greps, editors with the file open) from sneaking in."""
if not cmdline:
return False
rel = svc["name"] # 'reflect/foo.py' | 'frontends/bar.py'
- if svc["kind"] == "reflect":
- # agentmain.py --reflect reflect/foo.py
- has_main = any("agentmain.py" in (a or "") for a in cmdline)
- has_rel = any(rel.replace("/", os.sep) in (a or "") or rel in (a or "")
- for a in cmdline)
- return has_main and has_rel
- # frontend: either `python frontends/foo.py` or `python -m streamlit run frontends/stapp.py …`
- return any(rel.replace("/", os.sep) in (a or "") or rel in (a or "")
+ rel_norm = rel.replace("/", os.sep)
+ return any(rel_norm in (a or "") or rel in (a or "")
for a in cmdline)
@@ -503,7 +505,8 @@ def start_reflect_task(name: str) -> tuple[bool, str]:
("/goal", "[goal]", "进入 Goal 模式(需 condition 约束)"),
("/hive", "[target]", "进入 Hive 多 worker 协作模式"),
("/conductor", "[task]", "调用 frontends/conductor.py 多 subagent 编排"),
- ("/scheduler", "", "多选启动 reflect 任务 / 查看 cron"),
+ ("/scheduler", "", "多选启动/停止 reflect 任务(cron 由 reflect/scheduler.py 驱动)"),
+ ("/resume", "", "列出最近会话并恢复其中一个(GA 端展开 prompt)"),
]
diff --git a/frontends/tui_v3.py b/frontends/tui_v3.py
index 3ef2f2b6..d63e2abe 100644
--- a/frontends/tui_v3.py
+++ b/frontends/tui_v3.py
@@ -102,6 +102,8 @@
"Tip: /scheduler is live — untick a running row to stop it; tick again to relaunch.",
"Tip: Ctrl+S stashes your draft input — it's waiting for you next time you open a picker.",
"Tip: /scheduler lists reflect tasks and starts them via `/scheduler start a,b,c`.",
+ "Tip: prefix `!` runs the rest as a host shell command — output is folded into LLM history.",
+ "Tip: /resume lists recent sessions you can pick from to restore prior context.",
],
'zh': [
"Tip: 按 / 唤起命令面板 —— 方向键选择,Enter 落入输入框。",
@@ -119,6 +121,8 @@
"Tip: /update 是双分支 upstream 同步 —— 先 diff 预演,再分别快进。",
"Tip: /scheduler 里再点一下已勾选的任务可以 stop —— 取消勾选 = 停止。",
"Tip: Ctrl+S 把当前输入 stash 起来,下次 / 打开 picker 时还在。",
+ "Tip: 以 `!` 开头直接跑 shell —— 命令与输出都会进入 LLM 历史,agent 可以引用。",
+ "Tip: /resume 列出最近会话,可挑选一个恢复之前的上下文。",
],
}
@@ -146,6 +150,14 @@
'help.export': ' /export [sub] Export last reply: clip / file [name] / all',
'help.stop': ' /stop Abort current task',
'help.language': ' /language [code] View / switch interface language',
+ 'help.update': ' /update [note] Preview upstream commits & diff, then pull (no commit)',
+ 'help.autorun': ' /autorun [seed] Enter autonomous-operation mode',
+ 'help.morphling': ' /morphling [target] Distill / absorb an external skill',
+ 'help.goal': ' /goal [goal] Enter Goal mode (asks for budget / worker cap)',
+ 'help.hive': ' /hive [target] Enter Hive multi-worker mode',
+ 'help.conductor': ' /conductor [task] Hand task to conductor.py for multi-subagent run',
+ 'help.scheduler': ' /scheduler Multi-pick reflect tasks / view cron',
+ 'help.emoji': ' /emoji [style] Pick the spinner pet face (picker / direct switch)',
'help.quit': ' /quit Quit',
'help.esc': ' Esc Cancel ask · clear draft · stop task (no exit)',
'help.cc': ' Ctrl+C × 2 Quit (when idle; only aborts the task while running)',
@@ -193,7 +205,9 @@
'cmd.hive.desc': 'enter Hive multi-worker mode',
'cmd.conductor.arg': '[task]',
'cmd.conductor.desc': 'hand task to frontends/conductor.py for multi-subagent orchestration',
- 'cmd.scheduler.desc': 'multi-pick reflect tasks / show cron',
+ 'cmd.scheduler.desc': 'multi-pick start/stop reflect tasks (cron is driven by reflect/scheduler.py)',
+ 'cmd.emoji.arg': '[style]',
+ 'cmd.emoji.desc': 'pick the spinner pet face — opens picker; arg switches directly',
# status line (one-liner above input box)
'status.asking': '◉ waiting · Esc cancel',
@@ -296,9 +310,19 @@
# llm picker
'llm.title': 'Switch LLM',
+ # emoji picker (pet style)
+ 'emoji.title': 'Pick spinner pet style',
+ 'emoji.switched': 'pet style → `{style}`',
+ 'emoji.unknown': 'unknown style `{choice}` — valid: {valid}',
+ 'emoji.row.current': '● {name:<8} {sample}',
+ 'emoji.row.other': ' {name:<8} {sample}',
+ 'emoji.row.off': '(hide pet)',
+
# /scheduler picker (multi-pick reflect tasks / frontends)
'scheduler.pick.title': 'Pick services — checked = running (untick to stop)',
'scheduler.pick.hint': 'Space toggle · ↑↓ move · Enter next · Esc cancel · or /scheduler start a,b,c',
+ 'scheduler.cron.active': 'cron: {n} task(s) in sche_tasks/*.json · active (reflect/scheduler.py running)',
+ 'scheduler.cron.inactive': 'cron: {n} task(s) in sche_tasks/*.json · inactive (start reflect/scheduler.py to schedule)',
'scheduler.empty': '(no startable services: both reflect/*.py and frontends/*app*.py are empty)',
'scheduler.no_pick': '(no service picked)',
'scheduler.no_change': '(no change vs running set)',
@@ -333,6 +357,23 @@
# answer prefix when committing user reply to ask_user
'msg.answer_prefix': '[ans] {text}',
+
+ # pending input preview (queued while agent is busy)
+ 'pending.head_running': 'queued {n} (agent busy) · ↑ amend · Esc clear',
+ 'pending.head_cooldown': 'queued {n} · sending in {sec:.1f}s · ↑ amend · Esc cancel',
+ 'pending.cleared': 'cleared {n} pending message(s)',
+ 'pending.queued_marker': '[queued] {text}',
+
+ # shell-mode magic (`!` prefix)
+ 'shell.hint': '! for shell mode',
+ 'shell.timeout': '[shell: timeout {sec}s]',
+ 'shell.error': '[shell error: {err}]',
+ 'shell.empty': '(no output)',
+ 'shell.history': '[!shell] {cmd}\n```\n{out}\n```\n(exit {rc})',
+
+ # /resume
+ 'cmd.resume.desc': 'list recent sessions and pick one to recover',
+ 'help.resume': ' /resume List recent sessions and recover one',
},
'zh': {
@@ -354,6 +395,14 @@
'help.export': ' /export [sub] 导出最后回复:clip / file [name] / all',
'help.stop': ' /stop 中止当前任务',
'help.language': ' /language [code] 查看 / 切换界面语言',
+ 'help.update': ' /update [备注] 预览 upstream 提交与 diff,再 git pull(不 commit)',
+ 'help.autorun': ' /autorun [seed] 进入 autonomous_operation 自主模式',
+ 'help.morphling': ' /morphling [target] 启用 Morphling 蒸馏 / 吞噬外部技能',
+ 'help.goal': ' /goal [goal] 进入 Goal 模式(需 condition 约束)',
+ 'help.hive': ' /hive [target] 进入 Hive 多 worker 协作模式',
+ 'help.conductor': ' /conductor [task] 交给 conductor.py 做多 subagent 编排',
+ 'help.scheduler': ' /scheduler 多选启动 reflect 任务 / 查看 cron',
+ 'help.emoji': ' /emoji [style] 切换 spinner 宠物样式(picker / 直接传参)',
'help.quit': ' /quit 退出',
'help.esc': ' Esc 撤回提问 · 清草稿 · 停任务(不退出)',
'help.cc': ' Ctrl+C × 2 退出(空闲时;运行中只 abort 任务)',
@@ -401,7 +450,9 @@
'cmd.hive.desc': '进入 Hive 多 worker 协作模式',
'cmd.conductor.arg': '[任务]',
'cmd.conductor.desc': '调用 frontends/conductor.py 做多 subagent 编排',
- 'cmd.scheduler.desc': '多选启动 reflect 任务 / 查看 cron',
+ 'cmd.scheduler.desc': '多选启动/停止 reflect 任务(cron 由 reflect/scheduler.py 驱动)',
+ 'cmd.emoji.arg': '[样式]',
+ 'cmd.emoji.desc': '切换 spinner 宠物表情 — 打开 picker;带参数则直接切换',
# status line
'status.asking': '◉ 待答 · Esc 撤回提问',
@@ -504,9 +555,19 @@
# llm picker
'llm.title': '切换 LLM',
+ # emoji picker
+ 'emoji.title': '选择 spinner 宠物样式',
+ 'emoji.switched': '宠物样式 → `{style}`',
+ 'emoji.unknown': '未知样式 `{choice}` — 可选:{valid}',
+ 'emoji.row.current': '● {name:<8} {sample}',
+ 'emoji.row.other': ' {name:<8} {sample}',
+ 'emoji.row.off': '(隐藏 pet)',
+
# /scheduler picker (multi-pick reflect tasks / frontends)
'scheduler.pick.title': '挑选要启动的服务(已勾选 = 运行中,取消勾选即停止)',
'scheduler.pick.hint': 'Space 勾选 · ↑↓ 移动 · Enter 下一步 · Esc 取消 · 或 /scheduler start a,b,c',
+ 'scheduler.cron.active': 'cron:sche_tasks/*.json 共 {n} 个任务 · 已激活(reflect/scheduler.py 在运行)',
+ 'scheduler.cron.inactive': 'cron:sche_tasks/*.json 共 {n} 个任务 · 未激活(启动 reflect/scheduler.py 才会调度)',
'scheduler.empty': '(没有可启动的服务:reflect/*.py 与 frontends/*app*.py 均为空)',
'scheduler.no_pick': '(未选择任何服务)',
'scheduler.no_change': '(与当前运行集合相比无变化)',
@@ -541,6 +602,23 @@
# answer prefix
'msg.answer_prefix': '[答] {text}',
+
+ # pending input preview
+ 'pending.head_running': '已排队 {n} 条(agent 忙)· ↑ 回看修改 · Esc 清空',
+ 'pending.head_cooldown': '已排队 {n} 条 · {sec:.1f}s 后发送 · ↑ 回看修改 · Esc 取消',
+ 'pending.cleared': '已清空 {n} 条待发送消息',
+ 'pending.queued_marker': '[排队] {text}',
+
+ # shell-mode magic (`!` prefix)
+ 'shell.hint': '! 进入 shell 模式',
+ 'shell.timeout': '[shell:{sec}s 超时]',
+ 'shell.error': '[shell 错误:{err}]',
+ 'shell.empty': '(无输出)',
+ 'shell.history': '[!shell] {cmd}\n```\n{out}\n```\n(退出码 {rc})',
+
+ # /resume
+ 'cmd.resume.desc': '列出最近会话并恢复其中一个',
+ 'help.resume': ' /resume 列出最近会话并恢复其中一个',
},
}
@@ -1004,7 +1082,11 @@ def _cleanup():
# Keep SGR (color) codes, strip everything else
_ANSI_SGR_RE = re.compile(r'\x1b\[[0-9;]*m')
-_TURN_MARKER_RE = re.compile(r'\*\*LLM Running \(Turn (\d+)\).*?\*\*')
+# agent_loop.py emits `**LLM Running (Turn N) ...**` by default but switches
+# to the short `**Turn N ...**` when `handler.parent.task_dir` is set
+# (agent_loop.py:52). TUI v3 sets task_dir to enable the _intervene
+# injection hook, which silently activates the short form — match both.
+_TURN_MARKER_RE = re.compile(r'\*\*(?:LLM Running \()?Turn (\d+)\)?[^\n]*?\*\*')
_META_TAG_RE = re.compile(r'<(?:thinking|summary|tool_use|file_content)>.*?(?:thinking|summary|tool_use|file_content)>', re.DOTALL)
_TOOL_USE_BLOCK_RE = re.compile(r'```json\s*\{[^}]*"tool_name"[^}]*\}\s*```', re.DOTALL)
_TOOL_USE_TAG_RE = re.compile(r'\s*\{.*?"tool_name"\s*:\s*"([^"]+)".*?\}\s*', re.DOTALL)
@@ -1017,7 +1099,9 @@ def _cleanup():
# `` regex uses a negative lookahead so two adjacent summaries don't
# merge; the title cleaner strips fenced code + thinking before extraction.
_FENCE4_STASH_RE = re.compile(r'^`{4,}.*?^`{4,}\n?', re.DOTALL | re.MULTILINE)
-_TURN_SPLIT_FOLD_RE = re.compile(r'(\*\*LLM Running \(Turn \d+\) \.\.\.\*\*)')
+# Same as _TURN_MARKER_RE but capturing the WHOLE match for str.split() to
+# keep the marker as a separator token in the result list.
+_TURN_SPLIT_FOLD_RE = re.compile(r'(\*\*(?:LLM Running \()?Turn \d+\)? \.\.\.\*\*)')
_SUMMARY_PERTURN_RE = re.compile(r'\s*((?:(?!).)*?)\s*', re.DOTALL)
_TITLE_CLEAN_RE = re.compile(r'`{3,}.*?`{3,}|.*?', re.DOTALL)
_TITLE_ARGS_TAIL_RE = re.compile(r',?\s*args:.*$')
@@ -1229,6 +1313,14 @@ def __init__(self, llm_no: int = 0):
self.agent.llmclient = self.agent.llmclients[llm_no % len(self.agent.llmclients)]
self.agent.inc_out = True
self.agent.verbose = True
+ # Give the agent a `task_dir` so the per-turn `_intervene` file hook
+ # in ga.turn_end_callback (ga.py:576) can fire — that's how we
+ # smuggle pending user messages into the next LLM call while a
+ # turn is still in flight. Dedicated PID-scoped dir so concurrent
+ # TUI v3 processes don't step on each other's intervene files.
+ self.agent.task_dir = os.path.join(_ROOT, 'temp', f'_tui_v3_{os.getpid()}')
+ try: os.makedirs(self.agent.task_dir, exist_ok=True)
+ except Exception: pass
self.ask_user_queue: queue.Queue[AskUserEvent] = queue.Queue()
self._install_hook()
self._healthy = True
@@ -1239,6 +1331,43 @@ def __init__(self, llm_no: int = 0):
self._runner = threading.Thread(target=self._run_safe, daemon=True, name=f'ga-tui-agent')
self._runner.start()
+ def inject_intervene(self, text: str) -> bool:
+ """Write `text` to `/_intervene`. ga.turn_end_callback
+ consumes the file at the next turn boundary and APPENDS the content
+ to next_prompt as `[MASTER] ...`, so the agent sees it as part of
+ the upcoming user message without breaking the current turn.
+
+ Returns False if the agent isn't actually mid-turn (idle) — caller
+ should fall back to put_task in that case.
+
+ Append-mode write so that if the agent's `consume_file` reads and
+ deletes the file between our existence check and write, our text
+ lands in a freshly-created file (and fires the NEXT turn) instead
+ of duplicating already-consumed content from a read-modify-write
+ TOCTOU window. The `\\n\\n` separator only goes in when the file
+ already has content right before we open it — race-narrow but
+ idempotent because the worst case is a leading blank line."""
+ td = getattr(self.agent, 'task_dir', None)
+ if not td or not getattr(self.agent, 'is_running', False):
+ return False
+ try:
+ os.makedirs(td, exist_ok=True)
+ except Exception:
+ return False
+ fp = os.path.join(td, '_intervene')
+ try:
+ sep = ''
+ try:
+ if os.path.getsize(fp) > 0:
+ sep = '\n\n'
+ except OSError:
+ pass # file gone — fresh create on append, sep stays empty
+ with open(fp, 'a', encoding='utf-8') as f:
+ f.write(sep + text)
+ return True
+ except Exception:
+ return False
+
def _run_safe(self):
try:
self.agent.run()
@@ -1353,16 +1482,36 @@ def drain_display_queue(self, dq: queue.Queue, timeout: float = 0.25):
# 256 inverse (which renders muddy on Win Terminal dark themes) to truecolor.
_TILE_U = '\x1b[48;2;55;55;55m\x1b[38;2;230;230;230m'
_MARK = _ACCENT + '❯' + _RST # prompt mark — the single accent
+# Shell-mode (`!` magic prefix) accents — vivid pink so it stands out from
+# the normal accent purple without clashing with the heat-counter reds.
+_SHELL_ACCENT = '\x1b[38;5;205m' # hot pink for border / prompt mark
+_SHELL_BG = '\x1b[48;2;65;60;65m' # 65,60,65 charcoal-magenta (per spec)
+_SHELL_MARK = _SHELL_ACCENT + '!' + _RST
+# Full-row tile for committed shell rows (echo + each output line), so the
+# pair reads as one block in scrollback, matching cc-style. Slightly
+# warmer than _TILE_U (55,55,55) so the two row kinds are distinguishable
+# when interleaved. Black-terminal only — light themes get the bare band.
+_TILE_SHELL = '\x1b[48;2;65;60;65m\x1b[38;2;230;230;230m'
_BG_TOK = {str(n) for n in list(range(40, 48)) + [49] + list(range(100, 108))}
_SGR_RE = re.compile(r'\x1b\[([0-9;]*)m')
_CSI_ERASE_RE = re.compile(r'\x1b\[[0-9;?]*[JK]')
_SGR_TOKEN_RE = re.compile(r'\x1b\[[0-9;]*m')
-def _tile(s: str, style: str) -> str:
- # re-assert style after every reset so muted-markdown \x1b[0m can't punch
- # a hole in the block; \x1b[K fills to the edge (CJK-safe full-width tile).
- return style + s.replace(_RST, _RST + style) + '\x1b[K' + _RST
+def _tile(s: str, style: str, width: int | None = None) -> str:
+ # Re-assert style after every reset so muted-markdown \x1b[0m can't punch
+ # a hole in the block. When `width` is provided we pad with explicit
+ # bg-active spaces — prompt-toolkit's cell renderer doesn't honour
+ # \x1b[K (erase-to-EOL) inside its own buffer, so PTK-bound scrollback
+ # would otherwise leave the row gap exposed. Fall back to \x1b[K for
+ # legacy callers writing straight to the terminal (no PTK), where the
+ # erase command still fills correctly.
+ body = style + s.replace(_RST, _RST + style)
+ if width is None:
+ return body + '\x1b[K' + _RST
+ visible = _SGR_TOKEN_RE.sub('', s)
+ pad = max(0, width - cell_len(visible))
+ return body + ' ' * pad + _RST
def _border(left: str, right: str, width: int, style: str = _BORDER) -> str:
@@ -1399,8 +1548,8 @@ def repl(m: re.Match) -> str:
_PLACEHOLDER_RES = (_PASTE_PH_RE, _IMG_PH_RE, _FILE_PH_RE)
_IMAGE_EXTS = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.tiff', '.tif', '.ico'}
_TURN_MK_RE = re.compile(
- r'\*\*LLM Running \(Turn \d+\)[^\n]*\*\*' # native live-format marker
- r'|^[ \t]*Turn \d+\s*\.{3,}[ \t]*$', # plain subagent form on its own line
+ r'\*\*(?:LLM Running \()?Turn \d+\)?[^\n]*\*\*' # native + task-mode short form
+ r'|^[ \t]*Turn \d+\s*\.{3,}[ \t]*$', # plain subagent form on its own line
re.M)
_TOOL_RE = re.compile(
r'🛠️ Tool: `([^`]+)`[^\n]*\n' # 1 = name
@@ -1408,7 +1557,9 @@ def repl(m: re.Match) -> str:
r'(?:'
r'(`{5,})[^\n]*\n(.*?)\n\4[ \t]*\n*' # 4 = result fence (5-bt), 5 = body
r'|' # ─ OR ─
- r'(.*?)(?=^🛠️ Tool: `|^\*\*LLM Running|^|\Z)' # 6 = live exec trace
+ # Trail-end sentinel: either form of the `**Turn N ...**` marker, or a
+ # bare `Turn N ...` on its own line, or the next tool / summary tag.
+ r'(.*?)(?=^🛠️ Tool: `|^\*\*(?:LLM Running \()?Turn \d+|^Turn \d+ \.\.\.$|^|\Z)' # 6 = live exec trace
r')',
re.DOTALL | re.MULTILINE)
# Prompted-style tool wrappers GA models emit AS TEXT in saved logs (no
@@ -1449,6 +1600,15 @@ def _tool_status(result: str, trailing: str) -> str:
return 'error'
if re.match(r'^(?:Error[:\s]|Exception[:\s]|Traceback|❌|⛔)', s.lstrip(), re.I):
return 'error'
+ # ga.do_ask_user yields a 'Waiting for your answer ...' marker BEFORE the
+ # user has actually answered (it's the "I'm blocking on input" signal).
+ # The plain s.strip() truthy check below would otherwise light the chip
+ # ✓ ok the instant that marker appears — making the user think the tool
+ # has finished while the input prompt is, in reality, still waiting.
+ # Mark it pending (· …) until something else (the answer, a real status
+ # line) lands in the result.
+ if 'Waiting for your answer' in s and '✅' not in s and '成功' not in s:
+ return '?'
if '✅' in s or '成功' in s or s.strip():
return 'ok'
return '?'
@@ -1640,6 +1800,8 @@ def _cmds() -> list[tuple[str, str, str]]:
('/export', _t('cmd.export.arg'), _t('cmd.export.desc')),
('/stop', '', _t('cmd.stop.desc')),
('/language', _t('cmd.language.arg'), _t('cmd.language.desc')),
+ ('/emoji', _t('cmd.emoji.arg'), _t('cmd.emoji.desc')),
+ ('/resume', '', _t('cmd.resume.desc')),
('/quit', '', _t('cmd.quit.desc')),
]
@@ -1675,18 +1837,50 @@ def _gerund(el: float) -> str:
# width, making `(>_<)` look "fat" and shoving the heat counter sideways.
# `/emoji ascii` switches to bracketed glyphs that stay single-width on
# every terminal. `/emoji off` hides the pet entirely.
-_PETS_ASCII = (
- ('[:)] ', '[:)] ', '[:)] ', '[:|] '),
- ('[:|] ', '[;|] ', '[:|] ', '[|:] '),
- ('[-_-]', '[-_-]', '[---]', '[-_-]'),
- ('[>_<]', '[@_@]', '[>_<]', '[T_T]'),
+# Cat head — calm tier uses • (sleepy/cute look), `o` reserved for the
+# focused tier, `-` for the sleepy tier so each row's mood reads
+# distinctly. Each tier's 4 frames share a width within the tier.
+_PETS_CAT = (
+ ('=^•.•^=', '=^•.•^=', '=^-.-^=', '=^•.•^='),
+ ('=^o.o^=', '=^o.-^=', '=^o.o^=', '=^-.o^='),
+ ('=^-.-^=', '=^-.-^=', '=^v.v^=', '=^-.-^='),
+ ('=^>.<^=', '=^@.@^=', '=^>.<^=', '=^T.T^='),
+)
+# Bracketed dot-eye — same mood arc; `•` for calm, `o` for focused.
+_PETS_DOT = (
+ ('[•.•]', '[•.•]', '[-.-]', '[•.•]'),
+ ('[o.o]', '[o.-]', '[o.o]', '[-.o]'),
+ ('[-.-]', '[-.-]', '[v.v]', '[-.-]'),
+ ('[>.<]', '[@.@]', '[>.<]', '[T.T]'),
)
-_PET_STYLES = {'unicode': _PETS_UNICODE, 'ascii': _PETS_ASCII}
-_pet_style = 'unicode' # mutated by /emoji