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:
@@ -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 ────────────────────────────────────
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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/) 服务器一条命令即可添加外部工具:
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user