agent-session-manager 0.6.0__py3-none-any.whl
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.
- agent_session_manager-0.6.0.dist-info/METADATA +209 -0
- agent_session_manager-0.6.0.dist-info/RECORD +32 -0
- agent_session_manager-0.6.0.dist-info/WHEEL +4 -0
- agent_session_manager-0.6.0.dist-info/entry_points.txt +2 -0
- agent_session_manager-0.6.0.dist-info/licenses/LICENSE +21 -0
- asm/__init__.py +3 -0
- asm/__main__.py +54 -0
- asm/app.py +176 -0
- asm/i18n.py +467 -0
- asm/models.py +199 -0
- asm/screens/__init__.py +0 -0
- asm/screens/backups.py +713 -0
- asm/screens/codex_sessions.py +199 -0
- asm/screens/confirm.py +112 -0
- asm/screens/dashboard.py +402 -0
- asm/screens/debug_todos.py +633 -0
- asm/screens/file_history.py +348 -0
- asm/screens/input_dialog.py +71 -0
- asm/screens/migrate.py +474 -0
- asm/screens/projects.py +591 -0
- asm/services/__init__.py +0 -0
- asm/services/backup.py +593 -0
- asm/services/claude_data.py +877 -0
- asm/services/cleaner.py +334 -0
- asm/services/codex_data.py +368 -0
- asm/services/migrate.py +217 -0
- asm/services/pricing.py +141 -0
- asm/services/recovery.py +171 -0
- asm/services/update.py +107 -0
- asm/utils.py +12 -0
- asm/widgets/__init__.py +0 -0
- asm/widgets/action_bar.py +42 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: agent-session-manager
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: Manage Claude Code & Codex sessions, cost, and data from your terminal
|
|
5
|
+
Project-URL: Homepage, https://github.com/Bae-ChangHyun/agent-session-manager
|
|
6
|
+
Project-URL: Repository, https://github.com/Bae-ChangHyun/agent-session-manager
|
|
7
|
+
Project-URL: Issues, https://github.com/Bae-ChangHyun/agent-session-manager/issues
|
|
8
|
+
Author-email: bch <bch@users.noreply.github.com>
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: agent,claude,claude-code,codex,openai,session-manager,terminal,textual
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Environment :: Console
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: MacOS
|
|
17
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
18
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
24
|
+
Classifier: Topic :: Utilities
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.11
|
|
27
|
+
Requires-Dist: claude-agent-sdk>=0.1.40
|
|
28
|
+
Requires-Dist: send2trash>=1.8.0
|
|
29
|
+
Requires-Dist: textual>=1.0.0
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
<div align="center">
|
|
33
|
+
|
|
34
|
+
# agent-session-manager
|
|
35
|
+
|
|
36
|
+
**One terminal dashboard for everything Claude Code and Codex leave behind.**
|
|
37
|
+
See cost, browse sessions, and clean up `~/.claude` and `~/.codex` — side by side, with a Claude / Codex filter.
|
|
38
|
+
|
|
39
|
+
[](https://python.org)
|
|
40
|
+
[](https://github.com/Textualize/textual)
|
|
41
|
+
[](LICENSE)
|
|
42
|
+
[](#)
|
|
43
|
+
[](#)
|
|
44
|
+
|
|
45
|
+
`asm` · **[한국어](README.ko.md)**
|
|
46
|
+
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
> **⚠️ Heads up**
|
|
52
|
+
> This is a personal tool that reads and edits the **internal data of Claude Code and OpenAI Codex** (`~/.claude`, `~/.codex`). Every delete goes to the OS trash and is logged, but you should still back up before bulk operations. Not affiliated with Anthropic or OpenAI.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## What it is
|
|
57
|
+
|
|
58
|
+
If you live in **Claude Code** and **Codex**, the two of them quietly fill `~/.claude` and `~/.codex` with session transcripts, cost logs, debug files, task lists, and snapshots. After a few weeks it's impossible to tell which projects burned the most tokens, or which files are orphaned and safe to delete.
|
|
59
|
+
|
|
60
|
+
**agent-session-manager** (`asm`) puts all of it in one terminal dashboard. Both agents show up together, and a single keystroke filters the view to **All / Claude / Codex** so you can compare spend or drill into one tool.
|
|
61
|
+
|
|
62
|
+
### 💡 Why it exists
|
|
63
|
+
|
|
64
|
+
- **Problem:** cost and session data for two different coding agents live in two opaque directories with no shared view.
|
|
65
|
+
- **Solution:** one TUI that reads both, prices every model accurately, and lets you clean up safely — to the trash, with recovery snapshots.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Demo
|
|
70
|
+
|
|
71
|
+
The combined dashboard (cost across both agents, source-tagged rows), then the
|
|
72
|
+
same view filtered to **Claude** and to **Codex**:
|
|
73
|
+
|
|
74
|
+
<img src="docs/demo.gif" alt="asm demo" width="820"/>
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## ✨ Features
|
|
79
|
+
|
|
80
|
+
### Combined dashboard
|
|
81
|
+
- **Cost across both agents** with an in-app source filter — press `s` for All / Claude / Codex
|
|
82
|
+
- Per-model token & cost breakdown (Opus / Sonnet / Haiku / GPT-5.x), each row tagged by source
|
|
83
|
+
- Daily / weekly / monthly usage tables and a Top-10 project cost chart
|
|
84
|
+
- Accurate, current pricing from a LiteLLM-sourced rate table (new Opus/GPT models priced correctly, not at stale rates)
|
|
85
|
+
|
|
86
|
+
### Claude management
|
|
87
|
+
- **Projects:** tree from `.claude.json`, expand to preview session conversations, trash sessions, remove projects from config
|
|
88
|
+
- **Orphan cleanup:** detect and bulk-clean sessions, file-history, debug, and task entries with no matching project
|
|
89
|
+
- **Duplicate sessions:** find the same session copied across projects and delete individual copies
|
|
90
|
+
- **Migration:** copy sessions between projects (originals preserved), with paths auto-rewritten
|
|
91
|
+
|
|
92
|
+
### Codex sessions
|
|
93
|
+
- Browse `~/.codex` rollout sessions grouped by working directory
|
|
94
|
+
- Preview conversations and trash individual sessions
|
|
95
|
+
|
|
96
|
+
### Safe by default
|
|
97
|
+
- Every delete goes to the **OS trash** and is recorded in an audit log
|
|
98
|
+
- **Recovery snapshots** are taken before trashing (Claude and Codex)
|
|
99
|
+
- Backups for Claude (config / settings / plugins / sessions / full) and Codex (sessions, excluding huge caches), with auto safety-backup and rollback on restore
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## How it works
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
~/.claude ┐
|
|
107
|
+
├──► asm ──► one dashboard ──► filter: All / Claude / Codex
|
|
108
|
+
~/.codex ┘ (cost · sessions · cleanup · backup)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
`asm` reads both data directories directly — no daemon, no config. Claude data is grouped by project; Codex sessions are grouped by working directory. Pricing is computed from each session's recorded token usage.
|
|
112
|
+
|
|
113
|
+
| Path | What's there |
|
|
114
|
+
|:---|:---|
|
|
115
|
+
| `~/.claude.json` · `~/.claude/projects/` | Claude projects, costs, session JSONL |
|
|
116
|
+
| `~/.claude/file-history/` · `debug/` · `tasks/` | Snapshots, debug logs, per-session task lists |
|
|
117
|
+
| `~/.codex/sessions/` | Codex rollout session files |
|
|
118
|
+
| `~/.asm/backups/` · `trash-log.jsonl` | Backups (auto-migrated from old `~/.cc-tui`) and the deletion audit log |
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## 🛠️ Tech Stack
|
|
123
|
+
|
|
124
|
+
- **TUI:** [Textual](https://github.com/Textualize/textual) + [Rich](https://github.com/Textualize/rich)
|
|
125
|
+
- **Safety:** [send2trash](https://github.com/arsenetar/send2trash) (OS trash, not `rm`)
|
|
126
|
+
- **Sessions:** [claude-agent-sdk](https://pypi.org/project/claude-agent-sdk/) with a JSONL fallback parser
|
|
127
|
+
- **Python:** 3.11+
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## 🚀 Getting started
|
|
132
|
+
|
|
133
|
+
### Install (recommended)
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
# uv
|
|
137
|
+
uv tool install agent-session-manager
|
|
138
|
+
|
|
139
|
+
# pip
|
|
140
|
+
pip install agent-session-manager
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Both install a single `asm` command. On first run, data from the previous `~/.cc-tui` location is migrated to `~/.asm` automatically.
|
|
144
|
+
|
|
145
|
+
<details>
|
|
146
|
+
<summary><strong>Run from source</strong></summary>
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
git clone https://github.com/Bae-ChangHyun/agent-session-manager.git
|
|
150
|
+
cd agent-session-manager
|
|
151
|
+
uv sync && uv run asm
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
</details>
|
|
155
|
+
|
|
156
|
+
### Usage
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
asm # Launch — shows Claude + Codex together
|
|
160
|
+
asm --source codex # Start with the dashboard filtered to Codex
|
|
161
|
+
asm --path /your/project # Limit to one Claude project
|
|
162
|
+
asm --lang ko # Korean UI (or set ASM_LANG=ko)
|
|
163
|
+
asm --no-update-check # Skip the startup update check
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Both sources are always available; `--source` only sets the dashboard's initial filter, which you change anytime with `s`.
|
|
167
|
+
|
|
168
|
+
### Keyboard
|
|
169
|
+
|
|
170
|
+
| Key | Action |
|
|
171
|
+
|:---:|:---|
|
|
172
|
+
| `F1`–`F6` | Switch tabs |
|
|
173
|
+
| `s` | Dashboard source filter (All / Claude / Codex) |
|
|
174
|
+
| `Tab` / `Shift+Tab` · `1` `2` `3` | Dashboard period (Daily / Weekly / Monthly) |
|
|
175
|
+
| `d` / `D` | Trash selected / all orphaned |
|
|
176
|
+
| `Space` · `Enter` | Toggle selection · Preview conversation |
|
|
177
|
+
| `r` · `q` | Refresh all · Quit |
|
|
178
|
+
|
|
179
|
+
### Staying up to date
|
|
180
|
+
|
|
181
|
+
When a newer release is on PyPI, `asm` offers a `y/N` upgrade prompt on startup (via `uv tool` or `pip`). It's skipped in non-interactive shells and when offline.
|
|
182
|
+
|
|
183
|
+
---
|
|
184
|
+
|
|
185
|
+
## 🗺️ Roadmap
|
|
186
|
+
|
|
187
|
+
- [ ] Codex session **restore** (backup / list / delete exist today; restore does not)
|
|
188
|
+
- [ ] Per-source disk-usage and retention hints in the data overview
|
|
189
|
+
- [ ] Publish to PyPI as `agent-session-manager`
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## ⚠️ Status & scope
|
|
194
|
+
|
|
195
|
+
- **Personal / pre-release**, under active development.
|
|
196
|
+
- Operates directly on Claude Code and Codex internal data — **back up before bulk deletes**.
|
|
197
|
+
- Deletes go to the OS trash with recovery snapshots; nothing is `rm`'d in place.
|
|
198
|
+
- No warranty. Not affiliated with Anthropic or OpenAI.
|
|
199
|
+
|
|
200
|
+
---
|
|
201
|
+
|
|
202
|
+
## 📄 License
|
|
203
|
+
|
|
204
|
+
[MIT](LICENSE)
|
|
205
|
+
|
|
206
|
+
<div align="center">
|
|
207
|
+
<br/>
|
|
208
|
+
Made with <b>Claude Code</b> · and now <b>Codex</b> too
|
|
209
|
+
</div>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
asm/__init__.py,sha256=WyFNSRBKH4_6iRXbX936ujatR-T_FCKPTp25rai8Bhc,89
|
|
2
|
+
asm/__main__.py,sha256=xKVLV0RY53oAXg8nqbTkL2HTrT-Y0KdxC3Y-VuxcsZ0,1417
|
|
3
|
+
asm/app.py,sha256=ujwcASjLz3CJXYXm9PsdDXnJRvONLY_qvQsTAv-U7p0,6468
|
|
4
|
+
asm/i18n.py,sha256=2JZ8uDZppxhqcWUMeG5DjkHM5XeJz05kaWtA1ZLGrFw,29155
|
|
5
|
+
asm/models.py,sha256=sb9aV3Mjl9KyPDMDJXD9xa5vKcHnL5_lil6L16RvuKM,5406
|
|
6
|
+
asm/utils.py,sha256=8eKIXa3ShgkHylbxkfa_j3PBqGNh2HEQ0JcTudNrDBw,327
|
|
7
|
+
asm/screens/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
asm/screens/backups.py,sha256=OR9uXWXePMURKCR6BBdROa54l0vnbz5fjDIqfNlgCxs,27214
|
|
9
|
+
asm/screens/codex_sessions.py,sha256=YMU-hKVnNQLfqZSctdoI9-HwWyz5BmUuOb9pRrzHbOg,8087
|
|
10
|
+
asm/screens/confirm.py,sha256=33tt-INJOmsLhpnVam1RizKwiC_2kGNFdiRkdXmoKLQ,3265
|
|
11
|
+
asm/screens/dashboard.py,sha256=oGowORORz8BTxGgggKx4aPES6AOlcwi8WtzFKeeV3bU,16791
|
|
12
|
+
asm/screens/debug_todos.py,sha256=kxpbTwY8CyqUS1L9h9jAL9dzDv7YKC_y1fkESZATM9g,25094
|
|
13
|
+
asm/screens/file_history.py,sha256=07EqI-aBTXebLg2RpEJ7TAYPx8dwfwQiwtX196LMI_M,13314
|
|
14
|
+
asm/screens/input_dialog.py,sha256=9kW1wbUao9gPG5UQe6uAMnhEGsQghEKIsHlGU6qs8g8,1825
|
|
15
|
+
asm/screens/migrate.py,sha256=_4mee3wrhagcm2k3IzZZCqurAT30gnCLwdACr6zJcO8,19444
|
|
16
|
+
asm/screens/projects.py,sha256=zuA7wAaWSg5Sn7Uon9fy33U1Tedmh4cIJCrl534pisE,24834
|
|
17
|
+
asm/services/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
18
|
+
asm/services/backup.py,sha256=QRKZTdU-aGC-4rBe0fvHRiZOXlxPlKYwXYPDLo5aRN4,20373
|
|
19
|
+
asm/services/claude_data.py,sha256=VQr9hBI5Hgl3-uMDPqr7M16QN52o3VMjAPrwY0mladc,31275
|
|
20
|
+
asm/services/cleaner.py,sha256=Z7vrn3ecbuSdfgt4PcV8xaVD5oUKI80ph9n4ivkGOoU,10891
|
|
21
|
+
asm/services/codex_data.py,sha256=TsTeLjie5Zi0zTg94ez3YLFxhnk-queLMdYyRWjyes4,12731
|
|
22
|
+
asm/services/migrate.py,sha256=Zj-KMyQ4f3WvlS9i7OSXsQrMDKx068EArEJe1eaU8Ug,7764
|
|
23
|
+
asm/services/pricing.py,sha256=qCYSMsZoEmP9PDtdHeodwrYIyIeXqFpYysXsDM3C2rc,5377
|
|
24
|
+
asm/services/recovery.py,sha256=gFk5Z9z9qmUtIVHygshLjVuRlr-C831q1HFnYBThY1w,5938
|
|
25
|
+
asm/services/update.py,sha256=0u7DhBCTRjVQRnK2xdBJHVzcDiDBYXgVR6I33PlGpN8,3404
|
|
26
|
+
asm/widgets/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
|
+
asm/widgets/action_bar.py,sha256=mALwZ1e_MS9LdAKs1A3XD4oCZMALF7mTEbRJSHBdL60,1370
|
|
28
|
+
agent_session_manager-0.6.0.dist-info/METADATA,sha256=EU3Y35_Omwe26c2bggjqOFYNnBBHGo6SGjHFwZDN4IE,8312
|
|
29
|
+
agent_session_manager-0.6.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
30
|
+
agent_session_manager-0.6.0.dist-info/entry_points.txt,sha256=Ch8AIzXYp9Jyp3R9ttm2mE1v1ipdDp0XEGOqA60UVl8,42
|
|
31
|
+
agent_session_manager-0.6.0.dist-info/licenses/LICENSE,sha256=f2_s-scsQ4BeS_uuyLJZ6Aj5ah-pp1vox22S5whY95k,1076
|
|
32
|
+
agent_session_manager-0.6.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 cc-tui contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
asm/__init__.py
ADDED
asm/__main__.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Entry point for asm."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
|
|
5
|
+
from asm.app import CCTuiApp
|
|
6
|
+
from asm.i18n import init_lang
|
|
7
|
+
from asm.models import migrate_legacy_data_dir
|
|
8
|
+
from asm.services.update import maybe_prompt_update
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main():
|
|
12
|
+
parser = argparse.ArgumentParser(
|
|
13
|
+
prog="asm",
|
|
14
|
+
description="asm: manage Claude Code & Codex sessions, cost and data",
|
|
15
|
+
)
|
|
16
|
+
parser.add_argument(
|
|
17
|
+
"--path",
|
|
18
|
+
type=str,
|
|
19
|
+
default=None,
|
|
20
|
+
help="Specific project path to manage (default: global ~/.claude)",
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--lang",
|
|
24
|
+
type=str,
|
|
25
|
+
default=None,
|
|
26
|
+
choices=["en", "ko"],
|
|
27
|
+
help="UI language (default: en, or set ASM_LANG env var)",
|
|
28
|
+
)
|
|
29
|
+
parser.add_argument(
|
|
30
|
+
"--source",
|
|
31
|
+
type=str,
|
|
32
|
+
default="all",
|
|
33
|
+
choices=["all", "claude", "codex"],
|
|
34
|
+
help="Initial dashboard source filter: all (default), claude, or codex. "
|
|
35
|
+
"Both sources are always shown; toggle in-app with 's'.",
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument(
|
|
38
|
+
"--no-update-check",
|
|
39
|
+
action="store_true",
|
|
40
|
+
help="Skip the startup check for a newer release.",
|
|
41
|
+
)
|
|
42
|
+
args = parser.parse_args()
|
|
43
|
+
|
|
44
|
+
init_lang(args.lang)
|
|
45
|
+
migrate_legacy_data_dir()
|
|
46
|
+
if not args.no_update_check:
|
|
47
|
+
maybe_prompt_update()
|
|
48
|
+
|
|
49
|
+
app = CCTuiApp(target_path=args.path, source=args.source)
|
|
50
|
+
app.run()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
if __name__ == "__main__":
|
|
54
|
+
main()
|
asm/app.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Main Textual application."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from textual.app import App, ComposeResult
|
|
6
|
+
from textual.binding import Binding
|
|
7
|
+
from textual.widgets import Footer, Header, TabbedContent, TabPane
|
|
8
|
+
|
|
9
|
+
from asm.i18n import t
|
|
10
|
+
from asm.screens.backups import BackupsPane
|
|
11
|
+
from asm.screens.codex_sessions import CodexSessionsPane
|
|
12
|
+
from asm.screens.dashboard import DashboardPane
|
|
13
|
+
from asm.screens.debug_todos import DebugTodosPane
|
|
14
|
+
from asm.screens.file_history import FileHistoryPane
|
|
15
|
+
from asm.screens.migrate import MigratePane
|
|
16
|
+
from asm.screens.projects import ProjectsPane
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CCTuiApp(App):
|
|
20
|
+
"""asm — Claude Code & Codex session/data manager TUI."""
|
|
21
|
+
|
|
22
|
+
TITLE = "asm"
|
|
23
|
+
SUB_TITLE = "Claude Code & Codex session manager"
|
|
24
|
+
|
|
25
|
+
CSS = """
|
|
26
|
+
Screen {
|
|
27
|
+
background: $surface;
|
|
28
|
+
}
|
|
29
|
+
TabbedContent {
|
|
30
|
+
height: 1fr;
|
|
31
|
+
}
|
|
32
|
+
#main-tabs {
|
|
33
|
+
height: 1fr;
|
|
34
|
+
}
|
|
35
|
+
.pane-container {
|
|
36
|
+
height: 1fr;
|
|
37
|
+
padding: 1;
|
|
38
|
+
}
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
BINDINGS = [
|
|
42
|
+
Binding("tab", "tab_nav_next", "Next", show=False, priority=True),
|
|
43
|
+
Binding("shift+tab", "tab_nav_prev", "Previous", show=False, priority=True),
|
|
44
|
+
Binding("q", "quit", "Quit"),
|
|
45
|
+
Binding("r", "refresh", "Refresh"),
|
|
46
|
+
Binding("s", "dash_source", "Source", show=False),
|
|
47
|
+
Binding("1", "dash_daily", show=False),
|
|
48
|
+
Binding("2", "dash_weekly", show=False),
|
|
49
|
+
Binding("3", "dash_monthly", show=False),
|
|
50
|
+
Binding("f1", "tab('tab-dashboard')", "Dashboard"),
|
|
51
|
+
Binding("f2", "tab('tab-projects')", "Projects"),
|
|
52
|
+
Binding("f3", "tab('tab-file-history')", "File History"),
|
|
53
|
+
Binding("f4", "tab('tab-debug-todos')", "Debug/Todos"),
|
|
54
|
+
Binding("f5", "tab('tab-migrate')", "Migrate"),
|
|
55
|
+
Binding("f6", "tab('tab-backups')", "Backups"),
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
def __init__(self, target_path: str | None = None, source: str = "all", **kwargs):
|
|
59
|
+
super().__init__(**kwargs)
|
|
60
|
+
if target_path:
|
|
61
|
+
self.target_path = str(Path(target_path).resolve())
|
|
62
|
+
else:
|
|
63
|
+
self.target_path = None
|
|
64
|
+
# Initial dashboard source filter (all | claude | codex). Both sources
|
|
65
|
+
# are always loaded; this only sets the dashboard's starting view.
|
|
66
|
+
self.source = source
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def target_encoded(self) -> str | None:
|
|
70
|
+
"""Encoded project dir name for the target path."""
|
|
71
|
+
if self.target_path:
|
|
72
|
+
from asm.models import encode_path
|
|
73
|
+
return encode_path(self.target_path)
|
|
74
|
+
return None
|
|
75
|
+
|
|
76
|
+
def compose(self) -> ComposeResult:
|
|
77
|
+
if self.target_path:
|
|
78
|
+
self.sub_title = f"Project: {self.target_path}"
|
|
79
|
+
yield Header()
|
|
80
|
+
with TabbedContent(id="main-tabs"):
|
|
81
|
+
with TabPane(t("tab.dashboard"), id="tab-dashboard"):
|
|
82
|
+
yield DashboardPane()
|
|
83
|
+
# Claude-specific management tabs (operate on ~/.claude).
|
|
84
|
+
with TabPane(t("tab.projects"), id="tab-projects"):
|
|
85
|
+
yield ProjectsPane()
|
|
86
|
+
with TabPane(t("tab.file_history"), id="tab-file-history"):
|
|
87
|
+
yield FileHistoryPane()
|
|
88
|
+
with TabPane(t("tab.debug_todos"), id="tab-debug-todos"):
|
|
89
|
+
yield DebugTodosPane()
|
|
90
|
+
with TabPane(t("tab.migrate"), id="tab-migrate"):
|
|
91
|
+
yield MigratePane()
|
|
92
|
+
# Codex sessions (operate on ~/.codex); only shown when present.
|
|
93
|
+
if self._codex_available():
|
|
94
|
+
with TabPane(t("tab.codex_sessions"), id="tab-codex-sessions"):
|
|
95
|
+
yield CodexSessionsPane()
|
|
96
|
+
with TabPane(t("tab.backups"), id="tab-backups"):
|
|
97
|
+
yield BackupsPane()
|
|
98
|
+
yield Footer()
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def _codex_available() -> bool:
|
|
102
|
+
from asm.services import codex_data
|
|
103
|
+
return codex_data.is_available()
|
|
104
|
+
|
|
105
|
+
def action_tab(self, tab_id: str) -> None:
|
|
106
|
+
"""Switch to a specific tab by ID (ignored if the tab isn't present)."""
|
|
107
|
+
tabs = self.query_one("#main-tabs", TabbedContent)
|
|
108
|
+
try:
|
|
109
|
+
tabs.get_pane(tab_id)
|
|
110
|
+
except Exception:
|
|
111
|
+
return
|
|
112
|
+
tabs.active = tab_id
|
|
113
|
+
|
|
114
|
+
def on_tabbed_content_tab_activated(self, event: TabbedContent.TabActivated) -> None:
|
|
115
|
+
"""Focus the dashboard pane when its tab opens so its shortcuts work."""
|
|
116
|
+
if event.tabbed_content.id != "main-tabs":
|
|
117
|
+
return
|
|
118
|
+
if event.tab.id == "tab-dashboard":
|
|
119
|
+
self.call_after_refresh(self._focus_dashboard)
|
|
120
|
+
|
|
121
|
+
def _focus_dashboard(self) -> None:
|
|
122
|
+
try:
|
|
123
|
+
self.query_one("DashboardPane").focus()
|
|
124
|
+
except Exception:
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
def _dashboard_active(self) -> bool:
|
|
128
|
+
try:
|
|
129
|
+
return self.query_one("#main-tabs", TabbedContent).active == "tab-dashboard"
|
|
130
|
+
except Exception:
|
|
131
|
+
return False
|
|
132
|
+
|
|
133
|
+
def _dash(self, period: str | None) -> None:
|
|
134
|
+
if self._dashboard_active():
|
|
135
|
+
dash = self.query_one("DashboardPane")
|
|
136
|
+
dash.action_source_cycle() if period is None else dash.action_period(period)
|
|
137
|
+
|
|
138
|
+
def action_dash_source(self) -> None:
|
|
139
|
+
self._dash(None)
|
|
140
|
+
|
|
141
|
+
def action_dash_daily(self) -> None:
|
|
142
|
+
self._dash("daily")
|
|
143
|
+
|
|
144
|
+
def action_dash_weekly(self) -> None:
|
|
145
|
+
self._dash("weekly")
|
|
146
|
+
|
|
147
|
+
def action_dash_monthly(self) -> None:
|
|
148
|
+
self._dash("monthly")
|
|
149
|
+
|
|
150
|
+
def action_tab_nav_next(self) -> None:
|
|
151
|
+
"""Handle Tab navigation, with dashboard period cycling override."""
|
|
152
|
+
tabs = self.query_one("#main-tabs", TabbedContent)
|
|
153
|
+
if tabs.active == "tab-dashboard":
|
|
154
|
+
self.query_one("DashboardPane").action_period_next()
|
|
155
|
+
return
|
|
156
|
+
self.screen.focus_next()
|
|
157
|
+
|
|
158
|
+
def action_tab_nav_prev(self) -> None:
|
|
159
|
+
"""Handle reverse Tab navigation, with dashboard period cycling override."""
|
|
160
|
+
tabs = self.query_one("#main-tabs", TabbedContent)
|
|
161
|
+
if tabs.active == "tab-dashboard":
|
|
162
|
+
self.query_one("DashboardPane").action_period_prev()
|
|
163
|
+
return
|
|
164
|
+
self.screen.focus_previous()
|
|
165
|
+
|
|
166
|
+
def action_refresh(self) -> None:
|
|
167
|
+
"""Refresh all panes that have refresh_data."""
|
|
168
|
+
from asm.services import codex_data
|
|
169
|
+
codex_data.refresh()
|
|
170
|
+
for pane in self.query(
|
|
171
|
+
"DashboardPane, ProjectsPane, CodexSessionsPane, FileHistoryPane, "
|
|
172
|
+
"DebugTodosPane, MigratePane, BackupsPane"
|
|
173
|
+
):
|
|
174
|
+
if hasattr(pane, "refresh_data"):
|
|
175
|
+
pane.refresh_data()
|
|
176
|
+
self.notify(t("app.refreshed"))
|