From 7c4f025f2611e5ddfa4fc31ad68246d1e5145357 Mon Sep 17 00:00:00 2001 From: Wei Lee Date: Fri, 17 Jul 2026 13:13:06 +0800 Subject: [PATCH] docs: publish bilingual MkDocs site Keep the README focused on onboarding while publishing the complete operations reference through Material for MkDocs. Add localized navigation, strict Pages builds, contributor guidance, and session-state semantics aligned with current hook behavior. --- .github/workflows/docs.yml | 28 ++ .gitignore | 1 + CONTRIBUTING.md | 27 +- README.en.md | 481 ++---------------------------- README.md | 588 ++----------------------------------- docs/contributing.en.md | 152 ++++++++++ docs/contributing.md | 3 + docs/guide.en.md | 455 ++++++++++++++++++++++++++++ docs/guide.md | 477 ++++++++++++++++++++++++++++++ docs/index.en.md | 112 +++++++ docs/index.md | 112 +++++++ docs/session-states.en.md | 48 +++ docs/session-states.md | 48 +++ mkdocs.yml | 99 +++++++ pyproject.toml | 20 +- src/ring/config.py | 4 +- src/ring/registry.py | 2 +- uv.lock | 262 +++++++++++++++++ 18 files changed, 1898 insertions(+), 1021 deletions(-) create mode 100644 .github/workflows/docs.yml create mode 100644 docs/contributing.en.md create mode 100644 docs/contributing.md create mode 100644 docs/guide.en.md create mode 100644 docs/guide.md create mode 100644 docs/index.en.md create mode 100644 docs/index.md create mode 100644 docs/session-states.en.md create mode 100644 docs/session-states.md create mode 100644 mkdocs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..45c639a --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,28 @@ +name: Documentation +on: + push: + branches: + - master + - main +permissions: + contents: read + pages: write + id-token: write +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - uses: actions/configure-pages@v6 + - uses: actions/checkout@v7 + - uses: astral-sh/setup-uv@v8.3.0 + with: + python-version: "3.13" + - run: uv run --locked --group docs mkdocs build --strict --clean + - uses: actions/upload-pages-artifact@v5 + with: + path: site + - uses: actions/deploy-pages@v5 + id: deployment diff --git a/.gitignore b/.gitignore index c73b080..8181fac 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ __pycache__/ .venv/ .pytest_cache/ dist/ +site/ # gettext:.mo 是要 commit 的 ship 產物(無 build step,wheel 直接帶);.pot 是抽字串模板。 # 覆蓋全域 ~/.gitignore 的 *.mo / *.pot 規則。 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a175f5e..0878b1f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,6 @@ # Contributing to RiNG + 謝謝你願意幫 RiNG 變好。這份文件整理本 repo 的開發流程、測試方式與送 PR 前的檢查項目。 ## 開發環境 @@ -32,6 +33,19 @@ uv run pytest tests/test_cli.py uv run pytest tests/test_cli.py::test_name ``` +## 文件建置 + +文件使用 Material for MkDocs 與 `mkdocs-static-i18n`。`uv sync --all-groups` 會安裝文件依賴;本機預覽與正式檢查分別執行: + +```sh +uv run poe docs:serve # 在本機啟動可自動重載的文件站 +uv run poe docs:build # 以 strict mode 建置到 site/ +``` + +台灣華語是預設語言,來源檔使用 `name.md`;英文翻譯使用 `name.en.md`。兩種語言的文件內連結都應寫成未帶語言 suffix 的路徑,例如 `[Session 狀態](session-states.md)`,由 i18n plugin 自動連到目前語言。新增頁面時也要更新 `mkdocs.yml` 的 `nav`;menu 標題翻譯放在 `nav_translations`。 + +送出文件變更前請執行 strict build。不要 commit `site/` 產物;GitHub Pages workflow 會從 lockfile 安裝 `docs` dependency group 並重新建置。 + ## Pre-commit Hooks 建議安裝 hooks,讓格式化、lint、lockfile、commit message 檢查在本機先跑過。 @@ -49,7 +63,7 @@ uv run poe setup-pre-commit - Ruff 目標版本是 Python 3.13,行寬 120。 - Mypy 使用 strict mode;新增 API 時請補齊型別。 - CLI 行為、輸出格式、hook protocol、JSON keys 會被使用者腳本依賴;改動時請優先維持相容,必要 breaking change 要清楚標示。 -- 專案刻意允許台灣漢語註解與 UI 字串;新增英文 UI 時也要注意 i18n。 +- 專案刻意允許台灣華語註解與 UI 字串;新增英文 UI 時也要注意 i18n。 ## 測試準則 @@ -79,16 +93,17 @@ uv run poe all 改到使用者可見字串時,請更新翻譯檔。 ```sh -uv run poe i18n-extract -uv run poe i18n-check -uv run poe i18n-compile +uv run poe i18n:extract +uv run poe i18n:check +uv run poe i18n:compile ``` 注意:不要用 `pybabel update`。這會把空 `msgstr` 塞進 `.po`,可能弄壞 i18n 測試。新字串請手動把已翻譯條目加進各語言 `.po`,再重新 compile 並 commit `.po` 與 `.mo`。 ## Commit 與 Changelog -本專案使用 Commitizen / Conventional Commits,commit message 例如: +本專案使用 [Commitizen](https://commitizen-tools.github.io/commitizen/) / +[Conventional Commits](https://www.conventionalcommits.org/),commit message 例如: ```text feat: add webhook retry backoff @@ -108,6 +123,7 @@ test: cover tmux permission parsing - `uv.lock` 與依賴設定一致。 - 使用者可見字串已處理 i18n。 - README / docs 已同步更新 CLI 旗標、設定鍵、輸出格式或行為變更。 +- 文件變更已通過 `uv run poe docs:build`,中英文頁面、導覽與內部連結保持同步。 - 有 breaking change 時,PR 說明清楚列出影響與遷移方式。 ## 回報 Issue @@ -120,3 +136,4 @@ test: cover tmux permission parsing - 若和 hook / focus / permission reply 有關,請附 `ring doctor` 的相關段落。 請不要貼出含有 token、private key、完整私有 transcript 或其他敏感資訊的內容。 + diff --git a/README.en.md b/README.en.md index 4aaf857..5b09184 100644 --- a/README.en.md +++ b/README.en.md @@ -1,17 +1,15 @@ # RiNG 🎤 -[台灣漢語](https://github.com/Lee-W/ring/blob/main/README.md) · **English** +[台灣華語](README.md) · **English** [![PyPI](https://img.shields.io/pypi/v/ring-cli?label=PyPI)](https://pypi.org/project/ring-cli/) [![Python](https://img.shields.io/pypi/pyversions/ring-cli)](https://pypi.org/project/ring-cli/) [![License](https://img.shields.io/pypi/l/ring-cli)](LICENSE) > **R**ealtime **I**nstance **N**otification **G**rid -> — a local dashboard for active agent-CLI sessions. Claude Code and Codex are built in; other tools can plug in. +> — one local board for all active agent-CLI sessions. -When you run several Claude Code / Codex sessions at the same time, it is easy to lose track of -which one is still working, which one has finished a turn, and which one needs your response. -RiNG puts them on one board, with sessions waiting for you sorted first. +When several Claude Code, Codex, or local-model sessions are running, RiNG shows which ones are working, idle, or waiting for you. Sessions that need a response are sorted first. ```text 🎤 RiNG — 3 sessions on stage · 2 agent processes running @@ -21,468 +19,51 @@ RiNG puts them on one board, with sessions waiting for you sorted first. 🟡 commitizen 8m turn finished, idle ``` -## Who It Is For +## What It Does -- You run multiple Claude Code / Codex sessions in parallel. -- You want one board for “working”, “idle”, “waiting for me”, and “ended”. -- You want to jump from a TUI row back to the terminal where the session lives. -- You are willing to install hooks for precise waiting-state detection and system notifications. +- **One board for session state**: built-in sources for Claude Code, Codex, Ollama, and llama.cpp. +- **Puts waiting sessions first**: shows what 🔴 sessions need and sends desktop or phone notifications. +- **Returns you to the right terminal**: focuses tmux, iTerm2, Terminal.app, Neovim terminals, or Linux X11 windows from the TUI. +- **Avoids an extra context switch**: reply to permission requests in supported terminals and assign session names from the board. +- **Fits existing workflows**: status-bar output, JSON, a provider-neutral hook, and plugin extension points. -## Key Features +## Get Started in Three Steps -- **One board for every session**: Claude Code / Codex are built in; other tools can feed `ring hook`. -- **Waiting first**: sessions that need your response are highlighted and sorted above the rest. -- **Jump back to the terminal**: in the TUI, select a session and press `Enter` / `Space` to focus tmux, iTerm2, Terminal.app (macOS), or a Linux X11 window (`wmctrl`). -- **Notifies you without the board open**: with hooks installed, the moment a session turns 🔴 waiting it beeps and fires a system notification — even with no RiNG board running. With `terminal-notifier`, clicking the notification jumps back. -- **Reply to permission requests in place**: with the cursor on a 🔴 waiting row, press `p` — RiNG reads the permission dialog from that session's terminal screen, pops the numbered options in a modal, and presses your choice for you, so you don't have to jump over one by one (sessions inside tmux, plus plain iTerm2 tabs on macOS). -- **Name your sessions**: press `n` in the TUI to name a session, e.g. "auth refactor"; once named, the board and notifications show the name instead of the workspace directory. -- **See what it is waiting for**: in hook mode, a 🔴 waiting session carries the concrete pending item (the command to run, the question asked) — shown in the TUI and in the notification body. -- **Fits your status bar**: `ring --format oneline` prints a `🔴2 🟢1 🟡3` one-liner for tmux / SwiftBar / waybar; `--format json` feeds scripts. -- **Rings your phone too**: built-in ntfy / webhook notifiers push 🔴 waiting to your phone when you are away from the desk. -- **Local and extensible**: RiNG only reads local Claude Code / Codex data and writes `~/.config/ring/`; session sources, focusers, and notifiers are pluggable. - -## Run - -Requires Python 3.13+. The PyPI package is named `ring-cli`, while the import module and CLI command are both `ring`. +Requires Python 3.13+: ```sh -# Recommended: install the command from PyPI uv tool install 'ring-cli[tui]' - -# Or use pipx -pipx install 'ring-cli[tui]' - -# Then run it -ring ring --watch -ring --watch --interval 1 - -# Module form also works after installation -python -m ring -``` - -The `[tui]` extra installs the Textual interactive UI. Without it, `--watch` falls back to Rich polling, then plain text. - -For development inside the repository: - -```sh -uv run ring -uv run ring --watch -``` - -Zero-config mode can discover local Claude Code / Codex sessions without setup. For precise 🔴 waiting detection, install hooks: - -```sh -ring install-hooks # merges into Claude Code / Codex hook settings -ring install-hooks --dry-run # preview without writing -ring doctor # inspect hooks, notification backends, focusers, and config -ring gc --dry-run # preview RiNG-owned stale state cleanup -``` - -Hooks only apply to new sessions, so restart Claude Code / Codex sessions after installing. - -## Common Commands - -| Command | Purpose | -|---------|---------| -| `ring` | Print a one-shot snapshot | -| `ring --watch` | Keep refreshing; enters the TUI when Textual is installed | -| `ring --watch --interval 1` | Refresh every second | -| `ring --watch --count N` | Stop after N frames, useful for tests / CI | -| `ring --all` | Show ended sessions too | -| `ring --no-legend` | Hide the legend | -| `ring --lang zh-Hant` | Switch UI language | -| `ring focus SESSION_ID` | Focus a specific session; unique prefixes work | -| `ring config` | Show config path and effective settings | -| `ring config set KEY VALUE` | Write one config value | -| `ring doctor` | Read-only environment diagnosis | -| `ring digest --since 4h` | Away summary: recent session state and wait stats | -| `ring gc --dry-run` | Preview RiNG-owned stale state cleanup | -| `ring gc` | Clean RiNG-owned stale state files | -| `ring --format json` | Machine-readable board snapshot (for jq / scripts) | -| `ring --format oneline` | `🔴2 🟢1 🟡3` one-liner (for status bars) | -| `ring stats` | Waiting stats: how long agents kept 🔴 waiting in the last 7 days | -| `ring completion zsh` | Print a shell completion script (zsh / bash) | - -### Status Bar Integration (`--format`) - -```sh -ring --format oneline # 🔴2 🟢1 🟡3 (empty output when no sessions, so the segment collapses) -ring --format json | jq '.counts.waiting' -``` - -- **tmux**: `set -g status-right '#(ring --format oneline) …'` (with `status-interval 5`). -- **SwiftBar / xbar / waybar**: wrap `ring --format oneline` or consume the JSON. -- JSON keys are a stable interface (additive only), safe to script against. - -### Shell Completion (`ring completion`) - -```sh -# ~/.zshrc -eval "$(ring completion zsh)" -# ~/.bashrc -eval "$(ring completion bash)" -``` - -Completes subcommands, flags, and `config set` keys; `ring focus` prompts for a session id or unique prefix. - -## Watch Mode - -- With **Textual** (`[tui]` extra) in a real terminal: interactive TUI. - Use `↑/↓` to select, `Enter` / `Space` to jump, `p` to reply to a permission request in place, `n` to name a session, `a` to toggle ended sessions, `dd` to hide a session (it reappears automatically once it has new activity), `r` to refresh, and `q` to quit. - If you have vim muscle memory like I do, `j/k` move up/down and `g/G` jump to the first/last row. - When the selected row is 🔴 waiting, a line under the table shows **what it is concretely waiting for** (the command to run, the question asked; hook mode only). - Claude Code background agents carry a `⚙` badge. They have no terminal to jump to, so selecting one shows a `claude --resume` hint; completed agents are folded into ended sessions by default and remain available via `a`. -- Otherwise: Rich polling; without Rich, plain text. - -### Jump To A Session - -Select a session and press `Enter`, or run `ring focus SESSION_ID` with a full id or unique prefix. -RiNG focuses the terminal where that session is running. If the TUI is already open, `ring focus` -hands the request to the TUI so it selects that session; otherwise it focuses the terminal directly. - -- **tmux**: switches directly to the pane via `switch-client`. -- **Neovim `:terminal`**: uses the terminal job's inherited `$NVIM` server socket to switch to the exact buffer, then lets the outer tmux or terminal focuser raise its pane/window. -- **iTerm2 / Terminal.app** on macOS: uses the session `tty` and AppleScript to focus the matching tab. The first run may ask for macOS Automation permission. -- **Linux X11 window** (`wmctrl`, best-effort fallback): for Linux without tmux — walks from the `tty` up to the terminal window that owns it and raises it via `wmctrl`. **Limits**: X11 only (usually a no-op on Wayland), raises the whole window but cannot pick the tab, and gnome-terminal's client/server model may not match. Requires `apt install wmctrl`. - -TTY matching is most accurate in hook mode. Without hooks, Codex falls back to zero-config matching: -one live Codex session per cwd can jump correctly; multiple live Codex sessions in the same cwd are shown conservatively to avoid focusing the wrong tab. - -### Reply To Permission Requests In Place (`p`) - -With the cursor on a 🔴 waiting row, press `p`. RiNG reads that session's screen, parses the -permission dialog (Claude Code's "Do you want to proceed?" box, including background-subagent -ones with a "from the … agent" header), and lists the numbered options verbatim in a modal. -After you pick one, RiNG **captures the screen again** to confirm the dialog is still there and -unchanged (it may have been answered while you were deciding), only then sends that single -digit, and re-checks that the dialog actually disappeared. -Once the reply is verified, the TUI immediately clears that waiting revision. Only a newer hook -event, such as another permission request, can mark the session as waiting again. - -- **Sessions inside tmux**: reads the screen with `tmux capture-pane` and sends keys with - `tmux send-keys`. -- **Sessions in a plain iTerm2 tab on macOS** (no tmux): locates the matching iTerm2 session by - its `tty` via AppleScript, and both reads the screen and sends keys through `osascript`. The - first use triggers the macOS Automation permission prompt ("allow control of iTerm2") — allow - it once. - -Safety first: if no recognizable dialog can be parsed (missing markers, numbering, or cursor), -RiNG only shows a toast and **never sends a key** — without the dialog, keystrokes would land in -the chat input box as text. If the dialog happens to vanish in the instant the digit is sent and -the digit lands in the input box, RiNG sends a Backspace to clean it up and warns you. - -**Limits**: reading the screen requires a tmux pane coordinate, or (on macOS) an iTerm2 session -with a detectable tty; for other sessions, press `Enter` to jump over and reply there. - -### Notifications - -Default behavior: with hooks installed, the moment a session changes to 🔴 waiting, the hook beeps -and sends a system notification right then — **no RiNG board needs to be open**, so it rings you even -after you close the terminal (notifications are event-driven, not polled). While a TUI is open, it -also nudges you again in-app at 30s / 120s / 300s if the session keeps waiting. - -For clickable notifications on macOS, install `terminal-notifier`: - -```sh -brew install terminal-notifier -``` - -Without it, RiNG falls back to macOS text notifications without click-to-focus. -Notification sound, repeat timing, and backend selection are configurable; notification backends -are also pluggable via the `Notifier` extension point. - -#### Push To Your Phone (ntfy / webhook) - -Desktop notifications do not help when you are away from the desk. Point RiNG at an -[ntfy](https://ntfy.sh) topic to push to your phone: - -```toml -# ~/.config/ring/config.toml -notify_ntfy_url = "https://ntfy.sh/my-ring-topic" # subscribe to the same topic in the ntfy app -notify_also = ["ntfy"] # desktop notification as usual, plus a copy to the phone -``` - -`notify_backend = "ntfy"` pushes to the phone only. For Slack / your own bot / IFTTT, set -`notify_webhook_url` to use the generic webhook backend (JSON POST with a stable, additive-only payload). - -### Cleaning RiNG State Files (`ring gc`) - -When RiNG receives `SessionEnd`, it removes its own hook registry entry. If an agent crashes or the -final hook does not run, ended `~/.config/ring/sessions/*.json` files can remain. They are hidden from -the board by default, and you can remove them with `ring gc`. - -```sh -ring gc --dry-run # preview what would be deleted -ring gc # delete ended registry files older than 7 days, plus expired IPC files -ring gc --older-than 1d # use a 1-day threshold -ring gc --all-ended # delete every registry file currently classified as ended -``` - -`ring gc` only cleans state files RiNG owns under `~/.config/ring/`. It does not touch Claude Code / -Codex transcripts or state. `ring doctor` remains read-only and never deletes files. - -### Waiting Stats (`ring stats`) - -In hook mode, RiNG logs session **state transitions** to `~/.config/ring/events.jsonl` -(transitions only, tiny, self-trimming past a size cap). `ring stats` then tells you, per project, -how many times an agent 🔴 waited on you and for how long (avg / max / total). - -```sh -ring stats # last 7 days -ring stats --since 12h # custom window -``` - -Like precise notifications, 🔴 waiting is invisible to zero-config, so stats also needs hook mode. - -## Session Sources - -RiNG collects sessions from registered sources. Built-ins: - -| Source | Reads From | Precision | -|--------|------------|-----------| -| **Claude Code zero-config** | `~/.claude/projects/**/*.jsonl`, mtimes, and `cwd` fields | no setup; detects recent activity and turn completion. Precise user-action prompts require hooks | -| **Codex zero-config** | `~/.codex/state_5.sqlite`, rollout JSONL, and live `codex` processes | no setup; detects live / ended / turn completion. Use hooks for precise jumps when multiple sessions share a cwd | -| **Ollama zero-config** | interactive `ollama run` processes with a controlling terminal | process-liveness only; shows cwd, TTY, and model, and excludes `ollama serve` | -| **llama.cpp zero-config** | interactive `llama-cli` processes with a controlling terminal | process-liveness only; shows cwd, TTY, and model, and excludes `llama-server` | -| **hook registry** | `~/.config/ring/sessions/`, written by `ring hook` | precise: 🔴 waiting / 🟢 working / 🟡 idle / ⚫ ended | - -Zero-config needs no setup. For precise “who needs me”, install hooks so provider events feed the RiNG registry. -RiNG includes installers for Claude Code and Codex; other tools can use the provider-neutral `ring hook` protocol. - -Ollama and llama.cpp do not expose a session transcript or interaction hooks that RiNG can read, so their -zero-config rows stay 🟡: the row means the interactive CLI is alive, not that RiNG can distinguish generation -from waiting for the next prompt. The row disappears when the CLI exits. An outer agent that can emit lifecycle -events can use the provider-neutral hook protocol for precise states. - -## States - -RiNG reduces every session to four user-facing states. 🔴 waiting is sorted first. - -| State | Meaning | What RiNG Saw | -|-------|---------|---------------| -| 🔴 waiting | You should return now | hook event requiring a user response, such as permission or choices | -| 🟢 working | Agent is running | prompt submitted or recent activity | -| 🟡 idle | The current turn finished | `Stop`, or no new activity past `working_threshold_seconds` | -| ⚫ ended | Session is over | `SessionEnd`, process ended, or local records aged out | - -🔴 waiting requires hook mode. Zero-config can tell whether a session was recently active, but not whether it needs a decision from you. - -## Hook Mode - -Zero-config only has filesystem mtimes and local state snapshots, so it cannot reliably distinguish “needs a decision” from “just finished”. -Hooks send agent-CLI events directly to RiNG, making 🔴 waiting and notifications precise. - -### Claude Code / Codex Installer - -Hooks run `ring hook`, so install `ring` somewhere stable on `PATH` first: - -```sh -uv tool install 'ring-cli[tui]' -``` - -Then register hooks: - -```sh ring install-hooks -ring install-hooks --dry-run -``` - -Claude Code hooks are written to `~/.claude/settings.json`. If `~/.codex` exists, Codex hooks are also written to -`~/.codex/hooks.json`; Codex will ask you to trust the hook before it runs it. - -Claude Code events: - -| Claude Code Event | RiNG State | -|-------------------|------------| -| `SessionStart` / `UserPromptSubmit` | 🟢 working | -| `Stop` | 🟡 idle | -| `Notification` with `permission_prompt` / `elicitation_dialog` | 🔴 waiting | -| `PermissionRequest` / `PreToolUse` with `AskUserQuestion` | 🔴 waiting | -| `SessionEnd` | removed from the board | - -`Stop` has one more exception: an agent sometimes doesn't use `AskUserQuestion` or trigger a permission -request, and just asks a plain-text question before stopping (e.g. "want me to fix B too?"). RiNG looks at -the **end** of the last assistant message for this: if it ends in a question mark (`?` / `?`, with any -trailing fenced code block stripped first) the session is promoted to 🔴 waiting (`waiting_kind="question"`), -with the question as its detail; a question that only appears mid-message, with a statement at the end, -does not count — conservative on purpose, biased toward missing a case rather than a false positive. This -applies to both Claude Code and Codex (`Stop` payloads from both carry `last_assistant_message`); the -`detect_stop_questions` config key can turn it off (on by default). - -Codex currently installs the supported interactive events: `PreToolUse`, `PermissionRequest`, `PostToolUse`, -and `Stop`. Codex also emits `PermissionRequest` before an existing policy auto-approves the call, so a bare -event stays 🟢 working at the hook level. Codex hooks have no "user approved" event and no heartbeat — while -an approval prompt is pending, the hook channel is **completely silent**. RiNG therefore treats the silence -itself as the signal: when the last hook event is a `PermissionRequest` and nothing has followed for more than -`codex_permission_wait_seconds` (default 10s), the board marks the session 🔴 waiting with the pending command -as its detail; any subsequent event (the next tool call, `Stop`) naturally clears it. - -Known Codex-side limitations (verified against 0.144.4; the hook payload has no field that could distinguish -these): - -- **Approve and deny are indistinguishable**: neither emits a hook event. After you approve a long-running - command, 🔴 lingers until the command finishes (`PostToolUse`); after a deny with no immediate follow-up - action, 🔴 lingers until the next event or `Stop`. -- The system notification for this timeout-promoted 🔴 is sent by the TUI's alert scheduler (there is no hook - event to notify from); headless `--watch` does not send this particular notification. - -Verify that hooks are writing: - -```sh -ls ~/.config/ring/sessions/ -``` - -### Other Providers - -Any tool can feed JSON into `ring hook`: - -```sh -ring hook --provider codex -# shorthand: -ring hook codex -``` - -Payload field names are intentionally loose. At minimum, provide a session id, event, and cwd: - -```json -{ - "provider": "codex", - "session_id": "thread-123", - "event": "Notification", - "notification_type": "permission_prompt", - "requires_action": true, - "waiting_for": "permission", - "cwd": "/repo/app", - "tty": "/dev/ttys003", - "last_action": "waiting for permission" -} -``` - -If a provider can distinguish “needs user action now” from “just waiting for the next prompt”, -send `requires_action = true/false` or `waiting_for = "permission" | "options" | "next_step"`. - -### agent-hooks - -Notification details: system notifications are sent by `ring hook` at the event (both `--watch` and -the TUI only render the board; they no longer send system notifications). The default backend is `auto`: -RiNG prefers clickable `terminal-notifier`, then falls back to `osascript` on macOS or `notify-send` -on Linux. If none are available, RiNG keeps the board running and skips notifications. - -Config keys include `notify_sound`, `notify_sound_name`, `notify_ignore_dnd`, -`notify_repeat_seconds`, `notify_repeat_max`, `notify_backend`, and `waiting_cooldown_seconds`. -Custom notification channels can be added by registering another `Notifier`. - -Background subagent permission requests can make a session flap between waiting and working in -quick succession. `waiting_cooldown_seconds` (180s by default) stops `ring hook`'s system -notification and the TUI's bell/reminder from firing again the instant a session re-enters waiting -within the cooldown window of its last alert; set it to `0` to disable the cooldown (alert on -every re-entry, the old behavior). - -`agent-hooks` is an optional external hook helper / decision UI. If it is installed and -`notify_backend = "agent-hooks"`, `ring hook` still writes RiNG registry state, then passes the -raw payload to `agent-hooks callback` for the synchronous decision UI. RiNG `--watch` will not send -a duplicate notification. If `agent-hooks` is not on `PATH`, RiNG automatically falls back to `auto`. - -### Remove Hooks - -```sh -ring remove-hooks -ring remove-hooks --dry-run ``` -This removes RiNG-installed hook entries from Claude Code / Codex settings. It does not delete -`~/.config/ring/` session records and does not touch other hooks. - -## Configuration - -`~/.config/ring/config.toml`, all optional: - -```toml -lang = "en" -interval = 2.0 -show_all = false -legend = true -active_window_seconds = 21600 -working_threshold_seconds = 90 -waiting_window_seconds = 1800 -codex_permission_wait_seconds = 10 # bare Codex PermissionRequest + hook silence beyond this → 🔴 waiting; 0 = off -detect_stop_questions = true # promote Stop to 🔴 waiting when it ends in a plain-text question; false = Stop always 🟡 -notify_sound = true -notify_sound_name = "Glass" -notify_ignore_dnd = false -notify_backend = "auto" # auto / terminal-notifier / osascript / notify-send / agent-hooks / none -notify_repeat_seconds = [30, 120, 300] -notify_repeat_max = 3 -waiting_cooldown_seconds = 180 # suppress an immediate re-alert on re-entering waiting within this window; 0 = off -notify_ntfy_url = "" # full ntfy topic URL enables phone push (e.g. https://ntfy.sh/my-topic) -notify_webhook_url = "" # URL enables the generic webhook backend (JSON POST) -notify_also = [] # extra backends fired besides the primary, e.g. ["ntfy"] -focusers = ["Neovim", "tmux", "iTerm2", "Terminal", "linux-wm"] -plugins = [] # external plugin modules imported at startup (see Extending) - -[colors] -waiting = "bold red" -working = "green" -idle = "yellow" -ended = "grey50" -project = "cyan" -location = "bright_blue" -muted = "grey50" -``` - -## Extending - -RiNG is not tied to a specific tool or terminal. - -| Extension Point | Purpose | Built-ins | -|-----------------|---------|-----------| -| `SessionSource` | find sessions | Claude Code, Codex, Ollama, llama.cpp, hook registry | -| `Focuser` | jump to terminals | tmux, iTerm2, Terminal.app, Linux X11 (wmctrl) | -| `Notifier` | notify when sessions are waiting | terminal-notifier, osascript, notify-send, ntfy, webhook | - -Each backend is a small module under `ring/sources/`, `ring/focus/`, or `ring/notify/`, registered via `register_*()`. - -### Loading Your Plugin Into An Installed `ring` - -`register_*()` only counts if something runs it. The installed `ring` command loads plugins from -two places at startup: - -1. **Entry point** (for published packages) — declare it in your package's `pyproject.toml`, - pointing at a module or callable (a module registers on import; a callable is invoked once - with no arguments): - - ```toml - [project.entry-points."ring.plugins"] - mytool = "ring_mytool.plugin" - ``` - -2. **Config** (for local scripts) — add `plugins = ["my_module"]` to `~/.config/ring/config.toml`; - the module must be importable (site-packages or `PYTHONPATH`). - -A broken plugin prints one warning line to stderr and never blocks the board. - -## Platform & Privacy +1. `ring --watch` opens the interactive board. Select a session and press `Enter` to return to its terminal. +2. `ring install-hooks` installs Claude Code and Codex hooks for precise 🔴 waiting states and system notifications. +3. Restart agent sessions after installing hooks. RiNG can then notify you even while its board is closed. -- **Platform**: macOS / Linux. Windows is not supported. -- **Privacy**: local by default. RiNG only reads local `~/.claude/` and `~/.codex/` data and writes - `~/.config/ring/`. No telemetry. The only network calls are the optional ntfy / webhook notifiers, - and only to URLs you configure yourself. +Use `pipx install 'ring-cli[tui]'` if you prefer pipx. Run `ring` for a one-shot snapshot. -## Name +## Common Operations -The name is a triple pun: +| Operation | Purpose | +|-----------|---------| +| `ring` | Print a snapshot of every current session | +| `ring --watch` | Open the continuously updating interactive TUI | +| `Enter` / `Space` | Return to the selected session's terminal | +| `p` | Reply to the selected session's permission request in place | +| `n` | Name the selected session | +| `ring doctor` | Check hooks, notifications, and terminal focus support | +| `ring --format oneline` | Produce a summary for tmux, SwiftBar, or waybar | -1. It **rings** you when a session needs a response. -2. “RiNG” is the live house from *BanG Dream!*. -3. **R**ealtime **I**nstance **N**otification **G**rid describes what it is. +Zero-config mode works without hooks. Precise 🔴 waiting states, pending details, and immediate notifications require hooks. Restart Claude Code or Codex sessions after installing or updating them. -## Non-Goals +## Learn More -RiNG tracks session state. It is not a token or cost dashboard. +- [Full guide](https://lee-w.github.io/ring/guide/): commands, TUI controls, hooks, notifications, configuration, extensions, and privacy +- [Session states](https://lee-w.github.io/ring/session-states/): how RiNG derives 🔴/🟢/🟡/⚫ +- [Contributing guide](CONTRIBUTING.md): development setup, tests, and PR checklist -Claude Code JSONL token counts are currently unreliable enough to make cost accounting misleading, -so RiNG deliberately avoids that surface. +RiNG supports macOS and Linux; Windows is not yet supported. By default it only reads local agent data and writes `~/.config/ring/`. Network access occurs only for ntfy or webhook notifiers you configure. ## License diff --git a/README.md b/README.md index f425a64..c7cebfe 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,15 @@ # RiNG 🎤 -**台灣漢語** · [English](README.en.md) +**台灣華語** · [English](README.en.md) [![PyPI](https://img.shields.io/pypi/v/ring-cli?label=PyPI)](https://pypi.org/project/ring-cli/) [![Python](https://img.shields.io/pypi/pyversions/ring-cli)](https://pypi.org/project/ring-cli/) [![License](https://img.shields.io/pypi/l/ring-cli)](LICENSE) > **R**ealtime **I**nstance **N**otification **G**rid -> ——看所有 active 的 agent CLI session 上台的**場館**(內建 Claude Code / Codex,可擴充)。 +> ——把所有 active 的 agent CLI session 放上同一張本機看板。 -你同時開了好幾個 Claude Code / Codex,不知道哪個正在等你回話、哪個還在跑、 -哪個早就停了。RiNG 把它們全部請上同一個舞台,一眼看完——**誰在等你,排最前面**。 -session 需要你回話時,它「**ring** 你」。 +同時開很多 Claude Code、Codex 或本機模型時,RiNG 讓你一眼看出誰還在工作、誰已停下、誰正在等你;需要你回應的 session 會排在最前面。 ```text 🎤 RiNG — 3 session 在場 · 2 個 agent process 跑著 @@ -21,575 +19,51 @@ session 需要你回話時,它「**ring** 你」。 🟡 commitizen 8m 跑完一回合、停著 ``` -## 適合誰用 +## 能做什麼 -- 你會同時開好幾個 Claude Code / Codex session。 -- 你想用一張看板分辨「正在跑」、「跑完停著」、「正在等你」。 -- 你想在 TUI 裡選 session 後直接跳回它所在的終端。 -- 你願意裝 hook,換取精準的「等你」狀態與系統通知。 +- **集中看狀態**:內建 Claude Code、Codex、Ollama 與 llama.cpp session 來源。 +- **需要你時優先**:精準標出 🔴 等你,顯示它在等什麼,並發送桌面或手機通知。 +- **直接回到現場**:從 TUI 跳回 tmux、iTerm2、Terminal.app、Neovim terminal 或 Linux X11 視窗。 +- **少切一次視窗**:支援的終端中可直接在 TUI 回覆權限請求,也能替 session 命名。 +- **接進既有工具**:提供 status bar 單行輸出、JSON、provider-neutral hook 與 plugin 擴充點。 -## 重點功能 +## 三步上手 -- **一張看板看全部 session**:Claude Code / Codex 內建支援,其他工具可接 `ring hook`。 -- **等你優先**:需要你回應的 session 會排最上面,避免被工作中的 session 淹掉。 -- **一鍵跳回終端**:在 TUI 選 session 後按 `Enter` / `Space`,跳回 tmux、iTerm2、Terminal.app(macOS)或 Linux X11 視窗(`wmctrl`)裡的原本位置。 -- **等你時通知,不必開著看板**:裝了 hook,session 轉 🔴 等你的**當下**就響鈴 + 發系統通知——關掉 RiNG 看板、關掉終端也照樣 ring 你。裝 `terminal-notifier` 後還能點通知跳回 session。 -- **就地回覆權限請求**:游標停在 🔴 等你的列按 `p`,RiNG 讀出那個 session 終端畫面上的權限對話框選項,開浮層讓你選、代你按下——不必一個個跳過去(支援 tmux 內的 session,以及 macOS 上直接開在 iTerm2 分頁的 session)。 -- **替 session 命名**:TUI 裡按 `n` 幫 session 取名,像「重構登入」;取了名,看板與通知就直接顯示名字,不再用專案目錄名猜它在做什麼。 -- **看得到它在等什麼**:hook 模式下,🔴 等你的 session 會帶「具體在等什麼」(要跑的指令、問的問題),TUI 選中即顯示、通知內文也帶——小事可以先放著。 -- **塞進 status bar**:`ring --format oneline` 印 `🔴2 🟢1 🟡3` 單行摘要給 tmux / SwiftBar / waybar;`--format json` 給腳本吃。 -- **人不在座位也 ring 你**:內建 ntfy / webhook 遠端通知後端,等你的當下直接推到手機。 -- **預設全本機、可擴充**:只讀本機 Claude Code / Codex 資料,只寫 `~/.config/ring/`;唯一會連網的是你自己設定的 ntfy / webhook 通知。source、focuser、notifier 都可插拔。 - -## 跑起來 - -需要 Python 3.13+。PyPI 發佈名是 `ring-cli`,但 module 與指令都叫 `ring`。 -`ring` 要先裝成指令;光 clone 下來打 `ring` 會是 `command not found`。 +需要 Python 3.13+: ```sh -# 推薦:從 PyPI 裝成全域指令 uv tool install 'ring-cli[tui]' - -# 或用 pipx -pipx install 'ring-cli[tui]' - -# 之後直接打 ring -ring # 快照 ring --watch -ring --watch --interval 1 # 自訂刷新秒數 - -# 安裝後也可用 module 形式跑 -python -m ring +ring install-hooks ``` -`[tui]` extra 會安裝 Textual 互動版;不裝也能跑,只是 `--watch` 會退回 Rich poll / 純文字模式。 +1. `ring --watch` 開啟互動看板;方向鍵選 session,`Enter` 跳回原本的終端。 +2. `ring install-hooks` 安裝 Claude Code / Codex hooks,取得精準的 🔴 等你狀態與系統通知。 +3. 安裝 hook 後重開 agent session;需要你回話時,即使看板沒開,RiNG 也會通知你。 -開發時可以在 repo 裡直接跑,`uv` 會 build entry point: +偏好 pipx 時可改用 `pipx install 'ring-cli[tui]'`。只想先看快照則執行 `ring`。 -```sh -uv run ring -uv run ring --watch -``` - -zero-config 不用設定就能看 Claude Code / Codex 的本機 session;要精準偵測 🔴 等你, -再裝 hook: - -```sh -ring install-hooks # 合併寫入 Claude Code / Codex 的 hook 設定 -ring install-hooks --dry-run # 只預覽,不改檔 -ring doctor # 檢查 hook、通知後端、focuser、設定檔 -ring gc --dry-run # 預覽 RiNG 自己的 stale 狀態檔清理 -``` +## 常用操作 -hook 只對新開的 session 生效,所以裝完要重開 Claude Code / Codex session。 - -## 常用指令 - -| 指令 | 用途 | +| 操作 | 用途 | |------|------| -| `ring` | 印一張當下快照 | -| `ring --watch` | 持續刷新;有 Textual 時進互動 TUI | -| `ring --watch --interval 1` | 每 1 秒刷新 | -| `ring --watch --count N` | 刷新 N 次後結束,方便測試 / CI | -| `ring --all` | 顯示已離場 session | -| `ring --no-legend` | 隱藏圖例 | -| `ring --lang en` | 切英文 UI | -| `ring focus SESSION_ID` | 聚焦指定 session;可用唯一前綴 | -| `ring config` | 顯示設定檔路徑與生效設定 | -| `ring config set KEY VALUE` | 寫入單一設定 | -| `ring doctor` | 唯讀環境診斷 | -| `ring digest --since 4h` | 離席摘要:最近 session 狀態與等待統計 | -| `ring gc --dry-run` | 預覽 RiNG 自己的 stale 狀態檔清理 | -| `ring gc` | 清理 RiNG 自己的 stale 狀態檔 | -| `ring --format json` | 整個看板的機器可讀快照(給 jq / 腳本) | -| `ring --format oneline` | `🔴2 🟢1 🟡3` 單行摘要(給 status bar) | -| `ring stats` | 等待統計:最近 7 天你讓 agent 🔴 等了多久 | -| `ring completion zsh` | 印 shell 補全腳本(zsh / bash) | - -### 塞進 status bar(`--format`) - -```sh -ring --format oneline # 🔴2 🟢1 🟡3(沒 session 時輸出空字串,段落自然收起) -ring --format json | jq '.counts.waiting' -``` - -- **tmux**:`set -g status-right '#(ring --format oneline) …'`(配 `status-interval 5`)。 -- **SwiftBar / xbar / waybar**:包一層腳本呼叫 `ring --format oneline` 或吃 JSON 自己排版。 -- JSON 的鍵名視為穩定介面(只加不改),放心接腳本。 - -### Shell 補全(`ring completion`) - -```sh -# ~/.zshrc -eval "$(ring completion zsh)" -# ~/.bashrc -eval "$(ring completion bash)" -``` - -子命令、旗標、`config set` 的鍵都補得到;`ring focus` 會提示 session id / 唯一前綴參數。 - -### `--watch` 的兩種樣子 - -- 裝了 **Textual**(`[tui]` extra)且在真終端 → **互動 TUI**: - `↑/↓` 選 session、`Enter` / `Space` 跳到它所在的終端、`p` 就地回覆權限請求、`n` 命名、`a` 切換是否顯示已離場、`dd` 隱藏 session(有新活動會自動重新出現)、`r` 刷新、`q` 離場。 - 如果你跟我一樣有 vim 手癖,也可以用 `j/k` 上下移動、`g/G` 跳到第一列 / 最後一列。 - 選中 🔴 等你的列時,表格下方會多一行顯示**它具體在等什麼**(要跑的指令、問的問題;hook 模式才有)。 - Claude Code 背景 agent 以 `⚙` 標示;它沒有可跳轉的終端,選取時會顯示 `claude --resume` 接回提示,完成後預設收進已離場(按 `a` 仍可查看)。 -- 否則 → **Rich poll**(清除畫面重畫);連 Rich 都沒有就純文字。三層優雅降級。 - -### 跳到 session(`Enter` / `Space` / `ring focus`) - -> 這裡的 `Space` 是鍵盤上的空白鍵,不是 BanG Dream! 裡的場館「SPACE」。 - -選一個 session 按 `Enter`,或用 `ring focus SESSION_ID`(完整 id 或唯一前綴),RiNG 把焦點帶到它真正所在的終端。 -如果 TUI 正在跑,`ring focus` 會先把請求交給 TUI,讓游標選中該 session;沒有 TUI 時才直接跳終端。目前內建支援: - -- **tmux**:`switch-client` 直接切到那個 pane(你跟它要在同一個 tmux server)。 -- **iTerm2 / Terminal.app**(macOS):用 session 的 `tty` 透過 AppleScript 聚焦對應分頁, - 自動分辨是哪個 app(沒在跑的 app 不會被喚醒)。第一次會跳系統「自動化」授權,准一次即可。 -- **Linux X11 視窗**(`wmctrl`,best-effort fallback):Linux 上沒跑 tmux 時的後備—— - 從 `tty` 追到擁有它的終端視窗,用 `wmctrl` 帶到前景。**限制**:只支援 X11(Wayland 通常無效)、 - 只能聚焦整個視窗無法選分頁、gnome-terminal 的 client/server 架構可能配不到。要先 `apt install wmctrl`。 - -「哪個 session 在哪個終端」靠它的 `tty` 對應——**hook 模式最精準**; -zero-config 下每個專案只開一個 session 時也對得上。Codex 沒裝 hook 時會走 zero-config: -同一個 cwd 只開一個 live Codex 時可跳轉;同 cwd 多個 Codex 只能保守顯示,避免跳錯。 - -### 就地回覆權限請求(`p`) - -游標停在 🔴 等你的列按 `p`,RiNG 讀出那個 session 畫面上的權限對話框 -(Claude Code 的「Do you want to proceed?」框,含背景 subagent 帶 -「from the … agent」標頭的),把編號選項原文列成浮層讓你選;選定後 RiNG 會**再抓一次 -畫面**確認對話框還在且沒變(防止你考慮期間它已被回掉),才代你按下那個數字, -並回頭驗證對話框確實消失。 -確認回覆成功後,TUI 會立即清掉該筆「等你」;只有時間較新的 hook 事件(例如下一個權限請求)能再次把它標成等待。 - -- **tmux 內的 session**:用 `tmux capture-pane` 抓畫面、`tmux send-keys` 送鍵。 -- **macOS 上直接開在 iTerm2 分頁的 session**(沒有 tmux):用 session 的 `tty` 透過 - AppleScript 找到對應 iTerm2 分頁,抓畫面、送鍵都走 `osascript`。第一次用時 macOS - 會跳「允許控制 iTerm2」的自動化授權框,允許一次即可。 - -安全底線:畫面上解析不到可辨識的對話框(標記、編號、游標任一缺)就只提示、**絕不送鍵**—— -對話框不在時按鍵會落進聊天輸入框變成文字;萬一送出的瞬間對話框剛好消失、數字落進輸入框, -RiNG 會自動補一個 Backspace 清掉並警告你。 - -**限制**:要有 tmux pane 座標、或(macOS 上)測得到 tty 的 iTerm2 session 才抓得到畫面; -其餘 session 請按 `Enter` 跳過去回。 - -### 等你時發通知 - -預設行為:裝了 hook 後,session 從工作中轉成 🔴 等你的**當下**,hook 就地**響鈴 + 發系統通知**—— -**不必開著 RiNG 看板**,關掉終端也照樣 ring 你(通知由事件觸發,不靠輪詢)。 -開著 TUI 時,若它一直停在等你,TUI 還會在 30s / 120s / 300s 各補一次 in-app 響鈴提醒。 -(zero-config 測不到「等你」,所以這個需要 hook 模式。) - -macOS 上若要點通知後直接跳回 RiNG TUI 並選中那個 session,需要安裝 -[terminal-notifier](https://github.com/julienXX/terminal-notifier)(brew 外部 binary): - -```sh -brew install terminal-notifier -``` - -沒裝時退化為 macOS 原生純文字通知(不可點擊跳轉),RiNG 會在第一次走到這條路時提示一次。 -通知聲音、重複提醒時間、後端選擇都能在 config 裡調整;通知後端也是可插拔的,見後面的 -`Notifier` 擴充說明。 - -#### 推到手機(ntfy / webhook) - -人不在座位時,桌面通知等於沒響。設定 [ntfy](https://ntfy.sh) topic URL 就能推到手機: - -```toml -# ~/.config/ring/config.toml -notify_ntfy_url = "https://ntfy.sh/my-ring-topic" # 手機裝 ntfy app、訂同一個 topic -notify_also = ["ntfy"] # 桌面通知照發,再「加發」一份到手機 -``` - -`notify_backend = "ntfy"` 則是只推手機、不發桌面通知。要接 Slack / 自家 bot / IFTTT, -用 `notify_webhook_url` 走通用 webhook 後端(JSON POST,欄位穩定只加不改)。 - -### 清理 RiNG 狀態檔(`ring gc`) - -RiNG 正常收到 `SessionEnd` 時會刪掉自己的 hook registry;如果 agent crash 或 hook 沒跑到結尾, -可能留下已離場的 `~/.config/ring/sessions/*.json`。這些檔案預設不會顯示在看板上,但可以用 -`ring gc` 清掉。 - -```sh -ring gc --dry-run # 預覽會刪哪些檔案 -ring gc # 清掉已離場且超過 7 天的 registry,以及過期 IPC 檔 -ring gc --older-than 1d # 改成 1 天 -ring gc --all-ended # 清掉所有目前判定已離場的 registry -``` - -`ring gc` 只清 RiNG 自己寫在 `~/.config/ring/` 底下的狀態檔,不會碰 Claude Code / Codex 的 -transcript 或 state。`ring doctor` 維持唯讀診斷,不會替你刪檔。 - -### 等待統計(`ring stats`) - -hook 模式下,RiNG 會把 session 的**狀態轉換**記進 `~/.config/ring/events.jsonl` -(只記轉換、量很小、超過上限自動砍半保新)。`ring stats` 據此告訴你:最近這段時間, -每個專案 🔴 等了你幾次、平均 / 最長 / 總共等多久。 - -```sh -ring stats # 最近 7 天 -ring stats --since 12h # 自訂時間窗 -``` - -跟精準通知一樣,zero-config 測不到 🔴,所以 stats 也需要 hook 模式。 - -## Session 來源 - -RiNG 會從已註冊的 source 收集 session;目前內建這幾種: - -| Source | 來源 | 狀態精度 | -|------|------|----------| -| **Claude Code zero-config**(預設) | 掃 `~/.claude/projects/**/*.jsonl` 的 mtime + 記錄裡的 `cwd` 欄位 | 免設定;可辨識近期活動與回合結束。需要回應的通知要靠 hook 才精準 | -| **Codex zero-config**(預設) | 讀 `~/.codex/state_5.sqlite` threads + rollout JSONL,並用 live `codex` process 配 tty | 免設定;可辨識 live / ended / 回合結束。同 cwd 多 session 建議裝 hook 取得精準跳轉 | -| **Ollama zero-config**(預設) | 偵測有控制終端的 `ollama run` process | 行程存活層級;顯示 cwd、TTY 與模型,不把 `ollama serve` 當 session | -| **llama.cpp zero-config**(預設) | 偵測有控制終端的 `llama-cli` process | 行程存活層級;顯示 cwd、TTY 與模型,不把 `llama-server` 當 session | -| **hook registry**(opt-in,精準) | RiNG hook 在 `Notification` / `UserPromptSubmit` / `Stop` / `SessionEnd` 即時寫 `~/.config/ring/sessions/` | 準(🔴 等你 / 🟢 工作中 / 🟡 跑完停著 / ⚫ 已離場) | - -zero-config 不必設定就能用;想要精準的「誰在等你」,就讓 provider 的 hook 餵進 RiNG registry。 -RiNG 內建 Claude Code / Codex hook 安裝器;其他工具可直接走 provider-neutral `ring hook` protocol。 - -Ollama 與 llama.cpp 本身沒有 RiNG 可讀的 session transcript 或互動 hook,因此 zero-config 列固定以 -🟡 顯示:它代表互動式 CLI 還活著,不代表 RiNG 能分辨它正在生成或等下一個 prompt。CLI 結束後列 -就會移除。若外層 agent 能送出生命週期事件,可用下方的中立 hook protocol 取得精準狀態。 - -## 狀態機 - -RiNG 把每個 session 壓成四種狀態。看板排序時,🔴 等你永遠排最上面並 highlight。 - -| 狀態 | 你可以怎麼理解 | RiNG 看到什麼 | -|------|----------------|---------------| -| 🔴 等你 | 需要你現在回去處理 | hook 收到權限請求、選項提問等需要回應的通知 | -| 🟢 工作中 | agent 正在跑,先不用管 | 你剛送出 prompt,或 session 最近仍有活動 | -| 🟡 跑完停著 | 這輪跑完了,停在那裡 | 收到 `Stop`,或超過 `working_threshold_seconds` 沒有新活動 | -| ⚫ 已離場 | session 已結束 | 收到 `SessionEnd`,或 process / 本機紀錄顯示它已離場 | - -狀態更新規則: - -| 發生的事 | 新狀態 | -|----------|--------| -| `SessionStart` / `UserPromptSubmit` | 🟢 工作中 | -| `Stop` | 🟡 跑完停著 | -| 權限請求、選項提問、`requires_action = true` | 🔴 等你 | -| `SessionEnd` / process 結束 | ⚫ 已離場 | -| zero-config 來源太久沒更新 | 🟢 工作中 退成 🟡 跑完停著 | - -🔴 等你只有 hook 模式測得到;zero-config 只能判斷「最近有沒有活動」, -分不出「需要你做決策」和「剛跑完停著」。 - -## hook 模式(精準的「等你」) - -zero-config 只靠檔案 mtime 或本機 state,**分不出「需要你做決策」還是「剛跑完」**。 -裝 hook 後,RiNG 直接收 agent CLI 的事件,狀態才會精準——🔴 等你 + 響鈴都要靠它。 - -### Claude Code / Codex:內建安裝器 - -hook 會被寫成「執行 `ring hook`」,所以 `ring` 要在 PATH 上、指向穩定路徑: - -```sh -uv tool install 'ring-cli[tui]' -``` - -註冊 hook: - -```sh -ring install-hooks # 寫進 Claude Code / Codex hook 設定(合併,不覆蓋既有 hooks) -ring install-hooks --dry-run # 只想先看會寫什麼、不動檔 -``` - -Claude Code 會寫進 `~/.claude/settings.json`。如果你有在用 Codex(`~/.codex` 存在), -RiNG 也會寫進 `~/.codex/hooks.json`;Codex 會要求你信任新 hook,信任後才會執行。 - -Claude Code 註冊這幾個事件,對應到狀態: - -| Claude Code 事件 | RiNG 狀態 | -|---|---| -| `SessionStart` / `UserPromptSubmit` | 🟢 工作中 | -| `Stop` | 🟡 跑完停著 | -| `Notification` 的 `permission_prompt` / `elicitation_dialog` | 🔴 等你(卡權限 / 需要選項) | -| `PermissionRequest` / `PreToolUse` 的 `AskUserQuestion` | 🔴 等你(權限 / 選項需要你決策) | -| `SessionEnd` | 從看板消失 | - -`Stop` 還有一種例外:agent 有時不用 `AskUserQuestion`、不觸發權限請求,只在文字裡問了一句話 -就停下來(例如「要不要順便修 B?」)。這種情況下 RiNG 會看最後一則 assistant 訊息的**結尾**—— -結尾是問句(`?` / `?`,圍欄程式碼區塊先剝掉再看)就升級成 🔴 等你(`waiting_kind="question"`), -detail 帶那句問句;問句只出現在訊息中段、結尾是陳述句則不算,保守判斷、寧可漏報不要誤報。 -Claude Code 與 Codex 兩邊都適用(兩者的 `Stop` payload 都帶 `last_assistant_message`); -`detect_stop_questions` 設定鍵可關閉這個行為(預設開)。 - -Codex 目前註冊 Codex 支援的互動事件:`PreToolUse`、`PermissionRequest`、`PostToolUse`、`Stop`。 -Codex 的 `PermissionRequest` 也會在既有 policy 自動放行前送出,所以裸事件在 hook 層維持 🟢 -工作中。Codex 的 hook 沒有「使用者已核可」事件、沒有心跳,等核可期間 hook 通道**完全靜默** -——RiNG 因此把靜默本身當訊號:最後一個 hook 事件是 `PermissionRequest`、又超過 -`codex_permission_wait_seconds`(預設 10 秒)沒有任何後續事件,看板就把它標成 🔴 等你, -detail 帶當初要跑的指令摘要;任何後續事件(下一個工具、`Stop`)到了就自然轉回。 - -Codex 側的已知限制(0.144.4 實證,hook payload 無任何欄位可區分): - -- **分不出核可還是拒絕**:你按下 allow 或 deny 時,hook 都不會收到事件。allow 之後若那條指令 - 本身跑很久,🔴 會多亮到指令跑完(`PostToolUse`)為止;deny 之後若 agent 沒有立即的後續動作, - 🔴 會亮到下一個事件或 `Stop` 才清。 -- 核可等待轉 🔴 的系統通知由 TUI 的提醒排程器代發(沒有 hook 事件可在當下發);headless - `--watch` 不發這一種通知。 - -hook 只對**新開的 session** 生效,所以裝完要重開。確認方法: - -```sh -ls ~/.config/ring/sessions/ # 出現 .json 就代表 hook 在寫了 -``` - -RiNG 一偵測到 `~/.config/ring/sessions/` 有資料就自動切精準模式(hook 來源優先、 -zero-config 掃描補上沒裝 hook 的 session)。 - -### 其他 provider:中立 hook protocol - -RiNG 不相依 agent-hooks,也不要求 provider 一定要用內建安裝器。任何工具只要在事件發生時把 JSON -餵給 `ring hook` 即可: - -```sh -ring hook --provider codex -# 或簡寫: -ring hook codex -``` - -payload 欄位採寬鬆命名,至少需要 session id、event、cwd: - -```json -{ - "provider": "codex", - "session_id": "thread-123", - "event": "Notification", - "notification_type": "permission_prompt", - "requires_action": true, - "waiting_for": "permission", - "cwd": "/repo/app", - "tty": "/dev/ttys003", - "last_action": "waiting for permission" -} -``` - -支援的事件語意與 Claude Code 一致:`SessionStart` / `UserPromptSubmit` → 🟢 工作中, -`Stop` → 🟡 跑完停著,需要回應的 `Notification` / `PermissionRequest` → 🔴 等你,`SessionEnd` → -從看板移除。非 Claude provider 的 session id 會自動加上 provider prefix -(例如 `codex:thread-123`),避免不同工具撞 id。 - -provider 若能分辨「立刻需要使用者」與「只是等下一步」,請直接給明確欄位: -`requires_action = true/false` 或 `waiting_for = "permission" | "options" | "next_step"`。 -RiNG 會優先相信這些欄位;沒有時才退回 event / notification type 推論。 - -### 系統通知(🔔 等你時自動通知 + 點擊聚焦) - -這裡補細節:系統通知由 `ring hook` 在事件當下送出(headless `--watch` 與 TUI 都只負責顯示 -看板,不發系統通知)。預設後端是 `auto`:有可點擊的 `terminal-notifier` 就優先用它;沒有時 -退回 `osascript`(macOS)或 `notify-send`(Linux)。如果全部不可用,就只保留看板,不讓通知失敗打斷主流程。 - -- **設定**:`notify_sound`、`notify_sound_name`、`notify_ignore_dnd`、`notify_repeat_seconds`、 - `notify_repeat_max`、`notify_backend`、`waiting_cooldown_seconds` 都可在 `~/.config/ring/config.toml` 調整。 -- **防翻轉轟炸**:背景 subagent 的權限請求可能讓 session 在等你/工作中之間快速翻轉。 - `waiting_cooldown_seconds`(預設 180 秒)讓 `ring hook` 的系統通知與 TUI 的響鈴/提醒, - 在 session 離開等你又很快轉回時,距上次通知未滿冷卻期就不再立即發;設 `0` 關閉冷卻 - (回到每次轉入都發)。 -- **點擊聚焦 + 聲音**:需先安裝 `terminal-notifier`(brew)。點擊通知後會直接跳回 RiNG TUI - 並選中 session;若沒有 TUI 在跑,則直接跳到對應終端。 - - ```sh - brew install terminal-notifier - ``` - -- **純文字通知**:未裝 `terminal-notifier` 則退化為 macOS 原生純文字通知(可帶聲音,點擊不可聚焦)。 -- **擴充**:要接其他桌面通知、webhook 或自訂提醒方式,可以新增 `Notifier` 後端並註冊, - 詳見「其他通知後端」。 - -- **交給 agent-hooks**:`agent-hooks` 是另一個可選的 hook helper / 決策 UI。若你已安裝它, - 並把 `notify_backend` 設成 `agent-hooks`,`ring hook` 會照樣寫 RiNG registry, - 同時把原始 payload 交給 `agent-hooks callback` 處理同步決策視窗;RiNG 的 `--watch` - 不會再重複發通知。若 PATH 上找不到 `agent-hooks`,會自動退回 `auto`。 - -### 移除 hooks - -不想再讓 agent CLI 事件寫進 RiNG registry 時,可以移除 RiNG 安裝的 hook 條目。 -這不會刪掉 `~/.config/ring/` 裡的 session 紀錄,也不會動到 Claude Code / Codex 的其他 hooks。 - -```sh -ring remove-hooks # 從 Claude Code / Codex hook 設定移除 RiNG hook 條目 -ring remove-hooks --dry-run # 只預覽,不改檔 -``` - -## 設定(選用) - -`~/.config/ring/config.toml`,全部選填、缺了就用預設: - -```toml -lang = "zh-Hant" # 預設語言(CLI --lang 最優先,再來 RING_LANG / LANG) -interval = 2.0 # watch 刷新秒數 -show_all = false # 是否預設顯示已離場 -legend = true # 是否預設顯示圖例 -active_window_seconds = 21600 # 只看最近這段時間動過的 session(預設 6h) -working_threshold_seconds = 90 # 多久沒動 → 🟢 工作中 變 🟡 閒置 -waiting_window_seconds = 1800 # 跑完停著升等你的時間窗上限(預設 30 分) -codex_permission_wait_seconds = 10 # Codex 裸 PermissionRequest 後 hook 靜默超過這秒數 → 🔴 等你;0 = 關閉 -detect_stop_questions = true # Stop 結尾是純文字提問時升級成 🔴 等你;false = Stop 一律 🟡 -notify_sound = true # 系統通知是否播放聲音 -notify_sound_name = "Glass" # macOS / terminal-notifier sound name -notify_ignore_dnd = false # macOS terminal-notifier 是否穿透勿擾 / Focus -notify_backend = "auto" # auto / terminal-notifier / osascript / notify-send / agent-hooks / none -notify_repeat_seconds = [30, 120, 300] # 持續等你時,幾秒後重複提醒 -notify_repeat_max = 3 # 重複提醒上限;0 = 不限 -waiting_cooldown_seconds = 180 # 離開等你又轉回時,距上次提醒未滿這段時間就不再立即提醒;0 = 關閉 -notify_ntfy_url = "" # 設完整 ntfy topic URL 啟用手機推播(如 https://ntfy.sh/my-topic) -notify_webhook_url = "" # 設 URL 啟用通用 webhook 後端(JSON POST) -notify_also = [] # 主後端之外「加發」的後端,如 ["ntfy"](桌面+手機各一份) -focusers = ["Neovim", "tmux", "iTerm2", "Terminal", "linux-wm"] # 跳轉嘗試順序 -plugins = [] # 啟動時 import 的外部 plugin 模組(見「支援的工具與擴充」) - -[colors] # Rich 樣式字串,逐項覆寫(深淺底安全色為預設) -waiting = "bold red" -working = "green" -idle = "yellow" -ended = "grey50" -project = "cyan" -location = "bright_blue" -muted = "grey50" -``` - -## 語言 / 翻譯 - -UI 用 **gettext**,msgid 直接是**台灣漢語**——所以預設(zh-Hant)不需要任何 `.mo`, -原始碼即翻譯。切語言:`--lang en`(或 `RING_LANG=en` / config 的 `lang`)。複數用 -`ngettext` 正確處理(`1 session` vs `2 sessions`)。 - -加一個語言、或改了字串要重抽: - -```sh -poe i18n-extract # 從原始碼抽 msgid → src/ring/locale/ring.pot -# 複製 ring.pot 成 src/ring/locale//LC_MESSAGES/ring.po,填 msgstr -poe i18n-compile # 各 .po → .mo(.mo 要 commit;wheel 會自動帶上) -``` - -## 支援的工具與擴充 - -core 不綁死任何特定工具或終端。三個維度都可插拔,每個都是「寫個小類別 + 註冊」、主流程零改動: - -| 維度 | 在做什麼 | 內建 | -|------|----------|------| -| `SessionSource` | 從哪裡找到 session | Claude Code、Codex、Ollama、llama.cpp、RiNG hook registry | -| `Focuser` | 跳轉時把焦點帶去哪個終端 | tmux、iTerm2、Terminal.app、Linux X11(wmctrl)| -| `Notifier` | 等你時怎麼發系統通知 | terminal-notifier、osascript、notify-send、ntfy、webhook | - -每個維度各自一個 package(`ring/sources/`、`ring/focus/`、`ring/notify/`),每個後端是裡面 -一個模組;要加新的=丟一個模組 + `register_*()`。 - -### 讓裝好的 `ring` 載入你的 plugin - -`register_*()` 要有人執行才算數。裝好的 `ring` 指令啟動時會自動載入兩種來源的 plugin: - -1. **entry point**(發佈成套件時)——在你套件的 `pyproject.toml` 宣告,指向一個模組或 - callable(模組在 import 時自行 `register_*()`;callable 會被無參數呼叫一次): - - ```toml - [project.entry-points."ring.plugins"] - mytool = "ring_mytool.plugin" - ``` - -2. **config**(本機腳本)——`~/.config/ring/config.toml` 寫 `plugins = ["my_module"]`, - 模組要在 `sys.path` 上(site-packages 或 `PYTHONPATH`)。 - -單一 plugin 壞掉只會在 stderr 警告一行、不擋看板。 - -### 其他 agent CLI(`SessionSource`) - -內建 `HookRegistrySource`(讀 `~/.config/ring/sessions/`)、`ClaudeCodeSource`(掃 -`~/.claude`)、`CodexSource`(讀 `~/.codex/state_5.sqlite`),以及 Ollama / llama.cpp -互動式 CLI 的 process source。要監測其他工具, -可以優先餵 `ring hook`;若工具沒有 hook,再寫一個 source 吐出 `Session`、註冊即可: - -```python -from ring.registry import Session, Status -from ring.sources import register_source - - -class MyToolSource: - name = "mytool" - - def discover(self) -> list[Session]: - return [Session(session_id="…", cwd="…", status=Status.WORKING, - last_active=0.0, last_action="→ …", source="mytool")] - - -register_source(MyToolSource()) -``` - -`Session` 是工具中立的(id / cwd / status / last_action / tty / todo…),各 source -自己決定怎麼填。 - -### 其他終端(`Focuser`) - -跳轉的終端整合也一樣——寫個符合 `Focuser` 協定的類別(`try_focus(session) -> -(ok, msg) | None`),呼叫 `ring.focus.register_focuser(MyFocuser())`。內建 Neovim terminal / tmux / -iTerm2 / Terminal.app / Linux X11 視窗(wmctrl),各自一個模組(`ring/focus/neovim.py`、 -`ring/focus/tmux.py` …)。Neovim focuser 會先透過 `$NVIM` server socket 切到承載該 session 的 -`:terminal` buffer,再交給外層 focuser 聚焦 pane 或視窗。 -要再加 Ghostty / Kitty / WezTerm,就照這個模式新增 focuser;嘗試順序也能用 config 的 `focusers` 調整。 - -### 其他通知後端(`Notifier`) - -系統通知也是可插拔的——寫個符合 `Notifier` 協定的類別,呼叫 -`ring.notify.register_notifier(MyNotifier())`。內建 terminal-notifier(可點擊跳轉)/ -osascript / notify-send,各自一個模組(`ring/notify/terminal_notifier.py` …): - -```python -from ring.notify import register_notifier - - -class MyNotifier: - name = "mytool-notify" - - def available(self) -> bool: ... # 這個後端現在能不能用(通常看 binary 在不在) - def supports_click(self) -> bool: ... # 點通知能不能跳回 session - def send(self, sessions): ... # 逐 session 各發一則通知 - - -register_notifier(MyNotifier()) -``` - -選哪個後端由 config 的 `notify_backend` 決定(`auto` / 指定名稱 / `none` 純看板不發通知)。 - -## 平台與隱私 - -- **平台**:macOS / Linux(靠 `ps` / `lsof` / `tmux` 偵測;Windows 未支援)。 -- **隱私**:全程在你本機跑——只**讀** `~/.claude/`、`~/.codex/` 的本機資料,只**寫** - `~/.config/ring/`。不連網、不上傳、不外送任何資料。 - -## 名字哪裡來 - -名字三重共鳴: - -1. 📞 它 **ring** 你(等你時發通知) -2. 🎤 BanG Dream! 的 live house「RiNG」
- (場館=你坐著看一團一團、也就是一個一個 session 演出的地方) -3. **R**ealtime **I**nstance **N**otification **G**rid(它到底是什麼) - -BanG Dream! 原作裡的 RiNG 是 CiRCLE 的**二號店**——因為少女樂團數量暴增、一間場館不夠用, -才在「老闆」的要求下蓋的;而且它跟 CiRCLE **共用同一套訂單/帳號系統**。 -這把工具的定位剛好也是這樣: - -- **一樣的誕生理由**:你的並行 agent CLI session 也暴增了,需要一個專門的地方一眼看完。 -- **不取代、是分店**:RiNG 不是另一個 client——它讀的是 Claude Code / Codex 的本機後台, - 只是替你多開的第二個觀測視窗。 - -如果你覺得好用、順手,那就回去補番吧! +| `ring` | 印出目前所有 session 的快照 | +| `ring --watch` | 開啟持續更新的互動 TUI | +| `Enter` / `Space` | 跳回選取 session 所在的終端 | +| `p` | 就地回覆選取 session 的權限請求 | +| `n` | 替選取的 session 命名 | +| `ring doctor` | 檢查 hooks、通知與終端聚焦設定 | +| `ring --format oneline` | 產生 tmux、SwiftBar、waybar 可用的摘要 | -## 不做什麼 +不裝 hooks 也能用零設定模式查看 session;但 🔴 等你、等待內容與即時通知需要 hooks。安裝或更新 hooks 後,記得重開 Claude Code / Codex session。 -RiNG 的範圍只放在「session 現在是什麼狀態、要不要你回去處理」。它不是 agent -用量分析器,也不打算變成另一個成本儀表板。 +## 接著看 -### 為什麼不碰 token / 花費統計 +- [完整使用手冊](https://lee-w.github.io/ring/guide/):所有指令、TUI 操作、hooks、通知、設定、擴充與隱私說明 +- [Session 狀態](https://lee-w.github.io/ring/session-states/):🔴/🟢/🟡/⚫ 的判定方式 +- [貢獻指南](CONTRIBUTING.md):開發環境、測試與 PR checklist -Claude Code 的 JSONL token 數字是壞的(input 差約 100 倍、output 差約 10 倍), -所有靠它做帳的工具都中招。RiNG 只看**狀態**,不做 cost accounting——刻意避開這雷。 +macOS / Linux 可用;Windows 尚未支援。預設只讀本機 agent 資料、只寫 `~/.config/ring/`;只有你自行設定的 ntfy / webhook 通知會連網。 ## License diff --git a/docs/contributing.en.md b/docs/contributing.en.md new file mode 100644 index 0000000..dfd6344 --- /dev/null +++ b/docs/contributing.en.md @@ -0,0 +1,152 @@ +# Contributing to RiNG + +Thanks for wanting to help make RiNG better. This page covers the dev workflow, how to test, and +what to check before opening a PR. + +## Dev environment + +RiNG needs Python 3.13+; the project manages dependencies and the run environment with `uv`. + +```sh +uv sync --all-groups +uv run ring +uv run ring --watch +``` + +The PyPI distribution name is `ring-cli`, but the Python module and CLI command are both `ring`. + +## Common commands + +```sh +uv run poe format # ruff check --fix + ruff format +uv run poe lint # ruff check + mypy +uv run poe test # pytest +uv run poe cover # pytest + coverage report +uv run poe all # format + lint + coverage +uv run poe ci # pre-commit hooks + coverage +``` + +Tests run in parallel via `pytest-xdist` by default; to run a single test, invoke pytest directly: + +```sh +uv run pytest tests/test_cli.py +uv run pytest tests/test_cli.py::test_name +``` + +## Documentation + +The site uses Material for MkDocs and `mkdocs-static-i18n`. `uv sync --all-groups` installs the documentation dependencies. Preview and validate the site with: + +```sh +uv run poe docs:serve # local server with live reload +uv run poe docs:build # strict build into site/ +``` + +Taiwanese Mandarin is the default language and uses `name.md`; English translations use `name.en.md`. Links in both languages must omit the locale suffix—for example, `[Session states](session-states.md)`—so the i18n plugin resolves the current language. When adding a page, update `nav` in `mkdocs.yml` and put menu-label translations under `nav_translations`. + +Run the strict build before submitting documentation changes. Do not commit `site/`; the GitHub Pages workflow installs the locked `docs` dependency group and rebuilds it. + +## Pre-commit hooks + +Installing the hooks is recommended, so formatting, lint, lockfile, and commit-message checks all +run locally first. + +```sh +uv run poe setup-pre-commit +``` + +The hooks currently check TOML/YAML, private keys, spelling, `uv.lock`, Ruff, Mypy, Commitizen, +and more. `no-commit-to-branch` blocks direct commits to protected branches — work on a feature +branch when needed. + +## Code style + +- Python source lives in `src/ring/`, tests in `tests/`. +- Ruff targets Python 3.13, line width 120. +- Mypy runs in strict mode; add complete types for any new API. +- CLI behavior, output formats, the hook protocol, and JSON keys are relied on by users' own + scripts — prefer compatibility when changing them, and clearly flag any necessary breaking + change. +- The project deliberately allows Taiwanese Mandarin comments and UI strings; mind i18n when + adding new English-facing UI too. + +## Testing guidelines + +Add tests scoped to what you changed: + +- CLI args and output: `tests/test_cli.py` +- TUI / render behavior: `tests/test_tui.py`, `tests/test_render.py` +- Source discovery: `tests/test_sources.py`, `tests/test_discover.py` +- Hook / registry / IPC: `tests/test_hook.py`, `tests/test_registry.py`, `tests/test_ipc.py` +- Notifier / focus / permission reply: `tests/test_notify.py`, `tests/test_focus.py`, + `tests/test_permission.py` respectively + +Run at least this before opening a PR: + +```sh +uv run poe lint +uv run poe test +``` + +If the change touches cross-module behavior, output formats, or release-related settings, run: + +```sh +uv run poe all +``` + +## i18n + +If your change touches user-facing strings, update the translation files. + +```sh +uv run poe i18n:extract +uv run poe i18n:check +uv run poe i18n:compile +``` + +Note: don't use `pybabel update` — it stuffs empty `msgstr` entries into `.po` files and can break +the i18n tests. Add new strings' translated entries by hand to each language's `.po` file, then +recompile and commit both the `.po` and `.mo` files. + +## Commits and changelog + +This project uses [Commitizen](https://commitizen-tools.github.io/commitizen/) / +[Conventional Commits](https://www.conventionalcommits.org/), e.g.: + +```text +feat: add webhook retry backoff +fix: keep stale codex sessions hidden +docs: document hook setup +test: cover tmux permission parsing +``` + +Versioning and the changelog are managed by Commitizen. Regular PRs shouldn't hand-edit +`CHANGELOG.md` unless you're doing a release / bump. + +## PR checklist + +Before opening a PR, confirm: + +- The change is scoped tightly, with no unrelated formatting or build artifacts mixed in. +- New features or bug fixes have matching tests, or the PR description explains why they can't be + tested. +- `uv.lock` is consistent with the dependency configuration. +- User-facing strings have been run through i18n. +- README / docs have been updated for any changed CLI flags, config keys, output format, or + behavior. +- Documentation changes pass `uv run poe docs:build`, with translated pages, navigation, and + internal links kept in sync. +- Any breaking change is called out clearly in the PR description, with its impact and migration + path. + +## Reporting an issue + +Please include, where possible: + +- OS and terminal environment, e.g. macOS + iTerm2, tmux, Linux X11 / Wayland. +- `ring --version` and how it was installed. +- The relevant commands and their actual output. +- If it's related to hooks / focus / permission reply, the relevant section of `ring doctor`. + +Please don't paste content containing tokens, private keys, full private transcripts, or other +sensitive information. diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000..58c07d1 --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,3 @@ +# 貢獻指南 + +--8<-- "CONTRIBUTING.md:body" diff --git a/docs/guide.en.md b/docs/guide.en.md new file mode 100644 index 0000000..49f19bd --- /dev/null +++ b/docs/guide.en.md @@ -0,0 +1,455 @@ +# RiNG 🎤 + +Start with the [documentation home page](index.md) for the product overview, installation, and first use. This page focuses on operations and advanced reference material. + +## Common Commands + +| Command | Purpose | +|---------|---------| +| `ring` | Print a one-shot snapshot | +| `ring --watch` | Keep refreshing; enters the TUI when Textual is installed | +| `ring --watch --interval 1` | Refresh every second | +| `ring --watch --count N` | Stop after N frames, useful for tests / CI | +| `ring --all` | Show ended sessions too | +| `ring --no-legend` | Hide the legend | +| `ring --lang zh-Hant` | Switch UI language | +| `ring focus SESSION_ID` | Focus a specific session; unique prefixes work | +| `ring config` | Show config path and effective settings | +| `ring config set KEY VALUE` | Write one config value | +| `ring doctor` | Read-only environment diagnosis | +| `ring digest --since 4h` | Away summary: recent session state and wait stats | +| `ring gc --dry-run` | Preview RiNG-owned stale state cleanup | +| `ring gc` | Clean RiNG-owned stale state files | +| `ring --format json` | Machine-readable board snapshot (for jq / scripts) | +| `ring --format oneline` | `🔴2 🟢1 🟡3` one-liner (for status bars) | +| `ring stats` | Waiting stats: how long agents kept 🔴 waiting in the last 7 days | +| `ring completion zsh` | Print a shell completion script (zsh / bash) | + +### Status Bar Integration (`--format`) + +```sh +ring --format oneline # 🔴2 🟢1 🟡3 (empty output when no sessions, so the segment collapses) +ring --format json | jq '.counts.waiting' +``` + +- **tmux**: `set -g status-right '#(ring --format oneline) …'` (with `status-interval 5`). +- **SwiftBar / xbar / waybar**: wrap `ring --format oneline` or consume the JSON. +- JSON keys are a stable interface (additive only), safe to script against. + +### Shell Completion (`ring completion`) + +```sh +# ~/.zshrc +eval "$(ring completion zsh)" +# ~/.bashrc +eval "$(ring completion bash)" +``` + +Completes subcommands, flags, and `config set` keys; `ring focus` prompts for a session id or unique prefix. + +## Watch Mode + +- With **Textual** (`[tui]` extra) in a real terminal: interactive TUI. + Use `↑/↓` to select, `Enter` / `Space` to jump, `p` to reply to a permission request in place, `n` to name a session, `a` to toggle ended sessions, `dd` to hide a session (it reappears automatically once it has new activity), `r` to refresh, and `q` to quit. + If you have vim muscle memory like I do, `j/k` move up/down and `g/G` jump to the first/last row. + When the selected row is 🔴 waiting, a line under the table shows **what it is concretely waiting for** (the command to run, the question asked; hook mode only). + Claude Code background agents carry a `⚙` badge. They have no terminal to jump to, so selecting one shows a `claude --resume` hint; completed agents are folded into ended sessions by default and remain available via `a`. +- Otherwise: Rich polling; without Rich, plain text. + +### Jump To A Session + +Select a session and press `Enter`, or run `ring focus SESSION_ID` with a full id or unique prefix. +RiNG focuses the terminal where that session is running. If the TUI is already open, `ring focus` +hands the request to the TUI so it selects that session; otherwise it focuses the terminal directly. + +- **tmux**: switches directly to the pane via `switch-client`. +- **Neovim `:terminal`**: uses the terminal job's inherited `$NVIM` server socket to switch to the exact buffer, then lets the outer tmux or terminal focuser raise its pane/window. +- **iTerm2 / Terminal.app** on macOS: uses the session `tty` and AppleScript to focus the matching tab. The first run may ask for macOS Automation permission. +- **Linux X11 window** (`wmctrl`, best-effort fallback): for Linux without tmux — walks from the `tty` up to the terminal window that owns it and raises it via `wmctrl`. **Limits**: X11 only (usually a no-op on Wayland), raises the whole window but cannot pick the tab, and gnome-terminal's client/server model may not match. Requires `apt install wmctrl`. + +TTY matching is most accurate in hook mode. Without hooks, Codex falls back to zero-config matching: +one live Codex session per cwd can jump correctly; multiple live Codex sessions in the same cwd are shown conservatively to avoid focusing the wrong tab. + +### Reply To Permission Requests In Place (`p`) + +With the cursor on a 🔴 waiting row, press `p`. RiNG reads that session's screen, parses the +permission dialog (Claude Code's "Do you want to proceed?" box, including background-subagent +ones with a "from the … agent" header), and lists the numbered options verbatim in a modal. +After you pick one, RiNG **captures the screen again** to confirm the dialog is still there and +unchanged (it may have been answered while you were deciding), only then sends that single +digit, and re-checks that the dialog actually disappeared. +Once the reply is verified, the TUI immediately clears that waiting revision. Only a newer hook +event, such as another permission request, can mark the session as waiting again. + +- **Sessions inside tmux**: reads the screen with `tmux capture-pane` and sends keys with + `tmux send-keys`. +- **Sessions in a plain iTerm2 tab on macOS** (no tmux): locates the matching iTerm2 session by + its `tty` via AppleScript, and both reads the screen and sends keys through `osascript`. The + first use triggers the macOS Automation permission prompt ("allow control of iTerm2") — allow + it once. + +Safety first: if no recognizable dialog can be parsed (missing markers, numbering, or cursor), +RiNG only shows a toast and **never sends a key** — without the dialog, keystrokes would land in +the chat input box as text. If the dialog happens to vanish in the instant the digit is sent and +the digit lands in the input box, RiNG sends a Backspace to clean it up and warns you. + +**Limits**: reading the screen requires a tmux pane coordinate, or (on macOS) an iTerm2 session +with a detectable tty; for other sessions, press `Enter` to jump over and reply there. + +### Notifications + +Default behavior: with hooks installed, the moment a session changes to 🔴 waiting, the hook beeps +and sends a system notification right then — **no RiNG board needs to be open**, so it rings you even +after you close the terminal (notifications are event-driven, not polled). While a TUI is open, it +also nudges you again in-app at 30s / 120s / 300s if the session keeps waiting. + +For clickable notifications on macOS, install `terminal-notifier`: + +```sh +brew install terminal-notifier +``` + +Without it, RiNG falls back to macOS text notifications without click-to-focus. +Notification sound, repeat timing, and backend selection are configurable; notification backends +are also pluggable via the `Notifier` extension point. + +#### Push To Your Phone (ntfy / webhook) + +Desktop notifications do not help when you are away from the desk. Point RiNG at an +[ntfy](https://ntfy.sh) topic to push to your phone: + +```toml +# ~/.config/ring/config.toml +notify_ntfy_url = "https://ntfy.sh/my-ring-topic" # subscribe to the same topic in the ntfy app +notify_also = ["ntfy"] # desktop notification as usual, plus a copy to the phone +``` + +`notify_backend = "ntfy"` pushes to the phone only. For Slack / your own bot / IFTTT, set +`notify_webhook_url` to use the generic webhook backend (JSON POST with a stable, additive-only payload). + +### Cleaning RiNG State Files (`ring gc`) + +When RiNG receives `SessionEnd`, it removes its own hook registry entry. If an agent crashes or the +final hook does not run, ended `~/.config/ring/sessions/*.json` files can remain. They are hidden from +the board by default, and you can remove them with `ring gc`. + +```sh +ring gc --dry-run # preview what would be deleted +ring gc # delete ended registry files older than 7 days, plus expired IPC files +ring gc --older-than 1d # use a 1-day threshold +ring gc --all-ended # delete every registry file currently classified as ended +``` + +`ring gc` only cleans state files RiNG owns under `~/.config/ring/`. It does not touch Claude Code / +Codex transcripts or state. `ring doctor` remains read-only and never deletes files. + +### Waiting Stats (`ring stats`) + +In hook mode, RiNG logs session **state transitions** to `~/.config/ring/events.jsonl` +(transitions only, tiny, self-trimming past a size cap). `ring stats` then tells you, per project, +how many times an agent 🔴 waited on you and for how long (avg / max / total). + +```sh +ring stats # last 7 days +ring stats --since 12h # custom window +``` + +Like precise notifications, 🔴 waiting is invisible to zero-config, so stats also needs hook mode. + +## Session Sources + +RiNG collects sessions from registered sources. Built-ins: + +| Source | Reads From | Precision | +|--------|------------|-----------| +| **Claude Code zero-config** | `~/.claude/projects/**/*.jsonl`, mtimes, and `cwd` fields | no setup; detects recent activity and turn completion. Precise user-action prompts require hooks | +| **Codex zero-config** | `~/.codex/state_5.sqlite`, rollout JSONL, and live `codex` processes | no setup; detects live / ended / turn completion. Use hooks for precise jumps when multiple sessions share a cwd | +| **Ollama zero-config** | interactive `ollama run` processes with a controlling terminal | process-liveness only; shows cwd, TTY, and model, and excludes `ollama serve` | +| **llama.cpp zero-config** | interactive `llama-cli` processes with a controlling terminal | process-liveness only; shows cwd, TTY, and model, and excludes `llama-server` | +| **hook registry** | `~/.config/ring/sessions/`, written by `ring hook` | precise: 🔴 waiting / 🟢 working / 🟡 idle / ⚫ ended | + +Zero-config needs no setup. For precise “who needs me”, install hooks so provider events feed the RiNG registry. +RiNG includes installers for Claude Code and Codex; other tools can use the provider-neutral `ring hook` protocol. + +Ollama and llama.cpp do not expose a session transcript or interaction hooks that RiNG can read, so their +zero-config rows stay 🟡: the row means the interactive CLI is alive, not that RiNG can distinguish generation +from waiting for the next prompt. The row disappears when the CLI exits. An outer agent that can emit lifecycle +events can use the provider-neutral hook protocol for precise states. + +## States + +RiNG reduces every session to four user-facing states. 🔴 waiting is sorted first. + +| State | Meaning | What RiNG Saw | +|-------|---------|---------------| +| 🔴 waiting | You should return now | hook event requiring a user response, such as permission or choices | +| 🟢 working | Agent is running | prompt submitted or recent activity | +| 🟡 idle | The current turn finished | `Stop`, or no new activity past `working_threshold_seconds` | +| ⚫ ended | Session is over | `SessionEnd`, process ended, or local records aged out | + +🔴 waiting requires hook mode. Zero-config can tell whether a session was recently active, but not whether it needs a decision from you. + +## Hook Mode + +Zero-config only has filesystem mtimes and local state snapshots, so it cannot reliably distinguish “needs a decision” from “just finished”. +Hooks send agent-CLI events directly to RiNG, making 🔴 waiting and notifications precise. + +### Claude Code / Codex Installer + +Hooks run `ring hook`, so install `ring` somewhere stable on `PATH` first: + +```sh +uv tool install 'ring-cli[tui]' +``` + +Then register hooks: + +```sh +ring install-hooks +ring install-hooks --dry-run +``` + +Claude Code hooks are written to `~/.claude/settings.json`. If `~/.codex` exists, Codex hooks are also written to +`~/.codex/hooks.json`; Codex will ask you to trust the hook before it runs it. + +Claude Code events: + +| Claude Code Event | RiNG State | +|-------------------|------------| +| `SessionStart` / `UserPromptSubmit` | 🟢 working | +| `Stop` | 🟡 idle | +| `Notification` with `permission_prompt` / `elicitation_dialog` | 🔴 waiting | +| `PermissionRequest` / `PreToolUse` with `AskUserQuestion` | 🔴 waiting | +| `SessionEnd` | removed from the board | + +`Stop` has one more exception: an agent sometimes doesn't use `AskUserQuestion` or trigger a permission +request, and just asks a plain-text question before stopping (e.g. "want me to fix B too?"). RiNG looks at +the **end** of the last assistant message for this: if it ends in a question mark (`?` / `?`, with any +trailing fenced code block stripped first) the session is promoted to 🔴 waiting (`waiting_kind="question"`), +with the question as its detail; a question that only appears mid-message, with a statement at the end, +does not count — conservative on purpose, biased toward missing a case rather than a false positive. This +applies to both Claude Code and Codex (`Stop` payloads from both carry `last_assistant_message`); the +`detect_stop_questions` config key can turn it off (on by default). + +Codex currently installs the supported interactive events: `PreToolUse`, `PermissionRequest`, `PostToolUse`, +and `Stop`. Codex also emits `PermissionRequest` before an existing policy auto-approves the call, so a bare +event stays 🟢 working at the hook level. Codex hooks have no "user approved" event and no heartbeat — while +an approval prompt is pending, the hook channel is **completely silent**. RiNG therefore treats the silence +itself as the signal: when the last hook event is a `PermissionRequest` and nothing has followed for more than +`codex_permission_wait_seconds` (default 10s), the board marks the session 🔴 waiting with the pending command +as its detail; any subsequent event (the next tool call, `Stop`) naturally clears it. + +Known Codex-side limitations (verified against 0.144.4; the hook payload has no field that could distinguish +these): + +- **Approve and deny are indistinguishable**: neither emits a hook event. After you approve a long-running + command, 🔴 lingers until the command finishes (`PostToolUse`); after a deny with no immediate follow-up + action, 🔴 lingers until the next event or `Stop`. +- The system notification for this timeout-promoted 🔴 is sent by the TUI's alert scheduler (there is no hook + event to notify from); headless `--watch` does not send this particular notification. + +Verify that hooks are writing: + +```sh +ls ~/.config/ring/sessions/ +``` + +### Other Providers + +Any tool can feed JSON into `ring hook`: + +```sh +ring hook --provider codex +# shorthand: +ring hook codex +``` + +Payload field names are intentionally loose. At minimum, provide a session id, event, and cwd: + +```json +{ + "provider": "codex", + "session_id": "thread-123", + "event": "Notification", + "notification_type": "permission_prompt", + "requires_action": true, + "waiting_for": "permission", + "cwd": "/repo/app", + "tty": "/dev/ttys003", + "last_action": "waiting for permission" +} +``` + +If a provider can distinguish “needs user action now” from “just waiting for the next prompt”, +send `requires_action = true/false` or `waiting_for = "permission" | "options" | "next_step"`. + +Provider-neutral events follow the built-in semantics: `SessionStart` / `UserPromptSubmit` → 🟢 +working; `Stop` → 🟡 idle by default, but a trailing plain-text question can promote it to 🔴 +waiting; an actionable `Notification` or a `PermissionRequest` with explicit interaction data → 🔴 +waiting; a bare `PermissionRequest` → 🟢 permission evaluation; `SessionEnd` removes the row. +Non-Claude session ids receive a provider prefix such as `codex:thread-123` to prevent collisions. + +### agent-hooks + +Notification details: system notifications are sent by `ring hook` at the event (both `--watch` and +the TUI only render the board; they no longer send system notifications). The default backend is `auto`: +RiNG prefers clickable `terminal-notifier`, then falls back to `osascript` on macOS or `notify-send` +on Linux. If none are available, RiNG keeps the board running and skips notifications. + +Config keys include `notify_sound`, `notify_sound_name`, `notify_ignore_dnd`, +`notify_repeat_seconds`, `notify_repeat_max`, `notify_backend`, and `waiting_cooldown_seconds`. +Custom notification channels can be added by registering another `Notifier`. + +Background subagent permission requests can make a session flap between waiting and working in +quick succession. `waiting_cooldown_seconds` (180s by default) stops `ring hook`'s system +notification and the TUI's bell/reminder from firing again the instant a session re-enters waiting +within the cooldown window of its last alert; set it to `0` to disable the cooldown (alert on +every re-entry, the old behavior). + +`agent-hooks` is an optional external hook helper / decision UI. If it is installed and +`notify_backend = "agent-hooks"`, `ring hook` still writes RiNG registry state, then passes the +raw payload to `agent-hooks callback` for the synchronous decision UI. RiNG `--watch` will not send +a duplicate notification. If `agent-hooks` is not on `PATH`, RiNG automatically falls back to `auto`. + +### Remove Hooks + +```sh +ring remove-hooks +ring remove-hooks --dry-run +``` + +This removes RiNG-installed hook entries from Claude Code / Codex settings. It does not delete +`~/.config/ring/` session records and does not touch other hooks. + +## Configuration + +`~/.config/ring/config.toml`, all optional: + +```toml +lang = "en" +interval = 2.0 +show_all = false +legend = true +active_window_seconds = 21600 +working_threshold_seconds = 90 +waiting_window_seconds = 1800 # zero-config window for collapsing recent end-of-turn rows to 🟡 +codex_permission_wait_seconds = 10 # bare Codex PermissionRequest + hook silence beyond this → 🔴 waiting; 0 = off +detect_stop_questions = true # promote Stop to 🔴 waiting when it ends in a plain-text question; false = Stop always 🟡 +notify_sound = true +notify_sound_name = "Glass" +notify_ignore_dnd = false +notify_backend = "auto" # auto / terminal-notifier / osascript / notify-send / agent-hooks / none +notify_repeat_seconds = [30, 120, 300] +notify_repeat_max = 3 +waiting_cooldown_seconds = 180 # suppress an immediate re-alert on re-entering waiting within this window; 0 = off +notify_ntfy_url = "" # full ntfy topic URL enables phone push (e.g. https://ntfy.sh/my-topic) +notify_webhook_url = "" # URL enables the generic webhook backend (JSON POST) +notify_also = [] # extra backends fired besides the primary, e.g. ["ntfy"] +focusers = ["Neovim", "tmux", "iTerm2", "Terminal", "linux-wm"] +plugins = [] # external plugin modules imported at startup (see Extending) +debug_payload_log = false # diagnostic raw hook payload log; may contain user input + +[colors] +waiting = "bold red" +working = "green" +idle = "yellow" +ended = "grey50" +project = "cyan" +location = "bright_blue" +muted = "grey50" +``` + +## Language / Translation + +The UI uses **gettext**. Source msgids are written in Taiwanese Mandarin, so the default +`zh-Hant` UI does not require a compiled catalog. Select English with `--lang en`, +`RING_LANG=en`, or the `lang` config key. Plurals use `ngettext`. + +After adding a language or changing a user-facing string, update and compile the catalogs: + +```sh +uv run poe i18n:extract # extract msgids to src/ring/locale/ring.pot +# Copy ring.pot to src/ring/locale//LC_MESSAGES/ring.po and translate msgstr entries. +uv run poe i18n:compile # compile .po to .mo; commit both files +``` + +## Extending + +RiNG is not tied to a specific tool or terminal. + +| Extension Point | Purpose | Built-ins | +|-----------------|---------|-----------| +| `SessionSource` | find sessions | Claude Code, Codex, Ollama, llama.cpp, hook registry | +| `Focuser` | jump to terminals | Neovim terminal, tmux, iTerm2, Terminal.app, Linux X11 (wmctrl) | +| `Notifier` | notify when sessions are waiting | terminal-notifier, osascript, notify-send, ntfy, webhook | + +Each backend is a small module under `ring/sources/`, `ring/focus/`, or `ring/notify/`, registered via `register_*()`. + +### Loading Your Plugin Into An Installed `ring` + +`register_*()` only counts if something runs it. The installed `ring` command loads plugins from +two places at startup: + +1. **Entry point** (for published packages) — declare it in your package's `pyproject.toml`, + pointing at a module or callable (a module registers on import; a callable is invoked once + with no arguments): + + ```toml + [project.entry-points."ring.plugins"] + mytool = "ring_mytool.plugin" + ``` + +2. **Config** (for local scripts) — add `plugins = ["my_module"]` to `~/.config/ring/config.toml`; + the module must be importable (site-packages or `PYTHONPATH`). + +A broken plugin prints one warning line to stderr and never blocks the board. + +### Other Agent CLIs (`SessionSource`) + +Built-ins include `HookRegistrySource`, `ClaudeCodeSource`, `CodexSource`, and process sources for +interactive Ollama / llama.cpp sessions. Prefer feeding `ring hook` when the tool exposes lifecycle +events. Otherwise, implement a source that returns `Session` objects and register it: + +```python +from ring.registry import Session, Status +from ring.sources import register_source + + +class MyToolSource: + name = "mytool" + + def discover(self) -> list[Session]: + return [Session(session_id="…", cwd="…", status=Status.WORKING, + last_active=0.0, last_action="→ …", source="mytool")] + + +register_source(MyToolSource()) +``` + +### Other Terminals (`Focuser`) + +Implement the `Focuser` protocol (`try_focus(session) -> (ok, msg) | None`) and call +`ring.focus.register_focuser(MyFocuser())`. The Neovim focuser first switches to the exact +`:terminal` buffer through the inherited `$NVIM` server socket, then lets an outer focuser raise the +tmux pane or terminal window. Configure attempt order with the `focusers` setting. + +### Other Notification Backends (`Notifier`) + +Implement the `Notifier` protocol and register it with `ring.notify.register_notifier()`: + +```python +from ring.notify import register_notifier + + +class MyNotifier: + name = "mytool-notify" + + def available(self) -> bool: ... + def supports_click(self) -> bool: ... + def send(self, sessions): ... + + +register_notifier(MyNotifier()) +``` + +Select the backend with `notify_backend` (`auto`, a registered name, or `none`). diff --git a/docs/guide.md b/docs/guide.md new file mode 100644 index 0000000..c053071 --- /dev/null +++ b/docs/guide.md @@ -0,0 +1,477 @@ +# RiNG 🎤 + +安裝、產品介紹與首次操作請先看[文件首頁](index.md);本頁集中收錄操作與進階參考。 + +## 常用指令 + +| 指令 | 用途 | +|------|------| +| `ring` | 印一張當下快照 | +| `ring --watch` | 持續刷新;有 Textual 時進互動 TUI | +| `ring --watch --interval 1` | 每 1 秒刷新 | +| `ring --watch --count N` | 刷新 N 次後結束,方便測試 / CI | +| `ring --all` | 顯示已離場 session | +| `ring --no-legend` | 隱藏圖例 | +| `ring --lang en` | 切英文 UI | +| `ring focus SESSION_ID` | 聚焦指定 session;可用唯一前綴 | +| `ring config` | 顯示設定檔路徑與生效設定 | +| `ring config set KEY VALUE` | 寫入單一設定 | +| `ring doctor` | 唯讀環境診斷 | +| `ring digest --since 4h` | 離席摘要:最近 session 狀態與等待統計 | +| `ring gc --dry-run` | 預覽 RiNG 自己的 stale 狀態檔清理 | +| `ring gc` | 清理 RiNG 自己的 stale 狀態檔 | +| `ring --format json` | 整個看板的機器可讀快照(給 jq / 腳本) | +| `ring --format oneline` | `🔴2 🟢1 🟡3` 單行摘要(給 status bar) | +| `ring stats` | 等待統計:最近 7 天你讓 agent 🔴 等了多久 | +| `ring completion zsh` | 印 shell 補全腳本(zsh / bash) | + +### 塞進 status bar(`--format`) + +```sh +ring --format oneline # 🔴2 🟢1 🟡3(沒 session 時輸出空字串,段落自然收起) +ring --format json | jq '.counts.waiting' +``` + +- **tmux**:`set -g status-right '#(ring --format oneline) …'`(配 `status-interval 5`)。 +- **SwiftBar / xbar / waybar**:包一層腳本呼叫 `ring --format oneline` 或吃 JSON 自己排版。 +- JSON 的鍵名視為穩定介面(只加不改),放心接腳本。 + +### Shell 補全(`ring completion`) + +```sh +# ~/.zshrc +eval "$(ring completion zsh)" +# ~/.bashrc +eval "$(ring completion bash)" +``` + +子命令、旗標、`config set` 的鍵都補得到;`ring focus` 會提示 session id / 唯一前綴參數。 + +### `--watch` 的兩種樣子 + +- 裝了 **Textual**(`[tui]` extra)且在真終端 → **互動 TUI**: + `↑/↓` 選 session、`Enter` / `Space` 跳到它所在的終端、`p` 就地回覆權限請求、`n` 命名、`a` 切換是否顯示已離場、`dd` 隱藏 session(有新活動會自動重新出現)、`r` 刷新、`q` 離場。 + 如果你跟我一樣有 vim 手癖,也可以用 `j/k` 上下移動、`g/G` 跳到第一列 / 最後一列。 + 選中 🔴 等你的列時,表格下方會多一行顯示**它具體在等什麼**(要跑的指令、問的問題;hook 模式才有)。 + Claude Code 背景 agent 以 `⚙` 標示;它沒有可跳轉的終端,選取時會顯示 `claude --resume` 接回提示,完成後預設收進已離場(按 `a` 仍可查看)。 +- 否則 → **Rich poll**(清除畫面重畫);連 Rich 都沒有就純文字。三層優雅降級。 + +### 跳到 session(`Enter` / `Space` / `ring focus`) + +> 這裡的 `Space` 是鍵盤上的空白鍵,不是 BanG Dream! 裡的場館「SPACE」。 + +選一個 session 按 `Enter`,或用 `ring focus SESSION_ID`(完整 id 或唯一前綴),RiNG 把焦點帶到它真正所在的終端。 +如果 TUI 正在跑,`ring focus` 會先把請求交給 TUI,讓游標選中該 session;沒有 TUI 時才直接跳終端。目前內建支援: + +- **tmux**:`switch-client` 直接切到那個 pane(你跟它要在同一個 tmux server)。 +- **iTerm2 / Terminal.app**(macOS):用 session 的 `tty` 透過 AppleScript 聚焦對應分頁, + 自動分辨是哪個 app(沒在跑的 app 不會被喚醒)。第一次會跳系統「自動化」授權,准一次即可。 +- **Linux X11 視窗**(`wmctrl`,best-effort fallback):Linux 上沒跑 tmux 時的後備—— + 從 `tty` 追到擁有它的終端視窗,用 `wmctrl` 帶到前景。**限制**:只支援 X11(Wayland 通常無效)、 + 只能聚焦整個視窗無法選分頁、gnome-terminal 的 client/server 架構可能配不到。要先 `apt install wmctrl`。 + +「哪個 session 在哪個終端」靠它的 `tty` 對應——**hook 模式最精準**; +zero-config 下每個專案只開一個 session 時也對得上。Codex 沒裝 hook 時會走 zero-config: +同一個 cwd 只開一個 live Codex 時可跳轉;同 cwd 多個 Codex 只能保守顯示,避免跳錯。 + +### 就地回覆權限請求(`p`) + +游標停在 🔴 等你的列按 `p`,RiNG 讀出那個 session 畫面上的權限對話框 +(Claude Code 的「Do you want to proceed?」框,含背景 subagent 帶 +「from the … agent」標頭的),把編號選項原文列成浮層讓你選;選定後 RiNG 會**再抓一次 +畫面**確認對話框還在且沒變(防止你考慮期間它已被回掉),才代你按下那個數字, +並回頭驗證對話框確實消失。 +確認回覆成功後,TUI 會立即清掉該筆「等你」;只有時間較新的 hook 事件(例如下一個權限請求)能再次把它標成等待。 + +- **tmux 內的 session**:用 `tmux capture-pane` 抓畫面、`tmux send-keys` 送鍵。 +- **macOS 上直接開在 iTerm2 分頁的 session**(沒有 tmux):用 session 的 `tty` 透過 + AppleScript 找到對應 iTerm2 分頁,抓畫面、送鍵都走 `osascript`。第一次用時 macOS + 會跳「允許控制 iTerm2」的自動化授權框,允許一次即可。 + +安全底線:畫面上解析不到可辨識的對話框(標記、編號、游標任一缺)就只提示、**絕不送鍵**—— +對話框不在時按鍵會落進聊天輸入框變成文字;萬一送出的瞬間對話框剛好消失、數字落進輸入框, +RiNG 會自動補一個 Backspace 清掉並警告你。 + +**限制**:要有 tmux pane 座標、或(macOS 上)測得到 tty 的 iTerm2 session 才抓得到畫面; +其餘 session 請按 `Enter` 跳過去回。 + +### 等你時發通知 + +預設行為:裝了 hook 後,session 從工作中轉成 🔴 等你的**當下**,hook 就地**響鈴 + 發系統通知**—— +**不必開著 RiNG 看板**,關掉終端也照樣 ring 你(通知由事件觸發,不靠輪詢)。 +開著 TUI 時,若它一直停在等你,TUI 還會在 30s / 120s / 300s 各補一次 in-app 響鈴提醒。 +(zero-config 測不到「等你」,所以這個需要 hook 模式。) + +macOS 上若要點通知後直接跳回 RiNG TUI 並選中那個 session,需要安裝 +[terminal-notifier](https://github.com/julienXX/terminal-notifier)(brew 外部 binary): + +```sh +brew install terminal-notifier +``` + +沒裝時退化為 macOS 原生純文字通知(不可點擊跳轉),RiNG 會在第一次走到這條路時提示一次。 +通知聲音、重複提醒時間、後端選擇都能在 config 裡調整;通知後端也是可插拔的,見後面的 +`Notifier` 擴充說明。 + +#### 推到手機(ntfy / webhook) + +人不在座位時,桌面通知等於沒響。設定 [ntfy](https://ntfy.sh) topic URL 就能推到手機: + +```toml +# ~/.config/ring/config.toml +notify_ntfy_url = "https://ntfy.sh/my-ring-topic" # 手機裝 ntfy app、訂同一個 topic +notify_also = ["ntfy"] # 桌面通知照發,再「加發」一份到手機 +``` + +`notify_backend = "ntfy"` 則是只推手機、不發桌面通知。要接 Slack / 自家 bot / IFTTT, +用 `notify_webhook_url` 走通用 webhook 後端(JSON POST,欄位穩定只加不改)。 + +### 清理 RiNG 狀態檔(`ring gc`) + +RiNG 正常收到 `SessionEnd` 時會刪掉自己的 hook registry;如果 agent crash 或 hook 沒跑到結尾, +可能留下已離場的 `~/.config/ring/sessions/*.json`。這些檔案預設不會顯示在看板上,但可以用 +`ring gc` 清掉。 + +```sh +ring gc --dry-run # 預覽會刪哪些檔案 +ring gc # 清掉已離場且超過 7 天的 registry,以及過期 IPC 檔 +ring gc --older-than 1d # 改成 1 天 +ring gc --all-ended # 清掉所有目前判定已離場的 registry +``` + +`ring gc` 只清 RiNG 自己寫在 `~/.config/ring/` 底下的狀態檔,不會碰 Claude Code / Codex 的 +transcript 或 state。`ring doctor` 維持唯讀診斷,不會替你刪檔。 + +### 等待統計(`ring stats`) + +hook 模式下,RiNG 會把 session 的**狀態轉換**記進 `~/.config/ring/events.jsonl` +(只記轉換、量很小、超過上限自動砍半保新)。`ring stats` 據此告訴你:最近這段時間, +每個專案 🔴 等了你幾次、平均 / 最長 / 總共等多久。 + +```sh +ring stats # 最近 7 天 +ring stats --since 12h # 自訂時間窗 +``` + +跟精準通知一樣,zero-config 測不到 🔴,所以 stats 也需要 hook 模式。 + +## Session 來源 + +RiNG 會從已註冊的 source 收集 session;目前內建這幾種: + +| Source | 來源 | 狀態精度 | +|------|------|----------| +| **Claude Code zero-config**(預設) | 掃 `~/.claude/projects/**/*.jsonl` 的 mtime + 記錄裡的 `cwd` 欄位 | 免設定;可辨識近期活動與回合結束。需要回應的通知要靠 hook 才精準 | +| **Codex zero-config**(預設) | 讀 `~/.codex/state_5.sqlite` threads + rollout JSONL,並用 live `codex` process 配 tty | 免設定;可辨識 live / ended / 回合結束。同 cwd 多 session 建議裝 hook 取得精準跳轉 | +| **Ollama zero-config**(預設) | 偵測有控制終端的 `ollama run` process | 行程存活層級;顯示 cwd、TTY 與模型,不把 `ollama serve` 當 session | +| **llama.cpp zero-config**(預設) | 偵測有控制終端的 `llama-cli` process | 行程存活層級;顯示 cwd、TTY 與模型,不把 `llama-server` 當 session | +| **hook registry**(opt-in,精準) | RiNG hook 在 `Notification` / `UserPromptSubmit` / `Stop` / `SessionEnd` 即時寫 `~/.config/ring/sessions/` | 準(🔴 等你 / 🟢 工作中 / 🟡 跑完停著 / ⚫ 已離場) | + +zero-config 不必設定就能用;想要精準的「誰在等你」,就讓 provider 的 hook 餵進 RiNG registry。 +RiNG 內建 Claude Code / Codex hook 安裝器;其他工具可直接走 provider-neutral `ring hook` protocol。 + +Ollama 與 llama.cpp 本身沒有 RiNG 可讀的 session transcript 或互動 hook,因此 zero-config 列固定以 +🟡 顯示:它代表互動式 CLI 還活著,不代表 RiNG 能分辨它正在生成或等下一個 prompt。CLI 結束後列 +就會移除。若外層 agent 能送出生命週期事件,可用下方的中立 hook protocol 取得精準狀態。 + +## 狀態機 + +RiNG 把每個 session 壓成四種狀態。看板排序時,🔴 等你永遠排最上面並 highlight。 + +| 狀態 | 你可以怎麼理解 | RiNG 看到什麼 | +|------|----------------|---------------| +| 🔴 等你 | 需要你現在回去處理 | hook 收到權限請求、選項提問等需要回應的通知 | +| 🟢 工作中 | agent 正在跑,先不用管 | 你剛送出 prompt,或 session 最近仍有活動 | +| 🟡 跑完停著 | 這輪跑完了,停在那裡 | 收到 `Stop`,或超過 `working_threshold_seconds` 沒有新活動 | +| ⚫ 已離場 | session 已結束 | 收到 `SessionEnd`,或 process / 本機紀錄顯示它已離場 | + +狀態更新規則: + +| 發生的事 | 新狀態 | +|----------|--------| +| `SessionStart` / `UserPromptSubmit` | 🟢 工作中 | +| `Stop` | 🟡 跑完停著 | +| 權限請求、選項提問、`requires_action = true` | 🔴 等你 | +| `SessionEnd` / process 結束 | ⚫ 已離場 | +| zero-config 來源太久沒更新 | 🟢 工作中 退成 🟡 跑完停著 | + +🔴 等你只有 hook 模式測得到;zero-config 只能判斷「最近有沒有活動」, +分不出「需要你做決策」和「剛跑完停著」。 + +## hook 模式(精準的「等你」) + +zero-config 只靠檔案 mtime 或本機 state,**分不出「需要你做決策」還是「剛跑完」**。 +裝 hook 後,RiNG 直接收 agent CLI 的事件,狀態才會精準——🔴 等你 + 響鈴都要靠它。 + +### Claude Code / Codex:內建安裝器 + +hook 會被寫成「執行 `ring hook`」,所以 `ring` 要在 PATH 上、指向穩定路徑: + +```sh +uv tool install 'ring-cli[tui]' +``` + +註冊 hook: + +```sh +ring install-hooks # 寫進 Claude Code / Codex hook 設定(合併,不覆蓋既有 hooks) +ring install-hooks --dry-run # 只想先看會寫什麼、不動檔 +``` + +Claude Code 會寫進 `~/.claude/settings.json`。如果你有在用 Codex(`~/.codex` 存在), +RiNG 也會寫進 `~/.codex/hooks.json`;Codex 會要求你信任新 hook,信任後才會執行。 + +Claude Code 註冊這幾個事件,對應到狀態: + +| Claude Code 事件 | RiNG 狀態 | +|---|---| +| `SessionStart` / `UserPromptSubmit` | 🟢 工作中 | +| `Stop` | 🟡 跑完停著 | +| `Notification` 的 `permission_prompt` / `elicitation_dialog` | 🔴 等你(卡權限 / 需要選項) | +| `PermissionRequest` / `PreToolUse` 的 `AskUserQuestion` | 🔴 等你(權限 / 選項需要你決策) | +| `SessionEnd` | 從看板消失 | + +`Stop` 還有一種例外:agent 有時不用 `AskUserQuestion`、不觸發權限請求,只在文字裡問了一句話 +就停下來(例如「要不要順便修 B?」)。這種情況下 RiNG 會看最後一則 assistant 訊息的**結尾**—— +結尾是問句(`?` / `?`,圍欄程式碼區塊先剝掉再看)就升級成 🔴 等你(`waiting_kind="question"`), +detail 帶那句問句;問句只出現在訊息中段、結尾是陳述句則不算,保守判斷、寧可漏報不要誤報。 +Claude Code 與 Codex 兩邊都適用(兩者的 `Stop` payload 都帶 `last_assistant_message`); +`detect_stop_questions` 設定鍵可關閉這個行為(預設開)。 + +Codex 目前註冊 Codex 支援的互動事件:`PreToolUse`、`PermissionRequest`、`PostToolUse`、`Stop`。 +Codex 的 `PermissionRequest` 也會在既有 policy 自動放行前送出,所以裸事件在 hook 層維持 🟢 +工作中。Codex 的 hook 沒有「使用者已核可」事件、沒有心跳,等核可期間 hook 通道**完全靜默** +——RiNG 因此把靜默本身當訊號:最後一個 hook 事件是 `PermissionRequest`、又超過 +`codex_permission_wait_seconds`(預設 10 秒)沒有任何後續事件,看板就把它標成 🔴 等你, +detail 帶當初要跑的指令摘要;任何後續事件(下一個工具、`Stop`)到了就自然轉回。 + +Codex 側的已知限制(0.144.4 實證,hook payload 無任何欄位可區分): + +- **分不出核可還是拒絕**:你按下 allow 或 deny 時,hook 都不會收到事件。allow 之後若那條指令 + 本身跑很久,🔴 會多亮到指令跑完(`PostToolUse`)為止;deny 之後若 agent 沒有立即的後續動作, + 🔴 會亮到下一個事件或 `Stop` 才清。 +- 核可等待轉 🔴 的系統通知由 TUI 的提醒排程器代發(沒有 hook 事件可在當下發);headless + `--watch` 不發這一種通知。 + +hook 只對**新開的 session** 生效,所以裝完要重開。確認方法: + +```sh +ls ~/.config/ring/sessions/ # 出現 .json 就代表 hook 在寫了 +``` + +RiNG 一偵測到 `~/.config/ring/sessions/` 有資料就自動切精準模式(hook 來源優先、 +zero-config 掃描補上沒裝 hook 的 session)。 + +### 其他 provider:中立 hook protocol + +RiNG 不相依 agent-hooks,也不要求 provider 一定要用內建安裝器。任何工具只要在事件發生時把 JSON +餵給 `ring hook` 即可: + +```sh +ring hook --provider codex +# 或簡寫: +ring hook codex +``` + +payload 欄位採寬鬆命名,至少需要 session id、event、cwd: + +```json +{ + "provider": "codex", + "session_id": "thread-123", + "event": "Notification", + "notification_type": "permission_prompt", + "requires_action": true, + "waiting_for": "permission", + "cwd": "/repo/app", + "tty": "/dev/ttys003", + "last_action": "waiting for permission" +} +``` + +支援的事件語意與內建 provider 一致:`SessionStart` / `UserPromptSubmit` → 🟢 工作中; +`Stop` 預設 → 🟡 跑完停著,但結尾純文字提問可升成 🔴 等你;需要回應的 `Notification` 或帶明確互動 payload 的 `PermissionRequest` → 🔴 等你;裸 `PermissionRequest` → 🟢 權限判定中;`SessionEnd` → +從看板移除。非 Claude provider 的 session id 會自動加上 provider prefix +(例如 `codex:thread-123`),避免不同工具撞 id。 + +provider 若能分辨「立刻需要使用者」與「只是等下一步」,請直接給明確欄位: +`requires_action = true/false` 或 `waiting_for = "permission" | "options" | "next_step"`。 +RiNG 會優先相信這些欄位;沒有時才退回 event / notification type 推論。 + +### 系統通知(🔔 等你時自動通知 + 點擊聚焦) + +這裡補細節:系統通知由 `ring hook` 在事件當下送出(headless `--watch` 與 TUI 都只負責顯示 +看板,不發系統通知)。預設後端是 `auto`:有可點擊的 `terminal-notifier` 就優先用它;沒有時 +退回 `osascript`(macOS)或 `notify-send`(Linux)。如果全部不可用,就只保留看板,不讓通知失敗打斷主流程。 + +- **設定**:`notify_sound`、`notify_sound_name`、`notify_ignore_dnd`、`notify_repeat_seconds`、 + `notify_repeat_max`、`notify_backend`、`waiting_cooldown_seconds` 都可在 `~/.config/ring/config.toml` 調整。 +- **防翻轉轟炸**:背景 subagent 的權限請求可能讓 session 在等你/工作中之間快速翻轉。 + `waiting_cooldown_seconds`(預設 180 秒)讓 `ring hook` 的系統通知與 TUI 的響鈴/提醒, + 在 session 離開等你又很快轉回時,距上次通知未滿冷卻期就不再立即發;設 `0` 關閉冷卻 + (回到每次轉入都發)。 +- **點擊聚焦 + 聲音**:需先安裝 `terminal-notifier`(brew)。點擊通知後會直接跳回 RiNG TUI + 並選中 session;若沒有 TUI 在跑,則直接跳到對應終端。 + + ```sh + brew install terminal-notifier + ``` + +- **純文字通知**:未裝 `terminal-notifier` 則退化為 macOS 原生純文字通知(可帶聲音,點擊不可聚焦)。 +- **擴充**:要接其他桌面通知、webhook 或自訂提醒方式,可以新增 `Notifier` 後端並註冊, + 詳見「其他通知後端」。 + +- **交給 agent-hooks**:`agent-hooks` 是另一個可選的 hook helper / 決策 UI。若你已安裝它, + 並把 `notify_backend` 設成 `agent-hooks`,`ring hook` 會照樣寫 RiNG registry, + 同時把原始 payload 交給 `agent-hooks callback` 處理同步決策視窗;RiNG 的 `--watch` + 不會再重複發通知。若 PATH 上找不到 `agent-hooks`,會自動退回 `auto`。 + +### 移除 hooks + +不想再讓 agent CLI 事件寫進 RiNG registry 時,可以移除 RiNG 安裝的 hook 條目。 +這不會刪掉 `~/.config/ring/` 裡的 session 紀錄,也不會動到 Claude Code / Codex 的其他 hooks。 + +```sh +ring remove-hooks # 從 Claude Code / Codex hook 設定移除 RiNG hook 條目 +ring remove-hooks --dry-run # 只預覽,不改檔 +``` + +## 設定(選用) + +`~/.config/ring/config.toml`,全部選填、缺了就用預設: + +```toml +lang = "zh-Hant" # 預設語言(CLI --lang 最優先,再來 RING_LANG / LANG) +interval = 2.0 # watch 刷新秒數 +show_all = false # 是否預設顯示已離場 +legend = true # 是否預設顯示圖例 +active_window_seconds = 21600 # 只看最近這段時間動過的 session(預設 6h) +working_threshold_seconds = 90 # 多久沒動 → 🟢 工作中 變 🟡 閒置 +waiting_window_seconds = 1800 # 零設定掃描中,近期回合結束列收斂成 🟡 的時間窗 +codex_permission_wait_seconds = 10 # Codex 裸 PermissionRequest 後 hook 靜默超過這秒數 → 🔴 等你;0 = 關閉 +detect_stop_questions = true # Stop 結尾是純文字提問時升級成 🔴 等你;false = Stop 一律 🟡 +notify_sound = true # 系統通知是否播放聲音 +notify_sound_name = "Glass" # macOS / terminal-notifier sound name +notify_ignore_dnd = false # macOS terminal-notifier 是否穿透勿擾 / Focus +notify_backend = "auto" # auto / terminal-notifier / osascript / notify-send / agent-hooks / none +notify_repeat_seconds = [30, 120, 300] # 持續等你時,幾秒後重複提醒 +notify_repeat_max = 3 # 重複提醒上限;0 = 不限 +waiting_cooldown_seconds = 180 # 離開等你又轉回時,距上次提醒未滿這段時間就不再立即提醒;0 = 關閉 +notify_ntfy_url = "" # 設完整 ntfy topic URL 啟用手機推播(如 https://ntfy.sh/my-topic) +notify_webhook_url = "" # 設 URL 啟用通用 webhook 後端(JSON POST) +notify_also = [] # 主後端之外「加發」的後端,如 ["ntfy"](桌面+手機各一份) +focusers = ["Neovim", "tmux", "iTerm2", "Terminal", "linux-wm"] # 跳轉嘗試順序 +plugins = [] # 啟動時 import 的外部 plugin 模組(見「支援的工具與擴充」) +debug_payload_log = false # 診斷時記錄原始 hook payload;可能含使用者輸入,預設關閉 + +[colors] # Rich 樣式字串,逐項覆寫(深淺底安全色為預設) +waiting = "bold red" +working = "green" +idle = "yellow" +ended = "grey50" +project = "cyan" +location = "bright_blue" +muted = "grey50" +``` + +## 語言 / 翻譯 + +UI 用 **gettext**,msgid 直接是**台灣漢語**——所以預設(zh-Hant)不需要任何 `.mo`, +原始碼即翻譯。切語言:`--lang en`(或 `RING_LANG=en` / config 的 `lang`)。複數用 +`ngettext` 正確處理(`1 session` vs `2 sessions`)。 + +加一個語言、或改了字串要重抽: + +```sh +poe i18n:extract # 從原始碼抽 msgid → src/ring/locale/ring.pot +# 複製 ring.pot 成 src/ring/locale//LC_MESSAGES/ring.po,填 msgstr +poe i18n:compile # 各 .po → .mo(.mo 要 commit;wheel 會自動帶上) +``` + +## 支援的工具與擴充 + +core 不綁死任何特定工具或終端。三個維度都可插拔,每個都是「寫個小類別 + 註冊」、主流程零改動: + +| 維度 | 在做什麼 | 內建 | +|------|----------|------| +| `SessionSource` | 從哪裡找到 session | Claude Code、Codex、Ollama、llama.cpp、RiNG hook registry | +| `Focuser` | 跳轉時把焦點帶去哪個終端 | tmux、iTerm2、Terminal.app、Linux X11(wmctrl)| +| `Notifier` | 等你時怎麼發系統通知 | terminal-notifier、osascript、notify-send、ntfy、webhook | + +每個維度各自一個 package(`ring/sources/`、`ring/focus/`、`ring/notify/`),每個後端是裡面 +一個模組;要加新的=丟一個模組 + `register_*()`。 + +### 讓裝好的 `ring` 載入你的 plugin + +`register_*()` 要有人執行才算數。裝好的 `ring` 指令啟動時會自動載入兩種來源的 plugin: + +1. **entry point**(發佈成套件時)——在你套件的 `pyproject.toml` 宣告,指向一個模組或 + callable(模組在 import 時自行 `register_*()`;callable 會被無參數呼叫一次): + + ```toml + [project.entry-points."ring.plugins"] + mytool = "ring_mytool.plugin" + ``` + +2. **config**(本機腳本)——`~/.config/ring/config.toml` 寫 `plugins = ["my_module"]`, + 模組要在 `sys.path` 上(site-packages 或 `PYTHONPATH`)。 + +單一 plugin 壞掉只會在 stderr 警告一行、不擋看板。 + +### 其他 agent CLI(`SessionSource`) + +內建 `HookRegistrySource`(讀 `~/.config/ring/sessions/`)、`ClaudeCodeSource`(掃 +`~/.claude`)、`CodexSource`(讀 `~/.codex/state_5.sqlite`),以及 Ollama / llama.cpp +互動式 CLI 的 process source。要監測其他工具, +可以優先餵 `ring hook`;若工具沒有 hook,再寫一個 source 吐出 `Session`、註冊即可: + +```python +from ring.registry import Session, Status +from ring.sources import register_source + + +class MyToolSource: + name = "mytool" + + def discover(self) -> list[Session]: + return [Session(session_id="…", cwd="…", status=Status.WORKING, + last_active=0.0, last_action="→ …", source="mytool")] + + +register_source(MyToolSource()) +``` + +`Session` 是工具中立的(id / cwd / status / last_action / tty / todo…),各 source +自己決定怎麼填。 + +### 其他終端(`Focuser`) + +跳轉的終端整合也一樣——寫個符合 `Focuser` 協定的類別(`try_focus(session) -> +(ok, msg) | None`),呼叫 `ring.focus.register_focuser(MyFocuser())`。內建 Neovim terminal / tmux / +iTerm2 / Terminal.app / Linux X11 視窗(wmctrl),各自一個模組(`ring/focus/neovim.py`、 +`ring/focus/tmux.py` …)。Neovim focuser 會先透過 `$NVIM` server socket 切到承載該 session 的 +`:terminal` buffer,再交給外層 focuser 聚焦 pane 或視窗。 +要再加 Ghostty / Kitty / WezTerm,就照這個模式新增 focuser;嘗試順序也能用 config 的 `focusers` 調整。 + +### 其他通知後端(`Notifier`) + +系統通知也是可插拔的——寫個符合 `Notifier` 協定的類別,呼叫 +`ring.notify.register_notifier(MyNotifier())`。內建 terminal-notifier(可點擊跳轉)/ +osascript / notify-send,各自一個模組(`ring/notify/terminal_notifier.py` …): + +```python +from ring.notify import register_notifier + + +class MyNotifier: + name = "mytool-notify" + + def available(self) -> bool: ... # 這個後端現在能不能用(通常看 binary 在不在) + def supports_click(self) -> bool: ... # 點通知能不能跳回 session + def send(self, sessions): ... # 逐 session 各發一則通知 + + +register_notifier(MyNotifier()) +``` + +選哪個後端由 config 的 `notify_backend` 決定(`auto` / 指定名稱 / `none` 純看板不發通知)。 diff --git a/docs/index.en.md b/docs/index.en.md new file mode 100644 index 0000000..f3db3c9 --- /dev/null +++ b/docs/index.en.md @@ -0,0 +1,112 @@ +# RiNG Documentation + +[![PyPI](https://img.shields.io/pypi/v/ring-cli?label=PyPI)](https://pypi.org/project/ring-cli/) +[![Python](https://img.shields.io/pypi/pyversions/ring-cli)](https://pypi.org/project/ring-cli/) +[![License](https://img.shields.io/pypi/l/ring-cli)](https://github.com/Lee-W/ring/blob/main/LICENSE) + +> **R**ealtime **I**nstance **N**otification **G**rid +> — one local board for all active agent-CLI sessions. + +RiNG puts Claude Code, Codex, and local-model sessions in one place, sorts work that needs your response first, and returns you directly to the original terminal. + +```text +🎤 RiNG — 3 sessions on stage · 2 agent processes running + + 🔴 maigo 12s → waiting for permission + 🟢 pelican-osm 3s → Edit + 🟡 commitizen 8m turn finished, idle +``` + +## Who It Is For + +- You run several Claude Code or Codex sessions at once. +- You want to distinguish working, idle, and waiting sessions. +- You want to return directly from the TUI to the session's terminal. +- You are willing to install hooks for precise waiting states and system notifications. + +## Key Features + +- **One board for every session**: built-in support for Claude Code, Codex, Ollama, and llama.cpp; other tools can use `ring hook`. +- **Waiting first**: sessions that need a response are sorted first and show what they need. +- **Jump back to the terminal**: supports tmux, iTerm2, Terminal.app, Neovim terminals, and Linux X11 windows. +- **Reply and name in place**: answer permission requests in supported terminals and assign recognizable session names. +- **Immediate notifications**: desktop notifications work without the board open; ntfy and webhooks can reach your phone. +- **Easy to integrate**: status-bar summaries, JSON, a provider-neutral hook, and plugin extension points. + +## Quick Start + +Requires Python 3.13+. The PyPI package is named `ring-cli`; the module and CLI command are named `ring`: + +```sh +# Recommended: install the global command from PyPI +uv tool install 'ring-cli[tui]' + +# Or use pipx +pipx install 'ring-cli[tui]' + +# Print a snapshot or open the interactive TUI +ring +ring --watch +ring --watch --interval 1 + +# Module form also works after installation +python -m ring +``` + +The `[tui]` extra installs the Textual interface. Without it, `--watch` falls back to Rich polling and then plain text. In the TUI, use `↑` / `↓` to select a session, `Enter` or `Space` to return to its terminal, and `q` to quit. + +For development from source, run inside the repository without installing a global command: + +```sh +uv sync --all-groups +uv run ring +uv run ring --watch +``` + +Zero-config mode discovers local sessions automatically. For precise 🔴 waiting states, pending details, and system notifications, also run: + +```sh +ring install-hooks # merge into Claude Code and Codex hook settings +ring install-hooks --dry-run # preview without changing settings +ring doctor # check hooks, notifications, and terminal focus +ring gc --dry-run # preview stale state cleanup +``` + +Restart Claude Code or Codex sessions after installing hooks. `ring doctor` checks hook registration, notification backends, and terminal focus support. + +## Where To Go Next + +- For TUI shortcuts, permission replies, notifications, or phone push, see the [full guide](guide.md). +- To add a summary to tmux, SwiftBar, or waybar, see [output formats](guide.md#status-bar-integration-format). +- To integrate another agent CLI, terminal, or notification service, see [extensions](guide.md#extending). +- To understand how 🔴/🟢/🟡/⚫ are derived, see [session states](session-states.md). +- To work on RiNG itself, see the [contributing guide](contributing.md). + +## Documentation + +- [Full guide](guide.md): commands, TUI controls, terminal focus, permission replies, notifications, hooks, configuration, plugins, platforms, and privacy +- [Session states](session-states.md): how hook and zero-config modes derive 🔴/🟢/🟡/⚫, with a complete event reference +- [Contributing guide](contributing.md): development setup, tests, code style, i18n, commits, and the PR checklist + +## Platform & Privacy + +- **Platform**: macOS and Linux are supported; Windows is not yet supported. +- **Privacy**: by default RiNG only reads local `~/.claude/` and `~/.codex/` data and writes `~/.config/ring/`. There is no telemetry. Network access occurs only for ntfy or webhook notifiers you configure. + +## Name + +The name has three meanings: + +1. It **rings** you when a session needs a response. +2. RiNG is the live house from *BanG Dream!*; like a venue, it lets you watch each session take the stage. +3. **R**ealtime **I**nstance **N**otification **G**rid describes the tool itself. + +In the original story, RiNG is CiRCLE's second location: more bands needed another venue sharing the same backend. This tool has the same role—it does not replace agent CLIs, but adds another view over many parallel sessions. + +## Non-Goals + +RiNG only tracks session state and whether you need to return. It is not an agent-usage analyzer or cost dashboard. Claude Code JSONL token values are not reliable enough for accounting, so RiNG deliberately omits token and cost statistics. + +## License + +MIT diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..0e3a3f0 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,112 @@ +# RiNG 文件 + +[![PyPI](https://img.shields.io/pypi/v/ring-cli?label=PyPI)](https://pypi.org/project/ring-cli/) +[![Python](https://img.shields.io/pypi/pyversions/ring-cli)](https://pypi.org/project/ring-cli/) +[![License](https://img.shields.io/pypi/l/ring-cli)](https://github.com/Lee-W/ring/blob/main/LICENSE) + +> **R**ealtime **I**nstance **N**otification **G**rid +> ——把所有 active 的 agent CLI session 放上同一張本機看板。 + +RiNG 把 Claude Code、Codex 與本機模型 session 放在同一張看板,讓需要你回應的工作排在最前面,並能直接跳回原本的終端。 + +```text +🎤 RiNG — 3 session 在場 · 2 個 agent process 跑著 + + 🔴 maigo 12s → 等你確認權限 + 🟢 pelican-osm 3s → Edit + 🟡 commitizen 8m 跑完一回合、停著 +``` + +## 適合誰用 + +- 你會同時開好幾個 Claude Code / Codex session。 +- 你想分辨哪些 session 正在跑、跑完停著或正在等你。 +- 你想從 TUI 直接跳回 session 所在的終端。 +- 你願意安裝 hooks,換取精準的等待狀態與系統通知。 + +## 重點功能 + +- **一張看板看全部 session**:內建 Claude Code、Codex、Ollama 與 llama.cpp 支援,其他工具可接 `ring hook`。 +- **等你優先**:需要你回應的 session 會排在最上面,並顯示具體在等什麼。 +- **一鍵跳回終端**:支援 tmux、iTerm2、Terminal.app、Neovim terminal 與 Linux X11 視窗。 +- **就地回覆與命名**:在支援的終端直接回覆權限請求,並替 session 取容易辨認的名字。 +- **即時通知**:看板沒開也能發送桌面通知;ntfy / webhook 可把通知推到手機。 +- **容易整合**:提供 status bar 單行摘要、JSON、provider-neutral hook 與 plugin 擴充點。 + +## 快速開始 + +需要 Python 3.13+。PyPI 套件名是 `ring-cli`,module 與 CLI 指令則叫 `ring`: + +```sh +# 推薦:從 PyPI 裝成全域指令 +uv tool install 'ring-cli[tui]' + +# 或使用 pipx +pipx install 'ring-cli[tui]' + +# 看一次快照或開啟互動 TUI +ring +ring --watch +ring --watch --interval 1 + +# 安裝後也能用 module 形式執行 +python -m ring +``` + +`[tui]` 會安裝 Textual 互動介面;沒有這個 extra 時,`--watch` 會依序退回 Rich poll 或純文字模式。開啟 TUI 後用 `↑` / `↓` 選擇 session,按 `Enter` 或 `Space` 跳回它所在的終端,按 `q` 離開。 + +從原始碼開發時不必先安裝全域指令,在 repository 內執行: + +```sh +uv sync --all-groups +uv run ring +uv run ring --watch +``` + +零設定模式會自動尋找本機 session。若要精準辨識 🔴 等你、顯示等待內容並發送系統通知,再執行: + +```sh +ring install-hooks # 合併寫入 Claude Code / Codex hook 設定 +ring install-hooks --dry-run # 只預覽,不修改設定 +ring doctor # 檢查 hooks、通知與終端聚焦能力 +ring gc --dry-run # 預覽 stale 狀態檔清理 +``` + +安裝 hooks 後要重開 Claude Code / Codex session。`ring doctor` 會檢查 hooks、通知後端與終端聚焦能力是否可用。 + +## 從哪裡繼續 + +- 想了解 TUI 快捷鍵、權限回覆、通知或手機推播:看[完整使用手冊](guide.md)。 +- 想把摘要放進 tmux、SwiftBar 或 waybar:看[輸出格式](guide.md#status-bar-format)。 +- 想接入其他 agent CLI、終端或通知服務:看[完整使用手冊的擴充說明](guide.md)。 +- 想釐清 🔴/🟢/🟡/⚫ 如何判定:看 [Session 狀態](session-states.md)。 +- 想參與開發:看[貢獻指南](contributing.md)。 + +## 文件導覽 + +- [完整使用手冊](guide.md):所有常用指令、TUI 操作、終端跳轉、權限回覆、通知、hooks、設定、plugin 擴充、平台與隱私 +- [Session 狀態](session-states.md):hook 與零設定模式如何判定 🔴/🟢/🟡/⚫,以及完整事件對照 +- [貢獻指南](contributing.md):開發環境、測試、程式風格、i18n、commit 與 PR checklist + +## 平台與隱私 + +- **平台**:支援 macOS 與 Linux;Windows 尚未支援。 +- **隱私**:預設只讀本機 `~/.claude/`、`~/.codex/` 資料,只寫 `~/.config/ring/`,沒有 telemetry。只有你自行設定的 ntfy / webhook 通知會連網。 + +## 名字哪裡來 + +名字有三層意思: + +1. 📞 session 需要回應時,它會 **ring** 你。 +2. 🎤 RiNG 是 *BanG Dream!* 裡的 live house;就像一座場館,讓你看一個個 session 上台。 +3. **R**ealtime **I**nstance **N**otification **G**rid 描述了這項工具本身。 + +原作裡的 RiNG 是 CiRCLE 的二號店:樂團數量變多,一間場館不夠用,於是用同一套後台再開一個觀測空間。這也正是本工具的定位——不取代 agent CLI,只替大量並行 session 多開一張看板。 + +## 不做什麼 + +RiNG 只關心 session 現在的狀態,以及是否需要你回去處理;它不是 agent 用量分析器或成本儀表板。Claude Code JSONL 的 token 數字不足以可靠計費,因此 RiNG 刻意不提供 token / 花費統計。 + +## License + +MIT diff --git a/docs/session-states.en.md b/docs/session-states.en.md new file mode 100644 index 0000000..7694878 --- /dev/null +++ b/docs/session-states.en.md @@ -0,0 +1,48 @@ +# Session States + +RiNG reduces every tracked session to four states, defined by `Status` in +[`src/ring/registry.py`](https://github.com/Lee-W/ring/blob/main/src/ring/registry.py): + +- 🔴 **Waiting (`WAITING`)**: the session needs a decision, such as permission, an answer, or a choice. +- 🟢 **Working (`WORKING`)**: the agent is running, a prompt was just submitted, or a tool call is in flight. +- 🟡 **Idle (`IDLE`)**: the current turn finished and RiNG does not believe immediate action is required. +- ⚫ **Ended (`ENDED`)**: the session ended or its local record aged past the active window. + +The diagram shows the most common hook-mode transitions. Labels include the actual hook event names where useful; explicit payload fields can still override the ordinary result, as described below. + +```mermaid +stateDiagram-v2 + state "🟢 Working(WORKING)" as WORKING + state "🟡 Idle(IDLE)" as IDLE + state "🔴 Waiting(WAITING)" as WAITING + state "⚫ Ended(ENDED)" as ENDED + + [*] --> WORKING: SessionStart / UserPromptSubmit + WORKING --> IDLE: Stop + IDLE --> WORKING: prompt submitted / tool started or completed + WAITING --> WORKING: tool started or completed after a reply + WORKING --> WAITING: permission prompt / actionable notification / trailing question + IDLE --> WAITING: permission prompt / actionable notification + WAITING --> IDLE: ordinary notification or Stop without a question + WORKING --> ENDED: SessionEnd + IDLE --> ENDED: SessionEnd + WAITING --> ENDED: SessionEnd + ENDED --> [*] +``` + +## Event Reference + +- **`SessionStart` / `UserPromptSubmit` → `WORKING`**: unconditional; a new session or a user reply means the agent is working. +- **`Stop` → `IDLE` or `WAITING`**: normalization defaults to `IDLE`. When `detect_stop_questions` is enabled and the last plain-text assistant message ends with a recognized question, the hook handler promotes it to `WAITING` with `waiting_kind=question`. +- **Bare `PermissionRequest` → `WORKING`**: this event only proves that the provider is evaluating permission. Many calls are auto-approved by policy, so it cannot immediately mean “needs you.” A payload that explicitly requires interaction still becomes `WAITING`. +- **Claude Code permission wait → `WAITING`**: when Claude Code actually stops, a later `permission_prompt` notification marks the session waiting and reuses the concrete command detail captured from the bare permission event. +- **Codex permission wait → delayed `WAITING`**: Codex has no later notification. If the last event remains a bare `PermissionRequest` beyond `codex_permission_wait_seconds`, the read path promotes that hook row to `WAITING`; any later hook event clears the condition. +- **`PreToolUse` → `WAITING` or `WORKING`**: `AskUserQuestion`, or non-empty `questions`, `options`, or `choices`, means `WAITING`; any other tool start means `WORKING`. +- **`PostToolUse` → `WORKING`**: the tool ran, so the prior interaction was handled and any stale waiting state is cleared. +- **`Notification` → `WAITING` or `IDLE`**: `permission_prompt`, `elicitation_dialog`, or another action-required payload means `WAITING`; an ordinary notification means `IDLE`. +- **Explicit override**: `requires_action`, `action_required`, `needs_user_action`, `requires_input`, `interactive`, or a recognized `waiting_for` value takes precedence and selects `WAITING` / `IDLE`. `SessionStart`, `UserPromptSubmit`, and `SessionEnd` are not overridden. +- **`SessionEnd` → `ENDED`**: the hook handler deletes the registry file, so the session leaves the default board. Visible `ENDED` rows mainly come from zero-config records aging beyond the active window. + +## Zero-Config Mode + +Without hooks, RiNG can only infer `WORKING`, `IDLE`, and `ENDED` from local records, processes, and idle time. The zero-config scan never guesses `WAITING`; precise waiting comes from hook payloads, trailing-question detection on Stop, or the delayed Codex permission rule. diff --git a/docs/session-states.md b/docs/session-states.md new file mode 100644 index 0000000..785efce --- /dev/null +++ b/docs/session-states.md @@ -0,0 +1,48 @@ +# Session 狀態 + +RiNG 把每個追蹤中的 session 收斂成四種狀態,定義在 +[`src/ring/registry.py`](https://github.com/Lee-W/ring/blob/main/src/ring/registry.py) 的 `Status`: + +- 🔴 **在等你(`WAITING`)**:session 需要你做決定,例如核准權限、回答問題或選擇選項。 +- 🟢 **工作中(`WORKING`)**:agent 正在執行,剛送出 prompt,或有工具呼叫正在進行。 +- 🟡 **跑完停著(`IDLE`)**:這一回合已結束,RiNG 不認為你需要立刻動作。 +- ⚫ **已離場(`ENDED`)**:session 已結束,或本機紀錄已超過活躍時間窗。 + +下圖顯示 hook 模式最常見的狀態轉換。箭頭使用台灣華語描述,括號內保留實際 hook 事件名稱;payload 的顯式欄位仍可能覆寫一般結果,完整規則列在圖後。 + +```mermaid +stateDiagram-v2 + state "🟢 工作中(WORKING)" as WORKING + state "🟡 跑完停著(IDLE)" as IDLE + state "🔴 在等你(WAITING)" as WAITING + state "⚫ 已離場(ENDED)" as ENDED + + [*] --> WORKING: 開始 session(SessionStart)/送出 prompt(UserPromptSubmit) + WORKING --> IDLE: 回合結束(Stop) + IDLE --> WORKING: 送出 prompt/工具開始或完成 + WAITING --> WORKING: 回覆後工具開始或完成 + WORKING --> WAITING: 權限提示/互動通知/結尾提問 + IDLE --> WAITING: 權限提示/互動通知 + WAITING --> IDLE: 一般通知或無提問的回合結束 + WORKING --> ENDED: session 結束(SessionEnd) + IDLE --> ENDED: session 結束(SessionEnd) + WAITING --> ENDED: session 結束(SessionEnd) + ENDED --> [*] +``` + +## 事件對照表 + +- **`SessionStart` / `UserPromptSubmit` → `WORKING`**:無條件,新的 session 或使用者剛回話都代表 agent 開始工作。 +- **`Stop` → `IDLE` 或 `WAITING`**:正規化的預設結果是 `IDLE`;如果 `detect_stop_questions` 開啟,而且最後一則 assistant 純文字訊息的結尾被辨識為提問,hook handler 會把它升成 `WAITING`,`waiting_kind` 設為 `question`。 +- **裸 `PermissionRequest` → `WORKING`**:事件只代表 provider 正在進行權限判定,許多呼叫會被 policy 自動放行,因此不能立即當成「需要你」。若 payload 明確要求互動,仍直接成為 `WAITING`。 +- **Claude Code 權限等待 → `WAITING`**:真的停下來等待時,後續 `permission_prompt` notification 會把 session 標成 `WAITING`;先前裸權限事件裡的具體指令摘要會沿用成等待內容。 +- **Codex 權限等待 → 延遲升成 `WAITING`**:Codex 沒有後續 notification。若最後事件仍是裸 `PermissionRequest`,且 hook 靜默時間超過 `codex_permission_wait_seconds`,讀取側會把該 hook row 升成 `WAITING`;任何後續事件都會清掉這項判定。 +- **`PreToolUse` → `WAITING` 或 `WORKING`**:`AskUserQuestion`,或 payload 帶有非空的 `questions`、`options`、`choices` 時是 `WAITING`;其餘工具開始執行時是 `WORKING`。 +- **`PostToolUse` → `WORKING`**:工具已經執行,代表先前的互動已處理,並清掉殘留的等待狀態。 +- **`Notification` → `WAITING` 或 `IDLE`**:`permission_prompt`、`elicitation_dialog` 或其他需要動作的 payload 是 `WAITING`;一般通知是 `IDLE`。 +- **顯式覆寫**:payload 的 `requires_action`、`action_required`、`needs_user_action`、`requires_input`、`interactive`,或可辨識的 `waiting_for` 會優先決定 `WAITING` / `IDLE`。`SessionStart`、`UserPromptSubmit` 與 `SessionEnd` 不受這項覆寫影響。 +- **`SessionEnd` → `ENDED`**:hook handler 直接刪除 registry 檔,session 會從預設看板消失;看板上可見的 `ENDED` 主要來自零設定掃描超過活躍時間窗的紀錄。 + +## 零設定模式 + +沒有 hooks 時,RiNG 只能依本機紀錄、process 與閒置時間推算 `WORKING`、`IDLE`、`ENDED`。零設定掃描不會自行猜測 `WAITING`;精準等待狀態來自 hook payload、Stop 結尾提問偵測,或 Codex 裸權限事件的延遲判定。 diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..9a62ebb --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,99 @@ +site_name: RiNG +site_description: RiNG — Realtime Instance Notification Grid for active agent CLI sessions. +site_author: Wei Lee +site_url: https://lee-w.github.io/ring/ +copyright: Copyright © 2026 The authors + +nav: + - Home: index.md + - Full Guide: guide.md + - Session States: session-states.md + - Contributing: contributing.md + +theme: + name: material + language: zh-TW + features: + - announce.dismiss + - content.code.annotate + - content.code.copy + - content.code.select + - content.footnote.tooltips + - content.tabs.link + - content.tooltips + - navigation.footer + - navigation.indexes + - navigation.path + - navigation.sections + - navigation.top + - navigation.tracking + - search.highlight + palette: + - scheme: default + toggle: + icon: material/weather-sunny + name: 切換至深色模式 + - scheme: slate + toggle: + icon: material/weather-night + name: 切換至淺色模式 + +plugins: + - search + - i18n: + docs_structure: suffix + fallback_to_default: false + languages: + - locale: zh-TW + default: true + name: 台灣華語 + build: true + nav_translations: + Home: 首頁 + Full Guide: 完整使用手冊 + Session States: Session 狀態 + Contributing: 貢獻指南 + - locale: en + name: English + build: true + +markdown_extensions: + - abbr + - admonition + - attr_list + - def_list + - footnotes + - md_in_html + - toc: + permalink: true + - pymdownx.arithmatex: + generic: true + - pymdownx.betterem + - pymdownx.caret + - pymdownx.details + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.keys + - pymdownx.magiclink + - pymdownx.mark + - pymdownx.smartsymbols + - pymdownx.snippets: + base_path: + - . + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.tabbed: + alternate_style: true + combine_header_slug: true + - pymdownx.tasklist: + custom_checkbox: true + - pymdownx.tilde diff --git a/pyproject.toml b/pyproject.toml index de635ff..733eef8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ dependencies = ["rich>=13"] [project.urls] Homepage = "https://github.com/Lee-W/ring" Repository = "https://github.com/Lee-W/ring" +Documentation = "https://lee-w.github.io/ring/" [project.optional-dependencies] # live TUI 進階版用 Textual(建在 Rich 之上)。 @@ -58,6 +59,7 @@ test = [ "textual>=0.80", ] linters = ["ruff>=0.15.1", "mypy>=1.19.1", "prek>=0.3.3", "commitizen>=4.13.9"] +docs = ["mkdocs-material>=9,<10", "mkdocs-static-i18n>=1.3,<2"] [build-system] requires = ["uv_build >= 0.10.3, <0.12.0"] @@ -112,14 +114,20 @@ ci.env = { SKIP = "no-commit-to-branch" } setup-pre-commit.help = "Install pre-commit hooks" setup-pre-commit.cmd = "prek install" -i18n-extract.help = "從原始碼抽出可翻譯字串到 .pot(改字串後重抽。不要用 pybabel update:會塞空 msgstr 進 .po,弄壞 i18n 測試。新字串請手動把已翻譯條目加進各 .po)" -i18n-extract.cmd = "pybabel extract -F babel.cfg -o src/ring/locale/ring.pot src/ring" +"i18n:extract".help = "從原始碼抽出可翻譯字串到 .pot(改字串後重抽。不要用 pybabel update:會塞空 msgstr 進 .po,弄壞 i18n 測試。新字串請手動把已翻譯條目加進各 .po)" +"i18n:extract".cmd = "pybabel extract -F babel.cfg -o src/ring/locale/ring.pot src/ring" -i18n-check.help = "列出原始碼有、但某語言 .po 還沒翻譯的字串(純資訊,不失敗)。先跑 i18n-extract" -i18n-check.cmd = "python scripts/i18n_check.py" +"i18n:check".help = "列出原始碼有、但某語言 .po 還沒翻譯的字串(純資訊,不失敗)。先跑 i18n:extract" +"i18n:check".cmd = "python scripts/i18n_check.py" -i18n-compile.help = "把各語言的 .po 編成 .mo(改 .po 後要重編並 commit)" -i18n-compile.cmd = "pybabel compile -d src/ring/locale -D ring" +"i18n:compile".help = "把各語言的 .po 編成 .mo(改 .po 後要重編並 commit)" +"i18n:compile".cmd = "pybabel compile -d src/ring/locale -D ring" + +"docs:serve".help = "Serve the docs site locally with live reload" +"docs:serve".cmd = "mkdocs serve" + +"docs:build".help = "Build the docs site into site/" +"docs:build".cmd = "mkdocs build --strict" [tool.commitizen] name = "cz_conventional_commits" diff --git a/src/ring/config.py b/src/ring/config.py index 1707e26..3b0bf22 100644 --- a/src/ring/config.py +++ b/src/ring/config.py @@ -8,7 +8,7 @@ legend = true active_window_seconds = 21600 # 只看最近這段時間動過的 session(預設 6h) working_threshold_seconds = 90 # 多久沒動就從 🟢 工作中 變 🟡 閒置 - waiting_window_seconds = 1800 # 跑完停著升等你的時間窗上限(預設 30 分) + waiting_window_seconds = 1800 # 零設定掃描中,近期回合結束列收斂成 IDLE 的時間窗 codex_permission_wait_seconds = 10 # Codex 裸 PermissionRequest 後 hook 靜默超過這秒數 # → 看板判定真的停下來等核可(🔴 等你);0 = 關閉 detect_stop_questions = true # Stop 事件時,若最後一則 assistant 訊息「結尾」是純文字 @@ -68,7 +68,7 @@ class Config: legend: bool = True active_window_seconds: int = 6 * 60 * 60 working_threshold_seconds: int = 90 - waiting_window_seconds: int = 1800 # 跑完停著升等你的時間窗上限(預設 30 分) + waiting_window_seconds: int = 1800 # 零設定掃描中,近期回合結束列收斂成 IDLE 的時間窗 # Codex 的 hook 沒有「使用者已核可」事件也沒有心跳(0.144.4 實證):policy 自動放行時 # 下一個事件幾秒內就到;真的停下來等人時 hook 通道完全靜默。所以「最後一個事件是 # PermissionRequest 且已靜默超過這個門檻」就判定在等核可。0 = 關閉這個判定。 diff --git a/src/ring/registry.py b/src/ring/registry.py index ad6788f..3f5ebaa 100644 --- a/src/ring/registry.py +++ b/src/ring/registry.py @@ -54,7 +54,7 @@ _CFG = get_config() ACTIVE_WINDOW_SECONDS = _CFG.active_window_seconds # 只看最近這段時間動過的 session(預設 6h) WORKING_THRESHOLD_SECONDS = _CFG.working_threshold_seconds # 多久沒動 → 🟢 工作中 變 🟡 閒置 -WAITING_WINDOW_SECONDS = _CFG.waiting_window_seconds # IDLE 升 WAITING 的時間窗上限(預設 30 分) +WAITING_WINDOW_SECONDS = _CFG.waiting_window_seconds # 近期 end_turn scan row 收斂成 IDLE 的時間窗 # codex 裸 PermissionRequest 後 hook 靜默超過這秒數 → 判定真的停下來等核可(0 = 關閉) CODEX_PERMISSION_WAIT_SECONDS = _CFG.codex_permission_wait_seconds _SUBPROCESS_CACHE_TTL = 1.0 # ps / tmux 結果的短快取,省掉同一次刷新內的重複呼叫 diff --git a/uv.lock b/uv.lock index d07039c..22271bc 100644 --- a/uv.lock +++ b/uv.lock @@ -64,6 +64,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/77/f5/21d2de20e8b8b0408f0681956ca2c69f1320a3848ac50e6e7f39c6159675/babel-2.18.0-py3-none-any.whl", hash = "sha256:e2b422b277c2b9a9630c1d7903c2a00d0830c409c59ac8cae9081c92f1aeba35", size = 10196845, upload-time = "2026-02-01T12:30:53.445Z" }, ] +[[package]] +name = "backrefs" +version = "7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/a7dd63622beef68cc0d3c3c36d472e143dd95443d5ebf14cd1a5b4dfbf11/backrefs-7.0.tar.gz", hash = "sha256:4989bb9e1e99eb23647c7160ed51fb21d0b41b5d200f2d3017da41e023097e82", size = 7012453, upload-time = "2026-04-28T16:28:04.215Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/39/39a31d7eae729ea14ed10c3ccef79371197177b9355a86cb3525709e8502/backrefs-7.0-py310-none-any.whl", hash = "sha256:b57cd227ea556b0aed3dc9b8da4628db4eabc0402c6d7fcfc69283a93955f7e9", size = 380824, upload-time = "2026-04-28T16:27:55.647Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b5/9302644225ba7dfa934a2ff2b9c7bb85701313a90dddb3dfaf693fa5bae2/backrefs-7.0-py311-none-any.whl", hash = "sha256:a0fa7360c63509e9e077e174ef4e6d3c21c8db94189b9d957289ae6d794b9475", size = 392626, upload-time = "2026-04-28T16:27:57.42Z" }, + { url = "https://files.pythonhosted.org/packages/36/da/87912ddec6e06feffbaa3d7aa18fc6352bee2e8f1fee185d7d1690f8f4e8/backrefs-7.0-py312-none-any.whl", hash = "sha256:ca42ce6a49ace3d75684dfa9937f3373902a63284ecb385ce36d15e5dcb41c12", size = 398537, upload-time = "2026-04-28T16:27:58.913Z" }, + { url = "https://files.pythonhosted.org/packages/00/bb/90ba423612b6aa0adccc6b1874bcd4a9b44b660c0c16f346611e00f64ac3/backrefs-7.0-py313-none-any.whl", hash = "sha256:f2c52955d631b9e1ac4cd56209f0a3a946d592b98e7790e77699339ae01c102a", size = 400491, upload-time = "2026-04-28T16:28:00.928Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5c/fb93d3092640a24dfb7bd7727a24016d7c01774ca013e60efd3f683c8002/backrefs-7.0-py314-none-any.whl", hash = "sha256:a6448b28180e3ca01134c9cf09dcebafad8531072e09903c5451748a05f24bc9", size = 412349, upload-time = "2026-04-28T16:28:02.412Z" }, +] + +[[package]] +name = "certifi" +version = "2026.6.17" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.7" @@ -121,6 +143,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, ] +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -252,6 +286,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, ] +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + [[package]] name = "iniconfig" version = "2.3.0" @@ -332,6 +387,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e", size = 19878, upload-time = "2026-03-01T07:48:46.098Z" }, ] +[[package]] +name = "markdown" +version = "3.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, +] + [[package]] name = "markdown-it-py" version = "4.2.0" @@ -422,6 +486,96 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/25/b3cccb187655b9393572bde9b09261d267c3bf2f2cdabe347673be5976a6/mkdocs_get_deps-0.2.2.tar.gz", hash = "sha256:8ee8d5f316cdbbb2834bc1df6e69c08fe769a83e040060de26d3c19fad3599a1", size = 11047, upload-time = "2026-03-10T02:46:33.632Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/29/744136411e785c4b0b744d5413e56555265939ab3a104c6a4b719dad33fd/mkdocs_get_deps-0.2.2-py3-none-any.whl", hash = "sha256:e7878cbeac04860b8b5e0ca31d3abad3df9411a75a32cde82f8e44b6c16ff650", size = 9555, upload-time = "2026-03-10T02:46:32.256Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/45/29/6d2bcf41ae40802c4beda2432396fff97b8456fb496371d1bc7aad6512ec/mkdocs_material-9.7.6.tar.gz", hash = "sha256:00bdde50574f776d328b1862fe65daeaf581ec309bd150f7bff345a098c64a69", size = 4097959, upload-time = "2026-03-19T15:41:58.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/01/bc663630c510822c95c47a66af9fa7a443c295b47d5f041e5e6ae62ef659/mkdocs_material-9.7.6-py3-none-any.whl", hash = "sha256:71b84353921b8ea1ba84fe11c50912cc512da8fe0881038fcc9a0761c0e635ba", size = 9305470, upload-time = "2026-03-19T15:41:55.217Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mkdocs-static-i18n" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/f9/51e2ffda9c7210bc35a24f3717b08c052cd4b728dfa87f901c00d8005259/mkdocs_static_i18n-1.3.1.tar.gz", hash = "sha256:a6125ea7db6cc1a900d76a967f262535af09831160a93c56d7f0d522a79b5faf", size = 1371325, upload-time = "2026-02-20T10:42:41.835Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/0b/43ff4afb6b438d47718b1959a22075ed95d8460d8c47381878b37a40de63/mkdocs_static_i18n-1.3.1-py3-none-any.whl", hash = "sha256:4036e24795a150c9c4d4b001ed24a43aec01335f76188dbe5a5d8fb4a27eba65", size = 21853, upload-time = "2026-02-20T10:42:40.551Z" }, +] + [[package]] name = "mypy" version = "2.1.0" @@ -477,6 +631,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + [[package]] name = "pastel" version = "0.2.1" @@ -571,6 +734,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pymdown-extensions" +version = "11.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/a9/5f0c535ba3b08fe09270c16808e053a968868242ecbd5676d4e3a488bf28/pymdown_extensions-11.0.1.tar.gz", hash = "sha256:dd2905ae6fc5b75582fafb139a1266ffc754705efa902aa50067fa7ff4f94ec0", size = 857113, upload-time = "2026-07-02T17:59:22.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/54/da572c98c0b77626a91b5d3b89f0231d8bff5125c225420908632f8b342d/pymdown_extensions-11.0.1-py3-none-any.whl", hash = "sha256:db3943a62bab7e03af1364f0c4083e64b91fb097675a4b6cceccfbe9a77e5eb2", size = 269455, upload-time = "2026-07-02T17:59:21.271Z" }, +] + [[package]] name = "pytest" version = "9.1.1" @@ -626,6 +802,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + [[package]] name = "pyyaml" version = "6.0.3" @@ -662,6 +850,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/2e/79c822141bfd05a853236b504869ebc6b70159afc570e1d5a20641782eaa/pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff", size = 5737, upload-time = "2025-05-13T15:24:01.64Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/11/432f32f8097b03e3cd5fe57e88efb685d964e2e5178a48ed61e841f7fdce/pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04", size = 4722, upload-time = "2025-05-13T15:23:59.629Z" }, +] + [[package]] name = "questionary" version = "2.1.1" @@ -674,6 +874,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" }, ] +[[package]] +name = "requests" +version = "2.34.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, +] + [[package]] name = "rich" version = "15.0.0" @@ -718,6 +933,10 @@ dev = [ { name = "ruff" }, { name = "textual" }, ] +docs = [ + { name = "mkdocs-material" }, + { name = "mkdocs-static-i18n" }, +] linters = [ { name = "commitizen" }, { name = "mypy" }, @@ -757,6 +976,10 @@ dev = [ { name = "ruff", specifier = ">=0.15.1" }, { name = "textual", specifier = ">=0.80" }, ] +docs = [ + { name = "mkdocs-material", specifier = ">=9,<10" }, + { name = "mkdocs-static-i18n", specifier = ">=1.3,<2" }, +] linters = [ { name = "commitizen", specifier = ">=4.13.9" }, { name = "mypy", specifier = ">=1.19.1" }, @@ -796,6 +1019,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/29/4c/67bb45e41609eb4726f1bfeb59e083cf91d14c696d4bd14c234a980be93d/ruff-0.15.18-py3-none-win_arm64.whl", hash = "sha256:b2c9257fcbd4a3e5b977a1904e6facca016bafe2edc17df24db67cfaee03b4e4", size = 11329958, upload-time = "2026-06-18T18:25:43.686Z" }, ] +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + [[package]] name = "termcolor" version = "3.3.0" @@ -849,6 +1081,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/61/73/d21edf5b204d1467e06500080a50f79d49ef2b997c79123a536d4a17d97c/uc_micro_py-2.0.0-py3-none-any.whl", hash = "sha256:3603a3859af53e5a39bc7677713c78ea6589ff188d70f4fee165db88e22b242c", size = 6383, upload-time = "2026-03-01T06:31:26.257Z" }, ] +[[package]] +name = "urllib3" +version = "2.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + [[package]] name = "wcwidth" version = "0.8.1"