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.
- claude_code_tools/__init__.py +1 -1
- claude_code_tools/action_rpc.py +16 -10
- claude_code_tools/aichat.py +793 -51
- claude_code_tools/claude_continue.py +4 -0
- claude_code_tools/codex_continue.py +48 -0
- claude_code_tools/export_session.py +94 -11
- claude_code_tools/find_claude_session.py +36 -12
- claude_code_tools/find_codex_session.py +33 -18
- claude_code_tools/find_session.py +30 -16
- claude_code_tools/gdoc2md.py +220 -0
- claude_code_tools/md2gdoc.py +549 -0
- claude_code_tools/search_index.py +119 -15
- claude_code_tools/session_menu_cli.py +1 -1
- claude_code_tools/session_utils.py +3 -3
- claude_code_tools/smart_trim.py +18 -8
- claude_code_tools/smart_trim_core.py +4 -2
- claude_code_tools/tmux_cli_controller.py +35 -25
- claude_code_tools/trim_session.py +28 -2
- claude_code_tools-1.4.6.dist-info/METADATA +1112 -0
- {claude_code_tools-1.0.6.dist-info → claude_code_tools-1.4.6.dist-info}/RECORD +31 -24
- {claude_code_tools-1.0.6.dist-info → claude_code_tools-1.4.6.dist-info}/entry_points.txt +2 -0
- docs/linked-in-20260102.md +32 -0
- docs/local-llm-setup.md +286 -0
- docs/reddit-aichat-resume-v2.md +80 -0
- docs/reddit-aichat-resume.md +29 -0
- docs/reddit-aichat.md +79 -0
- docs/rollover-details.md +67 -0
- node_ui/action_config.js +3 -3
- node_ui/menu.js +67 -113
- claude_code_tools/session_tui.py +0 -516
- claude_code_tools-1.0.6.dist-info/METADATA +0 -685
- {claude_code_tools-1.0.6.dist-info → claude_code_tools-1.4.6.dist-info}/WHEEL +0 -0
- {claude_code_tools-1.0.6.dist-info → claude_code_tools-1.4.6.dist-info}/licenses/LICENSE +0 -0
|
@@ -192,6 +192,10 @@ def claude_continue(
|
|
|
192
192
|
try:
|
|
193
193
|
# Parse first line and add metadata fields
|
|
194
194
|
first_line_data = json.loads(lines[0])
|
|
195
|
+
# Remove trim_metadata if present - a session is either continued
|
|
196
|
+
# OR trimmed, not both. continue_metadata.parent_session_file
|
|
197
|
+
# preserves ancestry.
|
|
198
|
+
first_line_data.pop("trim_metadata", None)
|
|
195
199
|
first_line_data.update(metadata_fields)
|
|
196
200
|
lines[0] = json.dumps(first_line_data) + "\n"
|
|
197
201
|
|
|
@@ -11,12 +11,14 @@ the context limit. It:
|
|
|
11
11
|
"""
|
|
12
12
|
|
|
13
13
|
import argparse
|
|
14
|
+
import datetime
|
|
14
15
|
import json
|
|
15
16
|
import os
|
|
16
17
|
import re
|
|
17
18
|
import shlex
|
|
18
19
|
import subprocess
|
|
19
20
|
import sys
|
|
21
|
+
from datetime import timezone
|
|
20
22
|
from pathlib import Path
|
|
21
23
|
from typing import List, Optional
|
|
22
24
|
|
|
@@ -24,6 +26,7 @@ from claude_code_tools.export_codex_session import resolve_session_path
|
|
|
24
26
|
from claude_code_tools.session_utils import (
|
|
25
27
|
build_rollover_prompt,
|
|
26
28
|
build_session_file_list,
|
|
29
|
+
get_session_uuid,
|
|
27
30
|
)
|
|
28
31
|
|
|
29
32
|
|
|
@@ -177,6 +180,51 @@ def codex_continue(
|
|
|
177
180
|
print(f"❌ Error: {e}", file=sys.stderr)
|
|
178
181
|
sys.exit(1)
|
|
179
182
|
|
|
183
|
+
# Inject continue_metadata into new session file
|
|
184
|
+
try:
|
|
185
|
+
new_session_file = resolve_session_path(thread_id, codex_home)
|
|
186
|
+
|
|
187
|
+
# Create metadata
|
|
188
|
+
metadata_fields = {
|
|
189
|
+
"continue_metadata": {
|
|
190
|
+
"parent_session_id": get_session_uuid(session_file.name),
|
|
191
|
+
"parent_session_file": str(session_file.absolute()),
|
|
192
|
+
"continued_at": datetime.datetime.now(timezone.utc).isoformat(),
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# Read the new session file and modify first line
|
|
197
|
+
if new_session_file.exists():
|
|
198
|
+
with open(new_session_file, "r") as f:
|
|
199
|
+
lines = f.readlines()
|
|
200
|
+
|
|
201
|
+
if lines:
|
|
202
|
+
try:
|
|
203
|
+
# Parse first line and add metadata fields
|
|
204
|
+
first_line_data = json.loads(lines[0])
|
|
205
|
+
# Remove trim_metadata if present - a session is either continued
|
|
206
|
+
# OR trimmed, not both. continue_metadata.parent_session_file
|
|
207
|
+
# preserves ancestry.
|
|
208
|
+
first_line_data.pop("trim_metadata", None)
|
|
209
|
+
first_line_data.update(metadata_fields)
|
|
210
|
+
lines[0] = json.dumps(first_line_data) + "\n"
|
|
211
|
+
|
|
212
|
+
# Write back the modified file
|
|
213
|
+
with open(new_session_file, "w") as f:
|
|
214
|
+
f.writelines(lines)
|
|
215
|
+
|
|
216
|
+
if verbose:
|
|
217
|
+
print(f"✅ Added continue_metadata to new session")
|
|
218
|
+
print()
|
|
219
|
+
except json.JSONDecodeError:
|
|
220
|
+
# If first line is malformed, skip adding metadata
|
|
221
|
+
if verbose:
|
|
222
|
+
print(f"⚠️ Could not add metadata (first line malformed)", file=sys.stderr)
|
|
223
|
+
except Exception as e:
|
|
224
|
+
# Don't fail the whole operation if metadata injection fails
|
|
225
|
+
if verbose:
|
|
226
|
+
print(f"⚠️ Could not add continue_metadata: {e}", file=sys.stderr)
|
|
227
|
+
|
|
180
228
|
# Step 3: Resume in interactive mode - hand off to Codex
|
|
181
229
|
# Resume with default model (not the mini model used for analysis)
|
|
182
230
|
from claude_code_tools.config import codex_default_model
|
|
@@ -2,11 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import os
|
|
5
|
+
import re
|
|
5
6
|
from datetime import datetime
|
|
6
7
|
from io import StringIO
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from typing import Any, Optional
|
|
9
10
|
|
|
11
|
+
# Known system-injected XML tags that appear at the start of messages.
|
|
12
|
+
# Using a whitelist of specific tags avoids filtering legitimate user
|
|
13
|
+
# messages that start with HTML/XML like <div> or <svg>.
|
|
14
|
+
NON_GENUINE_XML_TAGS = {
|
|
15
|
+
# Claude system tags (local command execution)
|
|
16
|
+
"command-name",
|
|
17
|
+
"command-message",
|
|
18
|
+
"command-args",
|
|
19
|
+
"local-command-stdout",
|
|
20
|
+
"bash-input",
|
|
21
|
+
"bash-stdout",
|
|
22
|
+
"bash-stderr",
|
|
23
|
+
"bash-notification",
|
|
24
|
+
# Codex system tags (environment/context injection)
|
|
25
|
+
"environment_context",
|
|
26
|
+
"user_instructions",
|
|
27
|
+
"user_shell_command",
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
# Regex patterns for non-genuine user messages (system-injected content).
|
|
31
|
+
# Messages matching any of these patterns are filtered out when finding
|
|
32
|
+
# the first real user message. Used for both Claude and Codex sessions.
|
|
33
|
+
NON_GENUINE_MSG_PATTERNS = [
|
|
34
|
+
re.compile(r"^Caveat:", re.IGNORECASE), # Caveat warnings about local commands
|
|
35
|
+
re.compile(r"^\s*\[SESSION LINEAGE\]", re.IGNORECASE), # Session continuation context
|
|
36
|
+
]
|
|
37
|
+
|
|
10
38
|
# Lazy import yaml to allow module to load even if not installed
|
|
11
39
|
try:
|
|
12
40
|
import yaml
|
|
@@ -139,22 +167,60 @@ def _extract_codex_message_text(data: dict) -> Optional[str]:
|
|
|
139
167
|
return None
|
|
140
168
|
|
|
141
169
|
|
|
170
|
+
def _is_meta_user_message(data: dict, text: str) -> bool:
|
|
171
|
+
"""
|
|
172
|
+
Check if a user message is a meta/system-injected message.
|
|
173
|
+
|
|
174
|
+
These include local command injections that Claude Code records
|
|
175
|
+
in the session file but aren't actual user queries.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
data: The parsed JSON data for the message
|
|
179
|
+
text: The extracted text content
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
True if this is a meta message that should be skipped
|
|
183
|
+
"""
|
|
184
|
+
# Check isMeta flag
|
|
185
|
+
if data.get("isMeta") is True:
|
|
186
|
+
return True
|
|
187
|
+
|
|
188
|
+
# Check against regex patterns (Caveat, SESSION LINEAGE, etc.)
|
|
189
|
+
for pattern in NON_GENUINE_MSG_PATTERNS:
|
|
190
|
+
if pattern.search(text):
|
|
191
|
+
return True
|
|
192
|
+
|
|
193
|
+
# Check if message starts with a known system-injected XML tag
|
|
194
|
+
text_stripped = text.strip()
|
|
195
|
+
match = re.match(r"^<([a-z][a-z0-9_-]*)>", text_stripped)
|
|
196
|
+
if match and match.group(1) in NON_GENUINE_XML_TAGS:
|
|
197
|
+
return True
|
|
198
|
+
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
|
|
142
202
|
def extract_first_last_messages(
|
|
143
203
|
session_file: Path, agent: str
|
|
144
|
-
) -> tuple[
|
|
204
|
+
) -> tuple[
|
|
205
|
+
Optional[dict[str, str]],
|
|
206
|
+
Optional[dict[str, str]],
|
|
207
|
+
Optional[dict[str, str]],
|
|
208
|
+
]:
|
|
145
209
|
"""
|
|
146
|
-
Extract first and
|
|
210
|
+
Extract first/last messages and the first real user message from a session.
|
|
147
211
|
|
|
148
212
|
Args:
|
|
149
213
|
session_file: Path to session JSONL file
|
|
150
214
|
agent: Agent type ('claude' or 'codex')
|
|
151
215
|
|
|
152
216
|
Returns:
|
|
153
|
-
Tuple of (first_msg, last_msg) where each is a dict
|
|
154
|
-
'content' keys, or None if not found
|
|
217
|
+
Tuple of (first_msg, last_msg, first_user_msg) where each is a dict
|
|
218
|
+
with 'role' and 'content' keys, or None if not found.
|
|
219
|
+
first_user_msg skips meta messages (local command injections).
|
|
155
220
|
"""
|
|
156
221
|
first_msg: Optional[dict[str, str]] = None
|
|
157
222
|
last_msg: Optional[dict[str, str]] = None
|
|
223
|
+
first_user_msg: Optional[dict[str, str]] = None
|
|
158
224
|
|
|
159
225
|
try:
|
|
160
226
|
with open(session_file, "r", encoding="utf-8") as f:
|
|
@@ -190,13 +256,20 @@ def extract_first_last_messages(
|
|
|
190
256
|
}
|
|
191
257
|
if first_msg is None:
|
|
192
258
|
first_msg = msg_dict
|
|
259
|
+
# Track first real user message (skip meta messages)
|
|
260
|
+
if (
|
|
261
|
+
role == "user"
|
|
262
|
+
and first_user_msg is None
|
|
263
|
+
and not _is_meta_user_message(data, text)
|
|
264
|
+
):
|
|
265
|
+
first_user_msg = msg_dict
|
|
193
266
|
# Always update last_msg to get the last one
|
|
194
267
|
last_msg = msg_dict
|
|
195
268
|
|
|
196
269
|
except (OSError, IOError):
|
|
197
270
|
pass
|
|
198
271
|
|
|
199
|
-
return first_msg, last_msg
|
|
272
|
+
return first_msg, last_msg, first_user_msg
|
|
200
273
|
|
|
201
274
|
|
|
202
275
|
def extract_session_metadata(session_file: Path, agent: str) -> dict[str, Any]:
|
|
@@ -216,6 +289,11 @@ def extract_session_metadata(session_file: Path, agent: str) -> dict[str, Any]:
|
|
|
216
289
|
Returns:
|
|
217
290
|
Dict with extracted metadata
|
|
218
291
|
"""
|
|
292
|
+
# Detect sidechain from filename pattern (agent-* prefix)
|
|
293
|
+
# This is more reliable than checking isSidechain field in JSON,
|
|
294
|
+
# which can be set on individual messages within main sessions
|
|
295
|
+
is_sidechain = session_file.name.startswith("agent-")
|
|
296
|
+
|
|
219
297
|
metadata: dict[str, Any] = {
|
|
220
298
|
"session_id": session_file.stem,
|
|
221
299
|
"agent": agent,
|
|
@@ -223,7 +301,7 @@ def extract_session_metadata(session_file: Path, agent: str) -> dict[str, Any]:
|
|
|
223
301
|
"cwd": None,
|
|
224
302
|
"branch": None,
|
|
225
303
|
"derivation_type": None,
|
|
226
|
-
"is_sidechain":
|
|
304
|
+
"is_sidechain": is_sidechain,
|
|
227
305
|
"session_type": None, # "helper" for SDK/headless sessions
|
|
228
306
|
"parent_session_id": None,
|
|
229
307
|
"parent_session_file": None,
|
|
@@ -231,6 +309,7 @@ def extract_session_metadata(session_file: Path, agent: str) -> dict[str, Any]:
|
|
|
231
309
|
"trim_stats": None,
|
|
232
310
|
"first_msg": None,
|
|
233
311
|
"last_msg": None,
|
|
312
|
+
"first_user_msg": None,
|
|
234
313
|
}
|
|
235
314
|
|
|
236
315
|
# Track session start timestamp from JSON metadata
|
|
@@ -278,10 +357,6 @@ def extract_session_metadata(session_file: Path, agent: str) -> dict[str, Any]:
|
|
|
278
357
|
metadata["parent_session_id"] = cm.get("parent_session_id")
|
|
279
358
|
metadata["parent_session_file"] = cm.get("parent_session_file")
|
|
280
359
|
|
|
281
|
-
# Check if sidechain (sub-agent session)
|
|
282
|
-
if "isSidechain" in data and data["isSidechain"] is True:
|
|
283
|
-
metadata["is_sidechain"] = True
|
|
284
|
-
|
|
285
360
|
# Extract sessionType (e.g., "helper" for SDK/headless sessions)
|
|
286
361
|
if "sessionType" in data and metadata["session_type"] is None:
|
|
287
362
|
metadata["session_type"] = data["sessionType"]
|
|
@@ -321,6 +396,9 @@ def extract_session_metadata(session_file: Path, agent: str) -> dict[str, Any]:
|
|
|
321
396
|
except (OSError, IOError):
|
|
322
397
|
pass
|
|
323
398
|
|
|
399
|
+
# Note: customTitle extraction is done in search_index.py's _extract_session_content
|
|
400
|
+
# during the single-pass content extraction, to avoid an extra file scan here.
|
|
401
|
+
|
|
324
402
|
# Get modified time from last JSONL entry's timestamp (reflects actual session
|
|
325
403
|
# activity, portable across machines). Fall back to file mtime if not found.
|
|
326
404
|
last_timestamp = _get_last_line_timestamp(session_file)
|
|
@@ -367,9 +445,12 @@ def extract_session_metadata(session_file: Path, agent: str) -> dict[str, Any]:
|
|
|
367
445
|
metadata["project"] = Path(metadata["cwd"]).name
|
|
368
446
|
|
|
369
447
|
# Extract first and last messages
|
|
370
|
-
first_msg, last_msg = extract_first_last_messages(
|
|
448
|
+
first_msg, last_msg, first_user_msg = extract_first_last_messages(
|
|
449
|
+
session_file, agent
|
|
450
|
+
)
|
|
371
451
|
metadata["first_msg"] = first_msg
|
|
372
452
|
metadata["last_msg"] = last_msg
|
|
453
|
+
metadata["first_user_msg"] = first_user_msg
|
|
373
454
|
|
|
374
455
|
return metadata
|
|
375
456
|
|
|
@@ -454,6 +535,8 @@ def generate_yaml_frontmatter(metadata: dict[str, Any]) -> str:
|
|
|
454
535
|
yaml_data["first_msg"] = metadata["first_msg"]
|
|
455
536
|
if metadata.get("last_msg"):
|
|
456
537
|
yaml_data["last_msg"] = metadata["last_msg"]
|
|
538
|
+
if metadata.get("first_user_msg"):
|
|
539
|
+
yaml_data["first_user_msg"] = metadata["first_user_msg"]
|
|
457
540
|
|
|
458
541
|
# Trim stats (only for trimmed sessions)
|
|
459
542
|
if metadata.get("trim_stats"):
|
|
@@ -92,12 +92,6 @@ def prompt_post_action() -> str:
|
|
|
92
92
|
return "back"
|
|
93
93
|
return "exit"
|
|
94
94
|
|
|
95
|
-
# Try to import TUI - it's optional
|
|
96
|
-
try:
|
|
97
|
-
from claude_code_tools.session_tui import run_session_tui
|
|
98
|
-
TUI_AVAILABLE = True
|
|
99
|
-
except ImportError:
|
|
100
|
-
TUI_AVAILABLE = False
|
|
101
95
|
from claude_code_tools.smart_trim_core import identify_trimmable_lines_cli
|
|
102
96
|
from claude_code_tools.smart_trim import trim_lines
|
|
103
97
|
from claude_code_tools.session_utils import (
|
|
@@ -833,6 +827,19 @@ def handle_suppress_resume_claude(
|
|
|
833
827
|
print(f"❌ Error trimming session: {e}")
|
|
834
828
|
return None
|
|
835
829
|
|
|
830
|
+
# Check if nothing to trim (savings below threshold)
|
|
831
|
+
from claude_code_tools.node_menu_ui import run_trim_confirm_ui
|
|
832
|
+
if result.get("nothing_to_trim"):
|
|
833
|
+
print(f" Savings too small ({result['tokens_saved']} tokens)")
|
|
834
|
+
action = run_trim_confirm_ui(
|
|
835
|
+
nothing_to_trim=True,
|
|
836
|
+
original_session_id=session_id,
|
|
837
|
+
)
|
|
838
|
+
if action == 'resume':
|
|
839
|
+
resume_session(session_id, project_path, claude_home=claude_home)
|
|
840
|
+
return None
|
|
841
|
+
return 'back'
|
|
842
|
+
|
|
836
843
|
new_session_id = result["session_id"]
|
|
837
844
|
new_session_file = Path(result["output_file"])
|
|
838
845
|
|
|
@@ -840,7 +847,6 @@ def handle_suppress_resume_claude(
|
|
|
840
847
|
total_trimmed = result['num_tools_trimmed'] + result['num_assistant_trimmed']
|
|
841
848
|
|
|
842
849
|
# Show confirmation UI
|
|
843
|
-
from claude_code_tools.node_menu_ui import run_trim_confirm_ui
|
|
844
850
|
action = run_trim_confirm_ui(
|
|
845
851
|
new_session_id=new_session_id,
|
|
846
852
|
lines_trimmed=total_trimmed,
|
|
@@ -1300,8 +1306,12 @@ def create_action_handler(claude_home: Optional[str] = None, nonlaunch_flag: Opt
|
|
|
1300
1306
|
"""
|
|
1301
1307
|
def handle_session_action(
|
|
1302
1308
|
session: Tuple, action: str, kwargs: Optional[dict] = None
|
|
1303
|
-
) -> None:
|
|
1304
|
-
"""Handle actions for a selected session.
|
|
1309
|
+
) -> str | None:
|
|
1310
|
+
"""Handle actions for a selected session.
|
|
1311
|
+
|
|
1312
|
+
Returns:
|
|
1313
|
+
'back' if the action wants to return to resume menu, None otherwise.
|
|
1314
|
+
"""
|
|
1305
1315
|
kwargs = kwargs or {}
|
|
1306
1316
|
|
|
1307
1317
|
if isinstance(session, dict):
|
|
@@ -1320,13 +1330,13 @@ def create_action_handler(claude_home: Optional[str] = None, nonlaunch_flag: Opt
|
|
|
1320
1330
|
if tools is None and threshold is None and trim_assistant is None:
|
|
1321
1331
|
options = prompt_suppress_options()
|
|
1322
1332
|
if not options:
|
|
1323
|
-
return
|
|
1333
|
+
return None
|
|
1324
1334
|
tools, threshold, trim_assistant = options
|
|
1325
|
-
handle_suppress_resume_claude(
|
|
1335
|
+
return handle_suppress_resume_claude(
|
|
1326
1336
|
session_id, project_path, tools, threshold or 500, trim_assistant, claude_home
|
|
1327
1337
|
)
|
|
1328
1338
|
elif action == "smart_trim_resume":
|
|
1329
|
-
handle_smart_trim_resume_claude(session_id, project_path, claude_home)
|
|
1339
|
+
return handle_smart_trim_resume_claude(session_id, project_path, claude_home)
|
|
1330
1340
|
elif action == "path":
|
|
1331
1341
|
# handled in Node UI via RPC
|
|
1332
1342
|
if nonlaunch_flag is not None:
|
|
@@ -1581,6 +1591,19 @@ To persist directory changes when resuming sessions:
|
|
|
1581
1591
|
rpc_path = str(Path(__file__).parent / "action_rpc.py")
|
|
1582
1592
|
|
|
1583
1593
|
if not args.simple_ui:
|
|
1594
|
+
from claude_code_tools.export_session import extract_session_metadata
|
|
1595
|
+
|
|
1596
|
+
def get_custom_title(session_id: str, cwd: str) -> str:
|
|
1597
|
+
"""Extract custom title from session file if present."""
|
|
1598
|
+
try:
|
|
1599
|
+
fp = get_session_file_path(session_id, cwd, args.claude_home)
|
|
1600
|
+
if fp:
|
|
1601
|
+
meta = extract_session_metadata(Path(fp), "claude")
|
|
1602
|
+
return meta.get("customTitle", "")
|
|
1603
|
+
except Exception:
|
|
1604
|
+
pass
|
|
1605
|
+
return ""
|
|
1606
|
+
|
|
1584
1607
|
limited = [
|
|
1585
1608
|
{
|
|
1586
1609
|
"agent": "claude",
|
|
@@ -1598,6 +1621,7 @@ To persist directory changes when resuming sessions:
|
|
|
1598
1621
|
"is_trimmed": s[8] if len(s) > 8 else False,
|
|
1599
1622
|
"derivation_type": None,
|
|
1600
1623
|
"is_sidechain": s[9] if len(s) > 9 else False,
|
|
1624
|
+
"custom_title": get_custom_title(s[0], s[6]),
|
|
1601
1625
|
}
|
|
1602
1626
|
for s in matching_sessions[: args.num_matches]
|
|
1603
1627
|
]
|
|
@@ -65,6 +65,7 @@ from claude_code_tools.smart_trim_core import (
|
|
|
65
65
|
from claude_code_tools.smart_trim import trim_lines
|
|
66
66
|
from claude_code_tools.session_utils import (
|
|
67
67
|
get_codex_home,
|
|
68
|
+
get_session_uuid,
|
|
68
69
|
format_session_id_display,
|
|
69
70
|
filter_sessions_by_time,
|
|
70
71
|
)
|
|
@@ -78,14 +79,6 @@ try:
|
|
|
78
79
|
except ImportError:
|
|
79
80
|
RICH_AVAILABLE = False
|
|
80
81
|
|
|
81
|
-
try:
|
|
82
|
-
from claude_code_tools.session_tui import run_session_tui
|
|
83
|
-
|
|
84
|
-
TUI_AVAILABLE = True
|
|
85
|
-
except ImportError:
|
|
86
|
-
TUI_AVAILABLE = False
|
|
87
|
-
|
|
88
|
-
|
|
89
82
|
def extract_session_id_from_filename(filename: str) -> Optional[str]:
|
|
90
83
|
"""
|
|
91
84
|
Extract session ID from Codex session filename.
|
|
@@ -585,6 +578,19 @@ def handle_suppress_resume_codex(
|
|
|
585
578
|
print(f"❌ Error trimming session: {e}")
|
|
586
579
|
return None
|
|
587
580
|
|
|
581
|
+
# Check if nothing to trim (savings below threshold)
|
|
582
|
+
from claude_code_tools.node_menu_ui import run_trim_confirm_ui
|
|
583
|
+
if result.get("nothing_to_trim"):
|
|
584
|
+
print(f" Savings too small ({result['tokens_saved']} tokens)")
|
|
585
|
+
action = run_trim_confirm_ui(
|
|
586
|
+
nothing_to_trim=True,
|
|
587
|
+
original_session_id=match["session_id"],
|
|
588
|
+
)
|
|
589
|
+
if action == 'resume':
|
|
590
|
+
resume_session(match["session_id"], match["cwd"])
|
|
591
|
+
return None
|
|
592
|
+
return 'back'
|
|
593
|
+
|
|
588
594
|
new_session_id = result["session_id"]
|
|
589
595
|
new_session_file = Path(result["output_file"])
|
|
590
596
|
|
|
@@ -592,7 +598,6 @@ def handle_suppress_resume_codex(
|
|
|
592
598
|
total_trimmed = result['num_tools_trimmed'] + result['num_assistant_trimmed']
|
|
593
599
|
|
|
594
600
|
# Show confirmation UI
|
|
595
|
-
from claude_code_tools.node_menu_ui import run_trim_confirm_ui
|
|
596
601
|
action = run_trim_confirm_ui(
|
|
597
602
|
new_session_id=new_session_id,
|
|
598
603
|
lines_trimmed=total_trimmed,
|
|
@@ -689,11 +694,17 @@ def handle_smart_trim_resume_codex(
|
|
|
689
694
|
}
|
|
690
695
|
|
|
691
696
|
# Generate new session ID (UUID only) and filename with Codex format
|
|
692
|
-
|
|
697
|
+
# New session goes in today's date folder (YYYY/MM/DD)
|
|
698
|
+
now = datetime.now()
|
|
699
|
+
timestamp = now.strftime("%Y-%m-%dT%H-%M-%S")
|
|
700
|
+
date_path = now.strftime("%Y/%m/%d")
|
|
693
701
|
new_session_id = str(uuid.uuid4())
|
|
694
702
|
|
|
695
|
-
#
|
|
696
|
-
|
|
703
|
+
# Find sessions root by going up from input file (sessions/YYYY/MM/DD/file.jsonl)
|
|
704
|
+
sessions_root = session_file.parent.parent.parent.parent
|
|
705
|
+
output_dir = sessions_root / date_path
|
|
706
|
+
output_dir.mkdir(parents=True, exist_ok=True)
|
|
707
|
+
output_file = output_dir / f"rollout-{timestamp}-{new_session_id}.jsonl"
|
|
697
708
|
|
|
698
709
|
# Perform trimming
|
|
699
710
|
stats = trim_lines(
|
|
@@ -760,7 +771,7 @@ def handle_export_session(session_file_path: str, dest_override: str | None = No
|
|
|
760
771
|
|
|
761
772
|
# Generate default export path using session's project directory
|
|
762
773
|
session_file = Path(session_file_path)
|
|
763
|
-
session_id = session_file.
|
|
774
|
+
session_id = get_session_uuid(session_file.name)
|
|
764
775
|
today = datetime.now().strftime("%Y%m%d")
|
|
765
776
|
|
|
766
777
|
# Infer project directory from session metadata
|
|
@@ -977,8 +988,12 @@ def resume_session(
|
|
|
977
988
|
|
|
978
989
|
def create_action_handler(shell_mode: bool = False, codex_home: Optional[Path] = None, nonlaunch_flag: Optional[dict] = None):
|
|
979
990
|
"""Create an action handler for the TUI."""
|
|
980
|
-
def action_handler(session, action: str, kwargs: Optional[dict] = None) -> None:
|
|
981
|
-
"""Handle actions from the TUI - session can be tuple or dict.
|
|
991
|
+
def action_handler(session, action: str, kwargs: Optional[dict] = None) -> str | None:
|
|
992
|
+
"""Handle actions from the TUI - session can be tuple or dict.
|
|
993
|
+
|
|
994
|
+
Returns:
|
|
995
|
+
'back' if the action wants to return to resume menu, None otherwise.
|
|
996
|
+
"""
|
|
982
997
|
kwargs = kwargs or {}
|
|
983
998
|
# Ensure session is a dict
|
|
984
999
|
if not isinstance(session, dict):
|
|
@@ -999,14 +1014,14 @@ def create_action_handler(shell_mode: bool = False, codex_home: Optional[Path] =
|
|
|
999
1014
|
if tools is None and threshold is None and trim_assistant is None:
|
|
1000
1015
|
options = prompt_suppress_options()
|
|
1001
1016
|
if not options:
|
|
1002
|
-
return
|
|
1017
|
+
return None
|
|
1003
1018
|
tools, threshold, trim_assistant = options
|
|
1004
|
-
handle_suppress_resume_codex(
|
|
1019
|
+
return handle_suppress_resume_codex(
|
|
1005
1020
|
session, tools, threshold or 500, trim_assistant, codex_home
|
|
1006
1021
|
)
|
|
1007
1022
|
elif action == "smart_trim_resume":
|
|
1008
1023
|
# Smart trim using parallel agents
|
|
1009
|
-
handle_smart_trim_resume_codex(session, codex_home)
|
|
1024
|
+
return handle_smart_trim_resume_codex(session, codex_home)
|
|
1010
1025
|
elif action == "path":
|
|
1011
1026
|
if nonlaunch_flag is not None:
|
|
1012
1027
|
nonlaunch_flag["done"] = True
|
|
@@ -70,14 +70,6 @@ try:
|
|
|
70
70
|
except ImportError:
|
|
71
71
|
RICH_AVAILABLE = False
|
|
72
72
|
|
|
73
|
-
try:
|
|
74
|
-
from claude_code_tools.session_tui import run_session_tui
|
|
75
|
-
|
|
76
|
-
TUI_AVAILABLE = True
|
|
77
|
-
except ImportError:
|
|
78
|
-
TUI_AVAILABLE = False
|
|
79
|
-
|
|
80
|
-
|
|
81
73
|
@dataclass
|
|
82
74
|
class AgentConfig:
|
|
83
75
|
"""Configuration for a coding agent."""
|
|
@@ -570,6 +562,18 @@ def handle_suppress_resume(
|
|
|
570
562
|
print(f"❌ Error trimming session: {e}", file=output)
|
|
571
563
|
return
|
|
572
564
|
|
|
565
|
+
# Check if nothing to trim (savings below threshold)
|
|
566
|
+
if result.get("nothing_to_trim"):
|
|
567
|
+
print(f"\n{'='*70}", file=output)
|
|
568
|
+
print(f"⚠️ NOTHING TO TRIM", file=output)
|
|
569
|
+
print(f"{'='*70}", file=output)
|
|
570
|
+
print(
|
|
571
|
+
f" Savings too small ({result['tokens_saved']} tokens) - "
|
|
572
|
+
f"no new session created",
|
|
573
|
+
file=output,
|
|
574
|
+
)
|
|
575
|
+
return
|
|
576
|
+
|
|
573
577
|
new_session_id = result["session_id"]
|
|
574
578
|
new_session_file = result["output_file"]
|
|
575
579
|
|
|
@@ -625,8 +629,12 @@ def create_action_handler(
|
|
|
625
629
|
):
|
|
626
630
|
"""Create an action handler for the TUI or Node UI."""
|
|
627
631
|
|
|
628
|
-
def action_handler(session, action: str, kwargs: Optional[dict] = None) -> None:
|
|
629
|
-
"""Handle actions from the UI - session can be tuple or dict.
|
|
632
|
+
def action_handler(session, action: str, kwargs: Optional[dict] = None) -> str | None:
|
|
633
|
+
"""Handle actions from the UI - session can be tuple or dict.
|
|
634
|
+
|
|
635
|
+
Returns:
|
|
636
|
+
'back' if the action wants to return to resume menu, None otherwise.
|
|
637
|
+
"""
|
|
630
638
|
# Convert session to dict if it's a tuple or dict-like
|
|
631
639
|
if isinstance(session, dict):
|
|
632
640
|
session_dict = session
|
|
@@ -634,7 +642,7 @@ def create_action_handler(
|
|
|
634
642
|
# Shouldn't happen in unified find, but handle gracefully
|
|
635
643
|
session_dict = {"session_id": str(session), "agent": "unknown"}
|
|
636
644
|
|
|
637
|
-
handle_action(
|
|
645
|
+
result = handle_action(
|
|
638
646
|
session_dict, action, shell_mode=shell_mode, action_kwargs=kwargs or {}
|
|
639
647
|
)
|
|
640
648
|
|
|
@@ -642,13 +650,19 @@ def create_action_handler(
|
|
|
642
650
|
nonlaunch_flag["done"] = True
|
|
643
651
|
nonlaunch_flag["session_id"] = session_dict.get("session_id")
|
|
644
652
|
|
|
653
|
+
return result
|
|
654
|
+
|
|
645
655
|
return action_handler
|
|
646
656
|
|
|
647
657
|
|
|
648
658
|
def handle_action(
|
|
649
659
|
session: dict, action: str, shell_mode: bool = False, action_kwargs: Optional[dict] = None
|
|
650
|
-
) -> None:
|
|
651
|
-
"""Handle the selected action based on agent type.
|
|
660
|
+
) -> str | None:
|
|
661
|
+
"""Handle the selected action based on agent type.
|
|
662
|
+
|
|
663
|
+
Returns:
|
|
664
|
+
'back' if the action wants to return to resume menu, None otherwise.
|
|
665
|
+
"""
|
|
652
666
|
agent = session["agent"]
|
|
653
667
|
action_kwargs = action_kwargs or {}
|
|
654
668
|
|
|
@@ -675,14 +689,14 @@ def handle_action(
|
|
|
675
689
|
if options:
|
|
676
690
|
tools, threshold, trim_assistant = options
|
|
677
691
|
else:
|
|
678
|
-
return
|
|
692
|
+
return None
|
|
679
693
|
handle_suppress_resume(
|
|
680
694
|
session, tools, threshold or 500, trim_assistant, shell_mode
|
|
681
695
|
)
|
|
682
696
|
|
|
683
697
|
elif action == "smart_trim_resume":
|
|
684
698
|
if agent == "claude":
|
|
685
|
-
handle_smart_trim_resume_claude(
|
|
699
|
+
return handle_smart_trim_resume_claude(
|
|
686
700
|
session["session_id"],
|
|
687
701
|
session["cwd"],
|
|
688
702
|
session.get("claude_home"),
|
|
@@ -690,7 +704,7 @@ def handle_action(
|
|
|
690
704
|
elif agent == "codex":
|
|
691
705
|
# Get file path for codex
|
|
692
706
|
file_path = session.get("file_path", "")
|
|
693
|
-
handle_smart_trim_resume_codex(file_path)
|
|
707
|
+
return handle_smart_trim_resume_codex(file_path)
|
|
694
708
|
|
|
695
709
|
elif action == "path":
|
|
696
710
|
if agent == "claude":
|