ring-cli 0.13.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.
Files changed (63) hide show
  1. ring_cli-0.14.0/PKG-INFO +96 -0
  2. ring_cli-0.14.0/README.en.md +70 -0
  3. {ring_cli-0.13.0 → ring_cli-0.14.0}/pyproject.toml +15 -7
  4. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/cli.py +37 -0
  5. ring_cli-0.14.0/src/ring/commands/quiet.py +54 -0
  6. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/config.py +29 -3
  7. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/hook.py +11 -0
  8. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/hook_protocol.py +5 -1
  9. ring_cli-0.14.0/src/ring/locale/en/LC_MESSAGES/ring.mo +0 -0
  10. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/locale/en/LC_MESSAGES/ring.po +83 -0
  11. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/locale/ring.pot +195 -103
  12. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/notify/__init__.py +61 -1
  13. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/notify/base.py +18 -1
  14. ring_cli-0.14.0/src/ring/notify_queue.py +332 -0
  15. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/registry.py +15 -3
  16. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/tui.py +39 -4
  17. ring_cli-0.13.0/PKG-INFO +0 -514
  18. ring_cli-0.13.0/README.en.md +0 -489
  19. ring_cli-0.13.0/src/ring/locale/en/LC_MESSAGES/ring.mo +0 -0
  20. {ring_cli-0.13.0 → ring_cli-0.14.0}/LICENSE +0 -0
  21. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/__init__.py +0 -0
  22. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/__main__.py +0 -0
  23. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/commands/__init__.py +0 -0
  24. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/commands/_args.py +0 -0
  25. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/commands/completion.py +0 -0
  26. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/commands/digest.py +0 -0
  27. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/commands/doctor.py +0 -0
  28. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/commands/focus.py +0 -0
  29. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/commands/gc.py +0 -0
  30. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/commands/hook.py +0 -0
  31. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/commands/stats.py +0 -0
  32. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/focus/__init__.py +0 -0
  33. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/focus/applescript.py +0 -0
  34. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/focus/base.py +0 -0
  35. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/focus/iterm2.py +0 -0
  36. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/focus/linux_wm.py +0 -0
  37. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/focus/neovim.py +0 -0
  38. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/focus/terminal.py +0 -0
  39. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/focus/tmux.py +0 -0
  40. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/gc.py +0 -0
  41. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/i18n.py +0 -0
  42. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/ipc.py +0 -0
  43. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/labels.py +0 -0
  44. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/notify/command.py +0 -0
  45. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/notify/notify_send.py +0 -0
  46. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/notify/ntfy.py +0 -0
  47. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/notify/osascript_notifier.py +0 -0
  48. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/notify/terminal_notifier.py +0 -0
  49. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/notify/webhook.py +0 -0
  50. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/osascript.py +0 -0
  51. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/payload_log.py +0 -0
  52. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/permission.py +0 -0
  53. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/plugins.py +0 -0
  54. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/question_detect.py +0 -0
  55. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/sources/__init__.py +0 -0
  56. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/sources/base.py +0 -0
  57. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/sources/claude_code.py +0 -0
  58. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/sources/codex.py +0 -0
  59. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/sources/hook_registry.py +0 -0
  60. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/sources/local_llm.py +0 -0
  61. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/stats.py +0 -0
  62. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/transcript.py +0 -0
  63. {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/watcher.py +0 -0
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/ring-cli?label=PyPI)](https://pypi.org/project/ring-cli/)
32
+ [![Python](https://img.shields.io/pypi/pyversions/ring-cli)](https://pypi.org/project/ring-cli/)
33
+ [![License](https://img.shields.io/pypi/l/ring-cli)](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
+ [![PyPI](https://img.shields.io/pypi/v/ring-cli?label=PyPI)](https://pypi.org/project/ring-cli/)
6
+ [![Python](https://img.shields.io/pypi/pyversions/ring-cli)](https://pypi.org/project/ring-cli/)
7
+ [![License](https://img.shields.io/pypi/l/ring-cli)](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.13.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-extract.help = "從原始碼抽出可翻譯字串到 .pot(改字串後重抽。不要用 pybabel update:會塞空 msgstr 進 .po,弄壞 i18n 測試。新字串請手動把已翻譯條目加進各 .po)"
116
- i18n-extract.cmd = "pybabel extract -F babel.cfg -o src/ring/locale/ring.pot src/ring"
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-check.help = "列出原始碼有、但某語言 .po 還沒翻譯的字串(純資訊,不失敗)。先跑 i18n-extract"
119
- i18n-check.cmd = "python scripts/i18n_check.py"
120
+ "i18n:check".help = "列出原始碼有、但某語言 .po 還沒翻譯的字串(純資訊,不失敗)。先跑 i18n:extract"
121
+ "i18n:check".cmd = "python scripts/i18n_check.py"
120
122
 
121
- i18n-compile.help = "把各語言的 .po 編成 .mo(改 .po 後要重編並 commit)"
122
- i18n-compile.cmd = "pybabel compile -d src/ring/locale -D ring"
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,16 +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 你。
405
420
  # 例外:codex 核可等待是讀取側的靜默逾時判定,沒有 hook 事件可發通知,由 TUI 的
406
421
  # 提醒排程器代發(tui._ring_on_waiting_alerts);headless watch 仍不發,是已知限制。
422
+ # debounce/quiet 合流 queue 的懶惰 flush:headless watch 沒有 hook 事件也沒有 TUI 輪詢
423
+ # 可以觸發,每輪自己補上一次,否則純 headless 使用者開了 quiet/debounce 通知會無限期卡住。
407
424
  frames = 0
408
425
  footer_text = _("每 {interval}s 刷新 · Ctrl-C 離場", interval=int(interval))
409
426
  if not HAVE_RICH:
410
427
  try:
411
428
  while True:
429
+ _watch_flush_if_due()
412
430
  sys.stdout.write("\033[2J\033[H")
413
431
  sessions = board(show_all)
414
432
  print(_render_plain(sessions, show_legend, show_tool_column(sessions)))
@@ -425,6 +443,7 @@ def watch(interval: float, count: int, show_all: bool, show_legend: bool) -> int
425
443
  try:
426
444
  with Live(console=console, screen=True, auto_refresh=False) as live:
427
445
  while True:
446
+ _watch_flush_if_due()
428
447
  sessions = board(show_all)
429
448
  body = _rich_renderable(sessions, show_legend, show_tool_column(sessions))
430
449
  live.update(Group(body, Text(f"\n{footer_text}", style=_MUTED)), refresh=True)
@@ -457,6 +476,10 @@ commands:
457
476
  config 顯示設定檔位置與目前生效的設定
458
477
  config get KEY 讀單一設定的目前值
459
478
  config set KEY VALUE 寫入單一設定(會重寫設定檔,不保留註解)
479
+ quiet 顯示 quiet(暫時全域靜音)現況
480
+ quiet on 開啟 quiet(手動解除前一直靜音)
481
+ quiet off 解除 quiet(立即補發合流的彙總通知)
482
+ quiet DURATION 開啟 quiet 一段時間(例如 30m、1h),到期自動解除
460
483
  focus SESSION_ID 聚焦指定 session;TUI 在跑時會回到 RiNG 並選中該列
461
484
  gc [--dry-run] 清理 RiNG 自己的 stale 狀態檔
462
485
  doctor 顯示環境診斷(唯讀)——hook、通知、focuser、維護提示
@@ -493,6 +516,17 @@ def _subcommand_help(name: str) -> str:
493
516
  不帶參數:顯示設定檔位置(~/.config/ring/config.toml)與目前生效的所有設定。
494
517
  get KEY 印出單一設定的目前值(colors 子鍵用 colors.<name>)。
495
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),到期自動解除。
496
530
  """
497
531
  ),
498
532
  "focus": _(
@@ -569,6 +603,7 @@ def main(argv: list[str] | None = None) -> int:
569
603
  "digest",
570
604
  "stats",
571
605
  "completion",
606
+ "quiet",
572
607
  }
573
608
  and any(arg in {"-h", "--help"} for arg in raw[1:])
574
609
  ):
@@ -583,6 +618,8 @@ def main(argv: list[str] | None = None) -> int:
583
618
  return run_remove_hooks(raw[1:])
584
619
  if raw and raw[0] == "config":
585
620
  return run_config(raw[1:])
621
+ if raw and raw[0] == "quiet":
622
+ return run_quiet(raw[1:])
586
623
  if raw and raw[0] == "gc":
587
624
  return run_gc(raw[1:])
588
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,7 @@
8
8
  legend = true
9
9
  active_window_seconds = 21600 # 只看最近這段時間動過的 session(預設 6h)
10
10
  working_threshold_seconds = 90 # 多久沒動就從 🟢 工作中 變 🟡 閒置
11
- waiting_window_seconds = 1800 # 跑完停著升等你的時間窗上限(預設 30 分)
11
+ waiting_window_seconds = 1800 # 零設定掃描中,近期回合結束列收斂成 IDLE 的時間窗
12
12
  codex_permission_wait_seconds = 10 # Codex 裸 PermissionRequest 後 hook 靜默超過這秒數
13
13
  # → 看板判定真的停下來等核可(🔴 等你);0 = 關閉
14
14
  detect_stop_questions = true # Stop 事件時,若最後一則 assistant 訊息「結尾」是純文字
@@ -27,6 +27,10 @@
27
27
  notify_repeat_max = 3 # 重複提醒上限;0 = 不限
28
28
  waiting_cooldown_seconds = 180 # session 離開 WAITING 又轉回時,距上次提醒未滿這段時間就
29
29
  # 不再當「新轉入」立即提醒(防翻轉轟炸);0 = 關閉(現行為)
30
+ notify_debounce_seconds = 0 # 多個 session 同時轉 🔴 等你時的通知合流窗口(秒):窗口內
31
+ # 第一批立即發(leading edge),之後的合併成一則彙總,由下個
32
+ # hook 事件 / TUI 輪詢 / quiet 解除懶惰 flush;0 = 關閉(現行為,
33
+ # 向後相容,每次轉入都各自發)
30
34
  notify_ntfy_url = "https://ntfy.sh/my-topic" # 設了才啟用 ntfy 後端(推到手機)
31
35
  notify_webhook_url = "https://example.com/hook" # 設了才啟用 webhook 後端(JSON POST)
32
36
  notify_also = ["ntfy"] # 主後端之外「加發」的後端(例如桌面通知+手機各一份)
@@ -40,12 +44,15 @@
40
44
 
41
45
  from __future__ import annotations
42
46
 
47
+ import sys
43
48
  import tomllib
44
49
  from collections.abc import Callable
45
- from dataclasses import dataclass, field
50
+ from dataclasses import dataclass, field, fields
46
51
  from functools import lru_cache
47
52
  from pathlib import Path
48
53
 
54
+ from ring.i18n import gettext as _
55
+
49
56
  CONFIG_PATH = Path.home() / ".config" / "ring" / "config.toml"
50
57
 
51
58
  # Rich 樣式字串。深淺底都看得到(避開 dim / ANSI blue)。config 的 [colors] 可逐項覆寫。
@@ -68,7 +75,7 @@ class Config:
68
75
  legend: bool = True
69
76
  active_window_seconds: int = 6 * 60 * 60
70
77
  working_threshold_seconds: int = 90
71
- waiting_window_seconds: int = 1800 # 跑完停著升等你的時間窗上限(預設 30 分)
78
+ waiting_window_seconds: int = 1800 # 零設定掃描中,近期回合結束列收斂成 IDLE 的時間窗
72
79
  # Codex 的 hook 沒有「使用者已核可」事件也沒有心跳(0.144.4 實證):policy 自動放行時
73
80
  # 下一個事件幾秒內就到;真的停下來等人時 hook 通道完全靜默。所以「最後一個事件是
74
81
  # PermissionRequest 且已靜默超過這個門檻」就判定在等核可。0 = 關閉這個判定。
@@ -88,6 +95,7 @@ class Config:
88
95
  notify_repeat_seconds: tuple[int, ...] = (30, 120, 300)
89
96
  notify_repeat_max: int = 3 # 0 = 不限
90
97
  waiting_cooldown_seconds: int = 180 # 離開 WAITING 又轉回時的提醒冷卻期;0 = 關閉(現行為)
98
+ notify_debounce_seconds: int = 0 # 跨 session 通知合流窗口(秒);0 = 關閉(現行為,向後相容)
91
99
  notify_ntfy_url: str = "" # 完整 ntfy topic URL;空=ntfy 後端不可用
92
100
  notify_webhook_url: str = "" # webhook URL;空=webhook 後端不可用
93
101
  notify_also: tuple[str, ...] = () # 主後端之外加發的後端名(如 ["ntfy"])
@@ -97,6 +105,21 @@ class Config:
97
105
  colors: dict[str, str] = field(default_factory=lambda: dict(_DEFAULT_COLORS))
98
106
 
99
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
+
100
123
  def _as_int(v: object, default: int) -> int:
101
124
  return v if isinstance(v, int) and not isinstance(v, bool) else default
102
125
 
@@ -137,6 +160,7 @@ def load(path: Path | None = None) -> Config:
137
160
  raw = tomllib.loads(p.read_text())
138
161
  except (OSError, tomllib.TOMLDecodeError):
139
162
  return Config()
163
+ _warn_unknown_keys(raw, p)
140
164
  d = Config()
141
165
  lang = raw.get("lang")
142
166
  # 任意非空字串都接受(後端名稱由 notify 層的可插拔 registry 決定);認不得的名稱
@@ -163,6 +187,7 @@ def load(path: Path | None = None) -> Config:
163
187
  notify_repeat_seconds=_as_positive_int_tuple(raw.get("notify_repeat_seconds"), d.notify_repeat_seconds),
164
188
  notify_repeat_max=max(0, _as_int(raw.get("notify_repeat_max"), d.notify_repeat_max)),
165
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)),
166
191
  notify_backend=notify_backend,
167
192
  notify_ntfy_url=(raw["notify_ntfy_url"] if isinstance(raw.get("notify_ntfy_url"), str) else ""),
168
193
  notify_webhook_url=(raw["notify_webhook_url"] if isinstance(raw.get("notify_webhook_url"), str) else ""),
@@ -237,6 +262,7 @@ _SETTERS: dict[str, Callable[[str], object]] = {
237
262
  "notify_repeat_seconds": _coerce_int_list,
238
263
  "notify_repeat_max": _coerce_int,
239
264
  "waiting_cooldown_seconds": _coerce_int,
265
+ "notify_debounce_seconds": _coerce_int,
240
266
  "notify_ntfy_url": str,
241
267
  "notify_webhook_url": str,
242
268
  "notify_also": _coerce_str_list,
@@ -154,7 +154,18 @@ def run_hook(provider: str = "claude-code") -> int:
154
154
  ``agent-hooks`` 時發生:把原始 payload 透傳給 ``agent-hooks callback``,由它同步出 modal、
155
155
  收按鈕、把決策寫到 stdout 回給 Claude(這條路下 ``_ring_waiting_now`` 自動短路、不重複發)。
156
156
  其餘情況 RiNG 記狀態 + 發通知後 exit 0(你在終端自己回答)。
157
+
158
+ 每次執行開頭先嘗試懶惰 flush 合流 queue(見 ``notify_queue.flush_if_due``)——沒有常駐
159
+ daemon,任何 hook 事件(不限於這次自己是不是 waiting 事件)都是「debounce 視窗過期」的
160
+ 天然觸發點,讓 headless(沒開 TUI)也能補發彙總通知。失敗安靜吞掉,不影響本次事件記錄。
157
161
  """
162
+ try:
163
+ from ring.notify_queue import flush_if_due
164
+
165
+ flush_if_due()
166
+ except Exception:
167
+ pass
168
+
158
169
  try:
159
170
  raw = sys.stdin.read()
160
171
  except OSError:
@@ -47,8 +47,9 @@ _ALWAYS_STATUS = {
47
47
  }
48
48
 
49
49
  _ACTION_REQUIRED_NOTIFICATION_TYPES = {
50
- "permission_prompt",
50
+ "agent_needs_input", # 背景 agent 停下來要使用者輸入(claude 2.1.215 binary 實證有此型別)
51
51
  "elicitation_dialog",
52
+ "permission_prompt",
52
53
  }
53
54
 
54
55
  _ACTION_REQUIRED_WAITING_FOR = {
@@ -245,6 +246,9 @@ def _waiting_kind(data: Mapping[str, Any], event: str, status: Status) -> str:
245
246
  # 先於 PermissionRequest 判斷:AskUserQuestion 也會以 PermissionRequest 事件
246
247
  # 進來(權限判定包著問題),但使用者要回的是「問題」不是「權限」。
247
248
  return "question"
249
+ if notification_type == "agent_needs_input":
250
+ # 背景 agent 要使用者輸入,性質是「回答」而非權限核可 → ❓ 而非 🔐。
251
+ return "question"
248
252
  if event == "PermissionRequest" or notification_type == "permission_prompt":
249
253
  return "permission"
250
254
  if waiting_for in {"approval", "permission"}:
@@ -239,6 +239,9 @@ msgstr "Change with `ring config set KEY VALUE`, or edit the file above; see the
239
239
  msgid "未知的鍵:{key}"
240
240
  msgstr "unknown key: {key}"
241
241
 
242
+ msgid "⚠️ {path} 有未知的設定鍵(可能打錯字,已忽略):{keys}"
243
+ msgstr "⚠️ {path} has unknown config keys (likely a typo, ignored): {keys}"
244
+
242
245
  msgid "用法:ring config get KEY"
243
246
  msgstr "usage: ring config get KEY"
244
247
 
@@ -265,6 +268,10 @@ msgid ""
265
268
  " config 顯示設定檔位置與目前生效的設定\n"
266
269
  " config get KEY 讀單一設定的目前值\n"
267
270
  " config set KEY VALUE 寫入單一設定(會重寫設定檔,不保留註解)\n"
271
+ " quiet 顯示 quiet(暫時全域靜音)現況\n"
272
+ " quiet on 開啟 quiet(手動解除前一直靜音)\n"
273
+ " quiet off 解除 quiet(立即補發合流的彙總通知)\n"
274
+ " quiet DURATION 開啟 quiet 一段時間(例如 30m、1h),到期自動解除\n"
268
275
  " focus SESSION_ID 聚焦指定 session;TUI 在跑時會回到 RiNG 並選中該列\n"
269
276
  " gc [--dry-run] 清理 RiNG 自己的 stale 狀態檔\n"
270
277
  " doctor 顯示環境診斷(唯讀)——hook、通知、focuser、維護提示\n"
@@ -281,6 +288,10 @@ msgstr ""
281
288
  " config show config file path and current settings\n"
282
289
  " config get KEY read one setting's current value\n"
283
290
  " config set KEY VALUE write one setting (rewrites config file, comments lost)\n"
291
+ " quiet show quiet (temporary global mute) status\n"
292
+ " quiet on turn quiet on (stays muted until manually cleared)\n"
293
+ " quiet off turn quiet off (flushes any coalesced summary right away)\n"
294
+ " quiet DURATION turn quiet on for a while (e.g. 30m, 1h), auto-clears on expiry\n"
284
295
  " focus SESSION_ID focus a session; if TUI is running, returns to RiNG and selects the row\n"
285
296
  " gc [--dry-run] clean RiNG's own stale state files\n"
286
297
  " doctor show environment diagnosis (read-only) — hooks, notify, focuser, maintenance hints\n"
@@ -329,6 +340,26 @@ msgstr ""
329
340
  " get KEY print the current value of one setting (use colors.<name> for color sub-keys).\n"
330
341
  " set KEY VALUE write one setting. Note: rewrites the entire config file; existing comments are lost.\n"
331
342
 
343
+ msgid ""
344
+ "usage: ring quiet [on | off | DURATION]\n"
345
+ "\n"
346
+ "暫時全域靜音:靜音期間所有轉 🔴 等你的通知都先進 queue,解除時懶惰補發一則彙總。\n"
347
+ "\n"
348
+ "不帶參數:顯示目前 quiet 現況(開/關、剩餘時間)。\n"
349
+ " on 開啟 quiet,手動解除前一直靜音。\n"
350
+ " off 解除 quiet,並立即補發合流的彙總通知。\n"
351
+ " DURATION 開啟 quiet 一段時間(例如 30m、1h),到期自動解除。\n"
352
+ msgstr ""
353
+ "usage: ring quiet [on | off | DURATION]\n"
354
+ "\n"
355
+ "Temporary global mute: while quiet, every 🔴 waiting-for-you notification is queued first,\n"
356
+ "and a coalesced summary is flushed lazily once quiet is cleared.\n"
357
+ "\n"
358
+ "No args: show the current quiet status (on/off, remaining time).\n"
359
+ " on turn quiet on; stays muted until manually cleared.\n"
360
+ " off turn quiet off, and flush any coalesced summary right away.\n"
361
+ " DURATION turn quiet on for a while (e.g. 30m, 1h); auto-clears on expiry.\n"
362
+
332
363
  msgid ""
333
364
  "usage: ring focus SESSION_ID\n"
334
365
  "\n"
@@ -695,3 +726,55 @@ msgstr "{project}: the dialog was gone and the digit landed in the input box —
695
726
 
696
727
  msgid "{project}:已送出但拿不到畫面驗證,請跳過去確認"
697
728
  msgstr "{project}: key sent but the screen could not be re-read to verify — jump over to check"
729
+
730
+ # ── notify_queue.py:debounce queue / quiet mode ──
731
+ msgid "{s} 秒"
732
+ msgstr "{s}s"
733
+
734
+ msgid "{m} 分"
735
+ msgstr "{m}m"
736
+
737
+ msgid "{h} 小時"
738
+ msgstr "{h}h"
739
+
740
+ # ── notify/__init__.py:合流彙總通知 ──
741
+ msgid "另有 {n} 個 session 在等你"
742
+ msgid_plural "另有 {n} 個 session 在等你"
743
+ msgstr[0] "{n} more session is waiting for you"
744
+ msgstr[1] "{n} more sessions are waiting for you"
745
+
746
+ # ── notify/base.py:彙總通知標題 ──
747
+ msgid "RiNG · 還有人在等你"
748
+ msgstr "RiNG · someone is waiting for you"
749
+
750
+ # ── tui.py:quiet / queue header badge ──
751
+ msgid "🔇 QUIET"
752
+ msgstr "🔇 QUIET"
753
+
754
+ msgid "🔇 QUIET · 剩 {remaining}"
755
+ msgstr "🔇 QUIET · {remaining} left"
756
+
757
+ msgid "queue: {n}"
758
+ msgstr "queue: {n}"
759
+
760
+ # ── commands/quiet.py:ring quiet CLI ──
761
+ msgid "🔈 quiet:目前關閉"
762
+ msgstr "🔈 quiet: off"
763
+
764
+ msgid "🔇 quiet:開啟中(手動解除前一直靜音)"
765
+ msgstr "🔇 quiet: on (stays muted until manually cleared)"
766
+
767
+ msgid "🔇 quiet:開啟中,剩 {remaining}"
768
+ msgstr "🔇 quiet: on, {remaining} left"
769
+
770
+ msgid "🔇 quiet 已開啟(手動解除前一直靜音)"
771
+ msgstr "🔇 quiet turned on (stays muted until manually cleared)"
772
+
773
+ msgid "🔈 quiet 已解除"
774
+ msgstr "🔈 quiet turned off"
775
+
776
+ msgid "無效的 duration:{value}(例如 30m、1h)"
777
+ msgstr "invalid duration: {value} (e.g. 30m, 1h)"
778
+
779
+ msgid "🔇 quiet 已開啟,{duration} 後自動解除"
780
+ msgstr "🔇 quiet turned on, auto-clears after {duration}"