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.
Files changed (56) hide show
  1. {ring_cli-0.11.0 → ring_cli-0.11.2}/PKG-INFO +4 -1
  2. {ring_cli-0.11.0 → ring_cli-0.11.2}/README.en.md +3 -0
  3. {ring_cli-0.11.0 → ring_cli-0.11.2}/pyproject.toml +1 -1
  4. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/locale/en/LC_MESSAGES/ring.mo +0 -0
  5. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/locale/en/LC_MESSAGES/ring.po +3 -0
  6. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/locale/ring.pot +52 -47
  7. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/registry.py +184 -26
  8. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/sources/__init__.py +10 -0
  9. ring_cli-0.11.2/src/ring/sources/claude_code.py +46 -0
  10. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/tui.py +53 -6
  11. ring_cli-0.11.0/src/ring/sources/claude_code.py +0 -23
  12. {ring_cli-0.11.0 → ring_cli-0.11.2}/LICENSE +0 -0
  13. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/__init__.py +0 -0
  14. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/__main__.py +0 -0
  15. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/cli.py +0 -0
  16. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/commands/__init__.py +0 -0
  17. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/commands/_args.py +0 -0
  18. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/commands/completion.py +0 -0
  19. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/commands/digest.py +0 -0
  20. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/commands/doctor.py +0 -0
  21. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/commands/focus.py +0 -0
  22. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/commands/gc.py +0 -0
  23. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/commands/hook.py +0 -0
  24. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/commands/stats.py +0 -0
  25. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/config.py +0 -0
  26. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/focus/__init__.py +0 -0
  27. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/focus/applescript.py +0 -0
  28. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/focus/base.py +0 -0
  29. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/focus/iterm2.py +0 -0
  30. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/focus/linux_wm.py +0 -0
  31. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/focus/neovim.py +0 -0
  32. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/focus/terminal.py +0 -0
  33. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/focus/tmux.py +0 -0
  34. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/gc.py +0 -0
  35. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/hook.py +0 -0
  36. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/hook_protocol.py +0 -0
  37. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/i18n.py +0 -0
  38. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/ipc.py +0 -0
  39. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/labels.py +0 -0
  40. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/notify/__init__.py +0 -0
  41. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/notify/base.py +0 -0
  42. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/notify/command.py +0 -0
  43. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/notify/notify_send.py +0 -0
  44. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/notify/ntfy.py +0 -0
  45. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/notify/osascript_notifier.py +0 -0
  46. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/notify/terminal_notifier.py +0 -0
  47. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/notify/webhook.py +0 -0
  48. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/osascript.py +0 -0
  49. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/permission.py +0 -0
  50. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/plugins.py +0 -0
  51. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/sources/base.py +0 -0
  52. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/sources/codex.py +0 -0
  53. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/sources/hook_registry.py +0 -0
  54. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/stats.py +0 -0
  55. {ring_cli-0.11.0 → ring_cli-0.11.2}/src/ring/transcript.py +0 -0
  56. {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.0
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.0"
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"
@@ -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:274
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:229
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:231
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:232
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:232
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:232
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:232
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:232
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:91
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:92
362
+ #: src/ring/tui.py:97
358
363
  msgid "這個 session 在做什麼…"
359
364
  msgstr ""
360
365
 
361
- #: src/ring/tui.py:136
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:141
371
+ #: src/ring/tui.py:146
367
372
  msgid "Enter 或數字鍵送出、Esc 取消"
368
373
  msgstr ""
369
374
 
370
- #: src/ring/tui.py:164
375
+ #: src/ring/tui.py:169
371
376
  msgid "離場"
372
377
  msgstr ""
373
378
 
374
- #: src/ring/tui.py:165
379
+ #: src/ring/tui.py:170
375
380
  msgid "刷新"
376
381
  msgstr ""
377
382
 
378
- #: src/ring/tui.py:166
383
+ #: src/ring/tui.py:171
379
384
  msgid "含已離場"
380
385
  msgstr ""
381
386
 
382
- #: src/ring/tui.py:167
387
+ #: src/ring/tui.py:172
383
388
  msgid "命名"
384
389
  msgstr ""
385
390
 
386
- #: src/ring/tui.py:168
391
+ #: src/ring/tui.py:173
387
392
  msgid "隱藏"
388
393
  msgstr ""
389
394
 
390
- #: src/ring/tui.py:169
395
+ #: src/ring/tui.py:174
391
396
  msgid "最久等待"
392
397
  msgstr ""
393
398
 
394
- #: src/ring/tui.py:170
399
+ #: src/ring/tui.py:175
395
400
  msgid "回覆權限"
396
401
  msgstr ""
397
402
 
398
- #: src/ring/tui.py:171 src/ring/tui.py:172
403
+ #: src/ring/tui.py:176 src/ring/tui.py:177
399
404
  msgid "跳過去"
400
405
  msgstr ""
401
406
 
402
- #: src/ring/tui.py:250
407
+ #: src/ring/tui.py:255
403
408
  msgid "💡 同專案開了多個 session,裝 hook 跳轉才精準:ring install-hooks"
404
409
  msgstr ""
405
410
 
406
- #: src/ring/tui.py:252
411
+ #: src/ring/tui.py:257
407
412
  msgid "↑/↓ 選一列,Enter 或 Space 跳到那個 session 的終端"
408
413
  msgstr ""
409
414
 
410
- #: src/ring/tui.py:288
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:329
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:334
425
+ #: src/ring/tui.py:339
421
426
  msgid "那個 session 已不在場"
422
427
  msgstr ""
423
428
 
424
- #: src/ring/tui.py:393
429
+ #: src/ring/tui.py:400
425
430
  msgid " ⚠ hook 可能失效:來源檔有更新但沒有 heartbeat"
426
431
  msgstr ""
427
432
 
428
- #: src/ring/tui.py:422 src/ring/tui.py:446 src/ring/tui.py:479
429
- #: src/ring/tui.py:544
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:453
438
+ #: src/ring/tui.py:462 src/ring/tui.py:470 src/ring/tui.py:500
434
439
  #, python-brace-format
435
- msgid "{project}{where}"
440
+ msgid "{project}{msg}"
436
441
  msgstr ""
437
442
 
438
- #: src/ring/tui.py:455
443
+ #: src/ring/tui.py:468
439
444
  #, python-brace-format
440
- msgid "{project}{msg}"
445
+ msgid "{project}{where}"
441
446
  msgstr ""
442
447
 
443
- #: src/ring/tui.py:463
448
+ #: src/ring/tui.py:478
444
449
  msgid "(沒有正在等待的 session)"
445
450
  msgstr ""
446
451
 
447
- #: src/ring/tui.py:486
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:493
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:497
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:526
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:530
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:531
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:532
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:533
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:535
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:537
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:552
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:557
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:565
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:567
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
- def running_claude_pids() -> list[int]:
524
- global _pids_cache
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 - _pids_cache[0] <= _SUBPROCESS_CACHE_TTL:
527
- return _pids_cache[1]
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
- pids: list[int] = []
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) >= 2 and os.path.basename(parts[1].strip()) == "claude":
536
- args = parts[2] if len(parts) == 3 else ""
537
- if _is_claude_background_process(args):
538
- continue
539
- try:
540
- pids.append(int(parts[0]))
541
- except ValueError:
542
- pass
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 備用 process 不是使用者可聚焦的 CLI session。
675
+ """Claude daemon / bg pty host 暖機承載者 / bg-spare 不是使用者可聚焦的 CLI session。
549
676
 
550
677
  ``--bg-spare`` 是 Claude Code 預熱的備用 process(供下一個 ``claude`` 呼叫快速接手),
551
- ``--bg-pty-host`` 一樣不代表真正的使用者 session,卻會被 ``_claude_procs`` 合成假
552
- session 列上看板,冒出幽靈列。token 形狀(`--bg-spare`,`--` flag,不是位置參數)取自
553
- 本機 claude CLI 2.1.206 二進位的 strings 掃描(無法用 ``ps`` 現場逮到活體 bg-spare
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
- return "--bg-pty-host" in tokens or "--bg-spare" in tokens
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,comm"], capture_output=True, text=True, timeout=3).stdout
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()[1:]:
576
- parts = line.split(None, 1)
577
- if len(parts) == 2 and os.path.basename(parts[1].strip()) == "codex":
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
- """每個還活著的 claude:(cwd, tty)。cwd 判活躍/分流,tty 給終端跳轉。
812
+ """每個可聚焦的前景 Claude:(cwd, tty)。cwd 判活躍/分流,tty 給終端跳轉。
662
813
 
663
- claude 不會把 session jsonl 一直開著(lsof 看不到),但給得到 cwd tty。
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 running_claude_pids():
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
- self._toast(
485
- _(
486
- "{project}:沒有 tmux 座標,且非 macOS 上的 iTerm2 session,無法就地回覆",
487
- project=name,
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