ring-cli 0.11.0__tar.gz → 0.11.2__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.11.0 → ring_cli-0.11.2}/PKG-INFO +4 -1
- {ring_cli-0.11.0 → ring_cli-0.11.2}/README.en.md +3 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/pyproject.toml +1 -1
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/locale/en/LC_MESSAGES/ring.mo +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/locale/en/LC_MESSAGES/ring.po +3 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/locale/ring.pot +52 -47
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/registry.py +184 -26
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/sources/__init__.py +10 -0
- ring_cli-0.11.2/src/ring/sources/claude_code.py +46 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/tui.py +53 -6
- ring_cli-0.11.0/src/ring/sources/claude_code.py +0 -23
- {ring_cli-0.11.0 → ring_cli-0.11.2}/LICENSE +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/__init__.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/__main__.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/cli.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/commands/__init__.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/commands/_args.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/commands/completion.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/commands/digest.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/commands/doctor.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/commands/focus.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/commands/gc.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/commands/hook.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/commands/stats.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/config.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/focus/__init__.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/focus/applescript.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/focus/base.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/focus/iterm2.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/focus/linux_wm.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/focus/neovim.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/focus/terminal.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/focus/tmux.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/gc.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/hook.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/hook_protocol.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/i18n.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/ipc.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/labels.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/notify/__init__.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/notify/base.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/notify/command.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/notify/notify_send.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/notify/ntfy.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/notify/osascript_notifier.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/notify/terminal_notifier.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/notify/webhook.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/osascript.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/permission.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/plugins.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/sources/base.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/sources/codex.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/sources/hook_registry.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/stats.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/transcript.py +0 -0
- {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/watcher.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ring-cli
|
|
3
|
-
Version: 0.11.
|
|
3
|
+
Version: 0.11.2
|
|
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
|
|
@@ -157,6 +157,7 @@ Completes subcommands, flags, and `config set` keys; `ring focus` prompts for a
|
|
|
157
157
|
Use `↑/↓` to select, `Enter` / `Space` to jump, `p` to reply to a permission request in place, `n` to name a session, `a` to toggle ended sessions, `dd` to hide a session (it reappears automatically once it has new activity), `r` to refresh, and `q` to quit.
|
|
158
158
|
If you have vim muscle memory like I do, `j/k` move up/down and `g/G` jump to the first/last row.
|
|
159
159
|
When the selected row is 🔴 waiting, a line under the table shows **what it is concretely waiting for** (the command to run, the question asked; hook mode only).
|
|
160
|
+
Claude Code background agents carry a `⚙` badge. They have no terminal to jump to, so selecting one shows a `claude --resume` hint; completed agents are folded into ended sessions by default and remain available via `a`.
|
|
160
161
|
- Otherwise: Rich polling; without Rich, plain text.
|
|
161
162
|
|
|
162
163
|
### Jump To A Session
|
|
@@ -181,6 +182,8 @@ ones with a "from the … agent" header), and lists the numbered options verbati
|
|
|
181
182
|
After you pick one, RiNG **captures the screen again** to confirm the dialog is still there and
|
|
182
183
|
unchanged (it may have been answered while you were deciding), only then sends that single
|
|
183
184
|
digit, and re-checks that the dialog actually disappeared.
|
|
185
|
+
Once the reply is verified, the TUI immediately clears that waiting revision. Only a newer hook
|
|
186
|
+
event, such as another permission request, can mark the session as waiting again.
|
|
184
187
|
|
|
185
188
|
- **Sessions inside tmux**: reads the screen with `tmux capture-pane` and sends keys with
|
|
186
189
|
`tmux send-keys`.
|
|
@@ -132,6 +132,7 @@ Completes subcommands, flags, and `config set` keys; `ring focus` prompts for a
|
|
|
132
132
|
Use `↑/↓` to select, `Enter` / `Space` to jump, `p` to reply to a permission request in place, `n` to name a session, `a` to toggle ended sessions, `dd` to hide a session (it reappears automatically once it has new activity), `r` to refresh, and `q` to quit.
|
|
133
133
|
If you have vim muscle memory like I do, `j/k` move up/down and `g/G` jump to the first/last row.
|
|
134
134
|
When the selected row is 🔴 waiting, a line under the table shows **what it is concretely waiting for** (the command to run, the question asked; hook mode only).
|
|
135
|
+
Claude Code background agents carry a `⚙` badge. They have no terminal to jump to, so selecting one shows a `claude --resume` hint; completed agents are folded into ended sessions by default and remain available via `a`.
|
|
135
136
|
- Otherwise: Rich polling; without Rich, plain text.
|
|
136
137
|
|
|
137
138
|
### Jump To A Session
|
|
@@ -156,6 +157,8 @@ ones with a "from the … agent" header), and lists the numbered options verbati
|
|
|
156
157
|
After you pick one, RiNG **captures the screen again** to confirm the dialog is still there and
|
|
157
158
|
unchanged (it may have been answered while you were deciding), only then sends that single
|
|
158
159
|
digit, and re-checks that the dialog actually disappeared.
|
|
160
|
+
Once the reply is verified, the TUI immediately clears that waiting revision. Only a newer hook
|
|
161
|
+
event, such as another permission request, can mark the session as waiting again.
|
|
159
162
|
|
|
160
163
|
- **Sessions inside tmux**: reads the screen with `tmux capture-pane` and sends keys with
|
|
161
164
|
`tmux send-keys`.
|
|
@@ -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.11.
|
|
5
|
+
version = "0.11.2"
|
|
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"
|
|
Binary file
|
|
@@ -161,6 +161,9 @@ msgstr "terminal tab not found (it may have been closed; if it still appears aft
|
|
|
161
161
|
msgid "沒有 focuser 接得住(裝 hook,或一個專案只開一個 session 才測得到 tty)"
|
|
162
162
|
msgstr "no focuser handled it (install hooks, or keep one session per project so tty is detectable)"
|
|
163
163
|
|
|
164
|
+
msgid "背景 agent,沒有終端可跳;用 `claude --resume {session_id}` 接回"
|
|
165
|
+
msgstr "background agent, no terminal to jump to; reconnect with `claude --resume {session_id}`"
|
|
166
|
+
|
|
164
167
|
# ── install-hooks ──
|
|
165
168
|
msgid "⚠️ {path} 不是合法 JSON,先處理它再來。"
|
|
166
169
|
msgstr "⚠️ {path} is not valid JSON; fix it first."
|
|
@@ -52,7 +52,7 @@ msgstr[1] ""
|
|
|
52
52
|
msgid "🎤 RiNG — {sess} · {proc}"
|
|
53
53
|
msgstr ""
|
|
54
54
|
|
|
55
|
-
#: src/ring/cli.py:163 src/ring/cli.py:216 src/ring/tui.py:
|
|
55
|
+
#: src/ring/cli.py:163 src/ring/cli.py:216 src/ring/tui.py:279
|
|
56
56
|
msgid "圖例"
|
|
57
57
|
msgstr ""
|
|
58
58
|
|
|
@@ -63,32 +63,32 @@ msgstr ""
|
|
|
63
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:234
|
|
67
67
|
msgid "狀態"
|
|
68
68
|
msgstr ""
|
|
69
69
|
|
|
70
|
-
#: src/ring/cli.py:186 src/ring/cli.py:235 src/ring/tui.py:
|
|
70
|
+
#: src/ring/cli.py:186 src/ring/cli.py:235 src/ring/tui.py:236
|
|
71
71
|
msgid "工具"
|
|
72
72
|
msgstr ""
|
|
73
73
|
|
|
74
74
|
#: src/ring/cli.py:187 src/ring/cli.py:235 src/ring/commands/stats.py:60
|
|
75
|
-
#: src/ring/tui.py:
|
|
75
|
+
#: src/ring/tui.py:237
|
|
76
76
|
msgid "專案"
|
|
77
77
|
msgstr ""
|
|
78
78
|
|
|
79
|
-
#: src/ring/cli.py:188 src/ring/cli.py:235 src/ring/tui.py:
|
|
79
|
+
#: src/ring/cli.py:188 src/ring/cli.py:235 src/ring/tui.py:237
|
|
80
80
|
msgid "進度"
|
|
81
81
|
msgstr ""
|
|
82
82
|
|
|
83
|
-
#: src/ring/cli.py:189 src/ring/cli.py:235 src/ring/tui.py:
|
|
83
|
+
#: src/ring/cli.py:189 src/ring/cli.py:235 src/ring/tui.py:237
|
|
84
84
|
msgid "閒置"
|
|
85
85
|
msgstr ""
|
|
86
86
|
|
|
87
|
-
#: src/ring/cli.py:190 src/ring/cli.py:235 src/ring/tui.py:
|
|
87
|
+
#: src/ring/cli.py:190 src/ring/cli.py:235 src/ring/tui.py:237
|
|
88
88
|
msgid "去哪"
|
|
89
89
|
msgstr ""
|
|
90
90
|
|
|
91
|
-
#: src/ring/cli.py:192 src/ring/cli.py:235 src/ring/tui.py:
|
|
91
|
+
#: src/ring/cli.py:192 src/ring/cli.py:235 src/ring/tui.py:237
|
|
92
92
|
msgid "動作"
|
|
93
93
|
msgstr ""
|
|
94
94
|
|
|
@@ -349,167 +349,172 @@ msgstr ""
|
|
|
349
349
|
msgid "⚠️ ring plugin '{name}' 載入失敗:{error}"
|
|
350
350
|
msgstr ""
|
|
351
351
|
|
|
352
|
-
#: src/ring/tui.py:
|
|
352
|
+
#: src/ring/tui.py:51
|
|
353
|
+
#, python-brace-format
|
|
354
|
+
msgid "背景 agent,沒有終端可跳;用 `claude --resume {session_id}` 接回"
|
|
355
|
+
msgstr ""
|
|
356
|
+
|
|
357
|
+
#: src/ring/tui.py:96
|
|
353
358
|
#, python-brace-format
|
|
354
359
|
msgid "為 {project} 命名(Enter 存、Esc 取消、清空移除)"
|
|
355
360
|
msgstr ""
|
|
356
361
|
|
|
357
|
-
#: src/ring/tui.py:
|
|
362
|
+
#: src/ring/tui.py:97
|
|
358
363
|
msgid "這個 session 在做什麼…"
|
|
359
364
|
msgstr ""
|
|
360
365
|
|
|
361
|
-
#: src/ring/tui.py:
|
|
366
|
+
#: src/ring/tui.py:141
|
|
362
367
|
#, python-brace-format
|
|
363
368
|
msgid "回覆 {project} 的權限請求"
|
|
364
369
|
msgstr ""
|
|
365
370
|
|
|
366
|
-
#: src/ring/tui.py:
|
|
371
|
+
#: src/ring/tui.py:146
|
|
367
372
|
msgid "Enter 或數字鍵送出、Esc 取消"
|
|
368
373
|
msgstr ""
|
|
369
374
|
|
|
370
|
-
#: src/ring/tui.py:
|
|
375
|
+
#: src/ring/tui.py:169
|
|
371
376
|
msgid "離場"
|
|
372
377
|
msgstr ""
|
|
373
378
|
|
|
374
|
-
#: src/ring/tui.py:
|
|
379
|
+
#: src/ring/tui.py:170
|
|
375
380
|
msgid "刷新"
|
|
376
381
|
msgstr ""
|
|
377
382
|
|
|
378
|
-
#: src/ring/tui.py:
|
|
383
|
+
#: src/ring/tui.py:171
|
|
379
384
|
msgid "含已離場"
|
|
380
385
|
msgstr ""
|
|
381
386
|
|
|
382
|
-
#: src/ring/tui.py:
|
|
387
|
+
#: src/ring/tui.py:172
|
|
383
388
|
msgid "命名"
|
|
384
389
|
msgstr ""
|
|
385
390
|
|
|
386
|
-
#: src/ring/tui.py:
|
|
391
|
+
#: src/ring/tui.py:173
|
|
387
392
|
msgid "隱藏"
|
|
388
393
|
msgstr ""
|
|
389
394
|
|
|
390
|
-
#: src/ring/tui.py:
|
|
395
|
+
#: src/ring/tui.py:174
|
|
391
396
|
msgid "最久等待"
|
|
392
397
|
msgstr ""
|
|
393
398
|
|
|
394
|
-
#: src/ring/tui.py:
|
|
399
|
+
#: src/ring/tui.py:175
|
|
395
400
|
msgid "回覆權限"
|
|
396
401
|
msgstr ""
|
|
397
402
|
|
|
398
|
-
#: src/ring/tui.py:
|
|
403
|
+
#: src/ring/tui.py:176 src/ring/tui.py:177
|
|
399
404
|
msgid "跳過去"
|
|
400
405
|
msgstr ""
|
|
401
406
|
|
|
402
|
-
#: src/ring/tui.py:
|
|
407
|
+
#: src/ring/tui.py:255
|
|
403
408
|
msgid "💡 同專案開了多個 session,裝 hook 跳轉才精準:ring install-hooks"
|
|
404
409
|
msgstr ""
|
|
405
410
|
|
|
406
|
-
#: src/ring/tui.py:
|
|
411
|
+
#: src/ring/tui.py:257
|
|
407
412
|
msgid "↑/↓ 選一列,Enter 或 Space 跳到那個 session 的終端"
|
|
408
413
|
msgstr ""
|
|
409
414
|
|
|
410
|
-
#: src/ring/tui.py:
|
|
415
|
+
#: src/ring/tui.py:293
|
|
411
416
|
#, python-brace-format
|
|
412
417
|
msgid "🔔 {names} 在等你回話"
|
|
413
418
|
msgstr ""
|
|
414
419
|
|
|
415
|
-
#: src/ring/tui.py:
|
|
420
|
+
#: src/ring/tui.py:334
|
|
416
421
|
#, python-brace-format
|
|
417
422
|
msgid "→ 已跳到 {project}(來自通知)"
|
|
418
423
|
msgstr ""
|
|
419
424
|
|
|
420
|
-
#: src/ring/tui.py:
|
|
425
|
+
#: src/ring/tui.py:339
|
|
421
426
|
msgid "那個 session 已不在場"
|
|
422
427
|
msgstr ""
|
|
423
428
|
|
|
424
|
-
#: src/ring/tui.py:
|
|
429
|
+
#: src/ring/tui.py:400
|
|
425
430
|
msgid " ⚠ hook 可能失效:來源檔有更新但沒有 heartbeat"
|
|
426
431
|
msgstr ""
|
|
427
432
|
|
|
428
|
-
#: src/ring/tui.py:
|
|
429
|
-
#: src/ring/tui.py:
|
|
433
|
+
#: src/ring/tui.py:431 src/ring/tui.py:455 src/ring/tui.py:494
|
|
434
|
+
#: src/ring/tui.py:562
|
|
430
435
|
msgid "(沒有選到 session)"
|
|
431
436
|
msgstr ""
|
|
432
437
|
|
|
433
|
-
#: src/ring/tui.py:
|
|
438
|
+
#: src/ring/tui.py:462 src/ring/tui.py:470 src/ring/tui.py:500
|
|
434
439
|
#, python-brace-format
|
|
435
|
-
msgid "
|
|
440
|
+
msgid "{project}:{msg}"
|
|
436
441
|
msgstr ""
|
|
437
442
|
|
|
438
|
-
#: src/ring/tui.py:
|
|
443
|
+
#: src/ring/tui.py:468
|
|
439
444
|
#, python-brace-format
|
|
440
|
-
msgid "{project}
|
|
445
|
+
msgid "→ {project}({where})"
|
|
441
446
|
msgstr ""
|
|
442
447
|
|
|
443
|
-
#: src/ring/tui.py:
|
|
448
|
+
#: src/ring/tui.py:478
|
|
444
449
|
msgid "(沒有正在等待的 session)"
|
|
445
450
|
msgstr ""
|
|
446
451
|
|
|
447
|
-
#: src/ring/tui.py:
|
|
452
|
+
#: src/ring/tui.py:504
|
|
448
453
|
#, python-brace-format
|
|
449
454
|
msgid "{project}:沒有 tmux 座標,且非 macOS 上的 iTerm2 session,無法就地回覆"
|
|
450
455
|
msgstr ""
|
|
451
456
|
|
|
452
|
-
#: src/ring/tui.py:
|
|
457
|
+
#: src/ring/tui.py:511
|
|
453
458
|
#, python-brace-format
|
|
454
459
|
msgid "{project}:讀不到 {backend} 畫面"
|
|
455
460
|
msgstr ""
|
|
456
461
|
|
|
457
|
-
#: src/ring/tui.py:
|
|
462
|
+
#: src/ring/tui.py:515
|
|
458
463
|
#, python-brace-format
|
|
459
464
|
msgid "{project}:畫面上沒有可辨識的權限對話框,未送出任何按鍵"
|
|
460
465
|
msgstr ""
|
|
461
466
|
|
|
462
|
-
#: src/ring/tui.py:
|
|
467
|
+
#: src/ring/tui.py:544
|
|
463
468
|
#, python-brace-format
|
|
464
469
|
msgid "→ {project}:已回覆權限({option})"
|
|
465
470
|
msgstr ""
|
|
466
471
|
|
|
467
|
-
#: src/ring/tui.py:
|
|
472
|
+
#: src/ring/tui.py:548
|
|
468
473
|
#, python-brace-format
|
|
469
474
|
msgid "{project}:權限對話框已不在,未送出任何按鍵"
|
|
470
475
|
msgstr ""
|
|
471
476
|
|
|
472
|
-
#: src/ring/tui.py:
|
|
477
|
+
#: src/ring/tui.py:549
|
|
473
478
|
#, python-brace-format
|
|
474
479
|
msgid "{project}:對話框內容已變化,未送出;請再按一次 p"
|
|
475
480
|
msgstr ""
|
|
476
481
|
|
|
477
|
-
#: src/ring/tui.py:
|
|
482
|
+
#: src/ring/tui.py:550
|
|
478
483
|
#, python-brace-format
|
|
479
484
|
msgid "{project}:tmux send-keys 失敗,沒能回覆"
|
|
480
485
|
msgstr ""
|
|
481
486
|
|
|
482
|
-
#: src/ring/tui.py:
|
|
487
|
+
#: src/ring/tui.py:551
|
|
483
488
|
#, python-brace-format
|
|
484
489
|
msgid "{project}:已送出但對話框還在,請跳過去確認"
|
|
485
490
|
msgstr ""
|
|
486
491
|
|
|
487
|
-
#: src/ring/tui.py:
|
|
492
|
+
#: src/ring/tui.py:553
|
|
488
493
|
#, python-brace-format
|
|
489
494
|
msgid "{project}:對話框已不在,數字落進輸入框,已補 Backspace 清掉"
|
|
490
495
|
msgstr ""
|
|
491
496
|
|
|
492
|
-
#: src/ring/tui.py:
|
|
497
|
+
#: src/ring/tui.py:555
|
|
493
498
|
#, python-brace-format
|
|
494
499
|
msgid "{project}:已送出但拿不到畫面驗證,請跳過去確認"
|
|
495
500
|
msgstr ""
|
|
496
501
|
|
|
497
|
-
#: src/ring/tui.py:
|
|
502
|
+
#: src/ring/tui.py:570
|
|
498
503
|
#, python-brace-format
|
|
499
504
|
msgid "再按一次 d 隱藏 {project}(有新活動會自動重新出現)"
|
|
500
505
|
msgstr ""
|
|
501
506
|
|
|
502
|
-
#: src/ring/tui.py:
|
|
507
|
+
#: src/ring/tui.py:575
|
|
503
508
|
#, python-brace-format
|
|
504
509
|
msgid "{project} 是仍在執行的 process,無法隱藏;請跳過去結束它"
|
|
505
510
|
msgstr ""
|
|
506
511
|
|
|
507
|
-
#: src/ring/tui.py:
|
|
512
|
+
#: src/ring/tui.py:583
|
|
508
513
|
#, python-brace-format
|
|
509
514
|
msgid "已隱藏 {project},並清掉 RiNG 狀態;有新活動會自動重新出現"
|
|
510
515
|
msgstr ""
|
|
511
516
|
|
|
512
|
-
#: src/ring/tui.py:
|
|
517
|
+
#: src/ring/tui.py:585
|
|
513
518
|
#, python-brace-format
|
|
514
519
|
msgid "已隱藏 {project}(沒有 RiNG registry 可清;有新活動會自動重新出現)"
|
|
515
520
|
msgstr ""
|
|
@@ -304,6 +304,7 @@ class Session:
|
|
|
304
304
|
provider: str = ""
|
|
305
305
|
waiting_kind: str = "" # permission | question | plan | idle;空代表非 WAITING 或舊 registry
|
|
306
306
|
waiting_detail: str = "" # 🔴 等你時「到底在等什麼」(權限指令 / 問題內容;hook 模式才有)
|
|
307
|
+
kind: str = "foreground" # "foreground" | "agent";背景 agent(bg-pty-host 承載)由 discover 貼標
|
|
307
308
|
_tail_kind: str = field(default="none", repr=False, compare=False) # 內部:scan 路徑暫存對話尾判定
|
|
308
309
|
origin_cwd: str = "" # 開場 cwd(session 第一筆帶 cwd 紀錄),用於歸屬;空時 fallback 到 cwd
|
|
309
310
|
|
|
@@ -518,48 +519,183 @@ def _tmux_process_tree_targets(sessions: list[Session]) -> dict[str, str]:
|
|
|
518
519
|
|
|
519
520
|
_pids_cache: tuple[float, list[int]] = (-1.0, [])
|
|
520
521
|
_codex_pids_cache: tuple[float, list[int]] = (-1.0, [])
|
|
522
|
+
_ps_claude_snapshot_cache: tuple[float, str] = (-1.0, "")
|
|
523
|
+
_bg_agent_session_ids_cache: tuple[float, frozenset[str]] = (-1.0, frozenset())
|
|
521
524
|
|
|
525
|
+
# args 內任一出現即可判定「這是 claude 安裝二進位在跑」的路徑標記。ps comm 對
|
|
526
|
+
# daemon-exec 的二進位常被截斷(如 `/Users/weilee/.l`),單看 comm 不可靠。
|
|
527
|
+
_CLAUDE_PATH_MARKERS = ("ClaudeCode.app", "claude/versions/", "/.claude/")
|
|
522
528
|
|
|
523
|
-
|
|
524
|
-
|
|
529
|
+
|
|
530
|
+
def _is_claude_session_line(comm: str, args: str) -> bool:
|
|
531
|
+
"""判定一行 ``ps`` 輸出是否為 claude session process(承載者或子行程皆算)。
|
|
532
|
+
|
|
533
|
+
comm basename 為 ``claude`` 是最常見的情況;但 daemon 承載的 process,ps comm
|
|
534
|
+
會被截斷成本機路徑片段(例如 ``/Users/weilee/.l``),此時改看 args 是否含可辨識
|
|
535
|
+
的 claude 安裝路徑標記,或 args 內任一 token 的 basename 為 ``claude``
|
|
536
|
+
(args 首 token 有時只是版本號如 ``2.1.187``,不能只看 args[0])。第三個 fallback
|
|
537
|
+
另外要求 args 內必須有 ``--session-id``,否則像 ``grep -r claude .``、
|
|
538
|
+
``less claude`` 這類完全無關但恰好帶 ``claude`` 字面的 process 會被誤收;
|
|
539
|
+
真正被截斷 comm 的 claude session(daemon 承載者與其子行程)必然帶
|
|
540
|
+
``--session-id``,所以這個限定不會犧牲 fallback 能力。
|
|
541
|
+
"""
|
|
542
|
+
if os.path.basename(comm.strip()) == "claude":
|
|
543
|
+
return True
|
|
544
|
+
if any(marker in args for marker in _CLAUDE_PATH_MARKERS):
|
|
545
|
+
return True
|
|
546
|
+
tokens = args.split()
|
|
547
|
+
if "--session-id" not in tokens:
|
|
548
|
+
return False
|
|
549
|
+
return any(os.path.basename(tok) == "claude" for tok in tokens)
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _ps_claude_snapshot() -> str:
|
|
553
|
+
"""``ps -Ao pid,comm,args`` 的短快取原始輸出,供多個 claude proc 判定函式共用。"""
|
|
554
|
+
global _ps_claude_snapshot_cache
|
|
525
555
|
now = time.monotonic()
|
|
526
|
-
if 0.0 <= now -
|
|
527
|
-
return
|
|
556
|
+
if 0.0 <= now - _ps_claude_snapshot_cache[0] <= _SUBPROCESS_CACHE_TTL:
|
|
557
|
+
return _ps_claude_snapshot_cache[1]
|
|
528
558
|
try:
|
|
529
559
|
out = subprocess.run(["ps", "-Ao", "pid,comm,args"], capture_output=True, text=True, timeout=3).stdout
|
|
530
560
|
except (OSError, subprocess.SubprocessError):
|
|
531
561
|
out = ""
|
|
532
|
-
|
|
562
|
+
_ps_claude_snapshot_cache = (now, out)
|
|
563
|
+
return out
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _parse_ps_claude_lines(out: str) -> list[tuple[int, str, bool]]:
|
|
567
|
+
"""把 ``ps`` 輸出解析成 claude session 行:``(pid, args, is_bg_pty_host)``。
|
|
568
|
+
|
|
569
|
+
不論是否為背景 process(daemon / bg-spare / bg-pty-host)都收進來——背景判定
|
|
570
|
+
交給呼叫端各自決定要不要濾除;``_hook_sessions`` 的活性判定與
|
|
571
|
+
``running_claude_pids`` 對「該濾誰」的答案不同,不能在這裡先幫忙決定。
|
|
572
|
+
"""
|
|
573
|
+
entries: list[tuple[int, str, bool]] = []
|
|
533
574
|
for line in out.splitlines()[1:]:
|
|
534
575
|
parts = line.split(None, 2)
|
|
535
|
-
if len(parts)
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
576
|
+
if len(parts) < 2:
|
|
577
|
+
continue
|
|
578
|
+
comm = parts[1].strip()
|
|
579
|
+
args = parts[2] if len(parts) == 3 else ""
|
|
580
|
+
if not _is_claude_session_line(comm, args):
|
|
581
|
+
continue
|
|
582
|
+
try:
|
|
583
|
+
pid = int(parts[0])
|
|
584
|
+
except ValueError:
|
|
585
|
+
continue
|
|
586
|
+
entries.append((pid, args, "--bg-pty-host" in args.split()))
|
|
587
|
+
return entries
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
def _arg_session_id(args: str) -> str | None:
|
|
591
|
+
"""解析 args 裡 ``--session-id`` 後面的那個 token;沒有就回 ``None``。"""
|
|
592
|
+
tokens = args.split()
|
|
593
|
+
for i, tok in enumerate(tokens):
|
|
594
|
+
if tok == "--session-id" and i + 1 < len(tokens):
|
|
595
|
+
return tokens[i + 1]
|
|
596
|
+
return None
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
def running_claude_pids() -> list[int]:
|
|
600
|
+
"""目前活著、使用者可聚焦的 claude CLI pid(daemon / bg-spare / bg 暖機承載者濾除)。
|
|
601
|
+
|
|
602
|
+
承載者(``--bg-pty-host`` + ``--session-id``)與其子行程常成對出現、共用同一個
|
|
603
|
+
session-id:兩者都算「真 session」不濾除,但只留一個 pid,偏好子行程——子行程
|
|
604
|
+
的 cwd(lsof 量得到)誠實,承載者的 cwd 常是 daemon 自己的 cwd,非專案目錄。
|
|
605
|
+
只有承載者、沒有子行程時(fallback)仍保留承載者這個 pid,好過整個 session 消失。
|
|
606
|
+
"""
|
|
607
|
+
global _pids_cache
|
|
608
|
+
now = time.monotonic()
|
|
609
|
+
if 0.0 <= now - _pids_cache[0] <= _SUBPROCESS_CACHE_TTL:
|
|
610
|
+
return _pids_cache[1]
|
|
611
|
+
entries = _parse_ps_claude_lines(_ps_claude_snapshot())
|
|
612
|
+
|
|
613
|
+
pids: list[int] = []
|
|
614
|
+
sid_index: dict[str, int] = {} # session-id → 該 pid 在 pids 裡的位置,供子行程晚到時換掉
|
|
615
|
+
sid_is_bg_host: dict[str, bool] = {}
|
|
616
|
+
for pid, args, is_bg_host in entries:
|
|
617
|
+
if _is_claude_background_process(args):
|
|
618
|
+
continue
|
|
619
|
+
session_id = _arg_session_id(args)
|
|
620
|
+
if session_id is None:
|
|
621
|
+
pids.append(pid)
|
|
622
|
+
continue
|
|
623
|
+
if session_id not in sid_index:
|
|
624
|
+
sid_index[session_id] = len(pids)
|
|
625
|
+
sid_is_bg_host[session_id] = is_bg_host
|
|
626
|
+
pids.append(pid)
|
|
627
|
+
elif sid_is_bg_host[session_id] and not is_bg_host:
|
|
628
|
+
# 子行程晚到:換掉先記到的承載者 pid,偏好子行程(cwd 誠實)。
|
|
629
|
+
pids[sid_index[session_id]] = pid
|
|
630
|
+
sid_is_bg_host[session_id] = False
|
|
631
|
+
|
|
543
632
|
_pids_cache = (now, pids)
|
|
544
633
|
return pids
|
|
545
634
|
|
|
546
635
|
|
|
636
|
+
def running_foreground_claude_pids() -> list[int]:
|
|
637
|
+
"""目前仍有可聚焦終端的 Claude session pid。
|
|
638
|
+
|
|
639
|
+
``running_claude_pids`` 也包含 agents mode 的背景 session,因為 scan 需要靠它們
|
|
640
|
+
找到對應 transcript;但 hook registry 不能拿背景 agent 的 cwd/數量替同專案的
|
|
641
|
+
舊前景 row 證明存活,否則一個 agent 就可能讓 crash 數小時的 session 繼續顯示。
|
|
642
|
+
"""
|
|
643
|
+
bg_ids = background_agent_session_ids()
|
|
644
|
+
if not bg_ids:
|
|
645
|
+
return running_claude_pids()
|
|
646
|
+
args_by_pid = {pid: args for pid, args, _is_bg_host in _parse_ps_claude_lines(_ps_claude_snapshot())}
|
|
647
|
+
return [
|
|
648
|
+
pid
|
|
649
|
+
for pid in running_claude_pids()
|
|
650
|
+
if (session_id := _arg_session_id(args_by_pid.get(pid, ""))) is None or session_id not in bg_ids
|
|
651
|
+
]
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def background_agent_session_ids() -> set[str]:
|
|
655
|
+
"""所有背景 agent(``--bg-pty-host`` 承載且已載入真 session)的 session-id 集合。
|
|
656
|
+
|
|
657
|
+
給 ``discover_sessions()`` 對應貼 ``kind="agent"`` 標籤用。與 ``running_claude_pids``
|
|
658
|
+
共用同一份 ``ps`` 快照(``_ps_claude_snapshot``),不額外多打一次 ``ps``。
|
|
659
|
+
"""
|
|
660
|
+
global _bg_agent_session_ids_cache
|
|
661
|
+
now = time.monotonic()
|
|
662
|
+
if 0.0 <= now - _bg_agent_session_ids_cache[0] <= _SUBPROCESS_CACHE_TTL:
|
|
663
|
+
return set(_bg_agent_session_ids_cache[1])
|
|
664
|
+
entries = _parse_ps_claude_lines(_ps_claude_snapshot())
|
|
665
|
+
ids = frozenset(
|
|
666
|
+
session_id
|
|
667
|
+
for _pid, args, is_bg_host in entries
|
|
668
|
+
if is_bg_host and (session_id := _arg_session_id(args)) is not None
|
|
669
|
+
)
|
|
670
|
+
_bg_agent_session_ids_cache = (now, ids)
|
|
671
|
+
return set(ids)
|
|
672
|
+
|
|
673
|
+
|
|
547
674
|
def _is_claude_background_process(args: str) -> bool:
|
|
548
|
-
"""Claude daemon / bg pty host / bg-spare
|
|
675
|
+
"""Claude daemon / bg pty host 暖機承載者 / bg-spare 不是使用者可聚焦的 CLI session。
|
|
549
676
|
|
|
550
677
|
``--bg-spare`` 是 Claude Code 預熱的備用 process(供下一個 ``claude`` 呼叫快速接手),
|
|
551
|
-
|
|
552
|
-
session 列上看板,冒出幽靈列。token 形狀(`--bg-spare
|
|
553
|
-
|
|
554
|
-
process,掃描時機是巧合——它壽命短、隨用隨滅):
|
|
678
|
+
跟尚未載入真 session 的 ``--bg-pty-host`` 承載者一樣不代表真正的使用者 session,卻會
|
|
679
|
+
被 ``_claude_procs`` 合成假 session 列上看板,冒出幽靈列。token 形狀(`--bg-spare`,
|
|
680
|
+
`--` flag,不是位置參數)取自本機 claude CLI 2.1.206 二進位的 strings 掃描(無法用
|
|
681
|
+
``ps`` 現場逮到活體 bg-spare process,掃描時機是巧合——它壽命短、隨用隨滅):
|
|
555
682
|
``[a,...l,"--bg-pty-host",r,"200","50","--",a,...l,"--bg-spare",n]`` spawn 呼叫,
|
|
556
683
|
以及 bg-spare process 自己啟動時對 ``process.argv`` 做的
|
|
557
684
|
``e.includes("--bg-spare", t+1)`` 檢查,兩處都證實是 ``--`` 前綴的 flag token。
|
|
685
|
+
|
|
686
|
+
``--bg-pty-host`` 本身不再一律濾除:暖機階段(spare sock,無 ``--session-id``)仍
|
|
687
|
+
濾除;一旦掛上真正的 ``--session-id``(使用者已進入 agents、真背景 session),就不
|
|
688
|
+
再視為背景 process——那是一個真人在跑的背景 agent,該讓它現身,只是要標成
|
|
689
|
+
``kind="agent"``(見 ``background_agent_session_ids``)。
|
|
558
690
|
"""
|
|
559
691
|
tokens = args.split()
|
|
560
692
|
if len(tokens) >= 3 and tokens[1:3] == ["daemon", "run"]:
|
|
561
693
|
return True
|
|
562
|
-
|
|
694
|
+
if "--bg-spare" in tokens:
|
|
695
|
+
return True
|
|
696
|
+
if "--bg-pty-host" in tokens:
|
|
697
|
+
return _arg_session_id(args) is None
|
|
698
|
+
return False
|
|
563
699
|
|
|
564
700
|
|
|
565
701
|
def running_codex_pids() -> list[int]:
|
|
@@ -568,13 +704,16 @@ def running_codex_pids() -> list[int]:
|
|
|
568
704
|
if 0.0 <= now - _codex_pids_cache[0] <= _SUBPROCESS_CACHE_TTL:
|
|
569
705
|
return _codex_pids_cache[1]
|
|
570
706
|
try:
|
|
571
|
-
out = subprocess.run(["ps", "-Ao", "pid
|
|
707
|
+
out = subprocess.run(["ps", "-Ao", "pid=,comm=,args="], capture_output=True, text=True, timeout=3).stdout
|
|
572
708
|
except (OSError, subprocess.SubprocessError):
|
|
573
709
|
out = ""
|
|
574
710
|
pids: list[int] = []
|
|
575
|
-
for line in out.splitlines()
|
|
576
|
-
parts = line.split(None,
|
|
577
|
-
if len(parts)
|
|
711
|
+
for line in out.splitlines():
|
|
712
|
+
parts = line.split(None, 2)
|
|
713
|
+
if len(parts) >= 2 and os.path.basename(parts[1].strip()) == "codex":
|
|
714
|
+
args = parts[2] if len(parts) == 3 else ""
|
|
715
|
+
if _is_codex_internal_process(args):
|
|
716
|
+
continue
|
|
578
717
|
try:
|
|
579
718
|
pids.append(int(parts[0]))
|
|
580
719
|
except ValueError:
|
|
@@ -583,6 +722,18 @@ def running_codex_pids() -> list[int]:
|
|
|
583
722
|
return pids
|
|
584
723
|
|
|
585
724
|
|
|
725
|
+
def _is_codex_internal_process(args: str) -> bool:
|
|
726
|
+
"""Codex app 為工具呼叫啟動的同名內部 process,不是獨立互動 session。"""
|
|
727
|
+
tokens = args.split()
|
|
728
|
+
if not tokens:
|
|
729
|
+
return False
|
|
730
|
+
try:
|
|
731
|
+
command_index = next(i for i, token in enumerate(tokens) if os.path.basename(token) == "codex")
|
|
732
|
+
except StopIteration:
|
|
733
|
+
return False
|
|
734
|
+
return command_index + 1 < len(tokens) and tokens[command_index + 1] in {"app-server", "sandbox"}
|
|
735
|
+
|
|
736
|
+
|
|
586
737
|
def running_agent_pids() -> list[int]:
|
|
587
738
|
"""所有內建來源看得到的 live agent CLI 行程。"""
|
|
588
739
|
return [*running_claude_pids(), *running_codex_pids()]
|
|
@@ -658,14 +809,15 @@ def _pid_tty(pid: int) -> str:
|
|
|
658
809
|
|
|
659
810
|
|
|
660
811
|
def _claude_procs() -> list[tuple[str, str]]:
|
|
661
|
-
"""
|
|
812
|
+
"""每個可聚焦的前景 Claude:(cwd, tty)。cwd 判活躍/分流,tty 給終端跳轉。
|
|
662
813
|
|
|
663
|
-
|
|
814
|
+
背景 agent 不在這裡按 cwd 配對;它們由 source 依 process args 的 session id 精準
|
|
815
|
+
認領,避免背景 process 的啟動 cwd 讓同專案舊 transcript 誤判成仍在場。
|
|
664
816
|
同一個 cwd 可能同時開好幾個 session,所以之後在 cwd 群組裡只有 mtime 最新的
|
|
665
817
|
這幾個算活著,其餘同專案的舊 session=已離場。
|
|
666
818
|
"""
|
|
667
819
|
procs: list[tuple[str, str]] = []
|
|
668
|
-
for pid in
|
|
820
|
+
for pid in running_foreground_claude_pids():
|
|
669
821
|
cwd = _pid_cwd(pid)
|
|
670
822
|
if cwd:
|
|
671
823
|
procs.append((cwd, _pid_tty(pid)))
|
|
@@ -1035,11 +1187,17 @@ def _hook_sessions(
|
|
|
1035
1187
|
proc_ttys.setdefault(key, set()).add(tty)
|
|
1036
1188
|
proc_cwds_by_provider.setdefault(pk, []).append(real_cwd)
|
|
1037
1189
|
|
|
1190
|
+
# 背景 agent 的 process 沒有可聚焦終端,不能拿來替同 cwd 的舊前景 hook row
|
|
1191
|
+
# 證明存活;它只精準認領 args 裡明載的 session id。Claude scan 仍使用包含背景
|
|
1192
|
+
# agent 的 session-id 配對,因此沒有 hook row 的 agent 也不會從看板消失。
|
|
1193
|
+
background_ids = background_agent_session_ids()
|
|
1038
1194
|
rows_by_key: dict[tuple[str, str], list[Session]] = {}
|
|
1039
1195
|
for s in out:
|
|
1040
1196
|
pk = _canonical_provider(s.provider)
|
|
1041
1197
|
if pk not in _PROVIDER_PROCS:
|
|
1042
1198
|
continue # 沒有 proc 偵測器 → 無法驗活性 → fail-open,交給 SessionEnd
|
|
1199
|
+
if pk == "claude-code" and s.session_id in background_ids:
|
|
1200
|
+
continue
|
|
1043
1201
|
rows_by_key.setdefault((pk, _real(s.cwd)), []).append(s)
|
|
1044
1202
|
|
|
1045
1203
|
for key, rows in rows_by_key.items():
|
|
@@ -56,6 +56,16 @@ def discover_sessions() -> list[Session]:
|
|
|
56
56
|
found.append(s)
|
|
57
57
|
# 否則:仍在隱藏保留期內、沒有新活動 → 不收進看板。
|
|
58
58
|
|
|
59
|
+
bg_agent_ids = registry.background_agent_session_ids()
|
|
60
|
+
for s in found:
|
|
61
|
+
if s.session_id in bg_agent_ids:
|
|
62
|
+
s.kind = "agent"
|
|
63
|
+
# 背景 agent 跑完後不會像前景 session 一樣停在終端等下一輪輸入;它只會由
|
|
64
|
+
# daemon 暫留 process。繼續顯示成「跑完停著」既無法跳轉也無事可處理,反而
|
|
65
|
+
# 把已完成工作混進在場清單。歸入 ENDED 後預設收起,`--all` / TUI 的 a 仍可查。
|
|
66
|
+
if s.status is Status.IDLE:
|
|
67
|
+
s.status = Status.ENDED
|
|
68
|
+
|
|
59
69
|
bound_targets = registry._tmux_pane_targets()
|
|
60
70
|
process_targets = registry._tmux_process_tree_targets(found)
|
|
61
71
|
targets = registry._tmux_targets()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""Claude Code:掃 ``~/.claude/projects`` 的 JSONL。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
|
|
7
|
+
import ring.registry as registry
|
|
8
|
+
from ring.registry import Session
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _activate_background_agents(sessions: list[Session], agent_ids: set[str]) -> None:
|
|
12
|
+
"""只依明確 session id 恢復仍有 process 的背景 agent,不用它們的 cwd 猜前景活性。"""
|
|
13
|
+
now = time.time()
|
|
14
|
+
for session in sessions:
|
|
15
|
+
if session.session_id not in agent_ids:
|
|
16
|
+
continue
|
|
17
|
+
idle = now - session.last_active
|
|
18
|
+
session.status = registry._scan_status(idle)
|
|
19
|
+
session.status = registry._apply_waiting(
|
|
20
|
+
session.status,
|
|
21
|
+
idle,
|
|
22
|
+
session._tail_kind,
|
|
23
|
+
registry.WAITING_WINDOW_SECONDS,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ClaudeCodeSource:
|
|
28
|
+
name = "claude-code"
|
|
29
|
+
|
|
30
|
+
def discover(self) -> list[Session]:
|
|
31
|
+
# 背景 agent 的 process cwd 可能仍是 daemon/啟動目錄,不能拿來按 cwd 猜哪些
|
|
32
|
+
# 前景 transcript 活著。前景先只用可聚焦 process 判活,背景再按 session id 認領。
|
|
33
|
+
procs = registry._claude_procs()
|
|
34
|
+
agent_ids = registry.background_agent_session_ids()
|
|
35
|
+
merged: dict[str, Session] = {}
|
|
36
|
+
scanned = registry._scan_sessions(procs)
|
|
37
|
+
_activate_background_agents(scanned, agent_ids)
|
|
38
|
+
for s in scanned:
|
|
39
|
+
merged.setdefault(s.session_id, s)
|
|
40
|
+
existing = list(merged.values())
|
|
41
|
+
for s in registry._synthetic_sessions(procs, existing):
|
|
42
|
+
merged.setdefault(s.session_id, s) # 合成列只填無 row 的 cwd
|
|
43
|
+
return list(merged.values())
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
source = ClaudeCodeSource()
|
|
@@ -46,6 +46,11 @@ from ring.watcher import WaitingAlertScheduler
|
|
|
46
46
|
_ORDER = (Status.WAITING, Status.WORKING, Status.IDLE, Status.ENDED)
|
|
47
47
|
|
|
48
48
|
|
|
49
|
+
def _agent_resume_hint(session_id: str) -> str:
|
|
50
|
+
"""背景 agent(無終端可跳)的誠實提示:講清楚沒有畫面可跳,附接回指令。"""
|
|
51
|
+
return _("背景 agent,沒有終端可跳;用 `claude --resume {session_id}` 接回", session_id=session_id)
|
|
52
|
+
|
|
53
|
+
|
|
49
54
|
class _Grid(DataTable[Text]):
|
|
50
55
|
"""看板表格。在 DataTable 既有的方向鍵之外,加上 vim 風的 j/k/g/G 導覽。
|
|
51
56
|
|
|
@@ -192,6 +197,9 @@ class RingApp(App[None]):
|
|
|
192
197
|
self._own_tty: str = self._detect_own_tty()
|
|
193
198
|
self._delete_armed_sid: str | None = None
|
|
194
199
|
self._delete_armed_until: float = 0.0
|
|
200
|
+
# p 已確認終端對話框消失,但 provider 尚未送後續 hook 時,別讓同一筆舊
|
|
201
|
+
# PermissionRequest 又把列拉回 WAITING。value 是已回覆那筆 row 的 last_active revision。
|
|
202
|
+
self._permission_acks: dict[str, float] = {}
|
|
195
203
|
|
|
196
204
|
@staticmethod
|
|
197
205
|
def _detect_own_tty() -> str:
|
|
@@ -340,6 +348,7 @@ class RingApp(App[None]):
|
|
|
340
348
|
# 退回去跳 session 自己的終端(scan 模式常沒 tty → 跳轉失敗)。
|
|
341
349
|
write_tui_presence()
|
|
342
350
|
self._sessions = board(self._show_all)
|
|
351
|
+
self._apply_permission_acks()
|
|
343
352
|
table = self.query_one(DataTable)
|
|
344
353
|
# cursor 快照要在「任何」clear 之前取:clear(columns=True) 會把 cursor_row reset 成 0。
|
|
345
354
|
cursor = table.cursor_row
|
|
@@ -368,6 +377,8 @@ class RingApp(App[None]):
|
|
|
368
377
|
suffix = f" {s.waiting_icon}" if s.status is Status.WAITING and s.waiting_icon else ""
|
|
369
378
|
if s.hook_stale:
|
|
370
379
|
suffix += " ⚠"
|
|
380
|
+
if s.kind == "agent":
|
|
381
|
+
suffix += " ⚙"
|
|
371
382
|
status_cell = Text(f"{marker}{s.status.marker} {status_label(s.status)}{suffix}", style=style)
|
|
372
383
|
progress = f"{s.todo[0]}/{s.todo[1]}" if s.todo else "·"
|
|
373
384
|
loc_cell = f"📍{_middle_truncate(s.location, _LOC_MAX)}"
|
|
@@ -382,6 +393,30 @@ class RingApp(App[None]):
|
|
|
382
393
|
self._update_detail()
|
|
383
394
|
self._poll_focus_request()
|
|
384
395
|
|
|
396
|
+
def _apply_permission_acks(self) -> None:
|
|
397
|
+
"""以終端回覆結果蓋過同一 revision 的 stale WAITING,直到 provider 送來新事件。"""
|
|
398
|
+
present_ids = {s.session_id for s in self._sessions}
|
|
399
|
+
for session_id in set(self._permission_acks) - present_ids:
|
|
400
|
+
del self._permission_acks[session_id]
|
|
401
|
+
|
|
402
|
+
for s in self._sessions:
|
|
403
|
+
acknowledged_revision = self._permission_acks.get(s.session_id)
|
|
404
|
+
if acknowledged_revision is None:
|
|
405
|
+
continue
|
|
406
|
+
if s.last_active > acknowledged_revision:
|
|
407
|
+
# 新 hook event(可能是下一個權限請求)已到,重新採信 provider 狀態。
|
|
408
|
+
del self._permission_acks[s.session_id]
|
|
409
|
+
continue
|
|
410
|
+
if s.status is Status.WAITING:
|
|
411
|
+
# 終端畫面已證實原對話框消失;provider 尚未回報後續狀態時先視為處理中。
|
|
412
|
+
s.status = Status.WORKING
|
|
413
|
+
s.waiting_kind = ""
|
|
414
|
+
s.waiting_detail = ""
|
|
415
|
+
else:
|
|
416
|
+
# provider 已用同 revision 清掉 WAITING,不再需要本地 acknowledgment。
|
|
417
|
+
del self._permission_acks[s.session_id]
|
|
418
|
+
self._sessions.sort(key=lambda s: (s.status.rank, s.idle_for))
|
|
419
|
+
|
|
385
420
|
def _update_detail(self) -> None:
|
|
386
421
|
"""detail 列:選中的 session 在 🔴 等什麼(hook 有給具體內容才顯示)。
|
|
387
422
|
|
|
@@ -395,6 +430,8 @@ class RingApp(App[None]):
|
|
|
395
430
|
if s is not None and s.status is Status.WAITING and s.waiting_detail:
|
|
396
431
|
icon = s.waiting_icon or "🔴"
|
|
397
432
|
widget.update(Text(f" {icon} {s.waiting_detail}", style=_STATUS_STYLE[Status.WAITING]))
|
|
433
|
+
elif s is not None and s.kind == "agent":
|
|
434
|
+
widget.update(Text(f" ⚙ {_agent_resume_hint(s.session_id)}", style="grey50"))
|
|
398
435
|
else:
|
|
399
436
|
widget.update("")
|
|
400
437
|
|
|
@@ -447,8 +484,14 @@ class RingApp(App[None]):
|
|
|
447
484
|
return
|
|
448
485
|
if s.session_id == self._focused_sid:
|
|
449
486
|
self._focused_sid = None # 你已親自跳過去處理,解除通知標記
|
|
450
|
-
ok, msg = focus_jump(s)
|
|
451
487
|
name = self._display_name(s)
|
|
488
|
+
if s.kind == "agent" and not s.tmux_target and not s.tty:
|
|
489
|
+
# 背景 agent 本就無終端可跳,不呼叫 focus_jump(沒有畫面可假裝跳得過去)。
|
|
490
|
+
text = _("{project}:{msg}", project=name, msg=_agent_resume_hint(s.session_id))
|
|
491
|
+
self._set_status(text)
|
|
492
|
+
self.notify(text, severity="warning", timeout=10)
|
|
493
|
+
return
|
|
494
|
+
ok, msg = focus_jump(s)
|
|
452
495
|
if ok:
|
|
453
496
|
text = _("→ {project}({where})", project=name, where=msg)
|
|
454
497
|
else:
|
|
@@ -481,12 +524,15 @@ class RingApp(App[None]):
|
|
|
481
524
|
name = self._display_name(s)
|
|
482
525
|
backend = permission.select_backend(s)
|
|
483
526
|
if backend is None:
|
|
484
|
-
|
|
485
|
-
_(
|
|
486
|
-
|
|
487
|
-
|
|
527
|
+
if s.kind == "agent":
|
|
528
|
+
self._toast(_("{project}:{msg}", project=name, msg=_agent_resume_hint(s.session_id)))
|
|
529
|
+
else:
|
|
530
|
+
self._toast(
|
|
531
|
+
_(
|
|
532
|
+
"{project}:沒有 tmux 座標,且非 macOS 上的 iTerm2 session,無法就地回覆",
|
|
533
|
+
project=name,
|
|
534
|
+
)
|
|
488
535
|
)
|
|
489
|
-
)
|
|
490
536
|
return
|
|
491
537
|
screen = backend.capture()
|
|
492
538
|
if screen is None:
|
|
@@ -523,6 +569,7 @@ class RingApp(App[None]):
|
|
|
523
569
|
if outcome is permission.ReplyOutcome.OK:
|
|
524
570
|
if s.session_id == self._focused_sid:
|
|
525
571
|
self._focused_sid = None # 已就地回覆,解除通知標記
|
|
572
|
+
self._permission_acks[s.session_id] = s.last_active
|
|
526
573
|
self._toast(_("→ {project}:已回覆權限({option})", project=name, option=option), ok=True)
|
|
527
574
|
self._reload()
|
|
528
575
|
return
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
"""Claude Code:掃 ``~/.claude/projects`` 的 JSONL。"""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import ring.registry as registry
|
|
6
|
-
from ring.registry import Session
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class ClaudeCodeSource:
|
|
10
|
-
name = "claude-code"
|
|
11
|
-
|
|
12
|
-
def discover(self) -> list[Session]:
|
|
13
|
-
procs = registry._claude_procs()
|
|
14
|
-
merged: dict[str, Session] = {}
|
|
15
|
-
for s in registry._scan_sessions(procs):
|
|
16
|
-
merged.setdefault(s.session_id, s)
|
|
17
|
-
existing = list(merged.values())
|
|
18
|
-
for s in registry._synthetic_sessions(procs, existing):
|
|
19
|
-
merged.setdefault(s.session_id, s) # 合成列只填無 row 的 cwd
|
|
20
|
-
return list(merged.values())
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
source = ClaudeCodeSource()
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|