ring-cli 0.8.1__tar.gz → 0.10.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.8.1 → ring_cli-0.10.0}/PKG-INFO +10 -11
- {ring_cli-0.8.1 → ring_cli-0.10.0}/README.en.md +9 -10
- {ring_cli-0.8.1 → ring_cli-0.10.0}/pyproject.toml +1 -1
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/cli.py +2 -23
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/config.py +5 -4
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/hook.py +38 -6
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/locale/en/LC_MESSAGES/ring.mo +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/locale/en/LC_MESSAGES/ring.po +0 -9
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/locale/ring.pot +69 -84
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/notify/__init__.py +4 -17
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/notify/base.py +1 -12
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/registry.py +82 -11
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/sources/__init__.py +8 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/tui.py +4 -16
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/watcher.py +29 -32
- {ring_cli-0.8.1 → ring_cli-0.10.0}/LICENSE +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/__init__.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/__main__.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/commands/__init__.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/commands/_args.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/commands/completion.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/commands/digest.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/commands/doctor.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/commands/focus.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/commands/gc.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/commands/hook.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/commands/stats.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/focus/__init__.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/focus/applescript.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/focus/base.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/focus/iterm2.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/focus/linux_wm.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/focus/neovim.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/focus/terminal.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/focus/tmux.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/gc.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/hook_protocol.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/i18n.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/ipc.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/labels.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/notify/command.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/notify/notify_send.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/notify/ntfy.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/notify/osascript_notifier.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/notify/terminal_notifier.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/notify/webhook.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/osascript.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/plugins.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/sources/base.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/sources/claude_code.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/sources/codex.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/sources/hook_registry.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/stats.py +0 -0
- {ring_cli-0.8.1 → ring_cli-0.10.0}/src/ring/transcript.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ring-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: RiNG — Realtime Instance Notification Grid for active agent CLI sessions.
|
|
5
5
|
Keywords: claude-code,codex,tui,dashboard,session,monitor,rich
|
|
6
6
|
Author: Wei Lee
|
|
@@ -179,13 +179,6 @@ and sends a system notification right then — **no RiNG board needs to be open*
|
|
|
179
179
|
after you close the terminal (notifications are event-driven, not polled). While a TUI is open, it
|
|
180
180
|
also nudges you again in-app at 30s / 120s / 300s if the session keeps waiting.
|
|
181
181
|
|
|
182
|
-
A second kind of reminder is 🟡 **idle for too long** (`idle_threshold_seconds`, default 2h): once a
|
|
183
|
-
session has sat idle past the threshold, it notifies once. Unlike waiting, idle is time-based — there
|
|
184
|
-
is no event to hang a hook on, so it can only be detected by polling. That means it **fires only while
|
|
185
|
-
`ring watch` or a TUI is open** (and when both are open, only one of them fires it, never twice). Close
|
|
186
|
-
every board and idle reminders go silent — waiting still rings you, since that path runs off the hook.
|
|
187
|
-
This is a deliberate trade-off: idle is a secondary signal, not worth a resident background process.
|
|
188
|
-
|
|
189
182
|
For clickable notifications on macOS, install `terminal-notifier`:
|
|
190
183
|
|
|
191
184
|
```sh
|
|
@@ -343,8 +336,14 @@ RiNG prefers clickable `terminal-notifier`, then falls back to `osascript` on ma
|
|
|
343
336
|
on Linux. If none are available, RiNG keeps the board running and skips notifications.
|
|
344
337
|
|
|
345
338
|
Config keys include `notify_sound`, `notify_sound_name`, `notify_ignore_dnd`,
|
|
346
|
-
`notify_repeat_seconds`, `notify_repeat_max`, and `
|
|
347
|
-
can be added by registering another `Notifier`.
|
|
339
|
+
`notify_repeat_seconds`, `notify_repeat_max`, `notify_backend`, and `waiting_cooldown_seconds`.
|
|
340
|
+
Custom notification channels can be added by registering another `Notifier`.
|
|
341
|
+
|
|
342
|
+
Background subagent permission requests can make a session flap between waiting and working in
|
|
343
|
+
quick succession. `waiting_cooldown_seconds` (180s by default) stops `ring hook`'s system
|
|
344
|
+
notification and the TUI's bell/reminder from firing again the instant a session re-enters waiting
|
|
345
|
+
within the cooldown window of its last alert; set it to `0` to disable the cooldown (alert on
|
|
346
|
+
every re-entry, the old behavior).
|
|
348
347
|
|
|
349
348
|
`agent-hooks` is an optional external hook helper / decision UI. If it is installed and
|
|
350
349
|
`notify_backend = "agent-hooks"`, `ring hook` still writes RiNG registry state, then passes the
|
|
@@ -373,13 +372,13 @@ legend = true
|
|
|
373
372
|
active_window_seconds = 21600
|
|
374
373
|
working_threshold_seconds = 90
|
|
375
374
|
waiting_window_seconds = 1800
|
|
376
|
-
idle_threshold_seconds = 7200 # 🟡 remind once idle for this long (default 2h; 0 = off)
|
|
377
375
|
notify_sound = true
|
|
378
376
|
notify_sound_name = "Glass"
|
|
379
377
|
notify_ignore_dnd = false
|
|
380
378
|
notify_backend = "auto" # auto / terminal-notifier / osascript / notify-send / agent-hooks / none
|
|
381
379
|
notify_repeat_seconds = [30, 120, 300]
|
|
382
380
|
notify_repeat_max = 3
|
|
381
|
+
waiting_cooldown_seconds = 180 # suppress an immediate re-alert on re-entering waiting within this window; 0 = off
|
|
383
382
|
notify_ntfy_url = "" # full ntfy topic URL enables phone push (e.g. https://ntfy.sh/my-topic)
|
|
384
383
|
notify_webhook_url = "" # URL enables the generic webhook backend (JSON POST)
|
|
385
384
|
notify_also = [] # extra backends fired besides the primary, e.g. ["ntfy"]
|
|
@@ -154,13 +154,6 @@ and sends a system notification right then — **no RiNG board needs to be open*
|
|
|
154
154
|
after you close the terminal (notifications are event-driven, not polled). While a TUI is open, it
|
|
155
155
|
also nudges you again in-app at 30s / 120s / 300s if the session keeps waiting.
|
|
156
156
|
|
|
157
|
-
A second kind of reminder is 🟡 **idle for too long** (`idle_threshold_seconds`, default 2h): once a
|
|
158
|
-
session has sat idle past the threshold, it notifies once. Unlike waiting, idle is time-based — there
|
|
159
|
-
is no event to hang a hook on, so it can only be detected by polling. That means it **fires only while
|
|
160
|
-
`ring watch` or a TUI is open** (and when both are open, only one of them fires it, never twice). Close
|
|
161
|
-
every board and idle reminders go silent — waiting still rings you, since that path runs off the hook.
|
|
162
|
-
This is a deliberate trade-off: idle is a secondary signal, not worth a resident background process.
|
|
163
|
-
|
|
164
157
|
For clickable notifications on macOS, install `terminal-notifier`:
|
|
165
158
|
|
|
166
159
|
```sh
|
|
@@ -318,8 +311,14 @@ RiNG prefers clickable `terminal-notifier`, then falls back to `osascript` on ma
|
|
|
318
311
|
on Linux. If none are available, RiNG keeps the board running and skips notifications.
|
|
319
312
|
|
|
320
313
|
Config keys include `notify_sound`, `notify_sound_name`, `notify_ignore_dnd`,
|
|
321
|
-
`notify_repeat_seconds`, `notify_repeat_max`, and `
|
|
322
|
-
can be added by registering another `Notifier`.
|
|
314
|
+
`notify_repeat_seconds`, `notify_repeat_max`, `notify_backend`, and `waiting_cooldown_seconds`.
|
|
315
|
+
Custom notification channels can be added by registering another `Notifier`.
|
|
316
|
+
|
|
317
|
+
Background subagent permission requests can make a session flap between waiting and working in
|
|
318
|
+
quick succession. `waiting_cooldown_seconds` (180s by default) stops `ring hook`'s system
|
|
319
|
+
notification and the TUI's bell/reminder from firing again the instant a session re-enters waiting
|
|
320
|
+
within the cooldown window of its last alert; set it to `0` to disable the cooldown (alert on
|
|
321
|
+
every re-entry, the old behavior).
|
|
323
322
|
|
|
324
323
|
`agent-hooks` is an optional external hook helper / decision UI. If it is installed and
|
|
325
324
|
`notify_backend = "agent-hooks"`, `ring hook` still writes RiNG registry state, then passes the
|
|
@@ -348,13 +347,13 @@ legend = true
|
|
|
348
347
|
active_window_seconds = 21600
|
|
349
348
|
working_threshold_seconds = 90
|
|
350
349
|
waiting_window_seconds = 1800
|
|
351
|
-
idle_threshold_seconds = 7200 # 🟡 remind once idle for this long (default 2h; 0 = off)
|
|
352
350
|
notify_sound = true
|
|
353
351
|
notify_sound_name = "Glass"
|
|
354
352
|
notify_ignore_dnd = false
|
|
355
353
|
notify_backend = "auto" # auto / terminal-notifier / osascript / notify-send / agent-hooks / none
|
|
356
354
|
notify_repeat_seconds = [30, 120, 300]
|
|
357
355
|
notify_repeat_max = 3
|
|
356
|
+
waiting_cooldown_seconds = 180 # suppress an immediate re-alert on re-entering waiting within this window; 0 = off
|
|
358
357
|
notify_ntfy_url = "" # full ntfy topic URL enables phone push (e.g. https://ntfy.sh/my-topic)
|
|
359
358
|
notify_webhook_url = "" # URL enables the generic webhook backend (JSON POST)
|
|
360
359
|
notify_also = [] # extra backends fired besides the primary, e.g. ["ntfy"]
|
|
@@ -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.10.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"
|
|
@@ -33,12 +33,10 @@ from ring.config import get_config as get_config
|
|
|
33
33
|
from ring.config import set_value as set_value
|
|
34
34
|
from ring.i18n import gettext as _
|
|
35
35
|
from ring.i18n import ngettext, set_lang
|
|
36
|
-
from ring.ipc import read_tui_presence
|
|
37
36
|
from ring.labels import load_labels
|
|
38
37
|
from ring.plugins import load_plugins
|
|
39
38
|
from ring.registry import Session, Status, running_agent_pids
|
|
40
39
|
from ring.sources import discover_sessions
|
|
41
|
-
from ring.watcher import IdleAlertScheduler
|
|
42
40
|
|
|
43
41
|
try:
|
|
44
42
|
from rich.box import SIMPLE_HEAD
|
|
@@ -402,33 +400,15 @@ def run_config(args: list[str]) -> int:
|
|
|
402
400
|
|
|
403
401
|
|
|
404
402
|
def watch(interval: float, count: int, show_all: bool, show_legend: bool) -> int:
|
|
405
|
-
#
|
|
406
|
-
#
|
|
407
|
-
# 閒置是時間累積出來的,沒有事件可掛,只能靠輪詢偵測。
|
|
403
|
+
# 系統通知由 ``ring hook`` 在 session 轉 🔴 等你的當下就地發出(見 hook._ring_waiting_now);
|
|
404
|
+
# watch 只負責顯示看板,不再輪詢發通知——這樣關掉看板也照樣 ring 你。
|
|
408
405
|
frames = 0
|
|
409
406
|
footer_text = _("每 {interval}s 刷新 · Ctrl-C 離場", interval=int(interval))
|
|
410
|
-
idle_alerts = IdleAlertScheduler(get_config().idle_threshold_seconds)
|
|
411
|
-
|
|
412
|
-
def _notify_idle_alerts(sessions: list[Session]) -> None:
|
|
413
|
-
# scheduler 照常 feed 讓狀態持續前進;但若 TUI(ring tui)也開著,
|
|
414
|
-
# 就讓它去發、watch 這邊不重複發系統通知——否則同一 session 會被兩個行程各響一次。
|
|
415
|
-
# 已標記為 due 的 session 在這裡仍算「提醒過」,所以 TUI 關掉後 watch 不會補發。
|
|
416
|
-
alerts = idle_alerts.feed(sessions)
|
|
417
|
-
if not alerts or read_tui_presence() is not None:
|
|
418
|
-
return
|
|
419
|
-
try:
|
|
420
|
-
from ring.notify import notify_idle
|
|
421
|
-
|
|
422
|
-
notify_idle(alerts)
|
|
423
|
-
except Exception:
|
|
424
|
-
pass
|
|
425
|
-
|
|
426
407
|
if not HAVE_RICH:
|
|
427
408
|
try:
|
|
428
409
|
while True:
|
|
429
410
|
sys.stdout.write("\033[2J\033[H")
|
|
430
411
|
sessions = board(show_all)
|
|
431
|
-
_notify_idle_alerts(sessions)
|
|
432
412
|
print(_render_plain(sessions, show_legend, show_tool_column(sessions)))
|
|
433
413
|
print(f"\n{footer_text}")
|
|
434
414
|
sys.stdout.flush()
|
|
@@ -444,7 +424,6 @@ def watch(interval: float, count: int, show_all: bool, show_legend: bool) -> int
|
|
|
444
424
|
with Live(console=console, screen=True, auto_refresh=False) as live:
|
|
445
425
|
while True:
|
|
446
426
|
sessions = board(show_all)
|
|
447
|
-
_notify_idle_alerts(sessions)
|
|
448
427
|
body = _rich_renderable(sessions, show_legend, show_tool_column(sessions))
|
|
449
428
|
live.update(Group(body, Text(f"\n{footer_text}", style=_MUTED)), refresh=True)
|
|
450
429
|
frames += 1
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
active_window_seconds = 21600 # 只看最近這段時間動過的 session(預設 6h)
|
|
10
10
|
working_threshold_seconds = 90 # 多久沒動就從 🟢 工作中 變 🟡 閒置
|
|
11
11
|
waiting_window_seconds = 1800 # 跑完停著升等你的時間窗上限(預設 30 分)
|
|
12
|
-
idle_threshold_seconds = 7200 # 🟡 閒置多久後提醒一次(預設 2h;0 = 關閉)
|
|
13
12
|
notify_sound = true # 系統通知帶聲音
|
|
14
13
|
notify_sound_name = "Glass" # macOS / terminal-notifier sound name
|
|
15
14
|
notify_ignore_dnd = false # terminal-notifier 是否加 -ignoreDnD(穿透勿擾 / Focus)
|
|
@@ -21,6 +20,8 @@
|
|
|
21
20
|
# "none" = 完全不發通知(RiNG 當純看板)
|
|
22
21
|
notify_repeat_seconds = [30, 120, 300] # 持續等你時,多久後重複提醒
|
|
23
22
|
notify_repeat_max = 3 # 重複提醒上限;0 = 不限
|
|
23
|
+
waiting_cooldown_seconds = 180 # session 離開 WAITING 又轉回時,距上次提醒未滿這段時間就
|
|
24
|
+
# 不再當「新轉入」立即提醒(防翻轉轟炸);0 = 關閉(現行為)
|
|
24
25
|
notify_ntfy_url = "https://ntfy.sh/my-topic" # 設了才啟用 ntfy 後端(推到手機)
|
|
25
26
|
notify_webhook_url = "https://example.com/hook" # 設了才啟用 webhook 後端(JSON POST)
|
|
26
27
|
notify_also = ["ntfy"] # 主後端之外「加發」的後端(例如桌面通知+手機各一份)
|
|
@@ -59,7 +60,6 @@ class Config:
|
|
|
59
60
|
active_window_seconds: int = 6 * 60 * 60
|
|
60
61
|
working_threshold_seconds: int = 90
|
|
61
62
|
waiting_window_seconds: int = 1800 # 跑完停著升等你的時間窗上限(預設 30 分)
|
|
62
|
-
idle_threshold_seconds: int = 2 * 60 * 60 # 🟡 閒置多久後提醒一次;0 = 關閉
|
|
63
63
|
notify_sound: bool = True
|
|
64
64
|
notify_sound_name: str = "Glass"
|
|
65
65
|
notify_ignore_dnd: bool = False
|
|
@@ -71,6 +71,7 @@ class Config:
|
|
|
71
71
|
notify_backend: str = "auto"
|
|
72
72
|
notify_repeat_seconds: tuple[int, ...] = (30, 120, 300)
|
|
73
73
|
notify_repeat_max: int = 3 # 0 = 不限
|
|
74
|
+
waiting_cooldown_seconds: int = 180 # 離開 WAITING 又轉回時的提醒冷卻期;0 = 關閉(現行為)
|
|
74
75
|
notify_ntfy_url: str = "" # 完整 ntfy topic URL;空=ntfy 後端不可用
|
|
75
76
|
notify_webhook_url: str = "" # webhook URL;空=webhook 後端不可用
|
|
76
77
|
notify_also: tuple[str, ...] = () # 主後端之外加發的後端名(如 ["ntfy"])
|
|
@@ -133,7 +134,6 @@ def load(path: Path | None = None) -> Config:
|
|
|
133
134
|
active_window_seconds=_as_int(raw.get("active_window_seconds"), d.active_window_seconds),
|
|
134
135
|
working_threshold_seconds=_as_int(raw.get("working_threshold_seconds"), d.working_threshold_seconds),
|
|
135
136
|
waiting_window_seconds=_as_int(raw.get("waiting_window_seconds"), d.waiting_window_seconds),
|
|
136
|
-
idle_threshold_seconds=max(0, _as_int(raw.get("idle_threshold_seconds"), d.idle_threshold_seconds)),
|
|
137
137
|
notify_sound=_as_bool(raw.get("notify_sound"), d.notify_sound),
|
|
138
138
|
notify_sound_name=(
|
|
139
139
|
raw["notify_sound_name"] if isinstance(raw.get("notify_sound_name"), str) else d.notify_sound_name
|
|
@@ -141,6 +141,7 @@ def load(path: Path | None = None) -> Config:
|
|
|
141
141
|
notify_ignore_dnd=_as_bool(raw.get("notify_ignore_dnd"), d.notify_ignore_dnd),
|
|
142
142
|
notify_repeat_seconds=_as_positive_int_tuple(raw.get("notify_repeat_seconds"), d.notify_repeat_seconds),
|
|
143
143
|
notify_repeat_max=max(0, _as_int(raw.get("notify_repeat_max"), d.notify_repeat_max)),
|
|
144
|
+
waiting_cooldown_seconds=max(0, _as_int(raw.get("waiting_cooldown_seconds"), d.waiting_cooldown_seconds)),
|
|
144
145
|
notify_backend=notify_backend,
|
|
145
146
|
notify_ntfy_url=(raw["notify_ntfy_url"] if isinstance(raw.get("notify_ntfy_url"), str) else ""),
|
|
146
147
|
notify_webhook_url=(raw["notify_webhook_url"] if isinstance(raw.get("notify_webhook_url"), str) else ""),
|
|
@@ -205,13 +206,13 @@ _SETTERS: dict[str, Callable[[str], object]] = {
|
|
|
205
206
|
"active_window_seconds": _coerce_int,
|
|
206
207
|
"working_threshold_seconds": _coerce_int,
|
|
207
208
|
"waiting_window_seconds": _coerce_int,
|
|
208
|
-
"idle_threshold_seconds": _coerce_int,
|
|
209
209
|
"notify_sound": _coerce_bool,
|
|
210
210
|
"notify_sound_name": str,
|
|
211
211
|
"notify_ignore_dnd": _coerce_bool,
|
|
212
212
|
"notify_backend": str,
|
|
213
213
|
"notify_repeat_seconds": _coerce_int_list,
|
|
214
214
|
"notify_repeat_max": _coerce_int,
|
|
215
|
+
"waiting_cooldown_seconds": _coerce_int,
|
|
215
216
|
"notify_ntfy_url": str,
|
|
216
217
|
"notify_webhook_url": str,
|
|
217
218
|
"notify_also": _coerce_str_list,
|
|
@@ -169,7 +169,8 @@ def _record_session_state(data: dict[str, Any], selected_provider: str) -> None:
|
|
|
169
169
|
|
|
170
170
|
unhide_session(event.session_id)
|
|
171
171
|
path = RING_REGISTRY / f"{quote(event.session_id, safe=':')}.json"
|
|
172
|
-
|
|
172
|
+
prev_row = _previous_row(path)
|
|
173
|
+
prev_status = str(prev_row["status"]) if "status" in prev_row else None
|
|
173
174
|
if event.status is Status.ENDED:
|
|
174
175
|
path.unlink(missing_ok=True) # 乾淨離場:直接消失
|
|
175
176
|
if prev_status is not None and prev_status != Status.ENDED.value:
|
|
@@ -213,6 +214,19 @@ def _record_session_state(data: dict[str, Any], selected_provider: str) -> None:
|
|
|
213
214
|
if tty:
|
|
214
215
|
payload["tty"] = tty
|
|
215
216
|
|
|
217
|
+
# waiting 通知冷卻(防翻轉轟炸):hook 是短命 process,冷卻狀態持久化在 registry row
|
|
218
|
+
# 的 waiting_notified_at(上次 waiting 通知的 epoch 時間戳)。離開 WAITING 也要把時間戳
|
|
219
|
+
# 帶進新 row(payload 每次重建,不帶會遺失),快速翻回 WAITING 時才判得出「還在冷卻期」。
|
|
220
|
+
# 舊 row 沒這欄位 = 從未通知過 → 照發。並發寫沿用既有 tmp+replace 的 last-write-wins。
|
|
221
|
+
last_notified = _as_epoch(prev_row.get("waiting_notified_at"))
|
|
222
|
+
if last_notified is not None:
|
|
223
|
+
payload["waiting_notified_at"] = last_notified
|
|
224
|
+
should_ring = False
|
|
225
|
+
if event.status is Status.WAITING:
|
|
226
|
+
should_ring = _waiting_ring_allowed(last_notified, now)
|
|
227
|
+
if should_ring:
|
|
228
|
+
payload["waiting_notified_at"] = now
|
|
229
|
+
|
|
216
230
|
RING_REGISTRY.mkdir(parents=True, exist_ok=True)
|
|
217
231
|
tmp = path.with_suffix(".json.tmp")
|
|
218
232
|
tmp.write_text(json.dumps(payload))
|
|
@@ -221,17 +235,35 @@ def _record_session_state(data: dict[str, Any], selected_provider: str) -> None:
|
|
|
221
235
|
if prev_status != event.status.value:
|
|
222
236
|
log_transition(event.session_id, event.provider, event.cwd, event.status.value)
|
|
223
237
|
|
|
224
|
-
if
|
|
238
|
+
if should_ring:
|
|
225
239
|
_ring_waiting_now(event, payload, last_action)
|
|
226
240
|
|
|
227
241
|
|
|
228
|
-
def
|
|
229
|
-
"""讀 registry
|
|
242
|
+
def _previous_row(path: Path) -> dict[str, Any]:
|
|
243
|
+
"""讀 registry 檔目前的內容(給轉換偵測與通知冷卻用);沒檔 / 壞檔回空 dict。"""
|
|
230
244
|
try:
|
|
231
245
|
data = json.loads(path.read_text())
|
|
232
|
-
return
|
|
246
|
+
return data if isinstance(data, dict) else {}
|
|
233
247
|
except Exception:
|
|
234
|
-
return
|
|
248
|
+
return {}
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _as_epoch(v: Any) -> float | None:
|
|
252
|
+
"""把 row 欄位值安全轉成 epoch float;缺欄位 / 型別不對回 None(視為從未通知)。"""
|
|
253
|
+
return float(v) if isinstance(v, (int, float)) and not isinstance(v, bool) else None
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _waiting_ring_allowed(last_notified: float | None, now: float) -> bool:
|
|
257
|
+
"""hook 的 waiting 通知是否放行:距上次通知未滿 ``waiting_cooldown_seconds`` 就不發。
|
|
258
|
+
|
|
259
|
+
背景 subagent 的權限請求會讓 session 以 30 秒~2 分鐘頻率在 working↔waiting 間翻轉,
|
|
260
|
+
每次轉入都發一則系統通知會轟炸使用者。cooldown=0 = 關閉(維持現行為:每次轉入都發);
|
|
261
|
+
從未通知過(``last_notified`` 為 None,含舊版 row 沒有該欄位)一律放行。
|
|
262
|
+
"""
|
|
263
|
+
cooldown = get_config().waiting_cooldown_seconds
|
|
264
|
+
if cooldown <= 0 or last_notified is None:
|
|
265
|
+
return True
|
|
266
|
+
return now - last_notified >= cooldown
|
|
235
267
|
|
|
236
268
|
|
|
237
269
|
def _ring_waiting_now(event: Any, payload: dict[str, Any], last_action: str) -> None:
|
|
Binary file
|
|
@@ -124,9 +124,6 @@ msgstr "↑/↓ to select, Enter or Space to jump to its terminal"
|
|
|
124
124
|
msgid "🔔 {names} 在等你回話"
|
|
125
125
|
msgstr "🔔 {names} waiting for your reply"
|
|
126
126
|
|
|
127
|
-
msgid "🟡 {names} 已閒置"
|
|
128
|
-
msgstr "🟡 {names} has gone idle"
|
|
129
|
-
|
|
130
127
|
msgid "(沒有選到 session)"
|
|
131
128
|
msgstr "(no session selected)"
|
|
132
129
|
|
|
@@ -198,15 +195,9 @@ msgid "✅ 已移除 RiNG hooks({events})從 {path}"
|
|
|
198
195
|
msgstr "✅ Removed RiNG hooks ({events}) from {path}"
|
|
199
196
|
|
|
200
197
|
# ── notify 文案明確化 ──
|
|
201
|
-
msgid "RiNG · {project} 已閒置"
|
|
202
|
-
msgstr "RiNG · {project} has gone idle"
|
|
203
|
-
|
|
204
198
|
msgid "RiNG · {project} 在等你回話"
|
|
205
199
|
msgstr "RiNG · {project} is waiting for you"
|
|
206
200
|
|
|
207
|
-
msgid "已閒置 {duration}"
|
|
208
|
-
msgstr "Idle for {duration}"
|
|
209
|
-
|
|
210
201
|
msgid "{project} · …/{tail}"
|
|
211
202
|
msgstr "{project} · …/{tail}"
|
|
212
203
|
|