Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/coding_agent_telegram/router/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,12 @@ def _chat_locale(self, chat_id: int) -> str:
def _t(self, update: Update | None, key: str, **kwargs) -> str:
return translate(self._locale(update), key, **kwargs)

def _affirmative_inline_button_kwargs(self) -> dict[str, dict[str, str]]:
return {"api_kwargs": {"style": "primary"}}

def _negative_inline_button_kwargs(self) -> dict[str, dict[str, str]]:
return {"api_kwargs": {"style": "danger"}}

async def _notify_if_current_project_busy(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> bool:
chat = update.effective_chat
if chat is None:
Expand Down
12 changes: 10 additions & 2 deletions src/coding_agent_telegram/router/git_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,16 @@ async def handle_push(self, update: Update, context: ContextTypes.DEFAULT_TYPE)
confirm_markup = InlineKeyboardMarkup(
[
[
InlineKeyboardButton(self._t(update, "git.push_confirm_button"), callback_data="push:confirm"),
InlineKeyboardButton(self._t(update, "git.cancel_button"), callback_data="push:cancel"),
InlineKeyboardButton(
self._t(update, "git.push_confirm_button"),
callback_data="push:confirm",
**self._affirmative_inline_button_kwargs(),
),
InlineKeyboardButton(
self._t(update, "git.cancel_button"),
callback_data="push:cancel",
**self._negative_inline_button_kwargs(),
),
]
]
)
Expand Down
8 changes: 8 additions & 0 deletions src/coding_agent_telegram/router/message_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import logging
import tempfile
from pathlib import Path
from types import SimpleNamespace

from telegram import Update
from telegram.ext import ContextTypes
Expand Down Expand Up @@ -66,6 +67,13 @@ async def _process_user_message(
async def handle_message(self, update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
if update.message is None or not update.message.text:
return
is_create_session, session_name = self._parse_create_session_text(update.message.text)
if is_create_session:
await self.handle_new(
update,
SimpleNamespace(args=[session_name] if session_name else [], bot=context.bot),
)
return
await self._process_user_message(update, context, update.message.text)

@require_allowed_chat()
Expand Down
12 changes: 10 additions & 2 deletions src/coding_agent_telegram/router/project_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,8 +331,16 @@ async def handle_project(self, update: Update, context: ContextTypes.DEFAULT_TYP
keyboard = InlineKeyboardMarkup(
[
[
InlineKeyboardButton(self._t(update, "queue.button_yes"), callback_data=f"trustproject:yes:{folder}"),
InlineKeyboardButton(self._t(update, "queue.button_no"), callback_data=f"trustproject:no:{folder}"),
InlineKeyboardButton(
self._t(update, "queue.button_yes"),
callback_data=f"trustproject:yes:{folder}",
**self._affirmative_inline_button_kwargs(),
),
InlineKeyboardButton(
self._t(update, "queue.button_no"),
callback_data=f"trustproject:no:{folder}",
**self._negative_inline_button_kwargs(),
),
]
]
)
Expand Down
18 changes: 15 additions & 3 deletions src/coding_agent_telegram/router/queue_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,16 @@ async def _prompt_continue_queued_questions(self, chat_id: int, context: Context
text=translate(locale, "queue.continue_prompt"),
reply_markup=InlineKeyboardMarkup(
[[
InlineKeyboardButton(translate(locale, "queue.button_yes"), callback_data="queuecontinue:yes"),
InlineKeyboardButton(translate(locale, "queue.button_no"), callback_data="queuecontinue:no"),
InlineKeyboardButton(
translate(locale, "queue.button_yes"),
callback_data="queuecontinue:yes",
**self._affirmative_inline_button_kwargs(),
),
InlineKeyboardButton(
translate(locale, "queue.button_no"),
callback_data="queuecontinue:no",
**self._negative_inline_button_kwargs(),
),
]]
),
)
Expand Down Expand Up @@ -234,7 +242,11 @@ async def _prompt_queue_batch_decision(
[[
InlineKeyboardButton(translate(locale, "queue.button_group"), callback_data="queuebatch:group"),
InlineKeyboardButton(translate(locale, "queue.button_single"), callback_data="queuebatch:single"),
InlineKeyboardButton(translate(locale, "queue.button_cancel"), callback_data="queuebatch:cancel"),
InlineKeyboardButton(
translate(locale, "queue.button_cancel"),
callback_data="queuebatch:cancel",
**self._negative_inline_button_kwargs(),
),
]]
),
)
Expand Down
14 changes: 14 additions & 0 deletions src/coding_agent_telegram/router/session_lifecycle_commands.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from __future__ import annotations

import re

from telegram import Update
from telegram.ext import ContextTypes

Expand All @@ -10,6 +12,18 @@


class SessionLifecycleCommandMixin:
_CREATE_SESSION_TEXT_RE = re.compile(r"^\s*create\s+session\s*:\s*(.*?)\s*$", re.IGNORECASE)

def _parse_create_session_text(self, text: str) -> tuple[bool, str | None]:
match = self._CREATE_SESSION_TEXT_RE.match(text)
if not match:
return False, None

session_name = match.group(1).strip() or None
if session_name and session_name.lower() == "new session":
session_name = None
return True, session_name

async def _prompt_for_provider_selection(
self,
update: Update,
Expand Down
12 changes: 10 additions & 2 deletions src/coding_agent_telegram/router/session_provider_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,16 @@ def button_label(provider: str) -> str:
return InlineKeyboardMarkup(
[
[
InlineKeyboardButton(button_label("codex"), callback_data="provider:set:codex"),
InlineKeyboardButton(button_label("copilot"), callback_data="provider:set:copilot"),
InlineKeyboardButton(
button_label("codex"),
callback_data="provider:set:codex",
api_kwargs={"style": "success"},
),
InlineKeyboardButton(
button_label("copilot"),
callback_data="provider:set:copilot",
api_kwargs={"style": "success"},
),
]
]
)
Expand Down
66 changes: 66 additions & 0 deletions tests/test_command_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -682,6 +682,11 @@ def test_project_command_warns_when_existing_project_is_untrusted(tmp_path: Path

assert bot.messages[-1][1] == "Do you trust this project?\nProject: <code>backend</code>"
assert bot.messages[-1][3] is not None
buttons = bot.messages[-1][3].inline_keyboard[0]
assert buttons[0].text == "Yes"
assert buttons[1].text == "No"
assert buttons[0].api_kwargs == {"style": "primary"}
assert buttons[1].api_kwargs == {"style": "danger"}
assert store.is_project_trusted("backend") is False


Expand Down Expand Up @@ -1537,6 +1542,50 @@ def test_new_without_name_ignores_existing_new_session_labels(tmp_path: Path):
assert "Session created successfully: sess_abc123" in bot.messages[-1][1]


def test_plain_text_create_session_new_session_uses_unnamed_flow(tmp_path: Path):
backend = tmp_path / "backend"
backend.mkdir()
runner = DummyRunner()
cfg = make_config(tmp_path)
store = SessionStore(cfg.state_file, cfg.state_backup_file)
store.set_current_project_folder("bot-a", 123, "backend")
store.set_current_provider("bot-a", 123, "codex")
router = CommandRouter(RouterDeps(cfg=cfg, store=store, agent_runner=runner, bot_id="bot-a"))
router._provider_available = lambda provider: True

update = make_update(text="Create session: new session")
bot = FakeBot()
context = SimpleNamespace(args=[], bot=bot)

asyncio.run(router.handle_message(update, context))

state = store.get_chat_state("bot-a", 123)
assert state["sessions"]["sess_abc123"]["name"] == "sess_abc123"
assert runner.create_calls[-1]["user_message"] == "Create session: new session"


def test_plain_text_create_session_with_name_matches_new_command(tmp_path: Path):
backend = tmp_path / "backend"
backend.mkdir()
runner = DummyRunner()
cfg = make_config(tmp_path)
store = SessionStore(cfg.state_file, cfg.state_backup_file)
store.set_current_project_folder("bot-a", 123, "backend")
store.set_current_provider("bot-a", 123, "codex")
router = CommandRouter(RouterDeps(cfg=cfg, store=store, agent_runner=runner, bot_id="bot-a"))
router._provider_available = lambda provider: True

update = make_update(text="Create session: release prep")
bot = FakeBot()
context = SimpleNamespace(args=[], bot=bot)

asyncio.run(router.handle_message(update, context))

state = store.get_chat_state("bot-a", 123)
assert state["sessions"]["sess_abc123"]["name"] == "release prep"
assert runner.create_calls[-1]["user_message"] == "Create session: release prep"


def test_provider_command_sends_inline_buttons(tmp_path: Path):
runner = DummyRunner()
cfg = make_config(tmp_path)
Expand All @@ -1561,6 +1610,8 @@ def test_provider_command_sends_inline_buttons(tmp_path: Path):
assert buttons[1].callback_data == "provider:set:copilot"
assert "missing" in buttons[0].text
assert "current" in buttons[1].text
assert buttons[0].api_kwargs == {"style": "success"}
assert buttons[1].api_kwargs == {"style": "success"}


def test_provider_callback_updates_current_provider(tmp_path: Path):
Expand Down Expand Up @@ -3680,6 +3731,12 @@ async def exercise():
assert buttons[0].callback_data == "queuebatch:group"
assert buttons[1].callback_data == "queuebatch:single"
assert buttons[2].callback_data == "queuebatch:cancel"
assert buttons[0].text == "Group the questions"
assert buttons[1].text == "Process one by one"
assert buttons[2].text == "Cancel"
assert buttons[0].api_kwargs == {}
assert buttons[1].api_kwargs == {}
assert buttons[2].api_kwargs == {"style": "danger"}

answers = []
edited = []
Expand Down Expand Up @@ -3882,6 +3939,10 @@ async def exercise():
buttons = keyboard.inline_keyboard[0]
assert buttons[0].callback_data == "queuecontinue:yes"
assert buttons[1].callback_data == "queuecontinue:no"
assert buttons[0].text == "Yes"
assert buttons[1].text == "No"
assert buttons[0].api_kwargs == {"style": "primary"}
assert buttons[1].api_kwargs == {"style": "danger"}

asyncio.run(exercise())

Expand Down Expand Up @@ -4469,6 +4530,11 @@ def test_push_uses_current_session_branch(tmp_path: Path):
assert bot.messages[-1][1] == "Push branch `feature-1` to `origin`?"
assert bot.messages[-1][2] == "Markdown"
assert bot.messages[-1][3] is not None
buttons = bot.messages[-1][3].inline_keyboard[0]
assert buttons[0].text == "Confirm push"
assert buttons[1].text == "Cancel"
assert buttons[0].api_kwargs == {"style": "primary"}
assert buttons[1].api_kwargs == {"style": "danger"}


def test_push_confirmation_executes_push(tmp_path: Path):
Expand Down
Loading