feat(telegram): make resume line optional (#100)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user