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.
@@ -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
+ [![Python](https://img.shields.io/badge/Python-3.11%2B-3776AB?style=flat-square&logo=python&logoColor=white)](https://python.org)
40
+ [![Built with Textual](https://img.shields.io/badge/Built%20with-Textual-5A2CA0?style=flat-square)](https://github.com/Textualize/textual)
41
+ [![License](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](LICENSE)
42
+ [![Platform](https://img.shields.io/badge/Platform-Linux%20%7C%20macOS%20%7C%20Windows-orange?style=flat-square)](#)
43
+ [![Status](https://img.shields.io/badge/Status-Personal--use-lightgrey?style=flat-square)](#)
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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ asm = asm.__main__:main
@@ -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
@@ -0,0 +1,3 @@
1
+ """asm: Terminal UI for managing Claude Code & Codex sessions."""
2
+
3
+ __version__ = "0.5.0"
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"))