mrstack 1.1.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.
- mrstack/__init__.py +4 -0
- mrstack/_data/config/com.mrstack.claude-telegram.plist +25 -0
- mrstack/_data/config/mcp-config.example.json +23 -0
- mrstack/_data/config/start-daemon.sh +53 -0
- mrstack/_data/config/start.sh +29 -0
- mrstack/_data/schedulers/manage-jobs.sh +87 -0
- mrstack/_data/schedulers/morning-briefing.sh +29 -0
- mrstack/_data/schedulers/register-jobs.py +182 -0
- mrstack/_data/schedulers/run-threads-briefing.sh +36 -0
- mrstack/_data/schedulers/weekly-review.sh +26 -0
- mrstack/_data/templates/DESIGN-GUIDE.md +160 -0
- mrstack/_data/templates/alert.md +56 -0
- mrstack/_data/templates/evening-summary.md +73 -0
- mrstack/_data/templates/jarvis-alert.md +64 -0
- mrstack/_data/templates/morning-briefing.md +53 -0
- mrstack/_data/templates/weekly-review.md +79 -0
- mrstack/_overlay/api/dashboard.py +223 -0
- mrstack/_overlay/api/templates/dashboard.html +328 -0
- mrstack/_overlay/bot/handlers/callback.py +1432 -0
- mrstack/_overlay/bot/handlers/command.py +1541 -0
- mrstack/_overlay/bot/utils/keyboards.py +125 -0
- mrstack/_overlay/bot/utils/ui_components.py +166 -0
- mrstack/_overlay/claude/session.py +341 -0
- mrstack/_overlay/jarvis/__init__.py +77 -0
- mrstack/_overlay/jarvis/coach.py +122 -0
- mrstack/_overlay/jarvis/context_engine.py +463 -0
- mrstack/_overlay/jarvis/pattern_learner.py +255 -0
- mrstack/_overlay/jarvis/persona.py +84 -0
- mrstack/_overlay/jarvis/platform.py +182 -0
- mrstack/_overlay/knowledge/__init__.py +6 -0
- mrstack/_overlay/knowledge/manager.py +464 -0
- mrstack/_overlay/knowledge/memory_index.py +180 -0
- mrstack/cli.py +330 -0
- mrstack/constants.py +77 -0
- mrstack/daemon.py +325 -0
- mrstack/patcher.py +169 -0
- mrstack/wizard.py +271 -0
- mrstack-1.1.0.dist-info/METADATA +640 -0
- mrstack-1.1.0.dist-info/RECORD +42 -0
- mrstack-1.1.0.dist-info/WHEEL +4 -0
- mrstack-1.1.0.dist-info/entry_points.txt +2 -0
- mrstack-1.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1432 @@
|
|
|
1
|
+
"""Handle inline keyboard callbacks."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import structlog
|
|
7
|
+
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
|
|
8
|
+
from telegram.ext import ContextTypes
|
|
9
|
+
|
|
10
|
+
from ...claude.facade import ClaudeIntegration
|
|
11
|
+
from ...config.settings import Settings
|
|
12
|
+
from ...security.audit import AuditLogger
|
|
13
|
+
from ...security.validators import SecurityValidator
|
|
14
|
+
from ..utils.html_format import escape_html
|
|
15
|
+
|
|
16
|
+
logger = structlog.get_logger()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _is_within_root(path: Path, root: Path) -> bool:
|
|
20
|
+
"""Check whether path is within root directory."""
|
|
21
|
+
try:
|
|
22
|
+
path.resolve().relative_to(root.resolve())
|
|
23
|
+
return True
|
|
24
|
+
except ValueError:
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _get_thread_project_root(
|
|
29
|
+
settings: Settings, context: ContextTypes.DEFAULT_TYPE
|
|
30
|
+
) -> Optional[Path]:
|
|
31
|
+
"""Get thread project root when strict thread mode is active."""
|
|
32
|
+
if not settings.enable_project_threads:
|
|
33
|
+
return None
|
|
34
|
+
thread_context = context.user_data.get("_thread_context")
|
|
35
|
+
if not thread_context:
|
|
36
|
+
return None
|
|
37
|
+
return Path(thread_context["project_root"]).resolve()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def handle_callback_query(
|
|
41
|
+
update: Update, context: ContextTypes.DEFAULT_TYPE
|
|
42
|
+
) -> None:
|
|
43
|
+
"""Route callback queries to appropriate handlers."""
|
|
44
|
+
query = update.callback_query
|
|
45
|
+
await query.answer() # Acknowledge the callback
|
|
46
|
+
|
|
47
|
+
user_id = query.from_user.id
|
|
48
|
+
data = query.data
|
|
49
|
+
|
|
50
|
+
logger.info("Processing callback query", user_id=user_id, callback_data=data)
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
# Parse callback data
|
|
54
|
+
if ":" in data:
|
|
55
|
+
action, param = data.split(":", 1)
|
|
56
|
+
else:
|
|
57
|
+
action, param = data, None
|
|
58
|
+
|
|
59
|
+
# Route to appropriate handler
|
|
60
|
+
handlers = {
|
|
61
|
+
"cd": handle_cd_callback,
|
|
62
|
+
"action": handle_action_callback,
|
|
63
|
+
"agentic": handle_agentic_callback,
|
|
64
|
+
"confirm": handle_confirm_callback,
|
|
65
|
+
"quick": handle_quick_action_callback,
|
|
66
|
+
"followup": handle_followup_callback,
|
|
67
|
+
"conversation": handle_conversation_callback,
|
|
68
|
+
"git": handle_git_callback,
|
|
69
|
+
"export": handle_export_callback,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
handler = handlers.get(action)
|
|
73
|
+
if handler:
|
|
74
|
+
await handler(query, param, context)
|
|
75
|
+
else:
|
|
76
|
+
await query.edit_message_text(
|
|
77
|
+
"❌ <b>Unknown Action</b>\n\n"
|
|
78
|
+
"This button action is not recognized. "
|
|
79
|
+
"The bot may have been updated since this message was sent.",
|
|
80
|
+
parse_mode="HTML",
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logger.error(
|
|
85
|
+
"Error handling callback query",
|
|
86
|
+
error=str(e),
|
|
87
|
+
user_id=user_id,
|
|
88
|
+
callback_data=data,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
await query.edit_message_text(
|
|
93
|
+
"❌ <b>Error Processing Action</b>\n\n"
|
|
94
|
+
"An error occurred while processing your request.\n"
|
|
95
|
+
"Please try again or use text commands.",
|
|
96
|
+
parse_mode="HTML",
|
|
97
|
+
)
|
|
98
|
+
except Exception:
|
|
99
|
+
# If we can't edit the message, send a new one
|
|
100
|
+
await query.message.reply_text(
|
|
101
|
+
"❌ <b>Error Processing Action</b>\n\n"
|
|
102
|
+
"An error occurred while processing your request.",
|
|
103
|
+
parse_mode="HTML",
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
async def handle_agentic_callback(
|
|
108
|
+
query, action_type: str, context: ContextTypes.DEFAULT_TYPE
|
|
109
|
+
) -> None:
|
|
110
|
+
"""Handle agentic mode callbacks (debug, full_error, continue, new_task)."""
|
|
111
|
+
# Map agentic actions to existing action handlers
|
|
112
|
+
action_map = {
|
|
113
|
+
"continue": "continue",
|
|
114
|
+
"new_task": "new_session",
|
|
115
|
+
"run_tests": "start_coding",
|
|
116
|
+
"show_diff": "start_coding",
|
|
117
|
+
"status": "status",
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# Simple redirect actions
|
|
121
|
+
if action_type in action_map:
|
|
122
|
+
mapped_action = action_map[action_type]
|
|
123
|
+
await handle_action_callback(query, mapped_action, context)
|
|
124
|
+
return
|
|
125
|
+
|
|
126
|
+
user_id = query.from_user.id
|
|
127
|
+
settings: Settings = context.bot_data["settings"]
|
|
128
|
+
claude_integration: ClaudeIntegration = context.bot_data.get("claude_integration")
|
|
129
|
+
|
|
130
|
+
if action_type == "full_error":
|
|
131
|
+
# Extract error text from the original message
|
|
132
|
+
original_text = query.message.text or query.message.text_html or ""
|
|
133
|
+
await query.edit_message_text(
|
|
134
|
+
f"<b>Full Error Details</b>\n\n"
|
|
135
|
+
f"<pre><code>{escape_html(original_text[:3500])}</code></pre>",
|
|
136
|
+
parse_mode="HTML",
|
|
137
|
+
reply_markup=InlineKeyboardMarkup([
|
|
138
|
+
[
|
|
139
|
+
InlineKeyboardButton("Debug", callback_data="agentic:debug"),
|
|
140
|
+
InlineKeyboardButton("New Session", callback_data="agentic:new_task"),
|
|
141
|
+
]
|
|
142
|
+
]),
|
|
143
|
+
)
|
|
144
|
+
return
|
|
145
|
+
|
|
146
|
+
if action_type == "debug":
|
|
147
|
+
if not claude_integration:
|
|
148
|
+
await query.edit_message_text(
|
|
149
|
+
"Claude integration is not available.",
|
|
150
|
+
parse_mode="HTML",
|
|
151
|
+
)
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
# Extract error from the original message to send to Claude for debugging
|
|
155
|
+
original_text = query.message.text or ""
|
|
156
|
+
current_dir = context.user_data.get(
|
|
157
|
+
"current_directory", settings.approved_directory
|
|
158
|
+
)
|
|
159
|
+
session_id = context.user_data.get("claude_session_id")
|
|
160
|
+
|
|
161
|
+
debug_prompt = (
|
|
162
|
+
f"The following error occurred. Analyze the root cause and suggest a fix:\n\n"
|
|
163
|
+
f"{original_text[:2000]}"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
await query.edit_message_text(
|
|
167
|
+
"Analyzing error...",
|
|
168
|
+
parse_mode="HTML",
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
try:
|
|
172
|
+
claude_response = await claude_integration.run_command(
|
|
173
|
+
prompt=debug_prompt,
|
|
174
|
+
working_directory=current_dir,
|
|
175
|
+
user_id=user_id,
|
|
176
|
+
session_id=session_id,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
if claude_response:
|
|
180
|
+
context.user_data["claude_session_id"] = claude_response.session_id
|
|
181
|
+
response_text = escape_html(claude_response.content[:3500])
|
|
182
|
+
await query.message.reply_text(
|
|
183
|
+
f"<b>Debug Analysis</b>\n\n{response_text}",
|
|
184
|
+
parse_mode="HTML",
|
|
185
|
+
reply_markup=InlineKeyboardMarkup([
|
|
186
|
+
[
|
|
187
|
+
InlineKeyboardButton("Continue", callback_data="agentic:continue"),
|
|
188
|
+
InlineKeyboardButton("New Session", callback_data="agentic:new_task"),
|
|
189
|
+
]
|
|
190
|
+
]),
|
|
191
|
+
)
|
|
192
|
+
else:
|
|
193
|
+
await query.edit_message_text(
|
|
194
|
+
"Failed to analyze the error. Try sending a new message.",
|
|
195
|
+
parse_mode="HTML",
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
except Exception as e:
|
|
199
|
+
logger.error("Debug analysis failed", error=str(e), user_id=user_id)
|
|
200
|
+
await query.edit_message_text(
|
|
201
|
+
f"Debug analysis failed: <code>{escape_html(str(e)[:200])}</code>\n\n"
|
|
202
|
+
"Try using /new to start a fresh session.",
|
|
203
|
+
parse_mode="HTML",
|
|
204
|
+
reply_markup=InlineKeyboardMarkup([
|
|
205
|
+
[InlineKeyboardButton("New Session", callback_data="agentic:new_task")]
|
|
206
|
+
]),
|
|
207
|
+
)
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
# Unknown agentic action
|
|
211
|
+
await query.edit_message_text(
|
|
212
|
+
f"Unknown action: {escape_html(action_type)}",
|
|
213
|
+
parse_mode="HTML",
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
async def handle_cd_callback(
|
|
218
|
+
query, project_name: str, context: ContextTypes.DEFAULT_TYPE
|
|
219
|
+
) -> None:
|
|
220
|
+
"""Handle directory change from inline keyboard."""
|
|
221
|
+
user_id = query.from_user.id
|
|
222
|
+
settings: Settings = context.bot_data["settings"]
|
|
223
|
+
security_validator: SecurityValidator = context.bot_data.get("security_validator")
|
|
224
|
+
audit_logger: AuditLogger = context.bot_data.get("audit_logger")
|
|
225
|
+
claude_integration: ClaudeIntegration = context.bot_data.get("claude_integration")
|
|
226
|
+
|
|
227
|
+
try:
|
|
228
|
+
current_dir = context.user_data.get(
|
|
229
|
+
"current_directory", settings.approved_directory
|
|
230
|
+
)
|
|
231
|
+
project_root = _get_thread_project_root(settings, context)
|
|
232
|
+
directory_root = project_root or settings.approved_directory
|
|
233
|
+
|
|
234
|
+
# Handle special paths
|
|
235
|
+
if project_name == "/":
|
|
236
|
+
new_path = directory_root
|
|
237
|
+
elif project_name == "..":
|
|
238
|
+
new_path = current_dir.parent
|
|
239
|
+
if not _is_within_root(new_path, directory_root):
|
|
240
|
+
new_path = directory_root
|
|
241
|
+
else:
|
|
242
|
+
if project_root:
|
|
243
|
+
new_path = current_dir / project_name
|
|
244
|
+
else:
|
|
245
|
+
new_path = settings.approved_directory / project_name
|
|
246
|
+
|
|
247
|
+
# Validate path if security validator is available
|
|
248
|
+
if security_validator:
|
|
249
|
+
# Pass the absolute path for validation
|
|
250
|
+
valid, resolved_path, error = security_validator.validate_path(
|
|
251
|
+
str(new_path), settings.approved_directory
|
|
252
|
+
)
|
|
253
|
+
if not valid:
|
|
254
|
+
await query.edit_message_text(
|
|
255
|
+
f"❌ <b>Access Denied</b>\n\n{escape_html(error)}",
|
|
256
|
+
parse_mode="HTML",
|
|
257
|
+
)
|
|
258
|
+
return
|
|
259
|
+
# Use the validated path
|
|
260
|
+
new_path = resolved_path
|
|
261
|
+
|
|
262
|
+
if project_root and not _is_within_root(new_path, project_root):
|
|
263
|
+
await query.edit_message_text(
|
|
264
|
+
"❌ <b>Access Denied</b>\n\n"
|
|
265
|
+
"In thread mode, navigation is limited to the current project root.",
|
|
266
|
+
parse_mode="HTML",
|
|
267
|
+
)
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
# Check if directory exists
|
|
271
|
+
if not new_path.exists() or not new_path.is_dir():
|
|
272
|
+
await query.edit_message_text(
|
|
273
|
+
f"❌ <b>Directory Not Found</b>\n\n"
|
|
274
|
+
f"The directory <code>{escape_html(project_name)}</code> no longer exists or is not accessible.",
|
|
275
|
+
parse_mode="HTML",
|
|
276
|
+
)
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
# Update directory and resume session for that directory when available
|
|
280
|
+
context.user_data["current_directory"] = new_path
|
|
281
|
+
|
|
282
|
+
resumed_session_info = ""
|
|
283
|
+
if claude_integration:
|
|
284
|
+
existing_session = await claude_integration._find_resumable_session(
|
|
285
|
+
user_id, new_path
|
|
286
|
+
)
|
|
287
|
+
if existing_session:
|
|
288
|
+
context.user_data["claude_session_id"] = existing_session.session_id
|
|
289
|
+
resumed_session_info = (
|
|
290
|
+
f"\n🔄 Resumed session <code>{escape_html(existing_session.session_id[:8])}...</code> "
|
|
291
|
+
f"({existing_session.message_count} messages)"
|
|
292
|
+
)
|
|
293
|
+
else:
|
|
294
|
+
context.user_data["claude_session_id"] = None
|
|
295
|
+
resumed_session_info = (
|
|
296
|
+
"\n🆕 No existing session. Send a message to start a new one."
|
|
297
|
+
)
|
|
298
|
+
else:
|
|
299
|
+
context.user_data["claude_session_id"] = None
|
|
300
|
+
resumed_session_info = "\n🆕 Send a message to start a new session."
|
|
301
|
+
|
|
302
|
+
# Send confirmation with new directory info
|
|
303
|
+
relative_base = project_root or settings.approved_directory
|
|
304
|
+
relative_path = new_path.relative_to(relative_base)
|
|
305
|
+
relative_display = "/" if str(relative_path) == "." else f"{relative_path}/"
|
|
306
|
+
|
|
307
|
+
# Add navigation buttons
|
|
308
|
+
keyboard = [
|
|
309
|
+
[
|
|
310
|
+
InlineKeyboardButton("📁 List Files", callback_data="action:ls"),
|
|
311
|
+
InlineKeyboardButton(
|
|
312
|
+
"🆕 New Session", callback_data="action:new_session"
|
|
313
|
+
),
|
|
314
|
+
],
|
|
315
|
+
[
|
|
316
|
+
InlineKeyboardButton(
|
|
317
|
+
"📋 Projects", callback_data="action:show_projects"
|
|
318
|
+
),
|
|
319
|
+
InlineKeyboardButton("📊 Status", callback_data="action:status"),
|
|
320
|
+
],
|
|
321
|
+
]
|
|
322
|
+
reply_markup = InlineKeyboardMarkup(keyboard)
|
|
323
|
+
|
|
324
|
+
await query.edit_message_text(
|
|
325
|
+
f"✅ <b>Directory Changed</b>\n\n"
|
|
326
|
+
f"📂 Current directory: <code>{escape_html(str(relative_display))}</code>"
|
|
327
|
+
f"{resumed_session_info}",
|
|
328
|
+
parse_mode="HTML",
|
|
329
|
+
reply_markup=reply_markup,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
# Log successful directory change
|
|
333
|
+
if audit_logger:
|
|
334
|
+
await audit_logger.log_command(
|
|
335
|
+
user_id=user_id, command="cd", args=[project_name], success=True
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
except Exception as e:
|
|
339
|
+
await query.edit_message_text(
|
|
340
|
+
f"❌ <b>Error changing directory</b>\n\n{escape_html(str(e))}",
|
|
341
|
+
parse_mode="HTML",
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
if audit_logger:
|
|
345
|
+
await audit_logger.log_command(
|
|
346
|
+
user_id=user_id, command="cd", args=[project_name], success=False
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
async def handle_action_callback(
|
|
351
|
+
query, action_type: str, context: ContextTypes.DEFAULT_TYPE
|
|
352
|
+
) -> None:
|
|
353
|
+
"""Handle general action callbacks."""
|
|
354
|
+
actions = {
|
|
355
|
+
"help": _handle_help_action,
|
|
356
|
+
"show_projects": _handle_show_projects_action,
|
|
357
|
+
"new_session": _handle_new_session_action,
|
|
358
|
+
"continue": _handle_continue_action,
|
|
359
|
+
"end_session": _handle_end_session_action,
|
|
360
|
+
"status": _handle_status_action,
|
|
361
|
+
"ls": _handle_ls_action,
|
|
362
|
+
"start_coding": _handle_start_coding_action,
|
|
363
|
+
"quick_actions": _handle_quick_actions_action,
|
|
364
|
+
"refresh_status": _handle_refresh_status_action,
|
|
365
|
+
"refresh_ls": _handle_refresh_ls_action,
|
|
366
|
+
"export": _handle_export_action,
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
handler = actions.get(action_type)
|
|
370
|
+
if handler:
|
|
371
|
+
await handler(query, context)
|
|
372
|
+
else:
|
|
373
|
+
await query.edit_message_text(
|
|
374
|
+
f"❌ <b>Unknown Action: {escape_html(action_type)}</b>\n\n"
|
|
375
|
+
"This action is not implemented yet.",
|
|
376
|
+
parse_mode="HTML",
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
async def handle_confirm_callback(
|
|
381
|
+
query, confirmation_type: str, context: ContextTypes.DEFAULT_TYPE
|
|
382
|
+
) -> None:
|
|
383
|
+
"""Handle confirmation dialogs."""
|
|
384
|
+
if confirmation_type == "yes":
|
|
385
|
+
await query.edit_message_text(
|
|
386
|
+
"✅ <b>Confirmed</b>\n\nAction will be processed.",
|
|
387
|
+
parse_mode="HTML",
|
|
388
|
+
)
|
|
389
|
+
elif confirmation_type == "no":
|
|
390
|
+
await query.edit_message_text(
|
|
391
|
+
"❌ <b>Cancelled</b>\n\nAction was cancelled.",
|
|
392
|
+
parse_mode="HTML",
|
|
393
|
+
)
|
|
394
|
+
else:
|
|
395
|
+
await query.edit_message_text(
|
|
396
|
+
"❓ <b>Unknown confirmation response</b>",
|
|
397
|
+
parse_mode="HTML",
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
# Action handlers
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
async def _handle_help_action(query, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
405
|
+
"""Handle help action."""
|
|
406
|
+
help_text = (
|
|
407
|
+
"🤖 <b>Quick Help</b>\n\n"
|
|
408
|
+
"<b>Navigation:</b>\n"
|
|
409
|
+
"• <code>/ls</code> - List files\n"
|
|
410
|
+
"• <code>/cd <dir></code> - Change directory\n"
|
|
411
|
+
"• <code>/projects</code> - Show projects\n\n"
|
|
412
|
+
"<b>Sessions:</b>\n"
|
|
413
|
+
"• <code>/new</code> - New Claude session\n"
|
|
414
|
+
"• <code>/status</code> - Session status\n\n"
|
|
415
|
+
"<b>Tips:</b>\n"
|
|
416
|
+
"• Send any text to interact with Claude\n"
|
|
417
|
+
"• Upload files for code review\n"
|
|
418
|
+
"• Use buttons for quick actions\n\n"
|
|
419
|
+
"Use <code>/help</code> for detailed help."
|
|
420
|
+
)
|
|
421
|
+
|
|
422
|
+
keyboard = [
|
|
423
|
+
[
|
|
424
|
+
InlineKeyboardButton("📖 Full Help", callback_data="action:full_help"),
|
|
425
|
+
InlineKeyboardButton("🏠 Main Menu", callback_data="action:main_menu"),
|
|
426
|
+
]
|
|
427
|
+
]
|
|
428
|
+
reply_markup = InlineKeyboardMarkup(keyboard)
|
|
429
|
+
|
|
430
|
+
await query.edit_message_text(
|
|
431
|
+
help_text, parse_mode="HTML", reply_markup=reply_markup
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
async def _handle_show_projects_action(
|
|
436
|
+
query, context: ContextTypes.DEFAULT_TYPE
|
|
437
|
+
) -> None:
|
|
438
|
+
"""Handle show projects action."""
|
|
439
|
+
settings: Settings = context.bot_data["settings"]
|
|
440
|
+
|
|
441
|
+
try:
|
|
442
|
+
if settings.enable_project_threads:
|
|
443
|
+
registry = context.bot_data.get("project_registry")
|
|
444
|
+
if not registry:
|
|
445
|
+
await query.edit_message_text(
|
|
446
|
+
"❌ <b>Project registry is not initialized.</b>",
|
|
447
|
+
parse_mode="HTML",
|
|
448
|
+
)
|
|
449
|
+
return
|
|
450
|
+
|
|
451
|
+
projects = registry.list_enabled()
|
|
452
|
+
if not projects:
|
|
453
|
+
await query.edit_message_text(
|
|
454
|
+
"📁 <b>No Projects Found</b>\n\n"
|
|
455
|
+
"No enabled projects found in projects config.",
|
|
456
|
+
parse_mode="HTML",
|
|
457
|
+
)
|
|
458
|
+
return
|
|
459
|
+
|
|
460
|
+
project_list = "\n".join(
|
|
461
|
+
[
|
|
462
|
+
f"• <b>{escape_html(p.name)}</b> "
|
|
463
|
+
f"(<code>{escape_html(p.slug)}</code>) "
|
|
464
|
+
f"→ <code>{escape_html(str(p.relative_path))}</code>"
|
|
465
|
+
for p in projects
|
|
466
|
+
]
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
await query.edit_message_text(
|
|
470
|
+
f"📁 <b>Configured Projects</b>\n\n{project_list}",
|
|
471
|
+
parse_mode="HTML",
|
|
472
|
+
)
|
|
473
|
+
return
|
|
474
|
+
|
|
475
|
+
# Get directories in approved directory
|
|
476
|
+
projects = []
|
|
477
|
+
for item in sorted(settings.approved_directory.iterdir()):
|
|
478
|
+
if item.is_dir() and not item.name.startswith("."):
|
|
479
|
+
projects.append(item.name)
|
|
480
|
+
|
|
481
|
+
if not projects:
|
|
482
|
+
await query.edit_message_text(
|
|
483
|
+
"📁 <b>No Projects Found</b>\n\n"
|
|
484
|
+
"No subdirectories found in your approved directory.\n"
|
|
485
|
+
"Create some directories to organize your projects!",
|
|
486
|
+
parse_mode="HTML",
|
|
487
|
+
)
|
|
488
|
+
return
|
|
489
|
+
|
|
490
|
+
# Create project buttons
|
|
491
|
+
keyboard = []
|
|
492
|
+
for i in range(0, len(projects), 2):
|
|
493
|
+
row = []
|
|
494
|
+
for j in range(2):
|
|
495
|
+
if i + j < len(projects):
|
|
496
|
+
project = projects[i + j]
|
|
497
|
+
row.append(
|
|
498
|
+
InlineKeyboardButton(
|
|
499
|
+
f"📁 {project}", callback_data=f"cd:{project}"
|
|
500
|
+
)
|
|
501
|
+
)
|
|
502
|
+
keyboard.append(row)
|
|
503
|
+
|
|
504
|
+
# Add navigation buttons
|
|
505
|
+
keyboard.append(
|
|
506
|
+
[
|
|
507
|
+
InlineKeyboardButton("🏠 Root", callback_data="cd:/"),
|
|
508
|
+
InlineKeyboardButton(
|
|
509
|
+
"🔄 Refresh", callback_data="action:show_projects"
|
|
510
|
+
),
|
|
511
|
+
]
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
reply_markup = InlineKeyboardMarkup(keyboard)
|
|
515
|
+
project_list = "\n".join(
|
|
516
|
+
[f"• <code>{escape_html(project)}/</code>" for project in projects]
|
|
517
|
+
)
|
|
518
|
+
|
|
519
|
+
await query.edit_message_text(
|
|
520
|
+
f"📁 <b>Available Projects</b>\n\n"
|
|
521
|
+
f"{project_list}\n\n"
|
|
522
|
+
f"Click a project to navigate to it:",
|
|
523
|
+
parse_mode="HTML",
|
|
524
|
+
reply_markup=reply_markup,
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
except Exception as e:
|
|
528
|
+
await query.edit_message_text(f"❌ Error loading projects: {str(e)}")
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
async def _handle_new_session_action(query, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
532
|
+
"""Handle new session action."""
|
|
533
|
+
settings: Settings = context.bot_data["settings"]
|
|
534
|
+
|
|
535
|
+
# Clear session and force new on next message
|
|
536
|
+
context.user_data["claude_session_id"] = None
|
|
537
|
+
context.user_data["session_started"] = True
|
|
538
|
+
context.user_data["force_new_session"] = True
|
|
539
|
+
|
|
540
|
+
current_dir = context.user_data.get(
|
|
541
|
+
"current_directory", settings.approved_directory
|
|
542
|
+
)
|
|
543
|
+
relative_path = current_dir.relative_to(settings.approved_directory)
|
|
544
|
+
|
|
545
|
+
keyboard = [
|
|
546
|
+
[
|
|
547
|
+
InlineKeyboardButton(
|
|
548
|
+
"📝 Start Coding", callback_data="action:start_coding"
|
|
549
|
+
),
|
|
550
|
+
InlineKeyboardButton(
|
|
551
|
+
"📁 Change Project", callback_data="action:show_projects"
|
|
552
|
+
),
|
|
553
|
+
],
|
|
554
|
+
[
|
|
555
|
+
InlineKeyboardButton(
|
|
556
|
+
"📋 Quick Actions", callback_data="action:quick_actions"
|
|
557
|
+
),
|
|
558
|
+
InlineKeyboardButton("❓ Help", callback_data="action:help"),
|
|
559
|
+
],
|
|
560
|
+
]
|
|
561
|
+
reply_markup = InlineKeyboardMarkup(keyboard)
|
|
562
|
+
|
|
563
|
+
await query.edit_message_text(
|
|
564
|
+
f"🆕 <b>New Claude Code Session</b>\n\n"
|
|
565
|
+
f"📂 Working directory: <code>{escape_html(str(relative_path))}/</code>\n\n"
|
|
566
|
+
f"Ready to help you code! Send me a message to get started:",
|
|
567
|
+
parse_mode="HTML",
|
|
568
|
+
reply_markup=reply_markup,
|
|
569
|
+
)
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
async def _handle_end_session_action(query, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
573
|
+
"""Handle end session action."""
|
|
574
|
+
settings: Settings = context.bot_data["settings"]
|
|
575
|
+
|
|
576
|
+
# Check if there's an active session
|
|
577
|
+
claude_session_id = context.user_data.get("claude_session_id")
|
|
578
|
+
|
|
579
|
+
if not claude_session_id:
|
|
580
|
+
await query.edit_message_text(
|
|
581
|
+
"ℹ️ <b>No Active Session</b>\n\n"
|
|
582
|
+
"There's no active Claude session to end.\n\n"
|
|
583
|
+
"<b>What you can do:</b>\n"
|
|
584
|
+
"• Use the button below to start a new session\n"
|
|
585
|
+
"• Check your session status\n"
|
|
586
|
+
"• Send any message to start a conversation",
|
|
587
|
+
parse_mode="HTML",
|
|
588
|
+
reply_markup=InlineKeyboardMarkup(
|
|
589
|
+
[
|
|
590
|
+
[
|
|
591
|
+
InlineKeyboardButton(
|
|
592
|
+
"🆕 New Session", callback_data="action:new_session"
|
|
593
|
+
)
|
|
594
|
+
],
|
|
595
|
+
[InlineKeyboardButton("📊 Status", callback_data="action:status")],
|
|
596
|
+
]
|
|
597
|
+
),
|
|
598
|
+
)
|
|
599
|
+
return
|
|
600
|
+
|
|
601
|
+
# Get current directory for display
|
|
602
|
+
current_dir = context.user_data.get(
|
|
603
|
+
"current_directory", settings.approved_directory
|
|
604
|
+
)
|
|
605
|
+
relative_path = current_dir.relative_to(settings.approved_directory)
|
|
606
|
+
|
|
607
|
+
# Clear session data
|
|
608
|
+
context.user_data["claude_session_id"] = None
|
|
609
|
+
context.user_data["session_started"] = False
|
|
610
|
+
context.user_data["last_message"] = None
|
|
611
|
+
|
|
612
|
+
# Create quick action buttons
|
|
613
|
+
keyboard = [
|
|
614
|
+
[
|
|
615
|
+
InlineKeyboardButton("🆕 New Session", callback_data="action:new_session"),
|
|
616
|
+
InlineKeyboardButton(
|
|
617
|
+
"📁 Change Project", callback_data="action:show_projects"
|
|
618
|
+
),
|
|
619
|
+
],
|
|
620
|
+
[
|
|
621
|
+
InlineKeyboardButton("📊 Status", callback_data="action:status"),
|
|
622
|
+
InlineKeyboardButton("❓ Help", callback_data="action:help"),
|
|
623
|
+
],
|
|
624
|
+
]
|
|
625
|
+
reply_markup = InlineKeyboardMarkup(keyboard)
|
|
626
|
+
|
|
627
|
+
await query.edit_message_text(
|
|
628
|
+
"✅ <b>Session Ended</b>\n\n"
|
|
629
|
+
f"Your Claude session has been terminated.\n\n"
|
|
630
|
+
f"<b>Current Status:</b>\n"
|
|
631
|
+
f"• Directory: <code>{escape_html(str(relative_path))}/</code>\n"
|
|
632
|
+
f"• Session: None\n"
|
|
633
|
+
f"• Ready for new commands\n\n"
|
|
634
|
+
f"<b>Next Steps:</b>\n"
|
|
635
|
+
f"• Start a new session\n"
|
|
636
|
+
f"• Check status\n"
|
|
637
|
+
f"• Send any message to begin a new conversation",
|
|
638
|
+
parse_mode="HTML",
|
|
639
|
+
reply_markup=reply_markup,
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
|
|
643
|
+
async def _handle_continue_action(query, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
644
|
+
"""Handle continue session action."""
|
|
645
|
+
user_id = query.from_user.id
|
|
646
|
+
settings: Settings = context.bot_data["settings"]
|
|
647
|
+
claude_integration: ClaudeIntegration = context.bot_data.get("claude_integration")
|
|
648
|
+
|
|
649
|
+
current_dir = context.user_data.get(
|
|
650
|
+
"current_directory", settings.approved_directory
|
|
651
|
+
)
|
|
652
|
+
|
|
653
|
+
try:
|
|
654
|
+
if not claude_integration:
|
|
655
|
+
await query.edit_message_text(
|
|
656
|
+
"❌ <b>Claude Integration Not Available</b>\n\n"
|
|
657
|
+
"Claude integration is not properly configured.",
|
|
658
|
+
parse_mode="HTML",
|
|
659
|
+
)
|
|
660
|
+
return
|
|
661
|
+
|
|
662
|
+
# Check if there's an existing session in user context
|
|
663
|
+
claude_session_id = context.user_data.get("claude_session_id")
|
|
664
|
+
|
|
665
|
+
if claude_session_id:
|
|
666
|
+
# Continue with the existing session (no prompt = use --continue)
|
|
667
|
+
await query.edit_message_text(
|
|
668
|
+
f"🔄 <b>Continuing Session</b>\n\n"
|
|
669
|
+
f"Session ID: <code>{escape_html(claude_session_id[:8])}...</code>\n"
|
|
670
|
+
f"Directory: <code>{escape_html(str(current_dir.relative_to(settings.approved_directory)))}/</code>\n\n"
|
|
671
|
+
f"Continuing where you left off...",
|
|
672
|
+
parse_mode="HTML",
|
|
673
|
+
)
|
|
674
|
+
|
|
675
|
+
claude_response = await claude_integration.run_command(
|
|
676
|
+
prompt="", # Empty prompt triggers --continue
|
|
677
|
+
working_directory=current_dir,
|
|
678
|
+
user_id=user_id,
|
|
679
|
+
session_id=claude_session_id,
|
|
680
|
+
)
|
|
681
|
+
else:
|
|
682
|
+
# No session in context, try to find the most recent session
|
|
683
|
+
await query.edit_message_text(
|
|
684
|
+
"🔍 <b>Looking for Recent Session</b>\n\n"
|
|
685
|
+
"Searching for your most recent session in this directory...",
|
|
686
|
+
parse_mode="HTML",
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
claude_response = await claude_integration.continue_session(
|
|
690
|
+
user_id=user_id,
|
|
691
|
+
working_directory=current_dir,
|
|
692
|
+
prompt=None, # No prompt = use --continue
|
|
693
|
+
)
|
|
694
|
+
|
|
695
|
+
if claude_response:
|
|
696
|
+
# Update session ID in context
|
|
697
|
+
context.user_data["claude_session_id"] = claude_response.session_id
|
|
698
|
+
|
|
699
|
+
# Send Claude's response
|
|
700
|
+
await query.message.reply_text(
|
|
701
|
+
f"✅ <b>Session Continued</b>\n\n"
|
|
702
|
+
f"{escape_html(claude_response.content[:500])}{'...' if len(claude_response.content) > 500 else ''}",
|
|
703
|
+
parse_mode="HTML",
|
|
704
|
+
)
|
|
705
|
+
else:
|
|
706
|
+
# No session found to continue
|
|
707
|
+
await query.edit_message_text(
|
|
708
|
+
"❌ <b>No Session Found</b>\n\n"
|
|
709
|
+
f"No recent Claude session found in this directory.\n"
|
|
710
|
+
f"Directory: <code>{escape_html(str(current_dir.relative_to(settings.approved_directory)))}/</code>\n\n"
|
|
711
|
+
f"<b>What you can do:</b>\n"
|
|
712
|
+
f"• Use the button below to start a fresh session\n"
|
|
713
|
+
f"• Check your session status\n"
|
|
714
|
+
f"• Navigate to a different directory",
|
|
715
|
+
parse_mode="HTML",
|
|
716
|
+
reply_markup=InlineKeyboardMarkup(
|
|
717
|
+
[
|
|
718
|
+
[
|
|
719
|
+
InlineKeyboardButton(
|
|
720
|
+
"🆕 New Session", callback_data="action:new_session"
|
|
721
|
+
),
|
|
722
|
+
InlineKeyboardButton(
|
|
723
|
+
"📊 Status", callback_data="action:status"
|
|
724
|
+
),
|
|
725
|
+
]
|
|
726
|
+
]
|
|
727
|
+
),
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
except Exception as e:
|
|
731
|
+
logger.error("Error in continue action", error=str(e), user_id=user_id)
|
|
732
|
+
await query.edit_message_text(
|
|
733
|
+
f"❌ <b>Error Continuing Session</b>\n\n"
|
|
734
|
+
f"An error occurred: <code>{escape_html(str(e))}</code>\n\n"
|
|
735
|
+
f"Try starting a new session instead.",
|
|
736
|
+
parse_mode="HTML",
|
|
737
|
+
reply_markup=InlineKeyboardMarkup(
|
|
738
|
+
[
|
|
739
|
+
[
|
|
740
|
+
InlineKeyboardButton(
|
|
741
|
+
"🆕 New Session", callback_data="action:new_session"
|
|
742
|
+
)
|
|
743
|
+
]
|
|
744
|
+
]
|
|
745
|
+
),
|
|
746
|
+
)
|
|
747
|
+
|
|
748
|
+
|
|
749
|
+
async def _handle_status_action(query, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
750
|
+
"""Handle status action."""
|
|
751
|
+
# This essentially duplicates the /status command functionality
|
|
752
|
+
user_id = query.from_user.id
|
|
753
|
+
settings: Settings = context.bot_data["settings"]
|
|
754
|
+
|
|
755
|
+
claude_session_id = context.user_data.get("claude_session_id")
|
|
756
|
+
current_dir = context.user_data.get(
|
|
757
|
+
"current_directory", settings.approved_directory
|
|
758
|
+
)
|
|
759
|
+
relative_path = current_dir.relative_to(settings.approved_directory)
|
|
760
|
+
|
|
761
|
+
# Get session usage info
|
|
762
|
+
claude_integration: ClaudeIntegration = context.bot_data.get("claude_integration")
|
|
763
|
+
usage_info = ""
|
|
764
|
+
if claude_integration and claude_session_id:
|
|
765
|
+
try:
|
|
766
|
+
session_info = await claude_integration.session_manager.get_session_info(
|
|
767
|
+
claude_session_id
|
|
768
|
+
)
|
|
769
|
+
if session_info:
|
|
770
|
+
turns = session_info.get("turns", 0)
|
|
771
|
+
msgs = session_info.get("messages", 0)
|
|
772
|
+
tools = session_info.get("tools_used", [])
|
|
773
|
+
usage_info = (
|
|
774
|
+
f"📈 Session: {msgs} messages, {turns} turns\n"
|
|
775
|
+
f"🔧 Tools: {', '.join(tools[:5]) if tools else 'None'}\n"
|
|
776
|
+
)
|
|
777
|
+
except Exception:
|
|
778
|
+
usage_info = ""
|
|
779
|
+
|
|
780
|
+
status_lines = [
|
|
781
|
+
"📊 <b>Session Status</b>",
|
|
782
|
+
"",
|
|
783
|
+
f"📂 Directory: <code>{escape_html(str(relative_path))}/</code>",
|
|
784
|
+
f"🤖 Claude Session: {'✅ Active' if claude_session_id else '❌ None'}",
|
|
785
|
+
usage_info.rstrip(),
|
|
786
|
+
]
|
|
787
|
+
|
|
788
|
+
if claude_session_id:
|
|
789
|
+
status_lines.append(
|
|
790
|
+
f"🆔 Session ID: <code>{escape_html(claude_session_id[:8])}...</code>"
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
# Add action buttons
|
|
794
|
+
keyboard = []
|
|
795
|
+
if claude_session_id:
|
|
796
|
+
keyboard.append(
|
|
797
|
+
[
|
|
798
|
+
InlineKeyboardButton("🔄 Continue", callback_data="action:continue"),
|
|
799
|
+
InlineKeyboardButton(
|
|
800
|
+
"🛑 End Session", callback_data="action:end_session"
|
|
801
|
+
),
|
|
802
|
+
]
|
|
803
|
+
)
|
|
804
|
+
keyboard.append(
|
|
805
|
+
[
|
|
806
|
+
InlineKeyboardButton(
|
|
807
|
+
"🆕 New Session", callback_data="action:new_session"
|
|
808
|
+
),
|
|
809
|
+
]
|
|
810
|
+
)
|
|
811
|
+
else:
|
|
812
|
+
keyboard.append(
|
|
813
|
+
[
|
|
814
|
+
InlineKeyboardButton(
|
|
815
|
+
"🆕 Start Session", callback_data="action:new_session"
|
|
816
|
+
)
|
|
817
|
+
]
|
|
818
|
+
)
|
|
819
|
+
|
|
820
|
+
keyboard.append(
|
|
821
|
+
[
|
|
822
|
+
InlineKeyboardButton("🔄 Refresh", callback_data="action:refresh_status"),
|
|
823
|
+
InlineKeyboardButton("📁 Projects", callback_data="action:show_projects"),
|
|
824
|
+
]
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
reply_markup = InlineKeyboardMarkup(keyboard)
|
|
828
|
+
|
|
829
|
+
await query.edit_message_text(
|
|
830
|
+
"\n".join(status_lines), parse_mode="HTML", reply_markup=reply_markup
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
|
|
834
|
+
async def _handle_ls_action(query, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
835
|
+
"""Handle ls action."""
|
|
836
|
+
settings: Settings = context.bot_data["settings"]
|
|
837
|
+
current_dir = context.user_data.get(
|
|
838
|
+
"current_directory", settings.approved_directory
|
|
839
|
+
)
|
|
840
|
+
|
|
841
|
+
try:
|
|
842
|
+
# List directory contents (similar to /ls command)
|
|
843
|
+
items = []
|
|
844
|
+
directories = []
|
|
845
|
+
files = []
|
|
846
|
+
|
|
847
|
+
for item in sorted(current_dir.iterdir()):
|
|
848
|
+
if item.name.startswith("."):
|
|
849
|
+
continue
|
|
850
|
+
|
|
851
|
+
# Escape markdown special characters in filenames
|
|
852
|
+
safe_name = _escape_markdown(item.name)
|
|
853
|
+
|
|
854
|
+
if item.is_dir():
|
|
855
|
+
directories.append(f"📁 {safe_name}/")
|
|
856
|
+
else:
|
|
857
|
+
try:
|
|
858
|
+
size = item.stat().st_size
|
|
859
|
+
size_str = _format_file_size(size)
|
|
860
|
+
files.append(f"📄 {safe_name} ({size_str})")
|
|
861
|
+
except OSError:
|
|
862
|
+
files.append(f"📄 {safe_name}")
|
|
863
|
+
|
|
864
|
+
items = directories + files
|
|
865
|
+
relative_path = current_dir.relative_to(settings.approved_directory)
|
|
866
|
+
|
|
867
|
+
if not items:
|
|
868
|
+
message = f"📂 <code>{escape_html(str(relative_path))}/</code>\n\n<i>(empty directory)</i>"
|
|
869
|
+
else:
|
|
870
|
+
message = f"📂 <code>{escape_html(str(relative_path))}/</code>\n\n"
|
|
871
|
+
max_items = 30 # Limit for inline display
|
|
872
|
+
if len(items) > max_items:
|
|
873
|
+
shown_items = items[:max_items]
|
|
874
|
+
message += "\n".join(shown_items)
|
|
875
|
+
message += f"\n\n<i>... and {len(items) - max_items} more items</i>"
|
|
876
|
+
else:
|
|
877
|
+
message += "\n".join(items)
|
|
878
|
+
|
|
879
|
+
# Add buttons
|
|
880
|
+
keyboard = []
|
|
881
|
+
if current_dir != settings.approved_directory:
|
|
882
|
+
keyboard.append(
|
|
883
|
+
[
|
|
884
|
+
InlineKeyboardButton("⬆️ Go Up", callback_data="cd:.."),
|
|
885
|
+
InlineKeyboardButton("🏠 Root", callback_data="cd:/"),
|
|
886
|
+
]
|
|
887
|
+
)
|
|
888
|
+
|
|
889
|
+
keyboard.append(
|
|
890
|
+
[
|
|
891
|
+
InlineKeyboardButton("🔄 Refresh", callback_data="action:refresh_ls"),
|
|
892
|
+
InlineKeyboardButton(
|
|
893
|
+
"📋 Projects", callback_data="action:show_projects"
|
|
894
|
+
),
|
|
895
|
+
]
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
reply_markup = InlineKeyboardMarkup(keyboard)
|
|
899
|
+
|
|
900
|
+
await query.edit_message_text(
|
|
901
|
+
message, parse_mode="HTML", reply_markup=reply_markup
|
|
902
|
+
)
|
|
903
|
+
|
|
904
|
+
except Exception as e:
|
|
905
|
+
await query.edit_message_text(f"❌ Error listing directory: {str(e)}")
|
|
906
|
+
|
|
907
|
+
|
|
908
|
+
async def _handle_start_coding_action(
|
|
909
|
+
query, context: ContextTypes.DEFAULT_TYPE
|
|
910
|
+
) -> None:
|
|
911
|
+
"""Handle start coding action."""
|
|
912
|
+
await query.edit_message_text(
|
|
913
|
+
"🚀 <b>Ready to Code!</b>\n\n"
|
|
914
|
+
"Send me any message to start coding with Claude:\n\n"
|
|
915
|
+
"<b>Examples:</b>\n"
|
|
916
|
+
'• <i>"Create a Python script that..."</i>\n'
|
|
917
|
+
'• <i>"Help me debug this code..."</i>\n'
|
|
918
|
+
'• <i>"Explain how this file works..."</i>\n'
|
|
919
|
+
"• Upload a file for review\n\n"
|
|
920
|
+
"I'm here to help with all your coding needs!",
|
|
921
|
+
parse_mode="HTML",
|
|
922
|
+
)
|
|
923
|
+
|
|
924
|
+
|
|
925
|
+
async def _handle_quick_actions_action(
|
|
926
|
+
query, context: ContextTypes.DEFAULT_TYPE
|
|
927
|
+
) -> None:
|
|
928
|
+
"""Handle quick actions menu."""
|
|
929
|
+
keyboard = [
|
|
930
|
+
[
|
|
931
|
+
InlineKeyboardButton("🧪 Run Tests", callback_data="quick:test"),
|
|
932
|
+
InlineKeyboardButton("📦 Install Deps", callback_data="quick:install"),
|
|
933
|
+
],
|
|
934
|
+
[
|
|
935
|
+
InlineKeyboardButton("🎨 Format Code", callback_data="quick:format"),
|
|
936
|
+
InlineKeyboardButton("🔍 Find TODOs", callback_data="quick:find_todos"),
|
|
937
|
+
],
|
|
938
|
+
[
|
|
939
|
+
InlineKeyboardButton("🔨 Build", callback_data="quick:build"),
|
|
940
|
+
InlineKeyboardButton("🚀 Start Server", callback_data="quick:start"),
|
|
941
|
+
],
|
|
942
|
+
[
|
|
943
|
+
InlineKeyboardButton("📊 Git Status", callback_data="quick:git_status"),
|
|
944
|
+
InlineKeyboardButton("🔧 Lint Code", callback_data="quick:lint"),
|
|
945
|
+
],
|
|
946
|
+
[InlineKeyboardButton("⬅️ Back", callback_data="action:new_session")],
|
|
947
|
+
]
|
|
948
|
+
reply_markup = InlineKeyboardMarkup(keyboard)
|
|
949
|
+
|
|
950
|
+
await query.edit_message_text(
|
|
951
|
+
"🛠️ <b>Quick Actions</b>\n\n"
|
|
952
|
+
"Choose a common development task:\n\n"
|
|
953
|
+
"<i>Note: These will be fully functional once Claude Code integration is complete.</i>",
|
|
954
|
+
parse_mode="HTML",
|
|
955
|
+
reply_markup=reply_markup,
|
|
956
|
+
)
|
|
957
|
+
|
|
958
|
+
|
|
959
|
+
async def _handle_refresh_status_action(
|
|
960
|
+
query, context: ContextTypes.DEFAULT_TYPE
|
|
961
|
+
) -> None:
|
|
962
|
+
"""Handle refresh status action."""
|
|
963
|
+
await _handle_status_action(query, context)
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
async def _handle_refresh_ls_action(query, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
967
|
+
"""Handle refresh ls action."""
|
|
968
|
+
await _handle_ls_action(query, context)
|
|
969
|
+
|
|
970
|
+
|
|
971
|
+
async def _handle_export_action(query, context: ContextTypes.DEFAULT_TYPE) -> None:
|
|
972
|
+
"""Handle export action."""
|
|
973
|
+
await query.edit_message_text(
|
|
974
|
+
"📤 <b>Export Session</b>\n\n"
|
|
975
|
+
"Session export functionality will be available once the storage layer is implemented.\n\n"
|
|
976
|
+
"<b>Planned features:</b>\n"
|
|
977
|
+
"• Export conversation history\n"
|
|
978
|
+
"• Save session state\n"
|
|
979
|
+
"• Share conversations\n"
|
|
980
|
+
"• Create session backups\n\n"
|
|
981
|
+
"<i>Coming in the next development phase!</i>",
|
|
982
|
+
parse_mode="HTML",
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
async def handle_quick_action_callback(
|
|
987
|
+
query, action_id: str, context: ContextTypes.DEFAULT_TYPE
|
|
988
|
+
) -> None:
|
|
989
|
+
"""Handle quick action callbacks."""
|
|
990
|
+
user_id = query.from_user.id
|
|
991
|
+
|
|
992
|
+
# Get quick actions manager from bot data if available
|
|
993
|
+
quick_actions = context.bot_data.get("quick_actions")
|
|
994
|
+
|
|
995
|
+
if not quick_actions:
|
|
996
|
+
await query.edit_message_text(
|
|
997
|
+
"❌ <b>Quick Actions Not Available</b>\n\n"
|
|
998
|
+
"Quick actions feature is not available.",
|
|
999
|
+
parse_mode="HTML",
|
|
1000
|
+
)
|
|
1001
|
+
return
|
|
1002
|
+
|
|
1003
|
+
# Get Claude integration
|
|
1004
|
+
claude_integration: ClaudeIntegration = context.bot_data.get("claude_integration")
|
|
1005
|
+
if not claude_integration:
|
|
1006
|
+
await query.edit_message_text(
|
|
1007
|
+
"❌ <b>Claude Integration Not Available</b>\n\n"
|
|
1008
|
+
"Claude integration is not properly configured.",
|
|
1009
|
+
parse_mode="HTML",
|
|
1010
|
+
)
|
|
1011
|
+
return
|
|
1012
|
+
|
|
1013
|
+
settings: Settings = context.bot_data["settings"]
|
|
1014
|
+
current_dir = context.user_data.get(
|
|
1015
|
+
"current_directory", settings.approved_directory
|
|
1016
|
+
)
|
|
1017
|
+
|
|
1018
|
+
try:
|
|
1019
|
+
# Get the action from the manager
|
|
1020
|
+
action = quick_actions.actions.get(action_id)
|
|
1021
|
+
if not action:
|
|
1022
|
+
await query.edit_message_text(
|
|
1023
|
+
f"❌ <b>Action Not Found</b>\n\n"
|
|
1024
|
+
f"Quick action '{escape_html(action_id)}' is not available.",
|
|
1025
|
+
parse_mode="HTML",
|
|
1026
|
+
)
|
|
1027
|
+
return
|
|
1028
|
+
|
|
1029
|
+
# Execute the action
|
|
1030
|
+
await query.edit_message_text(
|
|
1031
|
+
f"🚀 <b>Executing {action.icon} {escape_html(action.name)}</b>\n\n"
|
|
1032
|
+
f"Running quick action in directory: <code>{escape_html(str(current_dir.relative_to(settings.approved_directory)))}/</code>\n\n"
|
|
1033
|
+
f"Please wait...",
|
|
1034
|
+
parse_mode="HTML",
|
|
1035
|
+
)
|
|
1036
|
+
|
|
1037
|
+
# Run the action through Claude
|
|
1038
|
+
claude_response = await claude_integration.run_command(
|
|
1039
|
+
prompt=action.prompt, working_directory=current_dir, user_id=user_id
|
|
1040
|
+
)
|
|
1041
|
+
|
|
1042
|
+
if claude_response:
|
|
1043
|
+
# Format and send the response
|
|
1044
|
+
response_text = escape_html(claude_response.content)
|
|
1045
|
+
if len(response_text) > 4000:
|
|
1046
|
+
response_text = (
|
|
1047
|
+
response_text[:4000] + "...\n\n<i>(Response truncated)</i>"
|
|
1048
|
+
)
|
|
1049
|
+
|
|
1050
|
+
await query.message.reply_text(
|
|
1051
|
+
f"✅ <b>{action.icon} {escape_html(action.name)} Complete</b>\n\n{response_text}",
|
|
1052
|
+
parse_mode="HTML",
|
|
1053
|
+
)
|
|
1054
|
+
else:
|
|
1055
|
+
await query.edit_message_text(
|
|
1056
|
+
f"❌ <b>Action Failed</b>\n\n"
|
|
1057
|
+
f"Failed to execute {escape_html(action.name)}. Please try again.",
|
|
1058
|
+
parse_mode="HTML",
|
|
1059
|
+
)
|
|
1060
|
+
|
|
1061
|
+
except Exception as e:
|
|
1062
|
+
logger.error("Quick action execution failed", error=str(e), user_id=user_id)
|
|
1063
|
+
await query.edit_message_text(
|
|
1064
|
+
f"❌ <b>Action Error</b>\n\n"
|
|
1065
|
+
f"An error occurred while executing {escape_html(action_id)}: {escape_html(str(e))}",
|
|
1066
|
+
parse_mode="HTML",
|
|
1067
|
+
)
|
|
1068
|
+
|
|
1069
|
+
|
|
1070
|
+
async def handle_followup_callback(
|
|
1071
|
+
query, suggestion_hash: str, context: ContextTypes.DEFAULT_TYPE
|
|
1072
|
+
) -> None:
|
|
1073
|
+
"""Handle follow-up suggestion callbacks."""
|
|
1074
|
+
user_id = query.from_user.id
|
|
1075
|
+
|
|
1076
|
+
# Get conversation enhancer from bot data if available
|
|
1077
|
+
conversation_enhancer = context.bot_data.get("conversation_enhancer")
|
|
1078
|
+
|
|
1079
|
+
if not conversation_enhancer:
|
|
1080
|
+
await query.edit_message_text(
|
|
1081
|
+
"❌ <b>Follow-up Not Available</b>\n\n"
|
|
1082
|
+
"Conversation enhancement features are not available.",
|
|
1083
|
+
parse_mode="HTML",
|
|
1084
|
+
)
|
|
1085
|
+
return
|
|
1086
|
+
|
|
1087
|
+
try:
|
|
1088
|
+
# Get stored suggestions (this would need to be implemented in the enhancer)
|
|
1089
|
+
# For now, we'll provide a generic response
|
|
1090
|
+
await query.edit_message_text(
|
|
1091
|
+
"💡 <b>Follow-up Suggestion Selected</b>\n\n"
|
|
1092
|
+
"This follow-up suggestion will be implemented once the conversation "
|
|
1093
|
+
"enhancement system is fully integrated with the message handler.\n\n"
|
|
1094
|
+
"<b>Current Status:</b>\n"
|
|
1095
|
+
"• Suggestion received ✅\n"
|
|
1096
|
+
"• Integration pending 🔄\n\n"
|
|
1097
|
+
"<i>You can continue the conversation by sending a new message.</i>",
|
|
1098
|
+
parse_mode="HTML",
|
|
1099
|
+
)
|
|
1100
|
+
|
|
1101
|
+
logger.info(
|
|
1102
|
+
"Follow-up suggestion selected",
|
|
1103
|
+
user_id=user_id,
|
|
1104
|
+
suggestion_hash=suggestion_hash,
|
|
1105
|
+
)
|
|
1106
|
+
|
|
1107
|
+
except Exception as e:
|
|
1108
|
+
logger.error(
|
|
1109
|
+
"Error handling follow-up callback",
|
|
1110
|
+
error=str(e),
|
|
1111
|
+
user_id=user_id,
|
|
1112
|
+
suggestion_hash=suggestion_hash,
|
|
1113
|
+
)
|
|
1114
|
+
|
|
1115
|
+
await query.edit_message_text(
|
|
1116
|
+
"❌ <b>Error Processing Follow-up</b>\n\n"
|
|
1117
|
+
"An error occurred while processing your follow-up suggestion.",
|
|
1118
|
+
parse_mode="HTML",
|
|
1119
|
+
)
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
async def handle_conversation_callback(
|
|
1123
|
+
query, action_type: str, context: ContextTypes.DEFAULT_TYPE
|
|
1124
|
+
) -> None:
|
|
1125
|
+
"""Handle conversation control callbacks."""
|
|
1126
|
+
user_id = query.from_user.id
|
|
1127
|
+
settings: Settings = context.bot_data["settings"]
|
|
1128
|
+
|
|
1129
|
+
if action_type == "continue":
|
|
1130
|
+
# Remove suggestion buttons and show continue message
|
|
1131
|
+
await query.edit_message_text(
|
|
1132
|
+
"✅ <b>Continuing Conversation</b>\n\n"
|
|
1133
|
+
"Send me your next message to continue coding!\n\n"
|
|
1134
|
+
"I'm ready to help with:\n"
|
|
1135
|
+
"• Code review and debugging\n"
|
|
1136
|
+
"• Feature implementation\n"
|
|
1137
|
+
"• Architecture decisions\n"
|
|
1138
|
+
"• Testing and optimization\n"
|
|
1139
|
+
"• Documentation\n\n"
|
|
1140
|
+
"<i>Just type your request or upload files.</i>",
|
|
1141
|
+
parse_mode="HTML",
|
|
1142
|
+
)
|
|
1143
|
+
|
|
1144
|
+
elif action_type == "end":
|
|
1145
|
+
# End the current session
|
|
1146
|
+
conversation_enhancer = context.bot_data.get("conversation_enhancer")
|
|
1147
|
+
if conversation_enhancer:
|
|
1148
|
+
conversation_enhancer.clear_context(user_id)
|
|
1149
|
+
|
|
1150
|
+
# Clear session data
|
|
1151
|
+
context.user_data["claude_session_id"] = None
|
|
1152
|
+
context.user_data["session_started"] = False
|
|
1153
|
+
|
|
1154
|
+
current_dir = context.user_data.get(
|
|
1155
|
+
"current_directory", settings.approved_directory
|
|
1156
|
+
)
|
|
1157
|
+
relative_path = current_dir.relative_to(settings.approved_directory)
|
|
1158
|
+
|
|
1159
|
+
# Create quick action buttons
|
|
1160
|
+
keyboard = [
|
|
1161
|
+
[
|
|
1162
|
+
InlineKeyboardButton(
|
|
1163
|
+
"🆕 New Session", callback_data="action:new_session"
|
|
1164
|
+
),
|
|
1165
|
+
InlineKeyboardButton(
|
|
1166
|
+
"📁 Change Project", callback_data="action:show_projects"
|
|
1167
|
+
),
|
|
1168
|
+
],
|
|
1169
|
+
[
|
|
1170
|
+
InlineKeyboardButton("📊 Status", callback_data="action:status"),
|
|
1171
|
+
InlineKeyboardButton("❓ Help", callback_data="action:help"),
|
|
1172
|
+
],
|
|
1173
|
+
]
|
|
1174
|
+
reply_markup = InlineKeyboardMarkup(keyboard)
|
|
1175
|
+
|
|
1176
|
+
await query.edit_message_text(
|
|
1177
|
+
"✅ <b>Conversation Ended</b>\n\n"
|
|
1178
|
+
f"Your Claude session has been terminated.\n\n"
|
|
1179
|
+
f"<b>Current Status:</b>\n"
|
|
1180
|
+
f"• Directory: <code>{escape_html(str(relative_path))}/</code>\n"
|
|
1181
|
+
f"• Session: None\n"
|
|
1182
|
+
f"• Ready for new commands\n\n"
|
|
1183
|
+
f"<b>Next Steps:</b>\n"
|
|
1184
|
+
f"• Start a new session\n"
|
|
1185
|
+
f"• Check status\n"
|
|
1186
|
+
f"• Send any message to begin a new conversation",
|
|
1187
|
+
parse_mode="HTML",
|
|
1188
|
+
reply_markup=reply_markup,
|
|
1189
|
+
)
|
|
1190
|
+
|
|
1191
|
+
logger.info("Conversation ended via callback", user_id=user_id)
|
|
1192
|
+
|
|
1193
|
+
else:
|
|
1194
|
+
await query.edit_message_text(
|
|
1195
|
+
f"❌ <b>Unknown Conversation Action: {escape_html(action_type)}</b>\n\n"
|
|
1196
|
+
"This conversation action is not recognized.",
|
|
1197
|
+
parse_mode="HTML",
|
|
1198
|
+
)
|
|
1199
|
+
|
|
1200
|
+
|
|
1201
|
+
async def handle_git_callback(
|
|
1202
|
+
query, git_action: str, context: ContextTypes.DEFAULT_TYPE
|
|
1203
|
+
) -> None:
|
|
1204
|
+
"""Handle git-related callbacks."""
|
|
1205
|
+
user_id = query.from_user.id
|
|
1206
|
+
settings: Settings = context.bot_data["settings"]
|
|
1207
|
+
features = context.bot_data.get("features")
|
|
1208
|
+
|
|
1209
|
+
if not features or not features.is_enabled("git"):
|
|
1210
|
+
await query.edit_message_text(
|
|
1211
|
+
"❌ <b>Git Integration Disabled</b>\n\n"
|
|
1212
|
+
"Git integration feature is not enabled.",
|
|
1213
|
+
parse_mode="HTML",
|
|
1214
|
+
)
|
|
1215
|
+
return
|
|
1216
|
+
|
|
1217
|
+
current_dir = context.user_data.get(
|
|
1218
|
+
"current_directory", settings.approved_directory
|
|
1219
|
+
)
|
|
1220
|
+
|
|
1221
|
+
try:
|
|
1222
|
+
git_integration = features.get_git_integration()
|
|
1223
|
+
if not git_integration:
|
|
1224
|
+
await query.edit_message_text(
|
|
1225
|
+
"❌ <b>Git Integration Unavailable</b>\n\n"
|
|
1226
|
+
"Git integration service is not available.",
|
|
1227
|
+
parse_mode="HTML",
|
|
1228
|
+
)
|
|
1229
|
+
return
|
|
1230
|
+
|
|
1231
|
+
if git_action == "status":
|
|
1232
|
+
# Refresh git status
|
|
1233
|
+
git_status = await git_integration.get_status(current_dir)
|
|
1234
|
+
status_message = git_integration.format_status(git_status)
|
|
1235
|
+
|
|
1236
|
+
keyboard = [
|
|
1237
|
+
[
|
|
1238
|
+
InlineKeyboardButton("📊 Show Diff", callback_data="git:diff"),
|
|
1239
|
+
InlineKeyboardButton("📜 Show Log", callback_data="git:log"),
|
|
1240
|
+
],
|
|
1241
|
+
[
|
|
1242
|
+
InlineKeyboardButton("🔄 Refresh", callback_data="git:status"),
|
|
1243
|
+
InlineKeyboardButton("📁 Files", callback_data="action:ls"),
|
|
1244
|
+
],
|
|
1245
|
+
]
|
|
1246
|
+
reply_markup = InlineKeyboardMarkup(keyboard)
|
|
1247
|
+
|
|
1248
|
+
await query.edit_message_text(
|
|
1249
|
+
status_message, parse_mode="HTML", reply_markup=reply_markup
|
|
1250
|
+
)
|
|
1251
|
+
|
|
1252
|
+
elif git_action == "diff":
|
|
1253
|
+
# Show git diff
|
|
1254
|
+
diff_output = await git_integration.get_diff(current_dir)
|
|
1255
|
+
|
|
1256
|
+
if not diff_output.strip():
|
|
1257
|
+
diff_message = "📊 <b>Git Diff</b>\n\n<i>No changes to show.</i>"
|
|
1258
|
+
else:
|
|
1259
|
+
# Clean up diff output for Telegram
|
|
1260
|
+
# Remove emoji symbols that interfere with parsing
|
|
1261
|
+
clean_diff = (
|
|
1262
|
+
diff_output.replace("➕", "+").replace("➖", "-").replace("📍", "@")
|
|
1263
|
+
)
|
|
1264
|
+
|
|
1265
|
+
# Limit diff output (leave room for header + HTML tags within
|
|
1266
|
+
# Telegram's 4096-char message limit)
|
|
1267
|
+
max_length = 3500
|
|
1268
|
+
if len(clean_diff) > max_length:
|
|
1269
|
+
clean_diff = (
|
|
1270
|
+
clean_diff[:max_length] + "\n\n... output truncated ..."
|
|
1271
|
+
)
|
|
1272
|
+
|
|
1273
|
+
escaped_diff = escape_html(clean_diff)
|
|
1274
|
+
diff_message = (
|
|
1275
|
+
f"📊 <b>Git Diff</b>\n\n<pre><code>{escaped_diff}</code></pre>"
|
|
1276
|
+
)
|
|
1277
|
+
|
|
1278
|
+
keyboard = [
|
|
1279
|
+
[
|
|
1280
|
+
InlineKeyboardButton("📜 Show Log", callback_data="git:log"),
|
|
1281
|
+
InlineKeyboardButton("📊 Status", callback_data="git:status"),
|
|
1282
|
+
]
|
|
1283
|
+
]
|
|
1284
|
+
reply_markup = InlineKeyboardMarkup(keyboard)
|
|
1285
|
+
|
|
1286
|
+
await query.edit_message_text(
|
|
1287
|
+
diff_message, parse_mode="HTML", reply_markup=reply_markup
|
|
1288
|
+
)
|
|
1289
|
+
|
|
1290
|
+
elif git_action == "log":
|
|
1291
|
+
# Show git log
|
|
1292
|
+
commits = await git_integration.get_file_history(current_dir, ".")
|
|
1293
|
+
|
|
1294
|
+
if not commits:
|
|
1295
|
+
log_message = "📜 <b>Git Log</b>\n\n<i>No commits found.</i>"
|
|
1296
|
+
else:
|
|
1297
|
+
log_message = "📜 <b>Git Log</b>\n\n"
|
|
1298
|
+
for commit in commits[:10]: # Show last 10 commits
|
|
1299
|
+
short_hash = commit.hash[:7]
|
|
1300
|
+
short_message = escape_html(commit.message[:60])
|
|
1301
|
+
if len(commit.message) > 60:
|
|
1302
|
+
short_message += "..."
|
|
1303
|
+
log_message += f"• <code>{short_hash}</code> {short_message}\n"
|
|
1304
|
+
|
|
1305
|
+
keyboard = [
|
|
1306
|
+
[
|
|
1307
|
+
InlineKeyboardButton("📊 Show Diff", callback_data="git:diff"),
|
|
1308
|
+
InlineKeyboardButton("📊 Status", callback_data="git:status"),
|
|
1309
|
+
]
|
|
1310
|
+
]
|
|
1311
|
+
reply_markup = InlineKeyboardMarkup(keyboard)
|
|
1312
|
+
|
|
1313
|
+
await query.edit_message_text(
|
|
1314
|
+
log_message, parse_mode="HTML", reply_markup=reply_markup
|
|
1315
|
+
)
|
|
1316
|
+
|
|
1317
|
+
else:
|
|
1318
|
+
await query.edit_message_text(
|
|
1319
|
+
f"❌ <b>Unknown Git Action: {escape_html(git_action)}</b>\n\n"
|
|
1320
|
+
"This git action is not recognized.",
|
|
1321
|
+
parse_mode="HTML",
|
|
1322
|
+
)
|
|
1323
|
+
|
|
1324
|
+
except Exception as e:
|
|
1325
|
+
logger.error(
|
|
1326
|
+
"Error in git callback",
|
|
1327
|
+
error=str(e),
|
|
1328
|
+
git_action=git_action,
|
|
1329
|
+
user_id=user_id,
|
|
1330
|
+
)
|
|
1331
|
+
await query.edit_message_text(
|
|
1332
|
+
f"❌ <b>Git Error</b>\n\n{escape_html(str(e))}",
|
|
1333
|
+
parse_mode="HTML",
|
|
1334
|
+
)
|
|
1335
|
+
|
|
1336
|
+
|
|
1337
|
+
async def handle_export_callback(
|
|
1338
|
+
query, export_format: str, context: ContextTypes.DEFAULT_TYPE
|
|
1339
|
+
) -> None:
|
|
1340
|
+
"""Handle export format selection callbacks."""
|
|
1341
|
+
user_id = query.from_user.id
|
|
1342
|
+
features = context.bot_data.get("features")
|
|
1343
|
+
|
|
1344
|
+
if export_format == "cancel":
|
|
1345
|
+
await query.edit_message_text(
|
|
1346
|
+
"📤 <b>Export Cancelled</b>\n\n" "Session export has been cancelled.",
|
|
1347
|
+
parse_mode="HTML",
|
|
1348
|
+
)
|
|
1349
|
+
return
|
|
1350
|
+
|
|
1351
|
+
session_exporter = features.get_session_export() if features else None
|
|
1352
|
+
if not session_exporter:
|
|
1353
|
+
await query.edit_message_text(
|
|
1354
|
+
"❌ <b>Export Unavailable</b>\n\n"
|
|
1355
|
+
"Session export service is not available.",
|
|
1356
|
+
parse_mode="HTML",
|
|
1357
|
+
)
|
|
1358
|
+
return
|
|
1359
|
+
|
|
1360
|
+
# Get current session
|
|
1361
|
+
claude_session_id = context.user_data.get("claude_session_id")
|
|
1362
|
+
if not claude_session_id:
|
|
1363
|
+
await query.edit_message_text(
|
|
1364
|
+
"❌ <b>No Active Session</b>\n\n" "There's no active session to export.",
|
|
1365
|
+
parse_mode="HTML",
|
|
1366
|
+
)
|
|
1367
|
+
return
|
|
1368
|
+
|
|
1369
|
+
try:
|
|
1370
|
+
# Show processing message
|
|
1371
|
+
await query.edit_message_text(
|
|
1372
|
+
f"📤 <b>Exporting Session</b>\n\n"
|
|
1373
|
+
f"Generating {escape_html(export_format.upper())} export...",
|
|
1374
|
+
parse_mode="HTML",
|
|
1375
|
+
)
|
|
1376
|
+
|
|
1377
|
+
# Export session
|
|
1378
|
+
exported_session = await session_exporter.export_session(
|
|
1379
|
+
claude_session_id, export_format
|
|
1380
|
+
)
|
|
1381
|
+
|
|
1382
|
+
# Send the exported file
|
|
1383
|
+
from io import BytesIO
|
|
1384
|
+
|
|
1385
|
+
file_bytes = BytesIO(exported_session.content.encode("utf-8"))
|
|
1386
|
+
file_bytes.name = exported_session.filename
|
|
1387
|
+
|
|
1388
|
+
await query.message.reply_document(
|
|
1389
|
+
document=file_bytes,
|
|
1390
|
+
filename=exported_session.filename,
|
|
1391
|
+
caption=(
|
|
1392
|
+
f"📤 <b>Session Export Complete</b>\n\n"
|
|
1393
|
+
f"Format: {escape_html(exported_session.format.upper())}\n"
|
|
1394
|
+
f"Size: {exported_session.size_bytes:,} bytes\n"
|
|
1395
|
+
f"Created: {exported_session.created_at.strftime('%Y-%m-%d %H:%M:%S')}"
|
|
1396
|
+
),
|
|
1397
|
+
parse_mode="HTML",
|
|
1398
|
+
)
|
|
1399
|
+
|
|
1400
|
+
# Update the original message
|
|
1401
|
+
await query.edit_message_text(
|
|
1402
|
+
f"✅ <b>Export Complete</b>\n\n"
|
|
1403
|
+
f"Your session has been exported as {escape_html(exported_session.filename)}.\n"
|
|
1404
|
+
f"Check the file above for your complete conversation history.",
|
|
1405
|
+
parse_mode="HTML",
|
|
1406
|
+
)
|
|
1407
|
+
|
|
1408
|
+
except Exception as e:
|
|
1409
|
+
logger.error(
|
|
1410
|
+
"Export failed", error=str(e), user_id=user_id, format=export_format
|
|
1411
|
+
)
|
|
1412
|
+
await query.edit_message_text(
|
|
1413
|
+
f"❌ <b>Export Failed</b>\n\n{escape_html(str(e))}",
|
|
1414
|
+
parse_mode="HTML",
|
|
1415
|
+
)
|
|
1416
|
+
|
|
1417
|
+
|
|
1418
|
+
def _format_file_size(size: int) -> str:
|
|
1419
|
+
"""Format file size in human-readable format."""
|
|
1420
|
+
for unit in ["B", "KB", "MB", "GB"]:
|
|
1421
|
+
if size < 1024:
|
|
1422
|
+
return f"{size:.1f}{unit}" if unit != "B" else f"{size}B"
|
|
1423
|
+
size /= 1024
|
|
1424
|
+
return f"{size:.1f}TB"
|
|
1425
|
+
|
|
1426
|
+
|
|
1427
|
+
def _escape_markdown(text: str) -> str:
|
|
1428
|
+
"""Escape HTML-special characters in text for Telegram.
|
|
1429
|
+
|
|
1430
|
+
Legacy name kept for compatibility with callers; actually escapes HTML.
|
|
1431
|
+
"""
|
|
1432
|
+
return escape_html(text)
|