ring-cli 0.8.0__tar.gz → 0.9.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.0 → ring_cli-0.9.0}/PKG-INFO +1 -9
- {ring_cli-0.8.0 → ring_cli-0.9.0}/README.en.md +0 -8
- {ring_cli-0.8.0 → ring_cli-0.9.0}/pyproject.toml +1 -1
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/__init__.py +3 -1
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/cli.py +2 -23
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/config.py +0 -4
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/locale/en/LC_MESSAGES/ring.mo +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/locale/en/LC_MESSAGES/ring.po +0 -9
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/locale/ring.pot +69 -84
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/notify/__init__.py +4 -17
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/notify/base.py +1 -12
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/registry.py +34 -8
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/sources/__init__.py +8 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/tui.py +1 -15
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/watcher.py +0 -30
- {ring_cli-0.8.0 → ring_cli-0.9.0}/LICENSE +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/__main__.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/commands/__init__.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/commands/_args.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/commands/completion.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/commands/digest.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/commands/doctor.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/commands/focus.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/commands/gc.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/commands/hook.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/commands/stats.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/focus/__init__.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/focus/applescript.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/focus/base.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/focus/iterm2.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/focus/linux_wm.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/focus/neovim.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/focus/terminal.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/focus/tmux.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/gc.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/hook.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/hook_protocol.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/i18n.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/ipc.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/labels.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/notify/command.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/notify/notify_send.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/notify/ntfy.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/notify/osascript_notifier.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/notify/terminal_notifier.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/notify/webhook.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/osascript.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/plugins.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/sources/base.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/sources/claude_code.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/sources/codex.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/sources/hook_registry.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.0}/src/ring/stats.py +0 -0
- {ring_cli-0.8.0 → ring_cli-0.9.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.9.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
|
|
@@ -373,7 +366,6 @@ legend = true
|
|
|
373
366
|
active_window_seconds = 21600
|
|
374
367
|
working_threshold_seconds = 90
|
|
375
368
|
waiting_window_seconds = 1800
|
|
376
|
-
idle_threshold_seconds = 7200 # 🟡 remind once idle for this long (default 2h; 0 = off)
|
|
377
369
|
notify_sound = true
|
|
378
370
|
notify_sound_name = "Glass"
|
|
379
371
|
notify_ignore_dnd = false
|
|
@@ -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
|
|
@@ -348,7 +341,6 @@ legend = true
|
|
|
348
341
|
active_window_seconds = 21600
|
|
349
342
|
working_threshold_seconds = 90
|
|
350
343
|
waiting_window_seconds = 1800
|
|
351
|
-
idle_threshold_seconds = 7200 # 🟡 remind once idle for this long (default 2h; 0 = off)
|
|
352
344
|
notify_sound = true
|
|
353
345
|
notify_sound_name = "Glass"
|
|
354
346
|
notify_ignore_dnd = false
|
|
@@ -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.9.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"
|
|
@@ -6,7 +6,9 @@ session 需要你回話時,它「響鈴」叫你。
|
|
|
6
6
|
|
|
7
7
|
from importlib.metadata import PackageNotFoundError, version
|
|
8
8
|
|
|
9
|
+
__distribution__ = "ring-cli"
|
|
10
|
+
|
|
9
11
|
try:
|
|
10
|
-
__version__ = version(
|
|
12
|
+
__version__ = version(__distribution__)
|
|
11
13
|
except PackageNotFoundError: # 直接從原始碼跑、尚未安裝
|
|
12
14
|
__version__ = "0.0.0+dev"
|
|
@@ -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)
|
|
@@ -59,7 +58,6 @@ class Config:
|
|
|
59
58
|
active_window_seconds: int = 6 * 60 * 60
|
|
60
59
|
working_threshold_seconds: int = 90
|
|
61
60
|
waiting_window_seconds: int = 1800 # 跑完停著升等你的時間窗上限(預設 30 分)
|
|
62
|
-
idle_threshold_seconds: int = 2 * 60 * 60 # 🟡 閒置多久後提醒一次;0 = 關閉
|
|
63
61
|
notify_sound: bool = True
|
|
64
62
|
notify_sound_name: str = "Glass"
|
|
65
63
|
notify_ignore_dnd: bool = False
|
|
@@ -133,7 +131,6 @@ def load(path: Path | None = None) -> Config:
|
|
|
133
131
|
active_window_seconds=_as_int(raw.get("active_window_seconds"), d.active_window_seconds),
|
|
134
132
|
working_threshold_seconds=_as_int(raw.get("working_threshold_seconds"), d.working_threshold_seconds),
|
|
135
133
|
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
134
|
notify_sound=_as_bool(raw.get("notify_sound"), d.notify_sound),
|
|
138
135
|
notify_sound_name=(
|
|
139
136
|
raw["notify_sound_name"] if isinstance(raw.get("notify_sound_name"), str) else d.notify_sound_name
|
|
@@ -205,7 +202,6 @@ _SETTERS: dict[str, Callable[[str], object]] = {
|
|
|
205
202
|
"active_window_seconds": _coerce_int,
|
|
206
203
|
"working_threshold_seconds": _coerce_int,
|
|
207
204
|
"waiting_window_seconds": _coerce_int,
|
|
208
|
-
"idle_threshold_seconds": _coerce_int,
|
|
209
205
|
"notify_sound": _coerce_bool,
|
|
210
206
|
"notify_sound_name": str,
|
|
211
207
|
"notify_ignore_dnd": _coerce_bool,
|
|
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
|
|
|
@@ -8,7 +8,7 @@ msgid ""
|
|
|
8
8
|
msgstr ""
|
|
9
9
|
"Project-Id-Version: PROJECT VERSION\n"
|
|
10
10
|
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
|
|
11
|
-
"POT-Creation-Date: 2026-07-
|
|
11
|
+
"POT-Creation-Date: 2026-07-10 15:32+0800\n"
|
|
12
12
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
13
13
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
14
14
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
@@ -17,144 +17,144 @@ msgstr ""
|
|
|
17
17
|
"Content-Transfer-Encoding: 8bit\n"
|
|
18
18
|
"Generated-By: Babel 2.18.0\n"
|
|
19
19
|
|
|
20
|
-
#: src/ring/cli.py:
|
|
20
|
+
#: src/ring/cli.py:127
|
|
21
21
|
msgid "等你"
|
|
22
22
|
msgstr ""
|
|
23
23
|
|
|
24
|
-
#: src/ring/cli.py:
|
|
24
|
+
#: src/ring/cli.py:128
|
|
25
25
|
msgid "工作中"
|
|
26
26
|
msgstr ""
|
|
27
27
|
|
|
28
|
-
#: src/ring/cli.py:
|
|
28
|
+
#: src/ring/cli.py:129
|
|
29
29
|
msgid "跑完停著"
|
|
30
30
|
msgstr ""
|
|
31
31
|
|
|
32
|
-
#: src/ring/cli.py:
|
|
32
|
+
#: src/ring/cli.py:130
|
|
33
33
|
msgid "已離場"
|
|
34
34
|
msgstr ""
|
|
35
35
|
|
|
36
|
-
#: src/ring/cli.py:
|
|
36
|
+
#: src/ring/cli.py:156
|
|
37
37
|
#, python-brace-format
|
|
38
38
|
msgid "{n} 個 session 在場"
|
|
39
39
|
msgid_plural "{n} 個 session 在場"
|
|
40
40
|
msgstr[0] ""
|
|
41
41
|
msgstr[1] ""
|
|
42
42
|
|
|
43
|
-
#: src/ring/cli.py:
|
|
43
|
+
#: src/ring/cli.py:157
|
|
44
44
|
#, python-brace-format
|
|
45
45
|
msgid "{n} 個 agent process 跑著"
|
|
46
46
|
msgid_plural "{n} 個 agent process 跑著"
|
|
47
47
|
msgstr[0] ""
|
|
48
48
|
msgstr[1] ""
|
|
49
49
|
|
|
50
|
-
#: src/ring/cli.py:
|
|
50
|
+
#: src/ring/cli.py:158
|
|
51
51
|
#, python-brace-format
|
|
52
52
|
msgid "🎤 RiNG — {sess} · {proc}"
|
|
53
53
|
msgstr ""
|
|
54
54
|
|
|
55
|
-
#: src/ring/cli.py:
|
|
55
|
+
#: src/ring/cli.py:163 src/ring/cli.py:216 src/ring/tui.py:213
|
|
56
56
|
msgid "圖例"
|
|
57
57
|
msgstr ""
|
|
58
58
|
|
|
59
|
-
#: src/ring/cli.py:
|
|
59
|
+
#: src/ring/cli.py:180 src/ring/cli.py:218
|
|
60
60
|
msgid "(場館目前沒人上台)"
|
|
61
61
|
msgstr ""
|
|
62
62
|
|
|
63
|
-
#: src/ring/cli.py:
|
|
63
|
+
#: src/ring/cli.py:184 src/ring/cli.py:338 src/ring/commands/doctor.py:31
|
|
64
64
|
#: src/ring/commands/doctor.py:68 src/ring/commands/doctor.py:70
|
|
65
65
|
#: src/ring/commands/doctor.py:72 src/ring/commands/doctor.py:134
|
|
66
|
-
#: src/ring/tui.py:
|
|
66
|
+
#: src/ring/tui.py:168
|
|
67
67
|
msgid "狀態"
|
|
68
68
|
msgstr ""
|
|
69
69
|
|
|
70
|
-
#: src/ring/cli.py:
|
|
70
|
+
#: src/ring/cli.py:186 src/ring/cli.py:235 src/ring/tui.py:170
|
|
71
71
|
msgid "工具"
|
|
72
72
|
msgstr ""
|
|
73
73
|
|
|
74
|
-
#: src/ring/cli.py:
|
|
75
|
-
#: src/ring/tui.py:
|
|
74
|
+
#: src/ring/cli.py:187 src/ring/cli.py:235 src/ring/commands/stats.py:60
|
|
75
|
+
#: src/ring/tui.py:171
|
|
76
76
|
msgid "專案"
|
|
77
77
|
msgstr ""
|
|
78
78
|
|
|
79
|
-
#: src/ring/cli.py:
|
|
79
|
+
#: src/ring/cli.py:188 src/ring/cli.py:235 src/ring/tui.py:171
|
|
80
80
|
msgid "進度"
|
|
81
81
|
msgstr ""
|
|
82
82
|
|
|
83
|
-
#: src/ring/cli.py:
|
|
83
|
+
#: src/ring/cli.py:189 src/ring/cli.py:235 src/ring/tui.py:171
|
|
84
84
|
msgid "閒置"
|
|
85
85
|
msgstr ""
|
|
86
86
|
|
|
87
|
-
#: src/ring/cli.py:
|
|
87
|
+
#: src/ring/cli.py:190 src/ring/cli.py:235 src/ring/tui.py:171
|
|
88
88
|
msgid "去哪"
|
|
89
89
|
msgstr ""
|
|
90
90
|
|
|
91
|
-
#: src/ring/cli.py:
|
|
91
|
+
#: src/ring/cli.py:192 src/ring/cli.py:235 src/ring/tui.py:171
|
|
92
92
|
msgid "動作"
|
|
93
93
|
msgstr ""
|
|
94
94
|
|
|
95
|
-
#: src/ring/cli.py:
|
|
95
|
+
#: src/ring/cli.py:320
|
|
96
96
|
msgid "(內建預設)"
|
|
97
97
|
msgstr ""
|
|
98
98
|
|
|
99
|
-
#: src/ring/cli.py:
|
|
99
|
+
#: src/ring/cli.py:336
|
|
100
100
|
msgid "RiNG 設定檔"
|
|
101
101
|
msgstr ""
|
|
102
102
|
|
|
103
|
-
#: src/ring/cli.py:
|
|
103
|
+
#: src/ring/cli.py:337 src/ring/commands/doctor.py:133
|
|
104
104
|
msgid "路徑"
|
|
105
105
|
msgstr ""
|
|
106
106
|
|
|
107
|
-
#: src/ring/cli.py:
|
|
107
|
+
#: src/ring/cli.py:338 src/ring/commands/doctor.py:134
|
|
108
108
|
msgid "已存在"
|
|
109
109
|
msgstr ""
|
|
110
110
|
|
|
111
|
-
#: src/ring/cli.py:
|
|
111
|
+
#: src/ring/cli.py:338 src/ring/commands/doctor.py:134
|
|
112
112
|
msgid "不存在(全部用內建預設)"
|
|
113
113
|
msgstr ""
|
|
114
114
|
|
|
115
|
-
#: src/ring/cli.py:
|
|
115
|
+
#: src/ring/cli.py:340
|
|
116
116
|
msgid "目前生效的設定(← = 你覆寫過的)"
|
|
117
117
|
msgstr ""
|
|
118
118
|
|
|
119
|
-
#: src/ring/cli.py:
|
|
119
|
+
#: src/ring/cli.py:348
|
|
120
120
|
msgid ""
|
|
121
121
|
"用 `ring config set KEY VALUE` 改,或直接編輯上面那個檔;完整選項見 src/ring/config.py 的 "
|
|
122
122
|
"docstring。"
|
|
123
123
|
msgstr ""
|
|
124
124
|
|
|
125
|
-
#: src/ring/cli.py:
|
|
125
|
+
#: src/ring/cli.py:359 src/ring/cli.py:362
|
|
126
126
|
#, python-brace-format
|
|
127
127
|
msgid "未知的鍵:{key}"
|
|
128
128
|
msgstr ""
|
|
129
129
|
|
|
130
|
-
#: src/ring/cli.py:
|
|
130
|
+
#: src/ring/cli.py:375
|
|
131
131
|
msgid "用法:ring config get KEY"
|
|
132
132
|
msgstr ""
|
|
133
133
|
|
|
134
|
-
#: src/ring/cli.py:
|
|
134
|
+
#: src/ring/cli.py:386
|
|
135
135
|
msgid "用法:ring config set KEY VALUE"
|
|
136
136
|
msgstr ""
|
|
137
137
|
|
|
138
|
-
#: src/ring/cli.py:
|
|
138
|
+
#: src/ring/cli.py:394
|
|
139
139
|
#, python-brace-format
|
|
140
140
|
msgid "✅ 已設定 {key} = {value}({path})"
|
|
141
141
|
msgstr ""
|
|
142
142
|
|
|
143
|
-
#: src/ring/cli.py:
|
|
143
|
+
#: src/ring/cli.py:395
|
|
144
144
|
msgid "註:set 會重寫整個設定檔,原有註解不會保留。"
|
|
145
145
|
msgstr ""
|
|
146
146
|
|
|
147
|
-
#: src/ring/cli.py:
|
|
147
|
+
#: src/ring/cli.py:398
|
|
148
148
|
#, python-brace-format
|
|
149
149
|
msgid "未知的 config 動作:{action}(用 get / set,或不帶參數看目前設定)"
|
|
150
150
|
msgstr ""
|
|
151
151
|
|
|
152
|
-
#: src/ring/cli.py:
|
|
152
|
+
#: src/ring/cli.py:406
|
|
153
153
|
#, python-brace-format
|
|
154
154
|
msgid "每 {interval}s 刷新 · Ctrl-C 離場"
|
|
155
155
|
msgstr ""
|
|
156
156
|
|
|
157
|
-
#: src/ring/cli.py:
|
|
157
|
+
#: src/ring/cli.py:449
|
|
158
158
|
msgid ""
|
|
159
159
|
"\n"
|
|
160
160
|
"commands:\n"
|
|
@@ -174,28 +174,28 @@ msgid ""
|
|
|
174
174
|
" completion SHELL 印出 shell 補全腳本(zsh / bash)\n"
|
|
175
175
|
msgstr ""
|
|
176
176
|
|
|
177
|
-
#: src/ring/cli.py:
|
|
177
|
+
#: src/ring/cli.py:471
|
|
178
178
|
msgid ""
|
|
179
179
|
"usage: ring hook [PROVIDER] [--provider PROVIDER]\n"
|
|
180
180
|
"\n"
|
|
181
181
|
"從 stdin 讀 hook JSON,依 provider 正規化後寫入 RiNG registry。\n"
|
|
182
182
|
msgstr ""
|
|
183
183
|
|
|
184
|
-
#: src/ring/cli.py:
|
|
184
|
+
#: src/ring/cli.py:477
|
|
185
185
|
msgid ""
|
|
186
186
|
"usage: ring install-hooks [--dry-run]\n"
|
|
187
187
|
"\n"
|
|
188
188
|
"安裝 Claude Code / Codex hooks。\n"
|
|
189
189
|
msgstr ""
|
|
190
190
|
|
|
191
|
-
#: src/ring/cli.py:
|
|
191
|
+
#: src/ring/cli.py:483
|
|
192
192
|
msgid ""
|
|
193
193
|
"usage: ring remove-hooks [--dry-run]\n"
|
|
194
194
|
"\n"
|
|
195
195
|
"從 Claude Code / Codex hook 設定移除 RiNG 安裝的 hooks。\n"
|
|
196
196
|
msgstr ""
|
|
197
197
|
|
|
198
|
-
#: src/ring/cli.py:
|
|
198
|
+
#: src/ring/cli.py:489
|
|
199
199
|
msgid ""
|
|
200
200
|
"usage: ring config [get KEY | set KEY VALUE]\n"
|
|
201
201
|
"\n"
|
|
@@ -204,14 +204,14 @@ msgid ""
|
|
|
204
204
|
" set KEY VALUE 寫入單一設定。注意:會重寫整個設定檔,原有註解不會保留。\n"
|
|
205
205
|
msgstr ""
|
|
206
206
|
|
|
207
|
-
#: src/ring/cli.py:
|
|
207
|
+
#: src/ring/cli.py:497
|
|
208
208
|
msgid ""
|
|
209
209
|
"usage: ring focus SESSION_ID\n"
|
|
210
210
|
"\n"
|
|
211
211
|
"聚焦指定 session;若 RiNG TUI 正在執行,會回到 TUI 並選中該列。\n"
|
|
212
212
|
msgstr ""
|
|
213
213
|
|
|
214
|
-
#: src/ring/cli.py:
|
|
214
|
+
#: src/ring/cli.py:503
|
|
215
215
|
msgid ""
|
|
216
216
|
"usage: ring gc [--dry-run] [--older-than DURATION] [--all-ended]\n"
|
|
217
217
|
"\n"
|
|
@@ -223,7 +223,7 @@ msgid ""
|
|
|
223
223
|
" --all-ended 清理所有目前判定已離場的 registry\n"
|
|
224
224
|
msgstr ""
|
|
225
225
|
|
|
226
|
-
#: src/ring/cli.py:
|
|
226
|
+
#: src/ring/cli.py:514
|
|
227
227
|
msgid ""
|
|
228
228
|
"usage: ring doctor\n"
|
|
229
229
|
"\n"
|
|
@@ -231,7 +231,7 @@ msgid ""
|
|
|
231
231
|
"不寫任何檔案、不安裝、不發通知;固定回傳 0。\n"
|
|
232
232
|
msgstr ""
|
|
233
233
|
|
|
234
|
-
#: src/ring/cli.py:
|
|
234
|
+
#: src/ring/cli.py:521
|
|
235
235
|
msgid ""
|
|
236
236
|
"usage: ring digest [--since DURATION] [--format text|json]\n"
|
|
237
237
|
"\n"
|
|
@@ -242,7 +242,7 @@ msgid ""
|
|
|
242
242
|
" --format text|json 輸出格式\n"
|
|
243
243
|
msgstr ""
|
|
244
244
|
|
|
245
|
-
#: src/ring/cli.py:
|
|
245
|
+
#: src/ring/cli.py:531
|
|
246
246
|
msgid ""
|
|
247
247
|
"usage: ring stats [--since DURATION]\n"
|
|
248
248
|
"\n"
|
|
@@ -253,46 +253,46 @@ msgid ""
|
|
|
253
253
|
" --since DURATION 統計時間窗(例如 12h、7d、30d;預設 7d)\n"
|
|
254
254
|
msgstr ""
|
|
255
255
|
|
|
256
|
-
#: src/ring/cli.py:
|
|
256
|
+
#: src/ring/cli.py:541
|
|
257
257
|
msgid ""
|
|
258
258
|
"usage: ring completion zsh|bash\n"
|
|
259
259
|
"\n"
|
|
260
260
|
"印出 shell 補全腳本。zsh 放 ~/.zshrc:eval \"$(ring completion zsh)\";bash 同理。\n"
|
|
261
261
|
msgstr ""
|
|
262
262
|
|
|
263
|
-
#: src/ring/cli.py:
|
|
263
|
+
#: src/ring/cli.py:599
|
|
264
264
|
msgid "看所有 agent CLI session 上台。"
|
|
265
265
|
msgstr ""
|
|
266
266
|
|
|
267
|
-
#: src/ring/cli.py:
|
|
267
|
+
#: src/ring/cli.py:604
|
|
268
268
|
msgid "持續刷新"
|
|
269
269
|
msgstr ""
|
|
270
270
|
|
|
271
|
-
#: src/ring/cli.py:
|
|
271
|
+
#: src/ring/cli.py:605
|
|
272
272
|
msgid "watch 刷新秒數"
|
|
273
273
|
msgstr ""
|
|
274
274
|
|
|
275
|
-
#: src/ring/cli.py:
|
|
275
|
+
#: src/ring/cli.py:606
|
|
276
276
|
msgid "watch 刷新幾格後自動結束(0=無限,預設)"
|
|
277
277
|
msgstr ""
|
|
278
278
|
|
|
279
|
-
#: src/ring/cli.py:
|
|
279
|
+
#: src/ring/cli.py:607
|
|
280
280
|
msgid "連已離場的 session 也顯示"
|
|
281
281
|
msgstr ""
|
|
282
282
|
|
|
283
|
-
#: src/ring/cli.py:
|
|
283
|
+
#: src/ring/cli.py:612
|
|
284
284
|
msgid "顯示顏色圖例(--no-legend 關閉)"
|
|
285
285
|
msgstr ""
|
|
286
286
|
|
|
287
|
-
#: src/ring/cli.py:
|
|
287
|
+
#: src/ring/cli.py:614
|
|
288
288
|
msgid "語言(如 en / zh-Hant;也吃 config / RING_LANG / LANG)"
|
|
289
289
|
msgstr ""
|
|
290
290
|
|
|
291
|
-
#: src/ring/cli.py:
|
|
291
|
+
#: src/ring/cli.py:619
|
|
292
292
|
msgid "輸出格式:table(預設)/ json(機器可讀)/ oneline(status bar 單行摘要)"
|
|
293
293
|
msgstr ""
|
|
294
294
|
|
|
295
|
-
#: src/ring/cli.py:
|
|
295
|
+
#: src/ring/cli.py:625
|
|
296
296
|
#, python-brace-format
|
|
297
297
|
msgid "--format {fmt} 只能用在快照模式,不能配 --watch。"
|
|
298
298
|
msgstr ""
|
|
@@ -386,71 +386,66 @@ msgstr ""
|
|
|
386
386
|
msgid "跳過去"
|
|
387
387
|
msgstr ""
|
|
388
388
|
|
|
389
|
-
#: src/ring/tui.py:
|
|
389
|
+
#: src/ring/tui.py:189
|
|
390
390
|
msgid "💡 同專案開了多個 session,裝 hook 跳轉才精準:ring install-hooks"
|
|
391
391
|
msgstr ""
|
|
392
392
|
|
|
393
|
-
#: src/ring/tui.py:
|
|
393
|
+
#: src/ring/tui.py:191
|
|
394
394
|
msgid "↑/↓ 選一列,Enter 或 Space 跳到那個 session 的終端"
|
|
395
395
|
msgstr ""
|
|
396
396
|
|
|
397
|
-
#: src/ring/tui.py:
|
|
397
|
+
#: src/ring/tui.py:227
|
|
398
398
|
#, python-brace-format
|
|
399
399
|
msgid "🔔 {names} 在等你回話"
|
|
400
400
|
msgstr ""
|
|
401
401
|
|
|
402
|
-
#: src/ring/tui.py:
|
|
403
|
-
#, python-brace-format
|
|
404
|
-
msgid "🟡 {names} 已閒置"
|
|
405
|
-
msgstr ""
|
|
406
|
-
|
|
407
|
-
#: src/ring/tui.py:281
|
|
402
|
+
#: src/ring/tui.py:268
|
|
408
403
|
#, python-brace-format
|
|
409
404
|
msgid "→ 已跳到 {project}(來自通知)"
|
|
410
405
|
msgstr ""
|
|
411
406
|
|
|
412
|
-
#: src/ring/tui.py:
|
|
407
|
+
#: src/ring/tui.py:273
|
|
413
408
|
msgid "那個 session 已不在場"
|
|
414
409
|
msgstr ""
|
|
415
410
|
|
|
416
|
-
#: src/ring/tui.py:
|
|
411
|
+
#: src/ring/tui.py:332
|
|
417
412
|
msgid " ⚠ hook 可能失效:來源檔有更新但沒有 heartbeat"
|
|
418
413
|
msgstr ""
|
|
419
414
|
|
|
420
|
-
#: src/ring/tui.py:
|
|
415
|
+
#: src/ring/tui.py:361 src/ring/tui.py:385 src/ring/tui.py:411
|
|
421
416
|
msgid "(沒有選到 session)"
|
|
422
417
|
msgstr ""
|
|
423
418
|
|
|
424
|
-
#: src/ring/tui.py:
|
|
419
|
+
#: src/ring/tui.py:392
|
|
425
420
|
#, python-brace-format
|
|
426
421
|
msgid "→ {project}({where})"
|
|
427
422
|
msgstr ""
|
|
428
423
|
|
|
429
|
-
#: src/ring/tui.py:
|
|
424
|
+
#: src/ring/tui.py:394
|
|
430
425
|
#, python-brace-format
|
|
431
426
|
msgid "{project}:{msg}"
|
|
432
427
|
msgstr ""
|
|
433
428
|
|
|
434
|
-
#: src/ring/tui.py:
|
|
429
|
+
#: src/ring/tui.py:402
|
|
435
430
|
msgid "(沒有正在等待的 session)"
|
|
436
431
|
msgstr ""
|
|
437
432
|
|
|
438
|
-
#: src/ring/tui.py:
|
|
433
|
+
#: src/ring/tui.py:419
|
|
439
434
|
#, python-brace-format
|
|
440
435
|
msgid "再按一次 d 隱藏 {project}(有新活動會自動重新出現)"
|
|
441
436
|
msgstr ""
|
|
442
437
|
|
|
443
|
-
#: src/ring/tui.py:
|
|
438
|
+
#: src/ring/tui.py:424
|
|
444
439
|
#, python-brace-format
|
|
445
440
|
msgid "{project} 是仍在執行的 process,無法隱藏;請跳過去結束它"
|
|
446
441
|
msgstr ""
|
|
447
442
|
|
|
448
|
-
#: src/ring/tui.py:
|
|
443
|
+
#: src/ring/tui.py:432
|
|
449
444
|
#, python-brace-format
|
|
450
445
|
msgid "已隱藏 {project},並清掉 RiNG 狀態;有新活動會自動重新出現"
|
|
451
446
|
msgstr ""
|
|
452
447
|
|
|
453
|
-
#: src/ring/tui.py:
|
|
448
|
+
#: src/ring/tui.py:434
|
|
454
449
|
#, python-brace-format
|
|
455
450
|
msgid "已隱藏 {project}(沒有 RiNG registry 可清;有新活動會自動重新出現)"
|
|
456
451
|
msgstr ""
|
|
@@ -790,21 +785,11 @@ msgstr ""
|
|
|
790
785
|
msgid "沒有 focuser 接得住(裝 hook,或一個專案只開一個 session 才測得到 tty)"
|
|
791
786
|
msgstr ""
|
|
792
787
|
|
|
793
|
-
#: src/ring/notify/__init__.py:
|
|
788
|
+
#: src/ring/notify/__init__.py:139
|
|
794
789
|
msgid "💡 裝 terminal-notifier 可點擊通知直接跳回 session:brew install terminal-notifier"
|
|
795
790
|
msgstr ""
|
|
796
791
|
|
|
797
|
-
#: src/ring/notify/base.py:
|
|
798
|
-
#, python-brace-format
|
|
799
|
-
msgid "RiNG · {project} 已閒置"
|
|
800
|
-
msgstr ""
|
|
801
|
-
|
|
802
|
-
#: src/ring/notify/base.py:43
|
|
792
|
+
#: src/ring/notify/base.py:41
|
|
803
793
|
#, python-brace-format
|
|
804
794
|
msgid "RiNG · {project} 在等你回話"
|
|
805
795
|
msgstr ""
|
|
806
|
-
|
|
807
|
-
#: src/ring/notify/base.py:53
|
|
808
|
-
#, python-brace-format
|
|
809
|
-
msgid "已閒置 {duration}"
|
|
810
|
-
msgstr ""
|
|
@@ -81,17 +81,14 @@ def _select_notifier(backend: str) -> Notifier | None:
|
|
|
81
81
|
return clickable[0] if clickable else available[0]
|
|
82
82
|
|
|
83
83
|
|
|
84
|
-
def
|
|
85
|
-
"""
|
|
86
|
-
|
|
87
|
-
每則通知的標題/內文會依 ``session.status`` 分流(見 ``notify/base.py``),
|
|
88
|
-
所以 waiting 與 idle 共用這一條路徑,只差呼叫端傳進來的 session 狀態。
|
|
84
|
+
def notify_waiting(sessions: list[Session]) -> str | None:
|
|
85
|
+
"""對一批「新轉為等你」的 session 發系統通知。
|
|
89
86
|
|
|
90
87
|
依 config ``notify_backend`` 選一個 notifier 後端發送(見 ``_select_notifier``)。
|
|
91
88
|
auto 模式下若只選到不支援點擊的後端、且在 macOS 上,回傳一次性的安裝引導字串
|
|
92
89
|
(建議裝 terminal-notifier 取得點擊跳轉);其餘情況回 ``None``。失敗一律安靜吞掉。
|
|
93
90
|
|
|
94
|
-
:param sessions:
|
|
91
|
+
:param sessions: 新轉為 waiting 的 session 清單;空清單時直接回傳。
|
|
95
92
|
:returns: 安裝引導提示字串或 ``None``。
|
|
96
93
|
"""
|
|
97
94
|
if not sessions:
|
|
@@ -113,16 +110,6 @@ def _dispatch(sessions: list[Session]) -> str | None:
|
|
|
113
110
|
return None
|
|
114
111
|
|
|
115
112
|
|
|
116
|
-
def notify_waiting(sessions: list[Session]) -> str | None:
|
|
117
|
-
"""對一批「新轉為等你」的 session 發系統通知。見 ``_dispatch`` 的回傳語義。"""
|
|
118
|
-
return _dispatch(sessions)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
def notify_idle(sessions: list[Session]) -> str | None:
|
|
122
|
-
"""對一批「首次超過閒置門檻」的 session 發系統通知。見 ``_dispatch`` 的回傳語義。"""
|
|
123
|
-
return _dispatch(sessions)
|
|
124
|
-
|
|
125
|
-
|
|
126
113
|
def _send_also(sessions: list[Session], also: tuple[str, ...], primary: Notifier | None) -> None:
|
|
127
114
|
"""notify_also 的加發:主後端之外,再對指定名稱的後端各發一份。
|
|
128
115
|
|
|
@@ -157,4 +144,4 @@ def _maybe_show_install_hint() -> str | None:
|
|
|
157
144
|
return None
|
|
158
145
|
|
|
159
146
|
|
|
160
|
-
__all__ = ["Notifier", "notifiers", "
|
|
147
|
+
__all__ = ["Notifier", "notifiers", "notify_waiting", "register_notifier"]
|
|
@@ -10,7 +10,7 @@ from typing import Protocol
|
|
|
10
10
|
|
|
11
11
|
from ring.i18n import gettext as _
|
|
12
12
|
from ring.labels import get_label
|
|
13
|
-
from ring.registry import Session
|
|
13
|
+
from ring.registry import Session
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
class Notifier(Protocol):
|
|
@@ -38,8 +38,6 @@ def display_name(session: Session) -> str:
|
|
|
38
38
|
|
|
39
39
|
def notify_title(session: Session) -> str:
|
|
40
40
|
"""通知標題——「哪個 session 在等你」,全後端共用一句。"""
|
|
41
|
-
if session.status is Status.IDLE:
|
|
42
|
-
return _("RiNG · {project} 已閒置", project=display_name(session))
|
|
43
41
|
return _("RiNG · {project} 在等你回話", project=display_name(session))
|
|
44
42
|
|
|
45
43
|
|
|
@@ -49,13 +47,4 @@ def notify_message(session: Session) -> str:
|
|
|
49
47
|
標題已經說了「誰在等你」,內文就補「等什麼、去哪」,讓你看一眼就能決定要不要現在回去。
|
|
50
48
|
"""
|
|
51
49
|
location = f"📍 {session.location}"
|
|
52
|
-
if session.status is Status.IDLE:
|
|
53
|
-
return _("已閒置 {duration}", duration=_idle_duration(session.idle_for)) + f"\n{location}"
|
|
54
50
|
return f"{session.waiting_detail}\n{location}" if session.waiting_detail else location
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def _idle_duration(seconds: float) -> str:
|
|
58
|
-
s = int(max(0, seconds))
|
|
59
|
-
if s < 3600:
|
|
60
|
-
return f"{s // 60}m"
|
|
61
|
-
return f"{s // 3600}h{(s % 3600) // 60:02d}m"
|
|
@@ -855,10 +855,24 @@ def _scan_sessions(procs: list[tuple[str, str]]) -> list[Session]:
|
|
|
855
855
|
out: list[Session] = []
|
|
856
856
|
for _cwd, group in by_cwd.items():
|
|
857
857
|
group.sort(key=lambda s: s.last_active, reverse=True)
|
|
858
|
+
skey = _real(group[0].cwd) # 同一 group 內 cwd 皆相同(by_cwd 就是照 s.cwd 分組)
|
|
859
|
+
live_n = counts.get(skey, 0)
|
|
860
|
+
ordered = group
|
|
861
|
+
if 0 < live_n < len(group):
|
|
862
|
+
# 曖昧情境:同 cwd 的 transcript 數多於 live claude 數,純 mtime 排名不可靠
|
|
863
|
+
# ——已崩潰的 session 若剛好在真正還活著、但已安靜一段時間的 session 之後
|
|
864
|
+
# 才寫入最後一筆,mtime 反而「更新」,會把真正活著的那個擠出 live_n 名額
|
|
865
|
+
# (見 session-detection-review.md 症狀 1)。若能從 tmux pane 子孫 process
|
|
866
|
+
# 的 args 找到明確提到 session id 的強訊號(比照 _tmux_process_tree_targets
|
|
867
|
+
# 用在 tmux_target 配對的同一套邏輯),優先信任它決定誰佔 live 名額;沒有這
|
|
868
|
+
# 種訊號(多數非 tmux/非 --resume 情境)就 fallback 回既有 mtime 排名。
|
|
869
|
+
confirmed = _tmux_process_tree_targets(group)
|
|
870
|
+
if confirmed:
|
|
871
|
+
front = [s for s in group if s.session_id in confirmed]
|
|
872
|
+
back = [s for s in group if s.session_id not in confirmed]
|
|
873
|
+
ordered = front + back
|
|
858
874
|
# 同一 cwd 組內,各 session 依自己的當下 cwd 查 live 名額與 tty
|
|
859
|
-
for i, s in enumerate(
|
|
860
|
-
skey = _real(s.cwd)
|
|
861
|
-
live_n = counts.get(skey, 0)
|
|
875
|
+
for i, s in enumerate(ordered):
|
|
862
876
|
# 當下 cwd 只有一個 claude 時,把它的 tty 給那個活著的 session(終端跳轉用);
|
|
863
877
|
# 多個 claude 同 cwd 無法精準對應,留給 hook 模式處理。
|
|
864
878
|
uniq_tty = cwd_ttys[skey][0] if live_n == 1 and cwd_ttys.get(skey) else ""
|
|
@@ -966,10 +980,13 @@ def _hook_sessions(
|
|
|
966
980
|
s.hook_stale = _hook_heartbeat_stale(s.source_path, s.heartbeat_at, s.status)
|
|
967
981
|
# SessionEnd 沒觸發(crash)會留下幽靈檔。判定離場:
|
|
968
982
|
# 1. 該 cwd 完全沒有 live proc → 一定離場。
|
|
969
|
-
# 2. 該 cwd 的 hook row
|
|
970
|
-
#
|
|
971
|
-
#
|
|
972
|
-
#
|
|
983
|
+
# 2. 該 cwd 的 hook row 數「多於一筆」時,用 tty 挑出 tty 對不上的那幾筆標離場——
|
|
984
|
+
# 不論這筆數是否 <= live proc 數:計數只是巧合對上(例如同 cwd 剛好有跟 RiNG
|
|
985
|
+
# hook 無關的 live process 佔掉名額),不代表每筆 row 都真的還活著;row 數 > 1
|
|
986
|
+
# 時 tty 交叉比對才有意義去挑出誰是 stale 的。
|
|
987
|
+
# 3. 該 cwd 只有「單一」hook row 時,無論 tty 是否對得上都不靠 tty 殺——hook 寫進來
|
|
988
|
+
# 的 tty 不一定可靠(終端 tty 會被作業系統重配,甚至跨 session 錯置),拿它隱藏
|
|
989
|
+
# 唯一活著的 session 會讓整列憑空消失。
|
|
973
990
|
if out:
|
|
974
991
|
proc_counts: dict[tuple[str, str], int] = {}
|
|
975
992
|
proc_ttys: dict[tuple[str, str], set[str]] = {}
|
|
@@ -994,7 +1011,10 @@ def _hook_sessions(
|
|
|
994
1011
|
for s in rows:
|
|
995
1012
|
s.status = Status.ENDED
|
|
996
1013
|
continue
|
|
997
|
-
|
|
1014
|
+
|
|
1015
|
+
# 單筆 row:不靠 tty 殺,理由見上方註解 3——避免唯一活著的 session 因 tty
|
|
1016
|
+
# 重配而憑空消失(test_hook_sessions_keeps_lone_live_session_with_wrong_tty)。
|
|
1017
|
+
if len(rows) == 1:
|
|
998
1018
|
continue
|
|
999
1019
|
|
|
1000
1020
|
live_ttys = proc_ttys.get(key, set())
|
|
@@ -1003,6 +1023,12 @@ def _hook_sessions(
|
|
|
1003
1023
|
if s.tty and s.tty not in live_ttys:
|
|
1004
1024
|
s.status = Status.ENDED
|
|
1005
1025
|
|
|
1026
|
+
if len(rows) <= live_n:
|
|
1027
|
+
# 沒有多餘列要修剪,但上面的 tty 交叉比對仍然有效——即使計數巧合對上,
|
|
1028
|
+
# tty 對不上的那幾筆(例如已 crash 的舊 row)還是會被標離場,不會永遠
|
|
1029
|
+
# 靠計數巧合躲過清理。
|
|
1030
|
+
continue
|
|
1031
|
+
|
|
1006
1032
|
remaining = [s for s in rows if s.status is not Status.ENDED]
|
|
1007
1033
|
if len(remaining) > live_n:
|
|
1008
1034
|
remaining.sort(key=lambda s: s.last_active, reverse=True)
|
|
@@ -91,6 +91,13 @@ def _merge_duplicate_session(current: Session, candidate: Session) -> Session:
|
|
|
91
91
|
action-required 狀態之後未必會送出能清掉 waiting 的 hook;如果 zero-config scan
|
|
92
92
|
已看到同一 session 有更新紀錄且不再是 WAITING,就用 scan 清掉 stale waiting,
|
|
93
93
|
同時保留 hook 拿到的 tty,避免跳轉能力退化。
|
|
94
|
+
|
|
95
|
+
只看 last_active 先後還不夠:Claude Code 在送出需要權限的 tool_use 之後,會繼續
|
|
96
|
+
往同一份 transcript 追加幾筆非對話簿記紀錄(last-prompt / ai-title / mode /
|
|
97
|
+
permission-mode 等),這些紀錄一樣會推進檔案 mtime,但不代表使用者真的回應了
|
|
98
|
+
權限請求。因此還要求 candidate 的 ``_tail_kind`` 不是 ``"interrupted"``——
|
|
99
|
+
只有真人 prompt("working")或對話真的收尾("waiting")才算「使用者真的回應了」,
|
|
100
|
+
單純簿記寫入(tail 仍是 interrupted)不該觸發覆蓋。
|
|
94
101
|
"""
|
|
95
102
|
if (
|
|
96
103
|
current.source == "hook"
|
|
@@ -98,6 +105,7 @@ def _merge_duplicate_session(current: Session, candidate: Session) -> Session:
|
|
|
98
105
|
and candidate.provider == current.provider
|
|
99
106
|
and candidate.status is not Status.WAITING
|
|
100
107
|
and candidate.last_active > current.last_active
|
|
108
|
+
and candidate._tail_kind != "interrupted"
|
|
101
109
|
):
|
|
102
110
|
if not candidate.tty:
|
|
103
111
|
candidate.tty = current.tty
|
|
@@ -38,7 +38,7 @@ from ring.i18n import set_lang
|
|
|
38
38
|
from ring.ipc import clear_tui_presence, read_focus_request, write_tui_presence
|
|
39
39
|
from ring.labels import get_label, load_labels, set_label
|
|
40
40
|
from ring.registry import Session, Status, delete_session_state, hide_session, running_agent_pids
|
|
41
|
-
from ring.watcher import
|
|
41
|
+
from ring.watcher import WaitingAlertScheduler
|
|
42
42
|
|
|
43
43
|
_ORDER = (Status.WAITING, Status.WORKING, Status.IDLE, Status.ENDED)
|
|
44
44
|
|
|
@@ -126,7 +126,6 @@ class RingApp(App[None]):
|
|
|
126
126
|
self._focused_sid: str | None = None
|
|
127
127
|
cfg = get_config()
|
|
128
128
|
self._alerts = WaitingAlertScheduler(cfg.notify_repeat_seconds, cfg.notify_repeat_max)
|
|
129
|
-
self._idle_alerts = IdleAlertScheduler(cfg.idle_threshold_seconds)
|
|
130
129
|
self.title = "RiNG 🎤"
|
|
131
130
|
# 記下自己的 controlling tty,供 _poll_focus_request activate 視窗用。
|
|
132
131
|
self._own_tty: str = self._detect_own_tty()
|
|
@@ -227,18 +226,6 @@ class RingApp(App[None]):
|
|
|
227
226
|
names = ", ".join(sorted(self._display_name(s) for s in alerts))
|
|
228
227
|
self.notify(_("🔔 {names} 在等你回話", names=names), timeout=8)
|
|
229
228
|
|
|
230
|
-
def _ring_on_idle_alerts(self, alerts: list[Session]) -> None:
|
|
231
|
-
if not alerts:
|
|
232
|
-
return
|
|
233
|
-
try:
|
|
234
|
-
from ring.notify import notify_idle
|
|
235
|
-
|
|
236
|
-
notify_idle(alerts)
|
|
237
|
-
except Exception:
|
|
238
|
-
pass
|
|
239
|
-
names = ", ".join(sorted(self._display_name(s) for s in alerts))
|
|
240
|
-
self.notify(_("🟡 {names} 已閒置", names=names), timeout=8)
|
|
241
|
-
|
|
242
229
|
def _activate_own_window(self) -> None:
|
|
243
230
|
"""把 RiNG 自己的終端視窗帶到前景(best-effort,失敗安靜吞)。
|
|
244
231
|
|
|
@@ -310,7 +297,6 @@ class RingApp(App[None]):
|
|
|
310
297
|
# 這裡只留 TUI 自己的 in-app 響鈴 / 訊息列與醒目標記,不重複發系統通知。
|
|
311
298
|
alerts = self._alerts.feed(self._sessions)
|
|
312
299
|
self._ring_on_waiting_alerts(alerts)
|
|
313
|
-
self._ring_on_idle_alerts(self._idle_alerts.feed(self._sessions))
|
|
314
300
|
self.sub_title = _header(len(self._sessions), len(running_agent_pids()))
|
|
315
301
|
labels = load_labels()
|
|
316
302
|
table.clear()
|
|
@@ -105,33 +105,3 @@ class WaitingAlertScheduler:
|
|
|
105
105
|
if state.repeats_sent < len(self._repeat_seconds):
|
|
106
106
|
return now - state.first_seen >= self._repeat_seconds[state.repeats_sent]
|
|
107
107
|
return now - state.last_alert >= self._repeat_seconds[-1]
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
class IdleAlertScheduler:
|
|
111
|
-
"""🟡 閒置提醒:超過門檻只通知一次,離開 idle 後 reset。"""
|
|
112
|
-
|
|
113
|
-
def __init__(self, threshold_seconds: int, *, now: Callable[[], float] = time.time) -> None:
|
|
114
|
-
self._threshold_seconds = max(0, threshold_seconds)
|
|
115
|
-
self._now = now
|
|
116
|
-
self._alerted: set[str] = set()
|
|
117
|
-
self._primed = False
|
|
118
|
-
|
|
119
|
-
def feed(self, sessions: list[Session]) -> list[Session]:
|
|
120
|
-
current = {s.session_id: s for s in sessions if s.status is Status.IDLE}
|
|
121
|
-
if not self._threshold_seconds:
|
|
122
|
-
self._alerted = set(current)
|
|
123
|
-
self._primed = True
|
|
124
|
-
return []
|
|
125
|
-
|
|
126
|
-
due = [
|
|
127
|
-
s
|
|
128
|
-
for sid, s in current.items()
|
|
129
|
-
if sid not in self._alerted and self._now() - s.last_active >= self._threshold_seconds
|
|
130
|
-
]
|
|
131
|
-
current_ids = set(current)
|
|
132
|
-
if not self._primed:
|
|
133
|
-
self._alerted = current_ids
|
|
134
|
-
self._primed = True
|
|
135
|
-
return []
|
|
136
|
-
self._alerted = (self._alerted & current_ids) | {s.session_id for s in due}
|
|
137
|
-
return due
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|