diff --git a/EvoScientist/cli/tui_interactive.py b/EvoScientist/cli/tui_interactive.py index ba5010d..dff4a8a 100644 --- a/EvoScientist/cli/tui_interactive.py +++ b/EvoScientist/cli/tui_interactive.py @@ -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 ──────────────────────────────────── diff --git a/README.md b/README.md index 73bec70..2e05e41 100644 --- a/README.md +++ b/README.md @@ -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(

🔝Back to top

+## 🍪 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)** + +

🔝Back to top

+ ## 🔌 MCP Integration Add external tools via [MCP](https://modelcontextprotocol.io/) servers with a single command: diff --git a/README.zh-CN.md b/README.zh-CN.md index 8a5808f..84c2e1e 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -116,6 +116,7 @@ EvoScientist 超越了传统的人在回路(Human-in-the-Loop)模式,采 - [📦 安装](#-安装) - [🔑 配置](#-配置) - [⚡ 快速上手](#-快速上手) +- [🍪 示例与实践](#-示例与实践) - [🔌 MCP 集成](#-mcp-集成) - [📱 渠道接入](#-渠道接入) - [📚 致谢](#-致谢) @@ -377,6 +378,14 @@ for state in EvoScientist_agent.stream(

🔝回到顶部

+## 🍪 示例与实践 + +收集了一些官方示例、进阶用法和社区贡献的实践方案,帮助你更好地使用 EvoScientist。 + +👉 **[浏览全部示例与实践 →](docs/README.md)** + +

🔝回到顶部

+ ## 🔌 MCP 集成 通过 [MCP](https://modelcontextprotocol.io/) 服务器一条命令即可添加外部工具: diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..1109b6d --- /dev/null +++ b/docs/README.md @@ -0,0 +1,17 @@ +

🍪 Examples & Recipes

+ +

Customize your EvoScientist — harness it, make it yours.

+ +| 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 diff --git a/docs/deployment-macos-24h.md b/docs/recipes/deployment-macos-24h.md similarity index 100% rename from docs/deployment-macos-24h.md rename to docs/recipes/deployment-macos-24h.md