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.
- ring_cli-0.14.0/PKG-INFO +96 -0
- ring_cli-0.14.0/README.en.md +70 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/pyproject.toml +15 -7
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/cli.py +37 -0
- ring_cli-0.14.0/src/ring/commands/quiet.py +54 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/config.py +29 -3
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/hook.py +11 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/hook_protocol.py +5 -1
- ring_cli-0.14.0/src/ring/locale/en/LC_MESSAGES/ring.mo +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/locale/en/LC_MESSAGES/ring.po +83 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/locale/ring.pot +195 -103
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/notify/__init__.py +61 -1
- {ring_cli-0.13.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.13.0 → ring_cli-0.14.0}/src/ring/registry.py +15 -3
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/tui.py +39 -4
- ring_cli-0.13.0/PKG-INFO +0 -514
- ring_cli-0.13.0/README.en.md +0 -489
- ring_cli-0.13.0/src/ring/locale/en/LC_MESSAGES/ring.mo +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/LICENSE +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/__init__.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/__main__.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/commands/__init__.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/commands/_args.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/commands/completion.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/commands/digest.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/commands/doctor.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/commands/focus.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/commands/gc.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/commands/hook.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/commands/stats.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/focus/__init__.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/focus/applescript.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/focus/base.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/focus/iterm2.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/focus/linux_wm.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/focus/neovim.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/focus/terminal.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/focus/tmux.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/gc.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/i18n.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/ipc.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/labels.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/notify/command.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/notify/notify_send.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/notify/ntfy.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/notify/osascript_notifier.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/notify/terminal_notifier.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/notify/webhook.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/osascript.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/payload_log.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/permission.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/plugins.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/question_detect.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/sources/__init__.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/sources/base.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/sources/claude_code.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/sources/codex.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/sources/hook_registry.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/sources/local_llm.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/stats.py +0 -0
- {ring_cli-0.13.0 → ring_cli-0.14.0}/src/ring/transcript.py +0 -0
- {ring_cli-0.13.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,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 #
|
|
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 #
|
|
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
|
-
"
|
|
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"}:
|
|
Binary file
|
|
@@ -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}"
|