cc-session-control 0.7.3__tar.gz → 0.7.5__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 (140) hide show
  1. {cc_session_control-0.7.3/src/cc_session_control.egg-info → cc_session_control-0.7.5}/PKG-INFO +24 -4
  2. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/README.md +16 -3
  3. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/pyproject.toml +41 -1
  4. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/src/cc_session_control/__init__.py +1 -1
  5. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/src/cc_session_control/__main__.py +1 -1
  6. cc_session_control-0.7.5/src/cc_session_control/actions/agent_ops.py +351 -0
  7. cc_session_control-0.7.5/src/cc_session_control/actions/feedback.py +77 -0
  8. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/src/cc_session_control/actions/resume_list.py +52 -14
  9. cc_session_control-0.7.5/src/cc_session_control/actions/runner.py +133 -0
  10. cc_session_control-0.7.5/src/cc_session_control/actions/session_ops.py +574 -0
  11. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/src/cc_session_control/actions/skill_ops.py +3 -1
  12. cc_session_control-0.7.5/src/cc_session_control/actions/tui_actions.py +383 -0
  13. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/src/cc_session_control/app.py +182 -60
  14. cc_session_control-0.7.5/src/cc_session_control/cli.py +229 -0
  15. cc_session_control-0.7.5/src/cc_session_control/cli_commands.py +546 -0
  16. cc_session_control-0.7.5/src/cc_session_control/cli_rc.py +335 -0
  17. cc_session_control-0.7.5/src/cc_session_control/cli_streams.py +25 -0
  18. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/src/cc_session_control/clipboard.py +6 -3
  19. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/src/cc_session_control/config.py +29 -2
  20. cc_session_control-0.7.5/src/cc_session_control/data/age_cleanup.py +185 -0
  21. cc_session_control-0.7.5/src/cc_session_control/data/cleanup.py +491 -0
  22. cc_session_control-0.7.5/src/cc_session_control/data/cleanup_anchors.py +119 -0
  23. cc_session_control-0.7.5/src/cc_session_control/data/cleanup_liveness.py +34 -0
  24. cc_session_control-0.7.5/src/cc_session_control/data/cleanup_selection.py +65 -0
  25. cc_session_control-0.7.5/src/cc_session_control/data/environment_ledger.py +483 -0
  26. cc_session_control-0.7.5/src/cc_session_control/data/environments.py +379 -0
  27. cc_session_control-0.7.5/src/cc_session_control/data/liveness.py +467 -0
  28. cc_session_control-0.7.5/src/cc_session_control/data/proc.py +416 -0
  29. cc_session_control-0.7.5/src/cc_session_control/data/project_settings.py +386 -0
  30. cc_session_control-0.7.5/src/cc_session_control/data/rc.py +493 -0
  31. cc_session_control-0.7.5/src/cc_session_control/data/rc_enabled.py +454 -0
  32. cc_session_control-0.7.5/src/cc_session_control/data/rc_environment.py +181 -0
  33. cc_session_control-0.7.5/src/cc_session_control/data/rc_outcomes.py +248 -0
  34. cc_session_control-0.7.5/src/cc_session_control/data/refresh.py +263 -0
  35. cc_session_control-0.7.5/src/cc_session_control/data/registry.py +297 -0
  36. cc_session_control-0.7.5/src/cc_session_control/data/removal.py +600 -0
  37. cc_session_control-0.7.5/src/cc_session_control/data/sessions.py +225 -0
  38. cc_session_control-0.7.5/src/cc_session_control/data/snapshot.py +147 -0
  39. cc_session_control-0.7.5/src/cc_session_control/data/tmux.py +544 -0
  40. cc_session_control-0.7.5/src/cc_session_control/data/tmux_outcomes.py +241 -0
  41. cc_session_control-0.7.5/src/cc_session_control/data/transcripts.py +254 -0
  42. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/src/cc_session_control/models.py +151 -46
  43. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/src/cc_session_control/skill/SKILL.md +17 -5
  44. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/src/cc_session_control/theme.py +86 -43
  45. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/src/cc_session_control/views/_base.py +23 -6
  46. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/src/cc_session_control/views/_colspec.py +5 -2
  47. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/src/cc_session_control/views/_confirm.py +45 -15
  48. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/src/cc_session_control/views/_keytable.py +5 -5
  49. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/src/cc_session_control/views/_rows.py +27 -10
  50. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/src/cc_session_control/views/_session_row.py +43 -23
  51. cc_session_control-0.7.5/src/cc_session_control/views/_sessions_cleanup.py +336 -0
  52. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/src/cc_session_control/views/agents.py +170 -107
  53. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/src/cc_session_control/views/rc.py +240 -109
  54. cc_session_control-0.7.5/src/cc_session_control/views/sessions.py +541 -0
  55. {cc_session_control-0.7.3 → cc_session_control-0.7.5/src/cc_session_control.egg-info}/PKG-INFO +24 -4
  56. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/src/cc_session_control.egg-info/SOURCES.txt +48 -1
  57. cc_session_control-0.7.5/src/cc_session_control.egg-info/requires.txt +9 -0
  58. cc_session_control-0.7.5/tests/test_action_runner.py +143 -0
  59. cc_session_control-0.7.5/tests/test_agent_ops.py +583 -0
  60. cc_session_control-0.7.5/tests/test_agent_takeover.py +355 -0
  61. cc_session_control-0.7.5/tests/test_agents.py +552 -0
  62. cc_session_control-0.7.5/tests/test_agents_async.py +153 -0
  63. cc_session_control-0.7.5/tests/test_app.py +739 -0
  64. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/tests/test_cleanup.py +243 -60
  65. cc_session_control-0.7.5/tests/test_cleanup_anchors.py +608 -0
  66. cc_session_control-0.7.5/tests/test_cleanup_fresh_evidence.py +470 -0
  67. cc_session_control-0.7.5/tests/test_cleanup_removal.py +554 -0
  68. cc_session_control-0.7.5/tests/test_cli.py +330 -0
  69. cc_session_control-0.7.5/tests/test_cli_entry.py +651 -0
  70. cc_session_control-0.7.5/tests/test_cli_entry_rc.py +472 -0
  71. cc_session_control-0.7.5/tests/test_cli_prune.py +702 -0
  72. cc_session_control-0.7.5/tests/test_cli_rc.py +625 -0
  73. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/tests/test_data.py +498 -201
  74. cc_session_control-0.7.5/tests/test_docs.py +121 -0
  75. cc_session_control-0.7.5/tests/test_environment_ledger.py +352 -0
  76. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/tests/test_environments.py +314 -41
  77. cc_session_control-0.7.5/tests/test_exit_intents.py +997 -0
  78. cc_session_control-0.7.5/tests/test_liveness.py +584 -0
  79. cc_session_control-0.7.5/tests/test_proc.py +276 -0
  80. cc_session_control-0.7.5/tests/test_project_settings.py +437 -0
  81. cc_session_control-0.7.5/tests/test_quality_scripts.py +138 -0
  82. cc_session_control-0.7.5/tests/test_rc.py +910 -0
  83. cc_session_control-0.7.5/tests/test_rc_enabled.py +581 -0
  84. cc_session_control-0.7.5/tests/test_rc_enabled_list_integration.py +139 -0
  85. cc_session_control-0.7.5/tests/test_rc_environment.py +553 -0
  86. cc_session_control-0.7.5/tests/test_rc_view_diagnostics.py +115 -0
  87. cc_session_control-0.7.5/tests/test_refresh.py +883 -0
  88. cc_session_control-0.7.5/tests/test_registry.py +316 -0
  89. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/tests/test_resume_list.py +46 -16
  90. cc_session_control-0.7.5/tests/test_sessions.py +519 -0
  91. cc_session_control-0.7.5/tests/test_sessions_async.py +334 -0
  92. cc_session_control-0.7.5/tests/test_sessions_tmux_residency_view.py +101 -0
  93. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/tests/test_skill_ops.py +1 -1
  94. cc_session_control-0.7.5/tests/test_smoke.py +135 -0
  95. cc_session_control-0.7.5/tests/test_snapshot.py +575 -0
  96. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/tests/test_theme.py +21 -8
  97. cc_session_control-0.7.5/tests/test_tmux.py +309 -0
  98. cc_session_control-0.7.5/tests/test_trust.py +390 -0
  99. cc_session_control-0.7.5/tests/test_tui_actions.py +793 -0
  100. cc_session_control-0.7.5/tests/test_validate_release_tag.py +120 -0
  101. cc_session_control-0.7.5/tests/test_views.py +793 -0
  102. cc_session_control-0.7.5/tests/test_views_cleanup.py +643 -0
  103. cc_session_control-0.7.5/tests/test_views_rc.py +690 -0
  104. cc_session_control-0.7.5/tests/test_workflows.py +191 -0
  105. cc_session_control-0.7.3/src/cc_session_control/actions/agent_ops.py +0 -179
  106. cc_session_control-0.7.3/src/cc_session_control/actions/session_ops.py +0 -281
  107. cc_session_control-0.7.3/src/cc_session_control/cli.py +0 -334
  108. cc_session_control-0.7.3/src/cc_session_control/data/cleanup.py +0 -496
  109. cc_session_control-0.7.3/src/cc_session_control/data/environments.py +0 -462
  110. cc_session_control-0.7.3/src/cc_session_control/data/liveness.py +0 -232
  111. cc_session_control-0.7.3/src/cc_session_control/data/proc.py +0 -233
  112. cc_session_control-0.7.3/src/cc_session_control/data/rc.py +0 -429
  113. cc_session_control-0.7.3/src/cc_session_control/data/registry.py +0 -149
  114. cc_session_control-0.7.3/src/cc_session_control/data/sessions.py +0 -222
  115. cc_session_control-0.7.3/src/cc_session_control/data/snapshot.py +0 -83
  116. cc_session_control-0.7.3/src/cc_session_control/data/tmux.py +0 -235
  117. cc_session_control-0.7.3/src/cc_session_control/views/_sessions_cleanup.py +0 -175
  118. cc_session_control-0.7.3/src/cc_session_control/views/sessions.py +0 -435
  119. cc_session_control-0.7.3/src/cc_session_control.egg-info/requires.txt +0 -4
  120. cc_session_control-0.7.3/tests/test_agent_ops.py +0 -245
  121. cc_session_control-0.7.3/tests/test_agents.py +0 -338
  122. cc_session_control-0.7.3/tests/test_app.py +0 -304
  123. cc_session_control-0.7.3/tests/test_cli.py +0 -112
  124. cc_session_control-0.7.3/tests/test_liveness.py +0 -236
  125. cc_session_control-0.7.3/tests/test_proc.py +0 -80
  126. cc_session_control-0.7.3/tests/test_rc.py +0 -254
  127. cc_session_control-0.7.3/tests/test_registry.py +0 -157
  128. cc_session_control-0.7.3/tests/test_sessions.py +0 -204
  129. cc_session_control-0.7.3/tests/test_smoke.py +0 -49
  130. cc_session_control-0.7.3/tests/test_snapshot.py +0 -94
  131. cc_session_control-0.7.3/tests/test_trust.py +0 -181
  132. cc_session_control-0.7.3/tests/test_views.py +0 -1285
  133. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/LICENSE +0 -0
  134. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/setup.cfg +0 -0
  135. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/src/cc_session_control/actions/__init__.py +0 -0
  136. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/src/cc_session_control/data/__init__.py +0 -0
  137. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/src/cc_session_control/views/__init__.py +0 -0
  138. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/src/cc_session_control.egg-info/dependency_links.txt +0 -0
  139. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/src/cc_session_control.egg-info/entry_points.txt +0 -0
  140. {cc_session_control-0.7.3 → cc_session_control-0.7.5}/src/cc_session_control.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cc-session-control
3
- Version: 0.7.3
3
+ Version: 0.7.5
4
4
  Summary: TUI manager for Claude Code sessions and Remote Control
5
5
  Author: dzshzx
6
6
  License-Expression: MIT
@@ -12,18 +12,26 @@ Classifier: Development Status :: 3 - Alpha
12
12
  Classifier: Environment :: Console
13
13
  Classifier: Operating System :: POSIX :: Linux
14
14
  Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
15
17
  Classifier: Topic :: Software Development :: User Interfaces
16
18
  Requires-Python: >=3.12
17
19
  Description-Content-Type: text/markdown
18
20
  License-File: LICENSE
19
21
  Requires-Dist: urwid>=2.0.0
20
22
  Provides-Extra: dev
23
+ Requires-Dist: coverage>=7.6; extra == "dev"
24
+ Requires-Dist: mypy>=1.15; extra == "dev"
21
25
  Requires-Dist: pytest>=7.0; extra == "dev"
26
+ Requires-Dist: pytest-cov>=6.0; extra == "dev"
27
+ Requires-Dist: pyyaml>=6.0; extra == "dev"
28
+ Requires-Dist: ruff>=0.12; extra == "dev"
22
29
  Dynamic: license-file
23
30
 
24
31
  # cc-session-control
25
32
 
26
- TUI manager for [Claude Code](https://claude.ai/code) sessions and Remote Control.
33
+ tmux-first TUI and headless CLI for [Claude Code](https://claude.ai/code)
34
+ sessions, background agents, and Remote Control.
27
35
 
28
36
  **CLI command: `csctl`**
29
37
 
@@ -41,7 +49,8 @@ Built with [urwid](https://urwid.org/).
41
49
 
42
50
  - Python 3.12+
43
51
  - [Claude Code](https://claude.ai/code) CLI installed and authenticated
44
- - tmux (for Remote Control management)
52
+ - tmux (the primary session-lifecycle carrier: launch, resume, background, and
53
+ survive terminal/SSH disconnects; managed Remote Control servers also use it)
45
54
  - Linux / WSL (macOS support is partial — `/proc`-based liveness detection is Linux-only)
46
55
 
47
56
  ## Installation
@@ -89,6 +98,10 @@ csctl rc list # Show auto-start list
89
98
  # Session cleanup
90
99
  csctl prune # Dry run: show stats
91
100
  csctl prune --max-prompts 1 --apply # Delete sessions with ≤1 prompt
101
+ csctl prune --sweep-orphans # Dry run: orphan sid-keyed artifact dirs
102
+ csctl prune --sweep-zombies # Dry run: dead sessions/<pid>.json files
103
+ csctl prune --sweep-aged # Dry run: age-keyed global entries
104
+ # Add --apply to exactly one of the sweep commands above to execute it.
92
105
 
93
106
  # Resume rescue (headless): list sessions across directories with
94
107
  # ready-to-copy resume commands (native /resume only searches the cwd
@@ -98,6 +111,10 @@ csctl resume mybug # Keyword: sid/cwd/title, then transcript body
98
111
  csctl resume --page 2 # Next page
99
112
  csctl resume --all # Everything, no paging
100
113
 
114
+ # Read-only inventory
115
+ csctl agents # Background agents: state, tempo, name, cwd
116
+ csctl env # Current + orphan bridge environments
117
+
101
118
  # Bundled Claude Code skill (session-doctor knowledge for the agent)
102
119
  csctl skill install # Write SKILL.md to ~/.claude/skills/
103
120
  csctl skill install --force # Replace an existing skill directory
@@ -114,10 +131,13 @@ csctl --version
114
131
  |---|---|---|
115
132
  | `CSCTL_RC_SESSION` | `rc` | tmux session name for RC servers |
116
133
  | `CSCTL_RC_STAGGER` | `2` | Seconds between starting RC servers |
134
+ | `CSCTL_CLEANUP_AGE_DAYS` | `14` | Minimum age in days for `csctl prune --sweep-aged` (must be an integer ≥ 1) |
117
135
  | `CSCTL_THEME` | `auto` | TUI palette: `auto` (detect the terminal background via OSC 11 / `$COLORFGBG`) / `dark` / `light`. tmux typically doesn't answer the OSC 11 query, so inside tmux `auto` falls back to `dark` — set this (or `--theme`) explicitly for a light terminal |
118
136
  | `XDG_CONFIG_HOME` | `~/.config` | Config directory base |
119
137
 
120
- RC auto-start list is stored at `$XDG_CONFIG_HOME/csctl/rc-enabled`.
138
+ The RC auto-start list is stored at
139
+ `$XDG_CONFIG_HOME/csctl/rc-enabled`; the bridge-environment ledger is stored
140
+ at `$XDG_CONFIG_HOME/csctl/environments.jsonl`.
121
141
 
122
142
  ## License
123
143
 
@@ -1,6 +1,7 @@
1
1
  # cc-session-control
2
2
 
3
- TUI manager for [Claude Code](https://claude.ai/code) sessions and Remote Control.
3
+ tmux-first TUI and headless CLI for [Claude Code](https://claude.ai/code)
4
+ sessions, background agents, and Remote Control.
4
5
 
5
6
  **CLI command: `csctl`**
6
7
 
@@ -18,7 +19,8 @@ Built with [urwid](https://urwid.org/).
18
19
 
19
20
  - Python 3.12+
20
21
  - [Claude Code](https://claude.ai/code) CLI installed and authenticated
21
- - tmux (for Remote Control management)
22
+ - tmux (the primary session-lifecycle carrier: launch, resume, background, and
23
+ survive terminal/SSH disconnects; managed Remote Control servers also use it)
22
24
  - Linux / WSL (macOS support is partial — `/proc`-based liveness detection is Linux-only)
23
25
 
24
26
  ## Installation
@@ -66,6 +68,10 @@ csctl rc list # Show auto-start list
66
68
  # Session cleanup
67
69
  csctl prune # Dry run: show stats
68
70
  csctl prune --max-prompts 1 --apply # Delete sessions with ≤1 prompt
71
+ csctl prune --sweep-orphans # Dry run: orphan sid-keyed artifact dirs
72
+ csctl prune --sweep-zombies # Dry run: dead sessions/<pid>.json files
73
+ csctl prune --sweep-aged # Dry run: age-keyed global entries
74
+ # Add --apply to exactly one of the sweep commands above to execute it.
69
75
 
70
76
  # Resume rescue (headless): list sessions across directories with
71
77
  # ready-to-copy resume commands (native /resume only searches the cwd
@@ -75,6 +81,10 @@ csctl resume mybug # Keyword: sid/cwd/title, then transcript body
75
81
  csctl resume --page 2 # Next page
76
82
  csctl resume --all # Everything, no paging
77
83
 
84
+ # Read-only inventory
85
+ csctl agents # Background agents: state, tempo, name, cwd
86
+ csctl env # Current + orphan bridge environments
87
+
78
88
  # Bundled Claude Code skill (session-doctor knowledge for the agent)
79
89
  csctl skill install # Write SKILL.md to ~/.claude/skills/
80
90
  csctl skill install --force # Replace an existing skill directory
@@ -91,10 +101,13 @@ csctl --version
91
101
  |---|---|---|
92
102
  | `CSCTL_RC_SESSION` | `rc` | tmux session name for RC servers |
93
103
  | `CSCTL_RC_STAGGER` | `2` | Seconds between starting RC servers |
104
+ | `CSCTL_CLEANUP_AGE_DAYS` | `14` | Minimum age in days for `csctl prune --sweep-aged` (must be an integer ≥ 1) |
94
105
  | `CSCTL_THEME` | `auto` | TUI palette: `auto` (detect the terminal background via OSC 11 / `$COLORFGBG`) / `dark` / `light`. tmux typically doesn't answer the OSC 11 query, so inside tmux `auto` falls back to `dark` — set this (or `--theme`) explicitly for a light terminal |
95
106
  | `XDG_CONFIG_HOME` | `~/.config` | Config directory base |
96
107
 
97
- RC auto-start list is stored at `$XDG_CONFIG_HOME/csctl/rc-enabled`.
108
+ The RC auto-start list is stored at
109
+ `$XDG_CONFIG_HOME/csctl/rc-enabled`; the bridge-environment ledger is stored
110
+ at `$XDG_CONFIG_HOME/csctl/environments.jsonl`.
98
111
 
99
112
  ## License
100
113
 
@@ -17,6 +17,8 @@ classifiers = [
17
17
  "Environment :: Console",
18
18
  "Operating System :: POSIX :: Linux",
19
19
  "Programming Language :: Python :: 3.12",
20
+ "Programming Language :: Python :: 3.13",
21
+ "Programming Language :: Python :: 3.14",
20
22
  "Topic :: Software Development :: User Interfaces",
21
23
  ]
22
24
 
@@ -29,7 +31,14 @@ Issues = "https://github.com/dzshzx/cc-session-control/issues"
29
31
  csctl = "cc_session_control.cli:main"
30
32
 
31
33
  [project.optional-dependencies]
32
- dev = ["pytest>=7.0"]
34
+ dev = [
35
+ "coverage>=7.6",
36
+ "mypy>=1.15",
37
+ "pytest>=7.0",
38
+ "pytest-cov>=6.0",
39
+ "pyyaml>=6.0",
40
+ "ruff>=0.12",
41
+ ]
33
42
 
34
43
  [tool.setuptools.dynamic]
35
44
  version = { attr = "cc_session_control.__version__" }
@@ -39,3 +48,34 @@ where = ["src"]
39
48
 
40
49
  [tool.setuptools.package-data]
41
50
  cc_session_control = ["skill/SKILL.md"]
51
+
52
+ [tool.ruff]
53
+ target-version = "py312"
54
+ line-length = 88
55
+
56
+ [tool.ruff.lint]
57
+ select = ["E", "F", "I", "UP", "B", "BLE"]
58
+ # Ruff's formatter deliberately leaves some long strings and comments intact,
59
+ # so format output, rather than E501, is the single line-wrapping authority.
60
+ ignore = ["E501"]
61
+
62
+ [tool.mypy]
63
+ python_version = "3.12"
64
+ files = ["src"]
65
+ check_untyped_defs = true
66
+ warn_unused_configs = true
67
+
68
+ [[tool.mypy.overrides]]
69
+ module = ["urwid"]
70
+ # Keep the third-party widget boundary dynamic while checking every
71
+ # cc_session_control module and every project-owned type contract.
72
+ ignore_missing_imports = true
73
+ follow_imports = "skip"
74
+
75
+ [tool.coverage.run]
76
+ branch = true
77
+ relative_files = true
78
+ source = ["cc_session_control"]
79
+
80
+ [tool.coverage.report]
81
+ show_missing = true
@@ -1,3 +1,3 @@
1
1
  """cc-session-control — TUI manager for Claude Code sessions and Remote Control."""
2
2
 
3
- __version__ = "0.7.3"
3
+ __version__ = "0.7.5"
@@ -2,4 +2,4 @@
2
2
 
3
3
  from .cli import main
4
4
 
5
- main()
5
+ raise SystemExit(main())
@@ -0,0 +1,351 @@
1
+ """Background-agent lifecycle actions (R4 / Phase 6).
2
+
3
+ The persistent truth for a background agent lives in `jobs/<short>/state.json`
4
+ (registry.read_agent_jobs → AgentJob); it carries NO pid. So a live worker's host
5
+ pid is resolved by JOINing the job's sid back to `sessions/<pid>.json`
6
+ (the production takeover path does this inside one typed liveness snapshot;
7
+ `job_host` is records-only compatibility) — a live worker with no sessions file
8
+ is therefore unstoppable, a documented orphan risk surfaced in `HELP`.
9
+
10
+ Capability red lines honoured here:
11
+ - respawn/takeover never replace the csctl process (respawn spawns a tmux
12
+ window; takeover hands a Session to the existing `do_resume` path run AFTER
13
+ the UI loop exits).
14
+ - stop only signals a confirmed-live joined host pid; killing a
15
+ `--remote-control`/bg worker does not always fully reap it (orphan risk).
16
+ - destructive ops (remove/stop) refuse when "current" can't be determined
17
+ (no `/proc`, R10) so they never blind-hit csctl's own session.
18
+
19
+ This is an action module: internals are English, but the user-facing label/help
20
+ constants the (Phase 7) background view reads are Simplified Chinese.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import shlex
26
+ from collections import deque
27
+ from dataclasses import dataclass
28
+ from enum import Enum
29
+
30
+ from ..config import cfg
31
+ from ..data import cleanup, liveness, registry, tmux
32
+ from ..data import proc as proc
33
+ from ..data.removal import CleanupExecution
34
+ from ..models import AgentJob, Session
35
+ from . import session_ops
36
+
37
+ # --- host-pid join (resume-takeover compatibility view) -----------------------
38
+
39
+
40
+ def job_host(job: AgentJob, *, max_age: float = 5.0) -> tuple[int | None, bool]:
41
+ """Resolve a background job's host pid + liveness — `(pid, alive)`.
42
+
43
+ `state.json` has no pid, so the worker's pid is JOINed from
44
+ `sessions/<pid>.json` on `job.sid` (a bg session proc; `kind` is typically
45
+ "bg"). Prefers a `/proc`-confirmed live match (so `alive=True` is trustworthy
46
+ and defeats pid reuse via `procStart`); falls back to the first sid match
47
+ with `alive=False`. Returns `(None, False)` when no sessions file exists for
48
+ the sid — that live worker is unstoppable (documented orphan risk).
49
+
50
+ Injects `/proc` liveness onto the registry rows, then defers to the single
51
+ pure join `registry.host_pid_for_sid` (shared with `snapshot._enrich_jobs`).
52
+ """
53
+ procs = liveness.live_session_procs(max_age=max_age)
54
+ return registry.host_pid_for_sid(job.sid, procs)
55
+
56
+
57
+ # --- respawn ------------------------------------------------------------------
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class RespawnResult:
62
+ """Exact command plus the tmux target that proves it was launched."""
63
+
64
+ command: str
65
+ target: str | None
66
+ detail: str = ""
67
+ tmux_result: tmux.TmuxWriteResult | None = None
68
+
69
+ @property
70
+ def success(self) -> bool:
71
+ return self.target is not None
72
+
73
+
74
+ def respawn_cmd(job: AgentJob) -> str:
75
+ """The exact relaunch command: `claude --resume <resume_sid> <flags> --bg`.
76
+
77
+ Pure string build via `shlex.join` (split from `respawn` so it can be copied
78
+ to the clipboard / asserted in tests). `respawn_flags` are reused verbatim
79
+ from the recorded job state.
80
+ """
81
+ args = ["claude", "--resume", job.resume_sid, *job.respawn_flags, "--bg"]
82
+ return shlex.join(args)
83
+
84
+
85
+ def _job_window(job: AgentJob) -> str:
86
+ """tmux window name for a respawned agent (name or short, suffixed)."""
87
+ base = (job.name or "bg").strip() or "bg"
88
+ return f"{base}-{job.short[:8]}"
89
+
90
+
91
+ def respawn_result(job: AgentJob) -> RespawnResult:
92
+ """Relaunch a background agent while retaining the tmux outcome."""
93
+ cmd = respawn_cmd(job)
94
+ result = tmux.run_in_tmux_result(
95
+ tmux.session_name_for(job.cwd),
96
+ _job_window(job),
97
+ cmd,
98
+ )
99
+ target = result.target if result.success else None
100
+ return RespawnResult(cmd, target, result.diagnostic, result)
101
+
102
+
103
+ # --- remove (settled agents only) ---------------------------------------------
104
+
105
+
106
+ def remove_job(job: AgentJob) -> CleanupExecution:
107
+ """Remove a SETTLED background agent: `jobs/<short>/` + its sid artifacts.
108
+
109
+ The public data-layer executor owns the final fresh liveness check, so no
110
+ caller-held evidence can authorize deletion.
111
+ """
112
+ result = CleanupExecution()
113
+ try:
114
+ anchors = cleanup.agent_removal_anchors(job.short, job.sid)
115
+ except OSError as exc:
116
+ result.refuse([job.short], f"cannot establish removal anchor: {exc}")
117
+ return result
118
+ return cleanup.remove_agent_artifacts(job.short, job.sid, anchors=anchors)
119
+
120
+
121
+ # --- watch (read-only) --------------------------------------------------------
122
+
123
+ _TIMELINE_LINE_LIMIT = 200
124
+
125
+
126
+ class TimelineReadState(Enum):
127
+ READY = "ready"
128
+ MISSING = "missing"
129
+ FAILED = "failed"
130
+
131
+
132
+ @dataclass(frozen=True)
133
+ class TimelineReadResult:
134
+ """Bounded timeline data for a main-loop read-only overlay."""
135
+
136
+ state: TimelineReadState
137
+ lines: tuple[str, ...] = ()
138
+ detail: str = ""
139
+
140
+
141
+ def watch(job: AgentJob) -> TimelineReadResult:
142
+ """Stream the last 200 timeline lines without loading the whole file."""
143
+ path = cfg.jobs_dir / job.short / "timeline.jsonl"
144
+ try:
145
+ with open(path, errors="ignore") as fh:
146
+ lines = deque(
147
+ (line.rstrip("\r\n") for line in fh),
148
+ maxlen=_TIMELINE_LINE_LIMIT,
149
+ )
150
+ except FileNotFoundError:
151
+ return TimelineReadResult(TimelineReadState.MISSING)
152
+ except OSError as exc:
153
+ return TimelineReadResult(TimelineReadState.FAILED, detail=str(exc))
154
+ return TimelineReadResult(TimelineReadState.READY, tuple(lines))
155
+
156
+
157
+ # --- resume takeover (reuses the existing foreground resume path) -------------
158
+
159
+
160
+ class TakeoverPreparationState(Enum):
161
+ """Whether one fresh generation permits constructing a takeover session."""
162
+
163
+ READY = "ready"
164
+ REFUSED = "refused"
165
+
166
+
167
+ @dataclass(frozen=True)
168
+ class TakeoverPreparationResult:
169
+ """Typed result of preparing one background-agent takeover."""
170
+
171
+ state: TakeoverPreparationState
172
+ session: Session | None = None
173
+ detail: str = ""
174
+
175
+
176
+ def _ready_takeover(
177
+ job: AgentJob,
178
+ *,
179
+ pid: int | None = None,
180
+ alive: bool = False,
181
+ current: bool = False,
182
+ proc_start: str = "",
183
+ tmux_target: str | None = None,
184
+ ) -> TakeoverPreparationResult:
185
+ """Build the existing resume-path adapter for one prepared agent."""
186
+ return TakeoverPreparationResult(
187
+ TakeoverPreparationState.READY,
188
+ session=Session(
189
+ sid=job.resume_sid,
190
+ cwd=job.cwd,
191
+ label=job.name or job.short,
192
+ mtime=0.0,
193
+ prompts=0,
194
+ pid=pid,
195
+ alive=alive,
196
+ current=current,
197
+ proc_start=proc_start,
198
+ source="bg",
199
+ agent_short=job.short,
200
+ tmux_target=tmux_target,
201
+ ),
202
+ )
203
+
204
+
205
+ def prepare_takeover(job: AgentJob) -> TakeoverPreparationResult:
206
+ """Prepare one background-agent resume without widening destructive gates.
207
+
208
+ Bringing a bg session to the foreground is just a resume of its
209
+ `resume_sid`, so a ready result carries the Session the view feeds to the SAME
210
+ `app.exit_with(ResumeIntent)` → `do_resume` pipeline used for foreground sessions —
211
+ all kill/exec/`_resume_plan` logic is reused, none duplicated (R4.4 takeover).
212
+
213
+ The published immutable ``job.host_alive`` decides whether this action can
214
+ kill. A dead job resumes directly without acquiring liveness or tmux state.
215
+ A live job acquires exactly one fresh typed generation; incomplete registry,
216
+ process, ancestor, or agents evidence is refused before any tmux lookup.
217
+ Host identity, liveness, proc-start, and current-session protection all come
218
+ from that same generation. If it proves the former host is gone, the action
219
+ safely contracts to the same non-destructive dead resume.
220
+ """
221
+ if not job.host_alive:
222
+ return _ready_takeover(job)
223
+
224
+ evidence = liveness.liveness_inputs()
225
+ if not evidence.complete:
226
+ return TakeoverPreparationResult(
227
+ TakeoverPreparationState.REFUSED,
228
+ detail=_incomplete_liveness_detail(evidence),
229
+ )
230
+ pid, alive = registry.host_pid_for_sid(job.sid, evidence.session_procs)
231
+ if not alive or pid is None:
232
+ return _ready_takeover(job)
233
+
234
+ proc_start = next(
235
+ (
236
+ item.proc_start
237
+ for item in evidence.session_procs
238
+ if item.sid == job.sid and item.pid == pid and item.proc_alive is True
239
+ ),
240
+ "",
241
+ )
242
+ residency = tmux.find_session_window_result([pid])
243
+ if not residency.complete:
244
+ detail = "; ".join(
245
+ f"{issue.source}"
246
+ + (f" at {issue.path}" if issue.path else "")
247
+ + f": {issue.detail}"
248
+ for issue in residency.issues
249
+ )
250
+ return TakeoverPreparationResult(
251
+ TakeoverPreparationState.REFUSED,
252
+ detail=detail,
253
+ )
254
+ return _ready_takeover(
255
+ job,
256
+ pid=pid,
257
+ alive=True,
258
+ current=pid in evidence.cur,
259
+ proc_start=proc_start,
260
+ tmux_target=residency.target,
261
+ )
262
+
263
+
264
+ # --- stop (live workers only) -------------------------------------------------
265
+
266
+
267
+ class AgentStopState(Enum):
268
+ """Observable outcome of stopping one background agent."""
269
+
270
+ STOPPED = "stopped"
271
+ NOT_RUNNING = "not-running"
272
+ REFUSED = "refused"
273
+ FAILED = "failed"
274
+
275
+
276
+ @dataclass(frozen=True)
277
+ class AgentStopResult:
278
+ state: AgentStopState
279
+ pid: int | None = None
280
+ detail: str = ""
281
+
282
+ @property
283
+ def success(self) -> bool:
284
+ return self.state is AgentStopState.STOPPED
285
+
286
+
287
+ def _incomplete_liveness_detail(evidence: liveness.LivenessSnapshot) -> str:
288
+ issues = []
289
+ for issue in evidence.issues:
290
+ location = f" at {issue.path}" if issue.path else ""
291
+ issues.append(f"{issue.source}{location}: {issue.detail}")
292
+ return "liveness evidence incomplete: " + "; ".join(issues)
293
+
294
+
295
+ def stop_job_result(job: AgentJob) -> AgentStopResult:
296
+ """Stop one live background agent from one fresh liveness snapshot.
297
+
298
+ The host pid is JOINed from `sessions/<pid>.json` (`job_host`); only a
299
+ confirmed-live pid is killed. Incomplete source evidence and unavailable
300
+ current-session determination refuse the destructive action. The same
301
+ snapshot supplies both the host pid and its proc-start identity, avoiding
302
+ a second compatibility scan before ``take_over_result`` rechecks liveness.
303
+
304
+ The kill itself is `session_ops.take_over_result` (the ONE primitive: R10 gate,
305
+ recheck, SIGTERM, cache invalidation). Its four outcomes map one-for-one to
306
+ this domain result.
307
+
308
+ Killing does not always fully reap a `--remote-control`/bg worker (orphan
309
+ risk, see `HELP`).
310
+ """
311
+ evidence = liveness.liveness_inputs()
312
+ if not evidence.complete:
313
+ return AgentStopResult(
314
+ AgentStopState.REFUSED,
315
+ detail=_incomplete_liveness_detail(evidence),
316
+ )
317
+ pid, alive = registry.host_pid_for_sid(job.sid, evidence.session_procs)
318
+ if not alive or not pid:
319
+ return AgentStopResult(
320
+ AgentStopState.NOT_RUNNING,
321
+ pid=pid,
322
+ detail="no live host for background agent",
323
+ )
324
+ proc_start = next(
325
+ (
326
+ item.proc_start
327
+ for item in evidence.session_procs
328
+ if item.sid == job.sid and item.pid == pid
329
+ ),
330
+ "",
331
+ )
332
+ outcome = session_ops.take_over_result(pid, proc_start)
333
+ if outcome.state is session_ops.TakeOverState.KILLED:
334
+ return AgentStopResult(AgentStopState.STOPPED, pid=pid)
335
+ if outcome.state is session_ops.TakeOverState.GONE:
336
+ return AgentStopResult(
337
+ AgentStopState.NOT_RUNNING,
338
+ pid=pid,
339
+ detail="background agent host is no longer running",
340
+ )
341
+ if outcome.state is session_ops.TakeOverState.REFUSED:
342
+ return AgentStopResult(
343
+ AgentStopState.REFUSED,
344
+ pid=pid,
345
+ detail=outcome.detail or "current session cannot be determined",
346
+ )
347
+ return AgentStopResult(
348
+ AgentStopState.FAILED,
349
+ pid=pid,
350
+ detail=outcome.detail or "failed to signal background agent host",
351
+ )
@@ -0,0 +1,77 @@
1
+ """Operator-facing summaries for typed cleanup execution results."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..data.removal import CleanupExecution
6
+
7
+
8
+ def _protection_issue(result: CleanupExecution) -> str:
9
+ issue = result.issues[0]
10
+ where = f"({issue.path})" if issue.path else ""
11
+ return f"{issue.source}{where}:{issue.error}"
12
+
13
+
14
+ def _details(result: CleanupExecution) -> list[str]:
15
+ details: list[str] = []
16
+ if result.failed and result.removed:
17
+ details.append(f"已删除路径 {len(result.removed)}")
18
+ if result.failed:
19
+ first = result.failed[0]
20
+ detail = f"失败 {len(result.failed)}"
21
+ if first.error:
22
+ detail += f"({first.path}: {first.error})"
23
+ details.append(detail)
24
+ if result.skipped:
25
+ details.append(f"跳过 {len(result.skipped)}({result.skipped[0].reason})")
26
+ if result.refused:
27
+ details.append(f"拒绝 {len(result.refused)}({result.refused[0].reason})")
28
+ if result.missing_targets:
29
+ details.append(f"已不存在 {len(result.missing_targets)}")
30
+ return details
31
+
32
+
33
+ def format_cleanup_notice(result: CleanupExecution, done_template: str) -> str:
34
+ """Summarize multi-target cleanup without turning partial work into success."""
35
+ if result.issues and not result.removed:
36
+ return f"保护证据不完整,未删除:{_protection_issue(result)}"
37
+ completed = len(result.completed)
38
+ details = _details(result)
39
+ if completed and not details:
40
+ return done_template.format(n=completed)
41
+ if completed:
42
+ return f"部分完成:{done_template.format(n=completed)};" + ";".join(details)
43
+ if result.removed:
44
+ return "清理部分失败:" + ";".join(details)
45
+ if result.refused:
46
+ return "已拒绝清理:" + ";".join(details)
47
+ if result.failed:
48
+ return "清理失败:" + ";".join(details)
49
+ if details:
50
+ return "未清理任何项目:" + ";".join(details)
51
+ return "无可清理内容"
52
+
53
+
54
+ def format_delete_notice(result: CleanupExecution) -> str:
55
+ """Summarize deletion of one session or background-agent artifact set."""
56
+ if result.issues and not result.removed:
57
+ return f"保护证据不完整,未删除:{_protection_issue(result)}"
58
+ if result.completed and not result.incomplete:
59
+ return "已删除"
60
+ if result.failed:
61
+ first = result.failed[0]
62
+ suffix = f":{first.error}" if first.error else ""
63
+ if result.removed:
64
+ return (
65
+ f"删除部分失败:已删除路径 {len(result.removed)};"
66
+ f"失败 {len(result.failed)}{suffix}"
67
+ )
68
+ return f"删除失败{suffix}"
69
+ if result.removed and result.incomplete:
70
+ return f"删除部分完成:已删除路径 {len(result.removed)};" + ";".join(
71
+ _details(result)
72
+ )
73
+ if result.refused:
74
+ return f"拒绝删除:{result.refused[0].reason}"
75
+ if result.skipped:
76
+ return f"未删除:{result.skipped[0].reason}"
77
+ return "无可删除内容"