feat(tui): double Ctrl+C quit confirmation & docs Examples/Recipes section (#78)

* feat: enhance quit handling with double Ctrl+C confirmation and cleanup logic

* feat: enhance TUI cancellation handling and improve user interruption messages

* feat: add Examples & Recipes section to documentation

* docs: remove guideline to follow the structure of existing recipes
This commit is contained in:
Xi Zhang
2026-03-20 01:25:31 +01:00
committed by GitHub
parent 649f2ca121
commit 210d71c590
5 changed files with 73 additions and 16 deletions
+38 -16
View File
@@ -268,7 +268,7 @@ def run_textual_interactive(
}
"""
BINDINGS: ClassVar[list[Binding]] = [
Binding("ctrl+c", "request_quit", "Quit", show=False),
Binding("ctrl+c", "request_quit", "Quit", show=False, priority=True),
Binding("ctrl+v", "paste_clipboard", "Paste", show=False),
Binding("tab", "tab_complete", show=False, priority=True),
Binding("up", "edit_queued", show=False, priority=True),
@@ -312,6 +312,7 @@ def run_textual_interactive(
self._mcp_browser_future: asyncio.Future | None = None
self._history_suggester = HistorySuggester(get_config_dir() / "history")
self._background_tasks: set[asyncio.Task] = set()
self._quit_pending: bool = False
# ── CommandUI implementation ─────────────────────────
@@ -1251,8 +1252,8 @@ def run_textual_interactive(
response = (state.response_text or "").strip()
except asyncio.CancelledError:
# Ctrl+C cancellation
pass
# Ctrl+C cancellation — re-raise so _run_turn can handle it
raise
except Exception as exc:
error_msg = str(exc)
if (
@@ -1345,7 +1346,7 @@ def run_textual_interactive(
await self._stream_with_widgets(user_text)
except asyncio.CancelledError:
cancelled = True
self._append_system("Interrupted.", style="yellow")
self._append_system("\nInterrupted by user", style="dim italic #ffe082")
finally:
self._busy = False
self._run_task = None
@@ -1518,6 +1519,7 @@ def run_textual_interactive(
text = event.value.strip()
prompt = self.query_one("#prompt", Input)
prompt.value = ""
self._quit_pending = False
if not text:
return
@@ -1832,8 +1834,32 @@ def run_textual_interactive(
# ── Quit handling ──────────────────────────────────────
def _arm_quit_pending(self, shortcut: str) -> None:
"""Set the pending-quit flag and show a matching hint."""
self._quit_pending = True
quit_timeout = 3 # seconds
self.notify(f"Press {shortcut} again to quit", timeout=quit_timeout)
self.set_timer(
quit_timeout,
lambda: setattr(self, "_quit_pending", False),
)
def _do_exit(self) -> None:
"""Clean up channels and exit."""
if self._channel_timer is not None:
self._channel_timer.stop()
self._channel_timer = None
self._started_channel_types.clear()
if _channels_is_running():
try:
_channels_stop()
except Exception:
pass
self.exit()
def action_request_quit(self) -> None:
if self._busy:
self._quit_pending = False
# Clear all queued messages on interrupt
if self._queued_messages:
self._queued_messages.clear()
@@ -1845,19 +1871,15 @@ def run_textual_interactive(
self._busy = False
self.query_one("#prompt", Input).focus()
self._render_status()
self._append_system("Interrupted.", style="yellow")
self._append_system(
"\nInterrupted by user", style="dim italic #ffe082"
)
return
# Clean up channels
if self._channel_timer is not None:
self._channel_timer.stop()
self._channel_timer = None
self._started_channel_types.clear()
if _channels_is_running():
try:
_channels_stop()
except Exception:
pass
self.exit()
# Double Ctrl+C to quit
if self._quit_pending:
self._do_exit()
else:
self._arm_quit_pending("Ctrl+C")
# ── Banner & status ────────────────────────────────────
+9
View File
@@ -107,6 +107,7 @@ Moving beyond traditional human-in-the-loop systems, EvoScientist adopts a human
- [📦 Installation](#-installation)
- [🔑 Configuration](#-configuration)
- [⚡ Quick Start](#-quick-start)
- [🍪 Examples & Recipes](#-examples--recipes)
- [🔌 MCP Integration](#-mcp-integration)
- [📱 Channels](#-channels)
- [📚 Acknowledgments](#-acknowledgments)
@@ -368,6 +369,14 @@ for state in EvoScientist_agent.stream(
<p align="right"><a href="#top">🔝Back to top</a></p>
## 🍪 Examples & Recipes
A curated collection of official examples, advanced usage patterns, and community-contributed recipes to help you get the most out of EvoScientist.
👉 **[Browse all examples & recipes](docs/README.md)**
<p align="right"><a href="#top">🔝Back to top</a></p>
## 🔌 MCP Integration
Add external tools via [MCP](https://modelcontextprotocol.io/) servers with a single command:
+9
View File
@@ -116,6 +116,7 @@ EvoScientist 超越了传统的人在回路(Human-in-the-Loop)模式,采
- [📦 安装](#-安装)
- [🔑 配置](#-配置)
- [⚡ 快速上手](#-快速上手)
- [🍪 示例与实践](#-示例与实践)
- [🔌 MCP 集成](#-mcp-集成)
- [📱 渠道接入](#-渠道接入)
- [📚 致谢](#-致谢)
@@ -377,6 +378,14 @@ for state in EvoScientist_agent.stream(
<p align="right"><a href="#top">🔝回到顶部</a></p>
## 🍪 示例与实践
收集了一些官方示例、进阶用法和社区贡献的实践方案,帮助你更好地使用 EvoScientist。
👉 **[浏览全部示例与实践 →](docs/README.md)**
<p align="right"><a href="#top">🔝回到顶部</a></p>
## 🔌 MCP 集成
通过 [MCP](https://modelcontextprotocol.io/) 服务器一条命令即可添加外部工具:
+17
View File
@@ -0,0 +1,17 @@
<h1 align="center">🍪 Examples & Recipes</h1>
<h3 align="center">Customize your EvoScientist — harness it, make it yours.</h3>
| Recipe | Description |
|------------------------------------------------------------|---------------------------------------------------------------------------------|
| [macOS 24/7 Deployment](recipes/deployment-macos-24h.md) | Run EvoScientist as an always-on service on macOS with ccproxy + Telegram + STT |
## Contributing a Recipe
See the [Contributing Guide](../CONTRIBUTING.md) for general guidelines. When adding a new recipe:
- **Use `EvoSci` CLI** — recipes should work with `EvoSci serve`, `EvoSci config`, or `EvoSci onboard`
- **Pin dependencies** — specify EvoScientist extras (e.g., `pip install -e ".[telegram,stt]"`)
- **Include a README** with clear setup and usage instructions
- **Keep it focused** — each recipe should demonstrate one deployment or integration scenario
- **Add to the table** above so others can discover it