ring-cli 0.12.0__tar.gz → 0.14.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ring_cli-0.14.0/PKG-INFO +96 -0
- ring_cli-0.14.0/README.en.md +70 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/pyproject.toml +15 -7
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/cli.py +39 -0
- ring_cli-0.14.0/src/ring/commands/quiet.py +54 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/config.py +47 -3
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/hook.py +99 -11
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/hook_protocol.py +20 -11
- ring_cli-0.14.0/src/ring/locale/en/LC_MESSAGES/ring.mo +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/locale/en/LC_MESSAGES/ring.po +83 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/locale/ring.pot +195 -103
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/notify/__init__.py +61 -1
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/notify/base.py +18 -1
- ring_cli-0.14.0/src/ring/notify_queue.py +332 -0
- ring_cli-0.14.0/src/ring/question_detect.py +93 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/registry.py +82 -22
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/transcript.py +20 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/tui.py +59 -6
- ring_cli-0.12.0/PKG-INFO +0 -490
- ring_cli-0.12.0/README.en.md +0 -465
- ring_cli-0.12.0/src/ring/locale/en/LC_MESSAGES/ring.mo +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/LICENSE +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/__init__.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/__main__.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/commands/__init__.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/commands/_args.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/commands/completion.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/commands/digest.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/commands/doctor.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/commands/focus.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/commands/gc.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/commands/hook.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/commands/stats.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/focus/__init__.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/focus/applescript.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/focus/base.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/focus/iterm2.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/focus/linux_wm.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/focus/neovim.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/focus/terminal.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/focus/tmux.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/gc.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/i18n.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/ipc.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/labels.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/notify/command.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/notify/notify_send.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/notify/ntfy.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/notify/osascript_notifier.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/notify/terminal_notifier.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/notify/webhook.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/osascript.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/payload_log.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/permission.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/plugins.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/sources/__init__.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/sources/base.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/sources/claude_code.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/sources/codex.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/sources/hook_registry.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/sources/local_llm.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/stats.py +0 -0
- {ring_cli-0.12.0 → ring_cli-0.14.0}/src/ring/watcher.py +0 -0
ring_cli-0.14.0/PKG-INFO
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ring-cli
|
|
3
|
+
Version: 0.14.0
|
|
4
|
+
Summary: RiNG — Realtime Instance Notification Grid for active agent CLI sessions.
|
|
5
|
+
Keywords: claude-code,codex,ollama,llama-cpp,tui,dashboard,session,monitor,rich
|
|
6
|
+
Author: Wei Lee
|
|
7
|
+
Author-email: Wei Lee <weilee.rx@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: MacOS
|
|
14
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Software Development
|
|
17
|
+
Classifier: Topic :: Utilities
|
|
18
|
+
Requires-Dist: rich>=13
|
|
19
|
+
Requires-Dist: textual>=0.80 ; extra == 'tui'
|
|
20
|
+
Requires-Python: >=3.13
|
|
21
|
+
Project-URL: Homepage, https://github.com/Lee-W/ring
|
|
22
|
+
Project-URL: Repository, https://github.com/Lee-W/ring
|
|
23
|
+
Project-URL: Documentation, https://lee-w.github.io/ring/
|
|
24
|
+
Provides-Extra: tui
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# RiNG 🎤
|
|
28
|
+
|
|
29
|
+
[台灣華語](README.md) · **English**
|
|
30
|
+
|
|
31
|
+
[](https://pypi.org/project/ring-cli/)
|
|
32
|
+
[](https://pypi.org/project/ring-cli/)
|
|
33
|
+
[](LICENSE)
|
|
34
|
+
|
|
35
|
+
> **R**ealtime **I**nstance **N**otification **G**rid
|
|
36
|
+
> — one local board for all active agent-CLI sessions.
|
|
37
|
+
|
|
38
|
+
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.
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
🎤 RiNG — 3 sessions on stage · 2 agent processes running
|
|
42
|
+
|
|
43
|
+
🔴 maigo 12s → waiting for permission
|
|
44
|
+
🟢 pelican-osm 3s → Edit
|
|
45
|
+
🟡 commitizen 8m turn finished, idle
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## What It Does
|
|
49
|
+
|
|
50
|
+
- **One board for session state**: built-in sources for Claude Code, Codex, Ollama, and llama.cpp.
|
|
51
|
+
- **Puts waiting sessions first**: shows what 🔴 sessions need and sends desktop or phone notifications.
|
|
52
|
+
- **Returns you to the right terminal**: focuses tmux, iTerm2, Terminal.app, Neovim terminals, or Linux X11 windows from the TUI.
|
|
53
|
+
- **Avoids an extra context switch**: reply to permission requests in supported terminals and assign session names from the board.
|
|
54
|
+
- **Fits existing workflows**: status-bar output, JSON, a provider-neutral hook, and plugin extension points.
|
|
55
|
+
|
|
56
|
+
## Get Started in Three Steps
|
|
57
|
+
|
|
58
|
+
Requires Python 3.13+:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
uv tool install 'ring-cli[tui]'
|
|
62
|
+
ring --watch
|
|
63
|
+
ring install-hooks
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
1. `ring --watch` opens the interactive board. Select a session and press `Enter` to return to its terminal.
|
|
67
|
+
2. `ring install-hooks` installs Claude Code and Codex hooks for precise 🔴 waiting states and system notifications.
|
|
68
|
+
3. Restart agent sessions after installing hooks. RiNG can then notify you even while its board is closed.
|
|
69
|
+
|
|
70
|
+
Use `pipx install 'ring-cli[tui]'` if you prefer pipx. Run `ring` for a one-shot snapshot.
|
|
71
|
+
|
|
72
|
+
## Common Operations
|
|
73
|
+
|
|
74
|
+
| Operation | Purpose |
|
|
75
|
+
|-----------|---------|
|
|
76
|
+
| `ring` | Print a snapshot of every current session |
|
|
77
|
+
| `ring --watch` | Open the continuously updating interactive TUI |
|
|
78
|
+
| `Enter` / `Space` | Return to the selected session's terminal |
|
|
79
|
+
| `p` | Reply to the selected session's permission request in place |
|
|
80
|
+
| `n` | Name the selected session |
|
|
81
|
+
| `ring doctor` | Check hooks, notifications, and terminal focus support |
|
|
82
|
+
| `ring --format oneline` | Produce a summary for tmux, SwiftBar, or waybar |
|
|
83
|
+
|
|
84
|
+
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.
|
|
85
|
+
|
|
86
|
+
## Learn More
|
|
87
|
+
|
|
88
|
+
- [Full guide](https://lee-w.github.io/ring/guide/): commands, TUI controls, hooks, notifications, configuration, extensions, and privacy
|
|
89
|
+
- [Session states](https://lee-w.github.io/ring/session-states/): how RiNG derives 🔴/🟢/🟡/⚫
|
|
90
|
+
- [Contributing guide](CONTRIBUTING.md): development setup, tests, and PR checklist
|
|
91
|
+
|
|
92
|
+
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.
|
|
93
|
+
|
|
94
|
+
## License
|
|
95
|
+
|
|
96
|
+
MIT
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# RiNG 🎤
|
|
2
|
+
|
|
3
|
+
[台灣華語](README.md) · **English**
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/ring-cli/)
|
|
6
|
+
[](https://pypi.org/project/ring-cli/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
> **R**ealtime **I**nstance **N**otification **G**rid
|
|
10
|
+
> — one local board for all active agent-CLI sessions.
|
|
11
|
+
|
|
12
|
+
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.
|
|
13
|
+
|
|
14
|
+
```text
|
|
15
|
+
🎤 RiNG — 3 sessions on stage · 2 agent processes running
|
|
16
|
+
|
|
17
|
+
🔴 maigo 12s → waiting for permission
|
|
18
|
+
🟢 pelican-osm 3s → Edit
|
|
19
|
+
🟡 commitizen 8m turn finished, idle
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## What It Does
|
|
23
|
+
|
|
24
|
+
- **One board for session state**: built-in sources for Claude Code, Codex, Ollama, and llama.cpp.
|
|
25
|
+
- **Puts waiting sessions first**: shows what 🔴 sessions need and sends desktop or phone notifications.
|
|
26
|
+
- **Returns you to the right terminal**: focuses tmux, iTerm2, Terminal.app, Neovim terminals, or Linux X11 windows from the TUI.
|
|
27
|
+
- **Avoids an extra context switch**: reply to permission requests in supported terminals and assign session names from the board.
|
|
28
|
+
- **Fits existing workflows**: status-bar output, JSON, a provider-neutral hook, and plugin extension points.
|
|
29
|
+
|
|
30
|
+
## Get Started in Three Steps
|
|
31
|
+
|
|
32
|
+
Requires Python 3.13+:
|
|
33
|
+
|
|
34
|
+
```sh
|
|
35
|
+
uv tool install 'ring-cli[tui]'
|
|
36
|
+
ring --watch
|
|
37
|
+
ring install-hooks
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
1. `ring --watch` opens the interactive board. Select a session and press `Enter` to return to its terminal.
|
|
41
|
+
2. `ring install-hooks` installs Claude Code and Codex hooks for precise 🔴 waiting states and system notifications.
|
|
42
|
+
3. Restart agent sessions after installing hooks. RiNG can then notify you even while its board is closed.
|
|
43
|
+
|
|
44
|
+
Use `pipx install 'ring-cli[tui]'` if you prefer pipx. Run `ring` for a one-shot snapshot.
|
|
45
|
+
|
|
46
|
+
## Common Operations
|
|
47
|
+
|
|
48
|
+
| Operation | Purpose |
|
|
49
|
+
|-----------|---------|
|
|
50
|
+
| `ring` | Print a snapshot of every current session |
|
|
51
|
+
| `ring --watch` | Open the continuously updating interactive TUI |
|
|
52
|
+
| `Enter` / `Space` | Return to the selected session's terminal |
|
|
53
|
+
| `p` | Reply to the selected session's permission request in place |
|
|
54
|
+
| `n` | Name the selected session |
|
|
55
|
+
| `ring doctor` | Check hooks, notifications, and terminal focus support |
|
|
56
|
+
| `ring --format oneline` | Produce a summary for tmux, SwiftBar, or waybar |
|
|
57
|
+
|
|
58
|
+
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.
|
|
59
|
+
|
|
60
|
+
## Learn More
|
|
61
|
+
|
|
62
|
+
- [Full guide](https://lee-w.github.io/ring/guide/): commands, TUI controls, hooks, notifications, configuration, extensions, and privacy
|
|
63
|
+
- [Session states](https://lee-w.github.io/ring/session-states/): how RiNG derives 🔴/🟢/🟡/⚫
|
|
64
|
+
- [Contributing guide](CONTRIBUTING.md): development setup, tests, and PR checklist
|
|
65
|
+
|
|
66
|
+
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.
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# import 的 module(見 [tool.uv.build-backend] module-name)與 CLI 指令仍是 `ring`。
|
|
3
3
|
[project]
|
|
4
4
|
name = "ring-cli"
|
|
5
|
-
version = "0.
|
|
5
|
+
version = "0.14.0"
|
|
6
6
|
description = "RiNG — Realtime Instance Notification Grid for active agent CLI sessions."
|
|
7
7
|
readme = "README.en.md"
|
|
8
8
|
requires-python = ">=3.13"
|
|
@@ -35,6 +35,7 @@ dependencies = ["rich>=13"]
|
|
|
35
35
|
[project.urls]
|
|
36
36
|
Homepage = "https://github.com/Lee-W/ring"
|
|
37
37
|
Repository = "https://github.com/Lee-W/ring"
|
|
38
|
+
Documentation = "https://lee-w.github.io/ring/"
|
|
38
39
|
|
|
39
40
|
[project.optional-dependencies]
|
|
40
41
|
# live TUI 進階版用 Textual(建在 Rich 之上)。
|
|
@@ -58,6 +59,7 @@ test = [
|
|
|
58
59
|
"textual>=0.80",
|
|
59
60
|
]
|
|
60
61
|
linters = ["ruff>=0.15.1", "mypy>=1.19.1", "prek>=0.3.3", "commitizen>=4.13.9"]
|
|
62
|
+
docs = ["mkdocs-material>=9,<10", "mkdocs-static-i18n>=1.3,<2"]
|
|
61
63
|
|
|
62
64
|
[build-system]
|
|
63
65
|
requires = ["uv_build >= 0.10.3, <0.12.0"]
|
|
@@ -112,14 +114,20 @@ ci.env = { SKIP = "no-commit-to-branch" }
|
|
|
112
114
|
setup-pre-commit.help = "Install pre-commit hooks"
|
|
113
115
|
setup-pre-commit.cmd = "prek install"
|
|
114
116
|
|
|
115
|
-
i18n
|
|
116
|
-
i18n
|
|
117
|
+
"i18n:extract".help = "從原始碼抽出可翻譯字串到 .pot(改字串後重抽。不要用 pybabel update:會塞空 msgstr 進 .po,弄壞 i18n 測試。新字串請手動把已翻譯條目加進各 .po)"
|
|
118
|
+
"i18n:extract".cmd = "pybabel extract -F babel.cfg -o src/ring/locale/ring.pot src/ring"
|
|
117
119
|
|
|
118
|
-
i18n
|
|
119
|
-
i18n
|
|
120
|
+
"i18n:check".help = "列出原始碼有、但某語言 .po 還沒翻譯的字串(純資訊,不失敗)。先跑 i18n:extract"
|
|
121
|
+
"i18n:check".cmd = "python scripts/i18n_check.py"
|
|
120
122
|
|
|
121
|
-
i18n
|
|
122
|
-
i18n
|
|
123
|
+
"i18n:compile".help = "把各語言的 .po 編成 .mo(改 .po 後要重編並 commit)"
|
|
124
|
+
"i18n:compile".cmd = "pybabel compile -d src/ring/locale -D ring"
|
|
125
|
+
|
|
126
|
+
"docs:serve".help = "Serve the docs site locally with live reload"
|
|
127
|
+
"docs:serve".cmd = "mkdocs serve"
|
|
128
|
+
|
|
129
|
+
"docs:build".help = "Build the docs site into site/"
|
|
130
|
+
"docs:build".cmd = "mkdocs build --strict"
|
|
123
131
|
|
|
124
132
|
[tool.commitizen]
|
|
125
133
|
name = "cz_conventional_commits"
|
|
@@ -25,6 +25,7 @@ from ring.commands.doctor import run_doctor
|
|
|
25
25
|
from ring.commands.focus import run_focus
|
|
26
26
|
from ring.commands.gc import run_gc
|
|
27
27
|
from ring.commands.hook import run_hook_command, run_install_hooks, run_remove_hooks
|
|
28
|
+
from ring.commands.quiet import run_quiet
|
|
28
29
|
from ring.commands.stats import run_stats
|
|
29
30
|
from ring.config import CONFIG_PATH as CONFIG_PATH
|
|
30
31
|
from ring.config import Config as Config
|
|
@@ -34,6 +35,7 @@ from ring.config import set_value as set_value
|
|
|
34
35
|
from ring.i18n import gettext as _
|
|
35
36
|
from ring.i18n import ngettext, set_lang
|
|
36
37
|
from ring.labels import load_labels
|
|
38
|
+
from ring.notify_queue import flush_if_due
|
|
37
39
|
from ring.plugins import load_plugins
|
|
38
40
|
from ring.registry import Session, Status, running_agent_pids
|
|
39
41
|
from ring.sources import discover_sessions
|
|
@@ -399,14 +401,32 @@ def run_config(args: list[str]) -> int:
|
|
|
399
401
|
return 2
|
|
400
402
|
|
|
401
403
|
|
|
404
|
+
def _watch_flush_if_due() -> None:
|
|
405
|
+
"""headless watch 輪詢時的懶惰 flush 觸發點(同 hook.py / tui.py 既有呼叫慣例)。
|
|
406
|
+
|
|
407
|
+
quiet active 或仍在 debounce 視窗內時,``flush_if_due`` 內部本來就會 skip(跟
|
|
408
|
+
``test_run_hook_does_not_flush_while_quiet_active`` 同機制),這裡不需另外判斷。
|
|
409
|
+
失敗安靜吞掉,不影響看板本身。
|
|
410
|
+
"""
|
|
411
|
+
try:
|
|
412
|
+
flush_if_due()
|
|
413
|
+
except Exception:
|
|
414
|
+
pass
|
|
415
|
+
|
|
416
|
+
|
|
402
417
|
def watch(interval: float, count: int, show_all: bool, show_legend: bool) -> int:
|
|
403
418
|
# 系統通知由 ``ring hook`` 在 session 轉 🔴 等你的當下就地發出(見 hook._ring_waiting_now);
|
|
404
419
|
# watch 只負責顯示看板,不再輪詢發通知——這樣關掉看板也照樣 ring 你。
|
|
420
|
+
# 例外:codex 核可等待是讀取側的靜默逾時判定,沒有 hook 事件可發通知,由 TUI 的
|
|
421
|
+
# 提醒排程器代發(tui._ring_on_waiting_alerts);headless watch 仍不發,是已知限制。
|
|
422
|
+
# debounce/quiet 合流 queue 的懶惰 flush:headless watch 沒有 hook 事件也沒有 TUI 輪詢
|
|
423
|
+
# 可以觸發,每輪自己補上一次,否則純 headless 使用者開了 quiet/debounce 通知會無限期卡住。
|
|
405
424
|
frames = 0
|
|
406
425
|
footer_text = _("每 {interval}s 刷新 · Ctrl-C 離場", interval=int(interval))
|
|
407
426
|
if not HAVE_RICH:
|
|
408
427
|
try:
|
|
409
428
|
while True:
|
|
429
|
+
_watch_flush_if_due()
|
|
410
430
|
sys.stdout.write("\033[2J\033[H")
|
|
411
431
|
sessions = board(show_all)
|
|
412
432
|
print(_render_plain(sessions, show_legend, show_tool_column(sessions)))
|
|
@@ -423,6 +443,7 @@ def watch(interval: float, count: int, show_all: bool, show_legend: bool) -> int
|
|
|
423
443
|
try:
|
|
424
444
|
with Live(console=console, screen=True, auto_refresh=False) as live:
|
|
425
445
|
while True:
|
|
446
|
+
_watch_flush_if_due()
|
|
426
447
|
sessions = board(show_all)
|
|
427
448
|
body = _rich_renderable(sessions, show_legend, show_tool_column(sessions))
|
|
428
449
|
live.update(Group(body, Text(f"\n{footer_text}", style=_MUTED)), refresh=True)
|
|
@@ -455,6 +476,10 @@ commands:
|
|
|
455
476
|
config 顯示設定檔位置與目前生效的設定
|
|
456
477
|
config get KEY 讀單一設定的目前值
|
|
457
478
|
config set KEY VALUE 寫入單一設定(會重寫設定檔,不保留註解)
|
|
479
|
+
quiet 顯示 quiet(暫時全域靜音)現況
|
|
480
|
+
quiet on 開啟 quiet(手動解除前一直靜音)
|
|
481
|
+
quiet off 解除 quiet(立即補發合流的彙總通知)
|
|
482
|
+
quiet DURATION 開啟 quiet 一段時間(例如 30m、1h),到期自動解除
|
|
458
483
|
focus SESSION_ID 聚焦指定 session;TUI 在跑時會回到 RiNG 並選中該列
|
|
459
484
|
gc [--dry-run] 清理 RiNG 自己的 stale 狀態檔
|
|
460
485
|
doctor 顯示環境診斷(唯讀)——hook、通知、focuser、維護提示
|
|
@@ -491,6 +516,17 @@ def _subcommand_help(name: str) -> str:
|
|
|
491
516
|
不帶參數:顯示設定檔位置(~/.config/ring/config.toml)與目前生效的所有設定。
|
|
492
517
|
get KEY 印出單一設定的目前值(colors 子鍵用 colors.<name>)。
|
|
493
518
|
set KEY VALUE 寫入單一設定。注意:會重寫整個設定檔,原有註解不會保留。
|
|
519
|
+
"""
|
|
520
|
+
),
|
|
521
|
+
"quiet": _(
|
|
522
|
+
"""usage: ring quiet [on | off | DURATION]
|
|
523
|
+
|
|
524
|
+
暫時全域靜音:靜音期間所有轉 🔴 等你的通知都先進 queue,解除時懶惰補發一則彙總。
|
|
525
|
+
|
|
526
|
+
不帶參數:顯示目前 quiet 現況(開/關、剩餘時間)。
|
|
527
|
+
on 開啟 quiet,手動解除前一直靜音。
|
|
528
|
+
off 解除 quiet,並立即補發合流的彙總通知。
|
|
529
|
+
DURATION 開啟 quiet 一段時間(例如 30m、1h),到期自動解除。
|
|
494
530
|
"""
|
|
495
531
|
),
|
|
496
532
|
"focus": _(
|
|
@@ -567,6 +603,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
567
603
|
"digest",
|
|
568
604
|
"stats",
|
|
569
605
|
"completion",
|
|
606
|
+
"quiet",
|
|
570
607
|
}
|
|
571
608
|
and any(arg in {"-h", "--help"} for arg in raw[1:])
|
|
572
609
|
):
|
|
@@ -581,6 +618,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
581
618
|
return run_remove_hooks(raw[1:])
|
|
582
619
|
if raw and raw[0] == "config":
|
|
583
620
|
return run_config(raw[1:])
|
|
621
|
+
if raw and raw[0] == "quiet":
|
|
622
|
+
return run_quiet(raw[1:])
|
|
584
623
|
if raw and raw[0] == "gc":
|
|
585
624
|
return run_gc(raw[1:])
|
|
586
625
|
if raw and raw[0] == "doctor":
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""``ring quiet`` command handler:暫時全域靜音(跟 debounce 共用同一份 queue/flush 機制)。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
from ring.commands._args import strip_lang
|
|
9
|
+
from ring.gc import parse_duration
|
|
10
|
+
from ring.i18n import gettext as _
|
|
11
|
+
from ring.notify_queue import clear_quiet, flush_if_due, format_remaining, quiet_active, quiet_remaining, set_quiet
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _print_status(now: float) -> None:
|
|
15
|
+
if not quiet_active(now):
|
|
16
|
+
print(_("🔈 quiet:目前關閉"))
|
|
17
|
+
return
|
|
18
|
+
remaining = quiet_remaining(now)
|
|
19
|
+
if remaining is None:
|
|
20
|
+
print(_("🔇 quiet:開啟中(手動解除前一直靜音)"))
|
|
21
|
+
else:
|
|
22
|
+
print(_("🔇 quiet:開啟中,剩 {remaining}", remaining=format_remaining(remaining)))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def run_quiet(args: list[str]) -> int:
|
|
26
|
+
"""不帶參數→顯示現況;``on``→無限靜音;``off``→解除並立即 flush;``<duration>``→限時靜音。"""
|
|
27
|
+
args = strip_lang(args)
|
|
28
|
+
now = time.time()
|
|
29
|
+
|
|
30
|
+
if not args:
|
|
31
|
+
_print_status(now)
|
|
32
|
+
return 0
|
|
33
|
+
|
|
34
|
+
action = args[0]
|
|
35
|
+
if action == "on":
|
|
36
|
+
set_quiet(None)
|
|
37
|
+
print(_("🔇 quiet 已開啟(手動解除前一直靜音)"))
|
|
38
|
+
return 0
|
|
39
|
+
|
|
40
|
+
if action == "off":
|
|
41
|
+
clear_quiet()
|
|
42
|
+
flush_if_due(force=True)
|
|
43
|
+
print(_("🔈 quiet 已解除"))
|
|
44
|
+
return 0
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
seconds = parse_duration(action)
|
|
48
|
+
except ValueError:
|
|
49
|
+
print(_("無效的 duration:{value}(例如 30m、1h)", value=action), file=sys.stderr)
|
|
50
|
+
return 2
|
|
51
|
+
|
|
52
|
+
set_quiet(now + seconds)
|
|
53
|
+
print(_("🔇 quiet 已開啟,{duration} 後自動解除", duration=action))
|
|
54
|
+
return 0
|
|
@@ -8,7 +8,12 @@
|
|
|
8
8
|
legend = true
|
|
9
9
|
active_window_seconds = 21600 # 只看最近這段時間動過的 session(預設 6h)
|
|
10
10
|
working_threshold_seconds = 90 # 多久沒動就從 🟢 工作中 變 🟡 閒置
|
|
11
|
-
waiting_window_seconds = 1800 #
|
|
11
|
+
waiting_window_seconds = 1800 # 零設定掃描中,近期回合結束列收斂成 IDLE 的時間窗
|
|
12
|
+
codex_permission_wait_seconds = 10 # Codex 裸 PermissionRequest 後 hook 靜默超過這秒數
|
|
13
|
+
# → 看板判定真的停下來等核可(🔴 等你);0 = 關閉
|
|
14
|
+
detect_stop_questions = true # Stop 事件時,若最後一則 assistant 訊息「結尾」是純文字
|
|
15
|
+
# 提問(沒用 AskUserQuestion、沒有權限請求)→ 🔴 等你
|
|
16
|
+
# (waiting_kind="question");關閉則 Stop 一律 🟡
|
|
12
17
|
notify_sound = true # 系統通知帶聲音
|
|
13
18
|
notify_sound_name = "Glass" # macOS / terminal-notifier sound name
|
|
14
19
|
notify_ignore_dnd = false # terminal-notifier 是否加 -ignoreDnD(穿透勿擾 / Focus)
|
|
@@ -22,6 +27,10 @@
|
|
|
22
27
|
notify_repeat_max = 3 # 重複提醒上限;0 = 不限
|
|
23
28
|
waiting_cooldown_seconds = 180 # session 離開 WAITING 又轉回時,距上次提醒未滿這段時間就
|
|
24
29
|
# 不再當「新轉入」立即提醒(防翻轉轟炸);0 = 關閉(現行為)
|
|
30
|
+
notify_debounce_seconds = 0 # 多個 session 同時轉 🔴 等你時的通知合流窗口(秒):窗口內
|
|
31
|
+
# 第一批立即發(leading edge),之後的合併成一則彙總,由下個
|
|
32
|
+
# hook 事件 / TUI 輪詢 / quiet 解除懶惰 flush;0 = 關閉(現行為,
|
|
33
|
+
# 向後相容,每次轉入都各自發)
|
|
25
34
|
notify_ntfy_url = "https://ntfy.sh/my-topic" # 設了才啟用 ntfy 後端(推到手機)
|
|
26
35
|
notify_webhook_url = "https://example.com/hook" # 設了才啟用 webhook 後端(JSON POST)
|
|
27
36
|
notify_also = ["ntfy"] # 主後端之外「加發」的後端(例如桌面通知+手機各一份)
|
|
@@ -35,12 +44,15 @@
|
|
|
35
44
|
|
|
36
45
|
from __future__ import annotations
|
|
37
46
|
|
|
47
|
+
import sys
|
|
38
48
|
import tomllib
|
|
39
49
|
from collections.abc import Callable
|
|
40
|
-
from dataclasses import dataclass, field
|
|
50
|
+
from dataclasses import dataclass, field, fields
|
|
41
51
|
from functools import lru_cache
|
|
42
52
|
from pathlib import Path
|
|
43
53
|
|
|
54
|
+
from ring.i18n import gettext as _
|
|
55
|
+
|
|
44
56
|
CONFIG_PATH = Path.home() / ".config" / "ring" / "config.toml"
|
|
45
57
|
|
|
46
58
|
# Rich 樣式字串。深淺底都看得到(避開 dim / ANSI blue)。config 的 [colors] 可逐項覆寫。
|
|
@@ -63,7 +75,14 @@ class Config:
|
|
|
63
75
|
legend: bool = True
|
|
64
76
|
active_window_seconds: int = 6 * 60 * 60
|
|
65
77
|
working_threshold_seconds: int = 90
|
|
66
|
-
waiting_window_seconds: int = 1800 #
|
|
78
|
+
waiting_window_seconds: int = 1800 # 零設定掃描中,近期回合結束列收斂成 IDLE 的時間窗
|
|
79
|
+
# Codex 的 hook 沒有「使用者已核可」事件也沒有心跳(0.144.4 實證):policy 自動放行時
|
|
80
|
+
# 下一個事件幾秒內就到;真的停下來等人時 hook 通道完全靜默。所以「最後一個事件是
|
|
81
|
+
# PermissionRequest 且已靜默超過這個門檻」就判定在等核可。0 = 關閉這個判定。
|
|
82
|
+
codex_permission_wait_seconds: int = 10
|
|
83
|
+
# Stop 事件時,若最後一則 assistant 訊息結尾是純文字提問(見 question_detect.py)
|
|
84
|
+
# → 升級成 🔴 等你(waiting_kind="question")。預設開;關閉則 Stop 一律維持 🟡。
|
|
85
|
+
detect_stop_questions: bool = True
|
|
67
86
|
notify_sound: bool = True
|
|
68
87
|
notify_sound_name: str = "Glass"
|
|
69
88
|
notify_ignore_dnd: bool = False
|
|
@@ -76,6 +95,7 @@ class Config:
|
|
|
76
95
|
notify_repeat_seconds: tuple[int, ...] = (30, 120, 300)
|
|
77
96
|
notify_repeat_max: int = 3 # 0 = 不限
|
|
78
97
|
waiting_cooldown_seconds: int = 180 # 離開 WAITING 又轉回時的提醒冷卻期;0 = 關閉(現行為)
|
|
98
|
+
notify_debounce_seconds: int = 0 # 跨 session 通知合流窗口(秒);0 = 關閉(現行為,向後相容)
|
|
79
99
|
notify_ntfy_url: str = "" # 完整 ntfy topic URL;空=ntfy 後端不可用
|
|
80
100
|
notify_webhook_url: str = "" # webhook URL;空=webhook 後端不可用
|
|
81
101
|
notify_also: tuple[str, ...] = () # 主後端之外加發的後端名(如 ["ntfy"])
|
|
@@ -85,6 +105,21 @@ class Config:
|
|
|
85
105
|
colors: dict[str, str] = field(default_factory=lambda: dict(_DEFAULT_COLORS))
|
|
86
106
|
|
|
87
107
|
|
|
108
|
+
# config.toml 頂層合法鍵(含巢狀 table 名,如 "colors")——都是 Config dataclass 的欄位名。
|
|
109
|
+
_KNOWN_KEYS = {f.name for f in fields(Config)}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _warn_unknown_keys(raw: dict[str, object], path: Path) -> None:
|
|
113
|
+
"""未知的頂層鍵(多半是打錯字)→ 印一次警告到 stderr;只警告,絕不 raise、不影響載入。"""
|
|
114
|
+
unknown = sorted(k for k in raw if k not in _KNOWN_KEYS)
|
|
115
|
+
if not unknown:
|
|
116
|
+
return
|
|
117
|
+
print(
|
|
118
|
+
_("⚠️ {path} 有未知的設定鍵(可能打錯字,已忽略):{keys}", path=path, keys=", ".join(unknown)),
|
|
119
|
+
file=sys.stderr,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
88
123
|
def _as_int(v: object, default: int) -> int:
|
|
89
124
|
return v if isinstance(v, int) and not isinstance(v, bool) else default
|
|
90
125
|
|
|
@@ -125,6 +160,7 @@ def load(path: Path | None = None) -> Config:
|
|
|
125
160
|
raw = tomllib.loads(p.read_text())
|
|
126
161
|
except (OSError, tomllib.TOMLDecodeError):
|
|
127
162
|
return Config()
|
|
163
|
+
_warn_unknown_keys(raw, p)
|
|
128
164
|
d = Config()
|
|
129
165
|
lang = raw.get("lang")
|
|
130
166
|
# 任意非空字串都接受(後端名稱由 notify 層的可插拔 registry 決定);認不得的名稱
|
|
@@ -139,6 +175,10 @@ def load(path: Path | None = None) -> Config:
|
|
|
139
175
|
active_window_seconds=_as_int(raw.get("active_window_seconds"), d.active_window_seconds),
|
|
140
176
|
working_threshold_seconds=_as_int(raw.get("working_threshold_seconds"), d.working_threshold_seconds),
|
|
141
177
|
waiting_window_seconds=_as_int(raw.get("waiting_window_seconds"), d.waiting_window_seconds),
|
|
178
|
+
codex_permission_wait_seconds=_as_int(
|
|
179
|
+
raw.get("codex_permission_wait_seconds"), d.codex_permission_wait_seconds
|
|
180
|
+
),
|
|
181
|
+
detect_stop_questions=_as_bool(raw.get("detect_stop_questions"), d.detect_stop_questions),
|
|
142
182
|
notify_sound=_as_bool(raw.get("notify_sound"), d.notify_sound),
|
|
143
183
|
notify_sound_name=(
|
|
144
184
|
raw["notify_sound_name"] if isinstance(raw.get("notify_sound_name"), str) else d.notify_sound_name
|
|
@@ -147,6 +187,7 @@ def load(path: Path | None = None) -> Config:
|
|
|
147
187
|
notify_repeat_seconds=_as_positive_int_tuple(raw.get("notify_repeat_seconds"), d.notify_repeat_seconds),
|
|
148
188
|
notify_repeat_max=max(0, _as_int(raw.get("notify_repeat_max"), d.notify_repeat_max)),
|
|
149
189
|
waiting_cooldown_seconds=max(0, _as_int(raw.get("waiting_cooldown_seconds"), d.waiting_cooldown_seconds)),
|
|
190
|
+
notify_debounce_seconds=max(0, _as_int(raw.get("notify_debounce_seconds"), d.notify_debounce_seconds)),
|
|
150
191
|
notify_backend=notify_backend,
|
|
151
192
|
notify_ntfy_url=(raw["notify_ntfy_url"] if isinstance(raw.get("notify_ntfy_url"), str) else ""),
|
|
152
193
|
notify_webhook_url=(raw["notify_webhook_url"] if isinstance(raw.get("notify_webhook_url"), str) else ""),
|
|
@@ -212,6 +253,8 @@ _SETTERS: dict[str, Callable[[str], object]] = {
|
|
|
212
253
|
"active_window_seconds": _coerce_int,
|
|
213
254
|
"working_threshold_seconds": _coerce_int,
|
|
214
255
|
"waiting_window_seconds": _coerce_int,
|
|
256
|
+
"codex_permission_wait_seconds": _coerce_int,
|
|
257
|
+
"detect_stop_questions": _coerce_bool,
|
|
215
258
|
"notify_sound": _coerce_bool,
|
|
216
259
|
"notify_sound_name": str,
|
|
217
260
|
"notify_ignore_dnd": _coerce_bool,
|
|
@@ -219,6 +262,7 @@ _SETTERS: dict[str, Callable[[str], object]] = {
|
|
|
219
262
|
"notify_repeat_seconds": _coerce_int_list,
|
|
220
263
|
"notify_repeat_max": _coerce_int,
|
|
221
264
|
"waiting_cooldown_seconds": _coerce_int,
|
|
265
|
+
"notify_debounce_seconds": _coerce_int,
|
|
222
266
|
"notify_ntfy_url": str,
|
|
223
267
|
"notify_webhook_url": str,
|
|
224
268
|
"notify_also": _coerce_str_list,
|