claude-code-tools 1.0.6__py3-none-any.whl → 1.4.6__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.
Files changed (33) hide show
  1. claude_code_tools/__init__.py +1 -1
  2. claude_code_tools/action_rpc.py +16 -10
  3. claude_code_tools/aichat.py +793 -51
  4. claude_code_tools/claude_continue.py +4 -0
  5. claude_code_tools/codex_continue.py +48 -0
  6. claude_code_tools/export_session.py +94 -11
  7. claude_code_tools/find_claude_session.py +36 -12
  8. claude_code_tools/find_codex_session.py +33 -18
  9. claude_code_tools/find_session.py +30 -16
  10. claude_code_tools/gdoc2md.py +220 -0
  11. claude_code_tools/md2gdoc.py +549 -0
  12. claude_code_tools/search_index.py +119 -15
  13. claude_code_tools/session_menu_cli.py +1 -1
  14. claude_code_tools/session_utils.py +3 -3
  15. claude_code_tools/smart_trim.py +18 -8
  16. claude_code_tools/smart_trim_core.py +4 -2
  17. claude_code_tools/tmux_cli_controller.py +35 -25
  18. claude_code_tools/trim_session.py +28 -2
  19. claude_code_tools-1.4.6.dist-info/METADATA +1112 -0
  20. {claude_code_tools-1.0.6.dist-info → claude_code_tools-1.4.6.dist-info}/RECORD +31 -24
  21. {claude_code_tools-1.0.6.dist-info → claude_code_tools-1.4.6.dist-info}/entry_points.txt +2 -0
  22. docs/linked-in-20260102.md +32 -0
  23. docs/local-llm-setup.md +286 -0
  24. docs/reddit-aichat-resume-v2.md +80 -0
  25. docs/reddit-aichat-resume.md +29 -0
  26. docs/reddit-aichat.md +79 -0
  27. docs/rollover-details.md +67 -0
  28. node_ui/action_config.js +3 -3
  29. node_ui/menu.js +67 -113
  30. claude_code_tools/session_tui.py +0 -516
  31. claude_code_tools-1.0.6.dist-info/METADATA +0 -685
  32. {claude_code_tools-1.0.6.dist-info → claude_code_tools-1.4.6.dist-info}/WHEEL +0 -0
  33. {claude_code_tools-1.0.6.dist-info → claude_code_tools-1.4.6.dist-info}/licenses/LICENSE +0 -0
@@ -1,516 +0,0 @@
1
- """
2
- Textual-based Terminal User Interface for session management.
3
-
4
- Provides an interactive, arrow-navigable interface for browsing and
5
- managing Claude Code and Codex sessions.
6
- """
7
-
8
- from typing import Any, Callable, Dict, List, Optional, Tuple
9
- from datetime import datetime
10
- import textwrap
11
-
12
- from textual.app import App, ComposeResult
13
- from textual.binding import Binding
14
- from textual.containers import Container, Vertical, VerticalScroll
15
- from textual.screen import ModalScreen, Screen
16
- from textual.widgets import Footer, Header, ListView, ListItem, OptionList, Static
17
- from textual.widgets.option_list import Option
18
- from textual.message import Message
19
- from rich.text import Text
20
-
21
-
22
- class ActionMenuScreen(ModalScreen[Optional[str]]):
23
- """Minimal modal screen for selecting session actions."""
24
-
25
- BINDINGS = [
26
- Binding("escape", "dismiss_none", "Back"),
27
- ]
28
-
29
- def __init__(
30
- self,
31
- session_id: str,
32
- agent: str,
33
- project_name: str,
34
- git_branch: Optional[str] = None,
35
- is_sidechain: bool = False,
36
- ):
37
- """
38
- Initialize action menu screen.
39
-
40
- Args:
41
- session_id: Session identifier
42
- agent: Agent type ('claude' or 'codex')
43
- project_name: Project or working directory name
44
- git_branch: Optional git branch name
45
- is_sidechain: If True, this is a sub-agent session
46
- """
47
- super().__init__()
48
- self.session_id = session_id
49
- self.agent = agent
50
- self.project_name = project_name
51
- self.git_branch = git_branch
52
- self.is_sidechain = is_sidechain
53
-
54
- def compose(self) -> ComposeResult:
55
- """Compose the action menu UI."""
56
- with Container(id="action-menu-container"):
57
- yield Static(
58
- f"[bold]{self.session_id[:8]}...[/] | "
59
- f"{self.agent.title()} | {self.project_name}"
60
- + (f" | {self.git_branch}" if self.git_branch else ""),
61
- id="session-header",
62
- )
63
-
64
- # Build options list
65
- options = []
66
- if not self.is_sidechain:
67
- options.append(Option("Resume Session", id="resume"))
68
- options.append(Option("Continue (Fresh Session)", id="continue"))
69
- options.extend([
70
- Option("Show File Path", id="path"),
71
- Option("Copy Session File", id="copy"),
72
- ])
73
- if not self.is_sidechain:
74
- options.append(Option("Clone & Resume", id="clone"))
75
- options.extend([
76
- Option("Export to Text", id="export"),
77
- Option("← Back", id="back"),
78
- ])
79
-
80
- yield OptionList(*options, id="action-options")
81
-
82
- def on_option_list_option_selected(
83
- self, event: OptionList.OptionSelected
84
- ) -> None:
85
- """Handle option selection."""
86
- if event.option.id == "back":
87
- self.dismiss(None)
88
- else:
89
- self.dismiss(event.option.id)
90
-
91
- def action_dismiss_none(self) -> None:
92
- """Dismiss with None (back action)."""
93
- self.dismiss(None)
94
-
95
-
96
- class SessionCard(Static):
97
- """A card widget displaying session information with multi-line preview."""
98
-
99
- DEFAULT_CSS = """
100
- SessionCard {
101
- height: auto;
102
- padding: 1 2;
103
- border: solid $panel-lighten-1;
104
- margin: 0 1;
105
- }
106
-
107
- SessionCard.selected {
108
- background: $accent;
109
- border: solid $accent-lighten-2;
110
- }
111
-
112
- SessionCard:hover {
113
- background: $panel-lighten-1;
114
- }
115
- """
116
-
117
- def __init__(
118
- self,
119
- session: Tuple[Any, ...],
120
- index: int,
121
- is_selected: bool = False,
122
- ):
123
- """
124
- Initialize session card.
125
-
126
- Args:
127
- session: Session tuple data
128
- index: Display index (1-based)
129
- is_selected: Whether this card is currently selected
130
- """
131
- super().__init__()
132
- self.session = session
133
- self.index = index
134
- self.is_selected = is_selected
135
-
136
- # Extract session details
137
- if isinstance(session, (tuple, list)) and len(session) >= 10: # Claude tuple format
138
- (
139
- session_id,
140
- mod_time,
141
- create_time,
142
- line_count,
143
- project_name,
144
- preview,
145
- project_path,
146
- git_branch,
147
- is_trimmed,
148
- is_sidechain,
149
- ) = session[:10]
150
- agent = "Claude"
151
- else: # Dict format
152
- session_id = session.get("session_id", "")
153
- mod_time = session.get("mod_time", 0)
154
- line_count = session.get("lines", 0)
155
- project_name = session.get("project", "")
156
- preview = session.get("preview", "")
157
- git_branch = session.get("branch", "")
158
- is_sidechain = session.get("is_sidechain", False)
159
- agent = session.get("agent_display", "Unknown")
160
-
161
- # Store extracted values
162
- self.session_id = session_id
163
- self.agent = agent
164
- self.project_name = project_name
165
- self.git_branch = git_branch
166
- self.mod_time = mod_time
167
- self.line_count = line_count
168
- self.preview = preview
169
- self.is_sidechain = is_sidechain
170
-
171
- def render(self) -> Text:
172
- """Render the session card content."""
173
- date_str = datetime.fromtimestamp(self.mod_time).strftime("%m/%d %H:%M")
174
-
175
- # First line: index, agent, project, branch, date
176
- branch_display = f"({self.git_branch})" if self.git_branch else ""
177
- header = Text()
178
- header.append(f"{self.index}. ", style="bold cyan")
179
- header.append(f"[{self.agent}] ", style="bold yellow")
180
- header.append(f"{self.project_name[:30]} ", style="white")
181
- if branch_display:
182
- header.append(f"{branch_display[:18]} ", style="dim")
183
- # Pad to align date to the right
184
- header.append(" " * (70 - len(header.plain)))
185
- header.append(f"{date_str}", style="dim")
186
-
187
- # Second line: Session ID and line count
188
- session_display = f"{self.session_id[:12]}..."
189
- if self.is_sidechain:
190
- session_display += " (s)"
191
-
192
- info = Text()
193
- info.append(f" Session: ", style="dim")
194
- info.append(session_display, style="cyan")
195
- info.append(f" {self.line_count:,} lines", style="dim")
196
-
197
- # Preview lines (wrapped, max 3 lines)
198
- preview_text = Text()
199
- if self.preview:
200
- preview_lines = textwrap.wrap(self.preview, width=70)[:3]
201
- preview_text.append(" Preview: ", style="dim")
202
- preview_text.append(preview_lines[0] if preview_lines else "")
203
- for line in preview_lines[1:]:
204
- preview_text.append("\n ")
205
- preview_text.append(line)
206
-
207
- # Combine all parts
208
- result = Text()
209
- result.append_text(header)
210
- result.append("\n")
211
- result.append_text(info)
212
- if self.preview:
213
- result.append("\n")
214
- result.append_text(preview_text)
215
-
216
- return result
217
-
218
- def on_mount(self) -> None:
219
- """Update classes when mounted."""
220
- if self.is_selected:
221
- self.add_class("selected")
222
-
223
-
224
- class SessionTableScreen(Screen):
225
- """Main screen displaying interactive session table."""
226
-
227
- BINDINGS = [
228
- Binding("escape", "quit_app", "Quit"),
229
- Binding("q", "quit_app", "Quit"),
230
- Binding("g", "goto_mode", "Goto Row"),
231
- Binding("1", "quick_select(1)", "Select 1"),
232
- Binding("2", "quick_select(2)", "Select 2"),
233
- Binding("3", "quick_select(3)", "Select 3"),
234
- Binding("4", "quick_select(4)", "Select 4"),
235
- Binding("5", "quick_select(5)", "Select 5"),
236
- Binding("6", "quick_select(6)", "Select 6"),
237
- Binding("7", "quick_select(7)", "Select 7"),
238
- Binding("8", "quick_select(8)", "Select 8"),
239
- Binding("9", "quick_select(9)", "Select 9"),
240
- ]
241
-
242
- def __init__(
243
- self,
244
- sessions: List[Tuple[Any, ...]],
245
- keywords: List[str],
246
- action_handler: Callable[[Tuple[Any, ...], str], None],
247
- ):
248
- """
249
- Initialize session table screen.
250
-
251
- Args:
252
- sessions: List of session tuples
253
- keywords: Search keywords used
254
- action_handler: Function to handle selected actions
255
- """
256
- super().__init__()
257
- self.sessions = sessions
258
- self.keywords = keywords
259
- self.action_handler = action_handler
260
- self.goto_mode = False
261
- self.goto_input = ""
262
- self.selected_index = 0
263
-
264
- def compose(self) -> ComposeResult:
265
- """Compose the session list UI."""
266
- title = (
267
- f"Sessions matching: {', '.join(self.keywords)}"
268
- if self.keywords
269
- else "All Sessions"
270
- )
271
- yield Header(show_clock=True)
272
- yield Static(f"[bold cyan]{title}[/]", id="table-title")
273
-
274
- # Create scrollable container for session cards
275
- with VerticalScroll(id="session-list"):
276
- for idx, session in enumerate(self.sessions, 1):
277
- yield SessionCard(session, idx, is_selected=(idx == 1))
278
-
279
- yield Static("", id="goto-input-display")
280
- yield Footer()
281
-
282
- def on_mount(self) -> None:
283
- """Initialize list when screen is mounted."""
284
- # Focus the scroll container
285
- self.query_one("#session-list").focus()
286
-
287
- # Auto-select if only one session
288
- if len(self.sessions) == 1:
289
- self.notify("Auto-selecting only session...")
290
- self.show_action_menu_for_row(0)
291
-
292
- def on_click(self, event) -> None:
293
- """Handle click on session card."""
294
- if not self.goto_mode:
295
- # Find which card was clicked
296
- card = event.widget
297
- if isinstance(card, SessionCard):
298
- # Find the index of this card
299
- cards = list(self.query(SessionCard))
300
- if card in cards:
301
- self.select_card(cards.index(card))
302
- self.show_action_menu_for_row(cards.index(card))
303
-
304
- def on_key(self, event) -> None:
305
- """Handle key press events."""
306
- if self.goto_mode:
307
- # Goto mode key handling
308
- if event.key == "escape":
309
- self.goto_mode = False
310
- self.goto_input = ""
311
- self.update_goto_display()
312
- event.prevent_default()
313
- elif event.key.isdigit():
314
- self.goto_input += event.key
315
- self.update_goto_display()
316
- event.prevent_default()
317
- elif event.key == "enter":
318
- if self.goto_input:
319
- row_index = int(self.goto_input) - 1
320
- if 0 <= row_index < len(self.sessions):
321
- self.select_card(row_index)
322
- self.show_action_menu_for_row(row_index)
323
- self.goto_mode = False
324
- self.goto_input = ""
325
- self.update_goto_display()
326
- event.prevent_default()
327
- else:
328
- # Normal navigation
329
- if event.key == "enter":
330
- self.show_action_menu_for_row(self.selected_index)
331
- event.prevent_default()
332
- elif event.key == "down":
333
- if self.selected_index < len(self.sessions) - 1:
334
- self.select_card(self.selected_index + 1)
335
- event.prevent_default()
336
- elif event.key == "up":
337
- if self.selected_index > 0:
338
- self.select_card(self.selected_index - 1)
339
- event.prevent_default()
340
-
341
- def select_card(self, index: int) -> None:
342
- """Select a card by index."""
343
- if 0 <= index < len(self.sessions):
344
- cards = list(self.query(SessionCard))
345
-
346
- # Remove selected class from all cards
347
- for card in cards:
348
- card.remove_class("selected")
349
-
350
- # Add selected class to new card
351
- if index < len(cards):
352
- cards[index].add_class("selected")
353
- # Scroll to make it visible
354
- cards[index].scroll_visible()
355
-
356
- self.selected_index = index
357
-
358
- def show_action_menu_for_row(self, row_index: int) -> None:
359
- """Show action menu for selected session."""
360
- if 0 <= row_index < len(self.sessions):
361
- session = self.sessions[row_index]
362
-
363
- # Extract session details
364
- if isinstance(session, (tuple, list)) and len(session) >= 10: # Claude tuple format
365
- session_id = session[0]
366
- project_name = session[4]
367
- git_branch = session[7]
368
- is_sidechain = session[9]
369
- agent = "claude"
370
- else: # Dict format
371
- session_id = session.get("session_id", "")
372
- project_name = session.get("project", "")
373
- git_branch = session.get("branch")
374
- is_sidechain = session.get("is_sidechain", False)
375
- agent = session.get("agent", "unknown")
376
-
377
- # Show action menu and handle result
378
- self.app.push_screen(
379
- ActionMenuScreen(
380
- session_id=session_id,
381
- agent=agent,
382
- project_name=project_name,
383
- git_branch=git_branch,
384
- is_sidechain=is_sidechain,
385
- ),
386
- callback=lambda action: self.handle_action_result(session, action),
387
- )
388
-
389
- def handle_action_result(
390
- self, session: Tuple[Any, ...], action: Optional[str]
391
- ) -> None:
392
- """Handle the result from action menu."""
393
- if action:
394
- # Call the action handler
395
- self.action_handler(session, action)
396
-
397
- # If action is resume, clone, or continue, exit the app
398
- if action in ("resume", "clone", "continue", "suppress_resume", "smart_trim_resume"):
399
- self.app.exit()
400
- else:
401
- # For other actions, show the action menu again (persistent loop)
402
- self.show_action_menu_for_row(self.selected_index)
403
-
404
- def action_quit_app(self) -> None:
405
- """Quit the application."""
406
- self.app.exit()
407
-
408
- def action_goto_mode(self) -> None:
409
- """Enter goto row mode."""
410
- self.goto_mode = True
411
- self.goto_input = ""
412
- self.update_goto_display()
413
-
414
- def action_quick_select(self, row_num: str) -> None:
415
- """Quick select row by number (1-9)."""
416
- if not self.goto_mode:
417
- row_index = int(row_num) - 1
418
- if 0 <= row_index < len(self.sessions):
419
- self.select_card(row_index)
420
- self.show_action_menu_for_row(row_index)
421
-
422
- def update_goto_display(self) -> None:
423
- """Update the goto input display."""
424
- display = self.query_one("#goto-input-display", Static)
425
- if self.goto_mode:
426
- display.update(
427
- f"[bold yellow]Goto row:[/] {self.goto_input}_"
428
- )
429
- else:
430
- display.update("")
431
-
432
-
433
- class SessionMenuApp(App):
434
- """Main TUI application for session management."""
435
-
436
- CSS = """
437
- #action-menu-container {
438
- width: 60;
439
- height: auto;
440
- padding: 1;
441
- background: $panel;
442
- border: solid $primary;
443
- }
444
-
445
- #session-header {
446
- padding: 0 1 1 1;
447
- text-align: center;
448
- }
449
-
450
- #action-options {
451
- height: auto;
452
- max-height: 15;
453
- }
454
-
455
- #table-title {
456
- padding: 1 2;
457
- background: $panel;
458
- }
459
-
460
- #goto-input-display {
461
- padding: 0 2;
462
- height: 1;
463
- background: $panel;
464
- }
465
-
466
- #session-list {
467
- height: 1fr;
468
- padding: 1 0;
469
- }
470
- """
471
-
472
- def __init__(
473
- self,
474
- sessions: List[Tuple[Any, ...]],
475
- keywords: List[str],
476
- action_handler: Callable[[Tuple[Any, ...], str], None],
477
- ):
478
- """
479
- Initialize the session menu TUI app.
480
-
481
- Args:
482
- sessions: List of session tuples
483
- keywords: Search keywords
484
- action_handler: Function to handle actions
485
- """
486
- super().__init__()
487
- self.sessions = sessions
488
- self.keywords = keywords
489
- self.action_handler = action_handler
490
-
491
- def on_mount(self) -> None:
492
- """Push the main session table screen on mount."""
493
- self.push_screen(
494
- SessionTableScreen(
495
- self.sessions,
496
- self.keywords,
497
- self.action_handler,
498
- )
499
- )
500
-
501
-
502
- def run_session_tui(
503
- sessions: List[Tuple[Any, ...]],
504
- keywords: List[str],
505
- action_handler: Callable[[Tuple[Any, ...], str], None],
506
- ) -> None:
507
- """
508
- Run the session management TUI.
509
-
510
- Args:
511
- sessions: List of session tuples
512
- keywords: Search keywords used to find sessions
513
- action_handler: Callback function to handle selected actions
514
- """
515
- app = SessionMenuApp(sessions, keywords, action_handler)
516
- app.run()