feat(telegram): make resume line optional (#100)

This commit is contained in:
banteg
2026-01-12 19:49:13 +04:00
committed by GitHub
parent 637a9fc3e2
commit 98ba41f8c7
6 changed files with 95 additions and 5 deletions
+5
View File
@@ -84,6 +84,7 @@ Configuration (under `[transports.telegram]`):
[transports.telegram.topics]
enabled = true
scope = "auto" # auto | main | projects | all
show_resume_line = true
```
Requirements:
@@ -95,6 +96,10 @@ Requirements:
- `auto`: if any project chats are configured, uses `projects`; otherwise `main`.
- The bot needs the **Manage Topics** permission in the relevant chat(s).
Optional:
- `show_resume_line`: set `false` to hide the resume command line in topic threads.
Commands:
- `main`: `/topic <project> @branch` creates a topic in the main chat and binds it.
+6
View File
@@ -262,6 +262,7 @@ Topics bind Telegram forum threads to specific project/branch contexts. They als
```toml
[transports.telegram.topics]
enabled = true
# show_resume_line = false
```
Your bot needs **Manage Topics** permission in the group.
@@ -269,6 +270,8 @@ Your bot needs **Manage Topics** permission in the group.
If any `projects.<alias>.chat_id` are configured, topics are managed in those
project chats; otherwise topics are managed in the main chat.
Set `show_resume_line = false` to hide the resume command line in topic threads.
### Topic behavior
```
@@ -321,6 +324,7 @@ chat_id = -1001234567890
[transports.telegram.topics]
enabled = true
# show_resume_line = false
```
**Project chats:**
@@ -331,6 +335,7 @@ chat_id = 123456789 # main chat (private, for non-project messages)
[transports.telegram.topics]
enabled = true
# show_resume_line = false
[projects.takopi]
path = "~/dev/takopi"
@@ -455,6 +460,7 @@ deny_globs = [".git/**", ".env", ".envrc", "**/*.pem", "**/.ssh/**"]
[transports.telegram.topics]
enabled = true
scope = "auto"
show_resume_line = true
# Project definitions
[projects.takopi]
+1
View File
@@ -57,6 +57,7 @@ class TelegramTopicsSettings(BaseModel):
enabled: bool = False
scope: Literal["auto", "main", "projects", "all"] = "auto"
show_resume_line: bool = True
class TelegramFilesSettings(BaseModel):
+41 -5
View File
@@ -1,9 +1,10 @@
from __future__ import annotations
from collections.abc import Awaitable, Callable, Sequence
from collections.abc import AsyncIterator, Awaitable, Callable, Sequence
from dataclasses import dataclass
from functools import partial
from pathlib import Path
from typing import cast
import anyio
@@ -21,7 +22,7 @@ from ..directives import DirectiveError
from ..ids import RESERVED_COMMAND_IDS, is_valid_id
from ..logging import bind_run_context, clear_context, get_logger
from ..markdown import MarkdownParts
from ..model import EngineId, ResumeToken
from ..model import EngineId, ResumeToken, TakopiEvent
from ..plugins import COMMAND_GROUP, list_entrypoints
from ..progress import ProgressTracker
from ..router import RunnerUnavailableError
@@ -67,6 +68,7 @@ from .topics import (
_maybe_update_topic_context,
_topic_key,
_topic_title,
_topics_chat_allowed,
_topics_chat_project,
_topics_command_error,
)
@@ -255,6 +257,29 @@ class _SavedFilePutGroup:
failed: list[_FilePutResult]
@dataclass(slots=True)
class _ResumeLineProxy:
runner: Runner
@property
def engine(self) -> str:
return self.runner.engine
def is_resume_line(self, line: str) -> bool:
return self.runner.is_resume_line(line)
def format_resume(self, _: ResumeToken) -> str:
return ""
def extract_resume(self, text: str | None) -> ResumeToken | None:
return self.runner.extract_resume(text)
def run(
self, prompt: str, resume: ResumeToken | None
) -> AsyncIterator[TakopiEvent]:
return self.runner.run(prompt, resume)
def resolve_file_put_paths(
plan: _FilePutPlan,
*,
@@ -1239,6 +1264,7 @@ async def _run_engine(
| None = None,
engine_override: EngineId | None = None,
thread_id: int | None = None,
show_resume_line: bool = True,
) -> None:
reply = partial(
send_plain,
@@ -1256,6 +1282,9 @@ async def _run_engine(
except RunnerUnavailableError as exc:
await reply(text=f"error:\n{exc}")
return
runner: Runner = entry.runner
if thread_id is not None and not show_resume_line:
runner = cast(Runner, _ResumeLineProxy(runner))
if not entry.available:
reason = entry.issue or "engine unavailable"
await _send_runner_unavailable(
@@ -1263,7 +1292,7 @@ async def _run_engine(
chat_id=chat_id,
user_msg_id=user_msg_id,
resume_token=resume_token,
runner=entry.runner,
runner=runner,
reason=reason,
thread_id=thread_id,
)
@@ -1278,7 +1307,7 @@ async def _run_engine(
run_fields = {
"chat_id": chat_id,
"user_msg_id": user_msg_id,
"engine": entry.runner.engine,
"engine": runner.engine,
"resume": resume_token.value if resume_token else None,
}
if context is not None:
@@ -1297,7 +1326,7 @@ async def _run_engine(
)
await handle_message(
exec_cfg,
runner=entry.runner,
runner=runner,
incoming=incoming,
resume_token=resume_token,
context=context,
@@ -1366,6 +1395,7 @@ class _TelegramCommandExecutor(CommandExecutor):
chat_id: int,
user_msg_id: int,
thread_id: int | None,
show_resume_line: bool,
) -> None:
self._exec_cfg = exec_cfg
self._runtime = runtime
@@ -1374,6 +1404,7 @@ class _TelegramCommandExecutor(CommandExecutor):
self._chat_id = chat_id
self._user_msg_id = user_msg_id
self._thread_id = thread_id
self._show_resume_line = show_resume_line
self._reply_ref = MessageRef(
channel_id=chat_id,
message_id=user_msg_id,
@@ -1443,6 +1474,7 @@ class _TelegramCommandExecutor(CommandExecutor):
on_thread_known=None,
engine_override=engine,
thread_id=self._thread_id,
show_resume_line=self._show_resume_line,
)
return RunResult(engine=engine, message=capture.last_message)
await _run_engine(
@@ -1458,6 +1490,7 @@ class _TelegramCommandExecutor(CommandExecutor):
on_thread_known=self._scheduler.note_thread_known,
engine_override=engine,
thread_id=self._thread_id,
show_resume_line=self._show_resume_line,
)
return RunResult(engine=engine, message=None)
@@ -1504,6 +1537,8 @@ async def _dispatch_command(
if msg.reply_to_message_id is not None
else None
)
topic_thread = msg.thread_id is not None and _topics_chat_allowed(cfg, msg.chat_id)
show_resume_line = cfg.topics.show_resume_line if topic_thread else True
executor = _TelegramCommandExecutor(
exec_cfg=cfg.exec_cfg,
runtime=cfg.runtime,
@@ -1512,6 +1547,7 @@ async def _dispatch_command(
chat_id=chat_id,
user_msg_id=user_msg_id,
thread_id=msg.thread_id,
show_resume_line=show_resume_line,
)
message_ref = MessageRef(
channel_id=chat_id, message_id=user_msg_id, thread_id=msg.thread_id
+4
View File
@@ -444,6 +444,9 @@ async def run_main_loop(
)
else None
)
show_resume_line = (
cfg.topics.show_resume_line if topic_key is not None else True
)
await _run_engine(
exec_cfg=cfg.exec_cfg,
runtime=cfg.runtime,
@@ -459,6 +462,7 @@ async def run_main_loop(
),
engine_override=engine_override,
thread_id=thread_id,
show_resume_line=show_resume_line,
)
async def run_thread_job(job: ThreadJob) -> None:
+38
View File
@@ -1261,6 +1261,44 @@ async def test_send_with_resume_reports_when_missing() -> None:
assert "resume token" in transport.send_calls[-1]["message"].text.lower()
@pytest.mark.anyio
async def test_run_engine_hides_resume_line_in_topics() -> None:
transport = telegram_commands._CaptureTransport()
runner = ScriptRunner(
[Return(answer="ok")],
engine=CODEX_ENGINE,
resume_value="resume-123",
)
exec_cfg = ExecBridgeConfig(
transport=transport,
presenter=MarkdownPresenter(),
final_notify=True,
)
runtime = TransportRuntime(
router=_make_router(runner),
projects=_empty_projects(),
)
await telegram_commands._run_engine(
exec_cfg=exec_cfg,
runtime=runtime,
running_tasks={},
chat_id=123,
user_msg_id=1,
text="hello",
resume_token=None,
context=None,
reply_ref=None,
on_thread_known=None,
engine_override=None,
thread_id=77,
show_resume_line=False,
)
assert transport.last_message is not None
assert "resume-123" not in transport.last_message.text
@pytest.mark.anyio
async def test_run_main_loop_routes_reply_to_running_resume() -> None:
progress_ready = anyio.Event()