diff --git a/telegram-agy-bot/bot.py b/telegram-agy-bot/bot.py
index 0b475ca..74c3aa1 100644
--- a/telegram-agy-bot/bot.py
+++ b/telegram-agy-bot/bot.py
@@ -884,12 +884,12 @@ def build_conversations_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
if is_fa:
header = (
f"📜 مدیریت و تاریخچه گفتگوها (پروژه: {escape_html(curr_proj.name)})\n\n"
- f"💡 روی هر گفتگو برای سوییچ سریع کلیک کنید، یا با دکمه 🗑️ آن را حذف نمایید:\n\n"
+ f"💡 جهت سوییچ یا مشاهده جزئیات و حذف هر گفتگو، روی دکمه مربوطه کلیک کنید:\n\n"
)
else:
header = (
f"📜 Conversation Manager & History (Project: {escape_html(curr_proj.name)})\n\n"
- f"💡 Click a conversation to switch, or tap 🗑️ to delete:\n\n"
+ f"💡 Click any conversation button to switch, view details, or delete:\n\n"
)
if not convs:
@@ -908,28 +908,36 @@ def build_conversations_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
for idx, c in enumerate(convs[:8], start=1):
cid = c["id"]
is_curr = c.get("is_current", False)
- badge = "🟢 [گفتگوی فعال] " if is_curr else f"#{idx} "
- prompt_snippet = c.get("first_prompt") or "(بدون متن)"
+ badge_text = "🟢 [فعال]" if is_curr else f"#{idx}"
+ custom_t = c.get("title")
+ prompt_snippet = custom_t if custom_t else (c.get("first_prompt") or "(بدون متن)")
prompt_clean = prompt_snippet.replace("\n", " ").strip()
- if len(prompt_clean) > 40:
- prompt_clean = prompt_clean[:37] + "..."
+
+ # Format text preview for message body (up to 80 chars)
+ prompt_body_preview = prompt_clean if len(prompt_clean) <= 80 else prompt_clean[:77] + "..."
+
+ # Format label for full-width button (up to 45 chars)
+ prompt_btn_preview = prompt_clean if len(prompt_clean) <= 45 else prompt_clean[:42] + "..."
turns = c.get("turns_count", 0)
+ title_prefix = "🏷️ " if custom_t else ""
if is_fa:
- body_lines.append(f"{badge}{cid[:8]}... ({turns} نوبت)\n └ «{escape_html(prompt_clean)}»\n")
+ status_tag = " (در حال استفاده)" if is_curr else ""
+ body_lines.append(f"{idx}. {badge_text}{status_tag} {cid[:8]} ({turns} نوبت)\n └ {title_prefix}«{escape_html(prompt_body_preview)}»\n")
else:
- body_lines.append(f"{badge}{cid[:8]}... ({turns} turns)\n └ \"{escape_html(prompt_clean)}\"\n")
+ status_tag = " (Active)" if is_curr else ""
+ body_lines.append(f"{idx}. {badge_text}{status_tag} {cid[:8]} ({turns} turns)\n └ {title_prefix}\"{escape_html(prompt_body_preview)}\"\n")
if is_curr:
- btn_label = f"🟢 #{idx}: {prompt_clean} (فعال)" if is_fa else f"🟢 #{idx}: {prompt_clean} (Active)"
+ btn_label = f"🟢 #{idx}: {title_prefix}{prompt_btn_preview} (فعال)" if is_fa else f"🟢 #{idx}: {title_prefix}{prompt_btn_preview} (Active)"
btn_action = f"conv_view_{cid}"
else:
- btn_label = f"💬 #{idx}: {prompt_clean}"
+ btn_label = f"💬 #{idx}: {title_prefix}{prompt_btn_preview}"
btn_action = f"conv_switch_{cid}"
+ # Full width button for maximum readability
keyboard.append([
- InlineKeyboardButton(btn_label[:38], callback_data=btn_action),
- InlineKeyboardButton("🗑️", callback_data=f"conv_del_ask_{cid}"),
+ InlineKeyboardButton(btn_label[:60], callback_data=btn_action),
])
action_row = [
@@ -953,6 +961,7 @@ def build_conversations_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
footer = (
f"\n💡 دستورات متنی:\n"
f"• سوییچ: /switchconv <شماره یا شناسه>\n"
+ f"• تغییر موضوع: /settopic <عنوان جدید>\n"
f"• حذف: /delconv <شماره یا شناسه>\n"
f"• پاکسازی همه: /clearconvs"
)
@@ -960,6 +969,7 @@ def build_conversations_menu(chat_id: int) -> tuple[str, InlineKeyboardMarkup]:
footer = (
f"\n💡 Text commands:\n"
f"• Switch: /switchconv <number or ID>\n"
+ f"• Rename: /settopic <new title>\n"
f"• Delete: /delconv <number or ID>\n"
f"• Clear all: /clearconvs"
)
@@ -981,6 +991,8 @@ def build_conversation_detail_menu(chat_id: int, conv_id: str) -> tuple[str, Inl
out = get_conversation_last_output(conv_id)
is_active = (curr_proj.conversation_id == conv_id)
first_prompt = meta.get("first_prompt") or "(شروع مکالمه)"
+ custom_title = getattr(curr_proj, "conversation_titles", {}).get(conv_id) if hasattr(curr_proj, "conversation_titles") else None
+ display_title = custom_title if custom_title else first_prompt
last_resp = out.get("text") or meta.get("last_response") or ""
if len(last_resp) > 200:
last_resp = last_resp[:197] + "..."
@@ -989,24 +1001,26 @@ def build_conversation_detail_menu(chat_id: int, conv_id: str) -> tuple[str, Inl
status_str_en = "🟢 Active (In use)" if is_active else "⚪ Inactive (Archived)"
if is_fa:
+ title_section = f"• 🏷️ عنوان / موضوع گفتگو: {escape_html(custom_title)}\n• 📝 اولین پیام: «{escape_html(first_prompt[:200])}»\n" if custom_title else f"📝 موضوع / اولین پیام:\n«{escape_html(first_prompt[:250])}»\n"
text = (
f"🔍 جزئیات گفتگو (Conversation Details)\n\n"
f"• 📁 پروژه: {escape_html(curr_proj.name)}\n"
f"• 💬 شناسه گفتگو: {conv_id}\n"
f"• 🏷 وضعیت: {status_str}\n"
f"• 🔢 تعداد نوبتها: {meta.get('turns_count', 0)} نوبت\n\n"
- f"📝 موضوع / اولین پیام:\n«{escape_html(first_prompt[:250])}»\n"
+ f"{title_section}"
)
if last_resp:
text += f"\n📋 آخرین پاسخ AI:\n«{escape_html(last_resp)}»\n"
else:
+ title_section = f"• 🏷️ Topic / Title: {escape_html(custom_title)}\n• 📝 First Prompt: \"{escape_html(first_prompt[:200])}\"\n" if custom_title else f"📝 First Prompt / Topic:\n\"{escape_html(first_prompt[:250])}\"\n"
text = (
f"🔍 Conversation Details\n\n"
f"• 📁 Project: {escape_html(curr_proj.name)}\n"
f"• 💬 Conversation ID: {conv_id}\n"
f"• 🏷 Status: {status_str_en}\n"
f"• 🔢 Turns: {meta.get('turns_count', 0)}\n\n"
- f"📝 First Prompt / Topic:\n\"{escape_html(first_prompt[:250])}\"\n"
+ f"{title_section}"
)
if last_resp:
text += f"\n📋 Last AI Response:\n\"{escape_html(last_resp)}\"\n"
@@ -2750,10 +2764,10 @@ async def process_agent_turn_by_chat_id(
model_display = get_model_display_name(model_to_use, effort_to_use)
initial_status = (
f"📁 پروژه: {escape_html(curr_proj.name)}\n\n"
- f"💭 در حال تفکر با مدل {escape_html(model_display)}..."
+ f"💭 {escape_html(model_display)}"
if is_fa else
f"📁 Project: {escape_html(curr_proj.name)}\n\n"
- f"💭 Thinking with model {escape_html(model_display)}..."
+ f"💭 {escape_html(model_display)}"
)
status_msg = await app.bot.send_message(
chat_id=chat_id,
@@ -2854,10 +2868,16 @@ def get_usage_buttons(lang: str = "fa") -> InlineKeyboardMarkup:
is_fa = (lang or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
restart_label = "🔄 گفتگوی جدید" if is_fa else "🔄 New Chat"
compact_label = "🗜️ فشردهسازی" if is_fa else "🗜️ Compact"
+ conv_label = "📜 سشنها / گفتگوها" if is_fa else "📜 Conversations"
+ last_conv_label = "⏮️ گفتگوی قبلی" if is_fa else "⏮️ Previous Chat"
keyboard = [
[
InlineKeyboardButton(compact_label, callback_data="btn_compact"),
InlineKeyboardButton(restart_label, callback_data="btn_restart"),
+ ],
+ [
+ InlineKeyboardButton(last_conv_label, callback_data="btn_conv_last"),
+ InlineKeyboardButton(conv_label, callback_data="btn_conv_menu"),
]
]
return InlineKeyboardMarkup(keyboard)
@@ -3243,6 +3263,56 @@ async def delete_conv_command(update: Update, context: ContextTypes.DEFAULT_TYPE
]
await update.message.reply_html(text, reply_markup=InlineKeyboardMarkup(keyboard))
+# Command: /settopic or /settitle or /convtitle
+@check_auth
+async def set_conv_title_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
+ chat_id = update.effective_chat.id
+ curr_proj = session_manager.get_current_project(chat_id)
+ session = session_manager.get_or_create(chat_id)
+ is_fa = (session.language or "").lower() in ("fa", "farsi", "persian", "🇮🇷 persian / farsi (فارسی)")
+
+ if not curr_proj:
+ msg = "⚠️ شما هنوز هیچ پروژهای ایجاد نکردهاید." if is_fa else "⚠️ No active project found."
+ await update.message.reply_html(msg)
+ return
+
+ if not context.args:
+ msg = (
+ "💡 راهنمای تنظیم عنوان گفتگو:\n\n"
+ "/settopic <عنوان یا موضوع جدید>\n"
+ "یا برای گفتگوی خاص:\n"
+ "/settopic <شماره یا شناسه گفتگو> <عنوان جدید>"
+ if is_fa else
+ "💡 Set Conversation Topic:\n\n"
+ "/settopic <New Title>\n"
+ "Or for a specific conversation:\n"
+ "/settopic <number or ID> <New Title>"
+ )
+ await update.message.reply_html(msg)
+ return
+
+ first_arg = context.args[0].strip()
+ # Check if first arg is conv index or id
+ if (first_arg.isdigit() or first_arg.startswith("#") or len(first_arg) > 20) and len(context.args) > 1:
+ target_cid = first_arg
+ new_title = " ".join(context.args[1:]).strip()
+ else:
+ target_cid = curr_proj.conversation_id
+ new_title = " ".join(context.args).strip()
+
+ ok, msg, matched_cid = session_manager.set_conversation_title(chat_id, title=new_title, conv_id=target_cid)
+ if not ok:
+ await update.message.reply_html(msg)
+ return
+
+ keyboard = [
+ [
+ InlineKeyboardButton("📜 تاریخچه گفتگوها" if is_fa else "📜 Conversations", callback_data="btn_conv_menu"),
+ InlineKeyboardButton("🏠 منوی اصلی" if is_fa else "🏠 Main Dashboard", callback_data="btn_dashboard"),
+ ]
+ ]
+ await update.message.reply_html(msg, reply_markup=InlineKeyboardMarkup(keyboard))
+
# Command: /clearconvs or /deleteallconvs
@check_auth
async def clear_convs_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
@@ -6465,12 +6535,12 @@ async def process_agent_turn(
if is_fa:
initial_status = (
f"📁 پروژه: {escape_html(curr_proj.name)}\n\n"
- f"💭 در حال تفکر با مدل {escape_html(model_display)}..."
+ f"💭 {escape_html(model_display)}"
)
else:
initial_status = (
f"📁 Project: {escape_html(curr_proj.name)}\n\n"
- f"💭 Thinking with model {escape_html(model_display)}..."
+ f"💭 {escape_html(model_display)}"
)
status_msg = await update.effective_message.reply_html(
@@ -6527,10 +6597,7 @@ async def process_agent_turn(
# 3. Status indicator at the bottom stating which model is being used
if not final:
- if is_fa:
- bottom_parts.append(f"💭 در حال تفکر با مدل {escape_html(model_display)}... ▌")
- else:
- bottom_parts.append(f"💭 Thinking with model {escape_html(model_display)}... ▌")
+ bottom_parts.append(f"💭 {escape_html(model_display)} ▌")
if raw_text.strip():
formatted_body = markdown_to_telegram_html(raw_text)
@@ -6544,7 +6611,7 @@ async def process_agent_turn(
if bottom_parts:
full_html = proj_header + "\n\n" + "\n\n".join(bottom_parts)
else:
- thinking_msg = f"💭 در حال تفکر با مدل {escape_html(model_display)}... ▌" if is_fa else f"💭 Thinking with model {escape_html(model_display)}... ▌"
+ thinking_msg = f"💭 {escape_html(model_display)} ▌"
full_html = proj_header + "\n\n" + thinking_msg
if len(full_html) > settings.max_message_length:
@@ -6594,6 +6661,14 @@ async def process_agent_turn(
tools.append(format_tool_call(name, args, lang=session.language))
asyncio.create_task(update_telegram_display(final=False))
+ def on_model_change(new_model: str, new_effort: Optional[str] = None):
+ nonlocal model_display
+ model_display = get_model_display_name(new_model, new_effort)
+ asyncio.create_task(update_telegram_display(final=False))
+
+ def on_reset():
+ accumulated_tokens.clear()
+
async def send_typing_loop():
while True:
try:
@@ -6614,6 +6689,8 @@ async def process_agent_turn(
on_delta=on_delta,
on_thought=on_thought,
on_tool=on_tool,
+ on_model_change=on_model_change,
+ on_reset=on_reset,
)
return result
finally:
@@ -6763,6 +6840,7 @@ async def on_startup(app: Application):
BotCommand("schedule", "افزودن زمانبندی / Schedule task"),
BotCommand("conversations", "مدیریت گفتگوها / Conversations"),
BotCommand("switchconv", "سوییچ گفتگو / Switch chat"),
+ BotCommand("settopic", "تغییر موضوع گفتگو / Rename chat"),
BotCommand("delconv", "حذف گفتگو / Delete chat"),
BotCommand("clearconvs", "پاکسازی همه / Clear chats"),
BotCommand("lastconv", "آخرین گفتگو / Open last chat"),
@@ -6970,6 +7048,7 @@ def main():
application.add_handler(CommandHandler(["lastconv", "lastconversation", "openlast", "resume", "open_last"], last_conversation_command))
application.add_handler(CommandHandler(["conversations", "convs", "history_conv", "dialogs", "chats"], conversations_command))
application.add_handler(CommandHandler(["switchconv", "switch_conv", "openconv", "useconv", "selectconv"], switch_conv_command))
+ application.add_handler(CommandHandler(["settopic", "set_topic", "settitle", "set_title", "convtitle", "title"], set_conv_title_command))
application.add_handler(CommandHandler(["delconv", "deleteconv", "del_conv", "delete_conv", "removeconv"], delete_conv_command))
application.add_handler(CommandHandler(["clearconvs", "clear_convs", "deleteallconvs"], clear_convs_command))
application.add_handler(CommandHandler("model", model_command))