remote-coder 0.5.0__tar.gz → 0.5.1__tar.gz
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.
- {remote_coder-0.5.0 → remote_coder-0.5.1}/PKG-INFO +1 -1
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/__init__.py +1 -1
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/ai/base.py +1 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/ai/claude.py +10 -3
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/jobs/execution_pipeline.py +1 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/jobs/fix_pipeline.py +1 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/jobs/heartbeat.py +19 -10
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/jobs/manager.py +32 -2
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/main.py +25 -7
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/monitoring/git.py +34 -16
- remote_coder-0.5.1/app/monitoring/memory.py +30 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/commands/base.py +33 -24
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/commands/status.py +2 -2
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/commands/system.py +15 -29
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/formatting.py +34 -4
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/i18n.py +59 -81
- remote_coder-0.5.1/app/telegram/lists.py +20 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/messages.py +24 -3
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/notifier.py +65 -20
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/notifier_protocol.py +2 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/plan_decisions_flow.py +1 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/webhook_registration.py +32 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/pyproject.toml +1 -1
- {remote_coder-0.5.0 → remote_coder-0.5.1}/remote_coder.egg-info/PKG-INFO +1 -1
- {remote_coder-0.5.0 → remote_coder-0.5.1}/remote_coder.egg-info/SOURCES.txt +3 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_claude_runner.py +26 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_cli.py +1 -1
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_commands.py +40 -16
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_i18n.py +25 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_job_manager.py +109 -1
- remote_coder-0.5.1/tests/test_lists.py +26 -0
- remote_coder-0.5.1/tests/test_main_lifespan.py +67 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_monitoring.py +26 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_notifier.py +157 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_telegram_formatting.py +31 -1
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_webhook.py +6 -9
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_webhook_registration.py +35 -0
- remote_coder-0.5.0/app/monitoring/memory.py +0 -20
- {remote_coder-0.5.0 → remote_coder-0.5.1}/LICENSE +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/README.md +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/admin/__init__.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/admin/advanced_settings.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/admin/database_browser.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/admin/router.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/admin/static/admin.css +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/admin/static/i18n.js +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/admin/static/icons/advanced.svg +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/admin/static/icons/database.svg +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/admin/static/icons/download.svg +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/admin/static/icons/home.svg +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/admin/static/icons/logs.svg +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/admin/static/icons/projects.svg +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/admin/static/summary.js +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/admin/templates/admin.html +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/admin/templates/advanced.html +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/admin/templates/database.html +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/admin/templates/logs.html +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/admin/templates/projects.html +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/ai/__init__.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/ai/codex.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/ai/factory.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/ai/gemini.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/ai/model_catalog.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/ai/usage.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/cli.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/config.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/diagnostics.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/git/__init__.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/git/ai_commit.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/git/branch_naming.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/git/commit_message.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/git/service.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/git/worktree_service.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/jobs/__init__.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/jobs/fix_support.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/jobs/plan_decisions.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/jobs/result_writer.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/jobs/schemas.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/jobs/store.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/jobs/worktree_planner.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/models.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/monitoring/__init__.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/monitoring/code.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/monitoring/events.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/monitoring/log_buffer.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/monitoring/model.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/projects/__init__.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/projects/registry.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/security/__init__.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/security/auth.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/system_startup.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/__init__.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/bot_instances.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/commands/__init__.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/commands/branch.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/commands/clear_stop.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/commands/fix.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/commands/model.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/commands/monitor.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/commands/registry.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/confirmations.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/conversation/__init__.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/conversation/collaborators.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/conversation/context.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/conversation/models.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/conversation/protocols.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/conversation/sqlite_rows.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/conversation/sqlite_store.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/conversation/store.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/handlers/__init__.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/handlers/callback_dispatcher.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/handlers/command_flow.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/handlers/fix_flow.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/handlers/job_submission.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/handlers/natural_flow.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/handlers/plan_flow.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/handlers/presenters.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/handlers/recent_updates.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/handlers/request.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/handlers/session_binding.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/handlers/update_handler.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/model_preferences.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/parser.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/telegram/webhook.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/app/tunnel.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/remote_coder.egg-info/dependency_links.txt +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/remote_coder.egg-info/entry_points.txt +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/remote_coder.egg-info/requires.txt +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/remote_coder.egg-info/top_level.txt +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/setup.cfg +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_admin_router.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_ai_base.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_ai_commit.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_ai_factory.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_auth.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_bot_instance_manager.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_branch_naming.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_codex_runner.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_command_parser.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_commit_message_formatter.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_config.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_conversation_store.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_database_browser.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_diagnostics.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_event_logger.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_gemini_runner.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_git_service.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_job_status.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_job_store.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_log_buffer.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_plan_decisions.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_project_registry.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_project_scoped_state.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_system_startup.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_tunnel.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.1}/tests/test_webhook_multibot.py +0 -0
|
@@ -12,13 +12,20 @@ class ClaudeRunner(BaseCliRunner):
|
|
|
12
12
|
self, runner_input: RunnerInput, before: dict[str, float]
|
|
13
13
|
) -> str | None:
|
|
14
14
|
# Claude owns its session id deterministically (we pass --session-id/--resume).
|
|
15
|
-
return runner_input
|
|
15
|
+
return self._effective_resume_token(runner_input) or runner_input.session_id
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def _effective_resume_token(runner_input: RunnerInput) -> str | None:
|
|
19
|
+
if not runner_input.native_resume_cwd_stable:
|
|
20
|
+
return None
|
|
21
|
+
return runner_input.resume_token
|
|
16
22
|
|
|
17
23
|
def build_argv(self, runner_input: RunnerInput) -> list[str]:
|
|
18
24
|
prompt = instruction_for_runner_mode(runner_input.instruction, runner_input.mode)
|
|
19
25
|
argv = ["claude", "-p", prompt, "--dangerously-skip-permissions"]
|
|
20
|
-
|
|
21
|
-
|
|
26
|
+
resume_token = self._effective_resume_token(runner_input)
|
|
27
|
+
if resume_token:
|
|
28
|
+
argv.extend(["--resume", resume_token])
|
|
22
29
|
elif runner_input.session_id:
|
|
23
30
|
argv.extend(["--session-id", runner_input.session_id])
|
|
24
31
|
if runner_input.model_id:
|
|
@@ -8,33 +8,42 @@ from app.jobs.schemas import Job
|
|
|
8
8
|
from app.telegram.notifier import Notifier, build_job_accepted_message, build_job_heartbeat_message
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
class HeartbeatHandle:
|
|
12
|
+
def __init__(self, stop_event: threading.Event, thread: threading.Thread | None) -> None:
|
|
13
|
+
self._stop = stop_event
|
|
14
|
+
self._thread = thread
|
|
15
|
+
|
|
16
|
+
def set(self) -> None:
|
|
17
|
+
# Stop the heartbeat and wait for any in-flight edit so the caller's next
|
|
18
|
+
# edit (send_job_result) wins the race for the same message_id.
|
|
19
|
+
self._stop.set()
|
|
20
|
+
if self._thread is not None:
|
|
21
|
+
self._thread.join(timeout=15)
|
|
22
|
+
|
|
23
|
+
|
|
11
24
|
def start_heartbeat(
|
|
12
25
|
*,
|
|
13
26
|
job: Job,
|
|
14
27
|
notifier_resolver: Callable[[str], Notifier],
|
|
15
28
|
interval_seconds: float,
|
|
16
|
-
) ->
|
|
29
|
+
) -> HeartbeatHandle:
|
|
17
30
|
# Periodically edit the "Job accepted" message so a long run shows live progress.
|
|
18
31
|
# No-op when there is no message to edit (e.g. notifier returned no id).
|
|
19
32
|
stop = threading.Event()
|
|
20
33
|
if job.accepted_message_id is None:
|
|
21
|
-
return stop
|
|
34
|
+
return HeartbeatHandle(stop, None)
|
|
22
35
|
notifier = notifier_resolver(job.request.project)
|
|
23
36
|
chat_id = job.request.chat_id
|
|
24
37
|
message_id = job.accepted_message_id
|
|
25
38
|
started = datetime.now(UTC)
|
|
26
|
-
|
|
27
|
-
edited = threading.Event()
|
|
39
|
+
_, stop_buttons = build_job_accepted_message(job)
|
|
28
40
|
|
|
29
41
|
def _beat() -> None:
|
|
30
42
|
while not stop.wait(interval_seconds):
|
|
31
43
|
elapsed_minutes = int((datetime.now(UTC) - started).total_seconds() // 60)
|
|
32
44
|
text = build_job_heartbeat_message(job, elapsed_minutes)
|
|
33
45
|
notifier.edit_message(chat_id, message_id, text, stop_buttons)
|
|
34
|
-
edited.set()
|
|
35
|
-
if edited.is_set():
|
|
36
|
-
# Restore the original accepted body without the now-useless Stop button.
|
|
37
|
-
notifier.edit_message(chat_id, message_id, accepted_text, [])
|
|
38
46
|
|
|
39
|
-
threading.Thread(target=_beat, daemon=True)
|
|
40
|
-
|
|
47
|
+
thread = threading.Thread(target=_beat, daemon=True)
|
|
48
|
+
thread.start()
|
|
49
|
+
return HeartbeatHandle(stop, thread)
|
|
@@ -20,7 +20,7 @@ from app.jobs.fix_support import (
|
|
|
20
20
|
list_fix_candidates,
|
|
21
21
|
resolve_fix_target_job,
|
|
22
22
|
)
|
|
23
|
-
from app.jobs.heartbeat import start_heartbeat
|
|
23
|
+
from app.jobs.heartbeat import HeartbeatHandle, start_heartbeat
|
|
24
24
|
from app.jobs.plan_decisions import PlanDecisionQuestion
|
|
25
25
|
from app.jobs.result_writer import (
|
|
26
26
|
make_output_summary,
|
|
@@ -37,6 +37,19 @@ from app.telegram.notifier import Notifier
|
|
|
37
37
|
|
|
38
38
|
_joblog = EventLogger("app.jobs.lifecycle", "job.lifecycle")
|
|
39
39
|
|
|
40
|
+
# Telegram only allows reactions from a fixed allow-list of emoji, so map the job
|
|
41
|
+
# lifecycle onto values from https://core.telegram.org/bots/api#reactiontypeemoji.
|
|
42
|
+
_REACTION_QUEUED = "👀"
|
|
43
|
+
_REACTION_SUCCEEDED = "🎉"
|
|
44
|
+
_REACTION_FAILED = "💔"
|
|
45
|
+
_REACTION_CANCELLED = "🤝"
|
|
46
|
+
|
|
47
|
+
_TERMINAL_REACTION_BY_STATUS = {
|
|
48
|
+
"succeeded": _REACTION_SUCCEEDED,
|
|
49
|
+
"failed": _REACTION_FAILED,
|
|
50
|
+
"cancelled": _REACTION_CANCELLED,
|
|
51
|
+
}
|
|
52
|
+
|
|
40
53
|
|
|
41
54
|
class JobManager:
|
|
42
55
|
def __init__(
|
|
@@ -73,7 +86,7 @@ class JobManager:
|
|
|
73
86
|
def _notifier_for(self, project: str) -> Notifier:
|
|
74
87
|
return self._notifier_resolver(project)
|
|
75
88
|
|
|
76
|
-
def _start_heartbeat(self, job: Job) ->
|
|
89
|
+
def _start_heartbeat(self, job: Job) -> HeartbeatHandle:
|
|
77
90
|
return start_heartbeat(
|
|
78
91
|
job=job,
|
|
79
92
|
notifier_resolver=self._notifier_for,
|
|
@@ -104,6 +117,21 @@ class JobManager:
|
|
|
104
117
|
self._notifier_for(job.request.project).send_job_result(job)
|
|
105
118
|
)
|
|
106
119
|
self._job_store.update(job)
|
|
120
|
+
self._react(job.request, _TERMINAL_REACTION_BY_STATUS.get(job.status.value))
|
|
121
|
+
|
|
122
|
+
def _react(self, request: JobRequest, emoji: str | None) -> None:
|
|
123
|
+
if request.message_id is None or emoji is None:
|
|
124
|
+
return
|
|
125
|
+
try:
|
|
126
|
+
self._notifier_for(request.project).set_reaction(
|
|
127
|
+
request.chat_id, request.message_id, emoji
|
|
128
|
+
)
|
|
129
|
+
except Exception: # pylint: disable=broad-except
|
|
130
|
+
_joblog.exception(
|
|
131
|
+
"set_reaction failed",
|
|
132
|
+
chat_id=request.chat_id,
|
|
133
|
+
project=request.project,
|
|
134
|
+
)
|
|
107
135
|
|
|
108
136
|
def submit(self, request: JobRequest) -> Job:
|
|
109
137
|
job = Job(id=request.job_id or self._make_job_id(), request=request)
|
|
@@ -120,6 +148,7 @@ class JobManager:
|
|
|
120
148
|
if accepted_message_id is not None:
|
|
121
149
|
job.accepted_message_id = accepted_message_id
|
|
122
150
|
self._job_store.update(job)
|
|
151
|
+
self._react(request, _REACTION_QUEUED)
|
|
123
152
|
return job
|
|
124
153
|
|
|
125
154
|
def cancel(self, job_id: str) -> bool:
|
|
@@ -218,6 +247,7 @@ class JobManager:
|
|
|
218
247
|
if accepted_message_id is not None:
|
|
219
248
|
job.accepted_message_id = accepted_message_id
|
|
220
249
|
self._job_store.update(job)
|
|
250
|
+
self._react(request, _REACTION_QUEUED)
|
|
221
251
|
with self._project_lock(request.project):
|
|
222
252
|
return self._run_fix(job.id)
|
|
223
253
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
3
|
import time
|
|
4
|
-
from contextlib import asynccontextmanager
|
|
4
|
+
from contextlib import asynccontextmanager, suppress
|
|
5
5
|
from dataclasses import replace
|
|
6
6
|
|
|
7
7
|
from fastapi import FastAPI, Request
|
|
@@ -137,9 +137,8 @@ webhook_registrar = (
|
|
|
137
137
|
else None
|
|
138
138
|
)
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
instances = bot_instance_manager.list_all()
|
|
140
|
+
|
|
141
|
+
def _run_startup_side_effects(instances: list[BotInstance], adv: AdvancedSettings) -> None:
|
|
143
142
|
startup_chat_total = sum(len(inst.auth_service.allowed_chat_ids) for inst in instances)
|
|
144
143
|
_systemlog.info(
|
|
145
144
|
"lifespan startup notifying allowed chats count=%d projects=%d default_model=%s",
|
|
@@ -147,9 +146,7 @@ async def lifespan(_app: FastAPI):
|
|
|
147
146
|
len(project_registry.list_projects()),
|
|
148
147
|
ModelName.CLAUDE.value,
|
|
149
148
|
)
|
|
150
|
-
|
|
151
|
-
await asyncio.to_thread(
|
|
152
|
-
run_startup_project_pulls,
|
|
149
|
+
run_startup_project_pulls(
|
|
153
150
|
pull_projects_on_server_startup_enabled=adv.pull_projects_on_server_startup_enabled,
|
|
154
151
|
project_registry=project_registry,
|
|
155
152
|
git_service=git_service,
|
|
@@ -178,7 +175,28 @@ async def lifespan(_app: FastAPI):
|
|
|
178
175
|
_systemlog.info("startup notification sent", chat_id=chat_id)
|
|
179
176
|
except Exception:
|
|
180
177
|
_systemlog.exception("startup notification failed", chat_id=chat_id)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
async def _run_startup_side_effects_in_background(
|
|
181
|
+
instances: list[BotInstance], adv: AdvancedSettings
|
|
182
|
+
) -> None:
|
|
183
|
+
try:
|
|
184
|
+
await asyncio.to_thread(_run_startup_side_effects, instances, adv)
|
|
185
|
+
except Exception:
|
|
186
|
+
_systemlog.exception("startup side effects failed")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@asynccontextmanager
|
|
190
|
+
async def lifespan(_app: FastAPI):
|
|
191
|
+
instances = bot_instance_manager.list_all()
|
|
192
|
+
startup_task = asyncio.create_task(
|
|
193
|
+
_run_startup_side_effects_in_background(instances, advanced_settings_store.get())
|
|
194
|
+
)
|
|
181
195
|
yield
|
|
196
|
+
if not startup_task.done():
|
|
197
|
+
startup_task.cancel()
|
|
198
|
+
with suppress(asyncio.CancelledError):
|
|
199
|
+
await startup_task
|
|
182
200
|
shutdown_instances = bot_instance_manager.list_all()
|
|
183
201
|
shutdown_chat_total = sum(len(inst.auth_service.allowed_chat_ids) for inst in shutdown_instances)
|
|
184
202
|
_systemlog.info("lifespan shutdown notifying allowed chats count=%d", shutdown_chat_total)
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
5
|
from app.git.service import GitWorktreeService
|
|
6
|
+
from app.telegram.lists import render_labeled_list
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
TELEGRAM_SAFE_LEN = 3800
|
|
@@ -24,16 +25,19 @@ def format_branch_monitor(
|
|
|
24
25
|
except RuntimeError as exc:
|
|
25
26
|
return f"/monitor branch failed: {exc}"
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
summary_rows = [
|
|
29
|
+
("Project", project_name),
|
|
30
|
+
("root", str(root)),
|
|
31
|
+
("Remote", remote),
|
|
32
|
+
("Current checkout", current),
|
|
33
|
+
("Local branches", str(local_n)),
|
|
34
|
+
("Remote-tracking branches", str(remote_n)),
|
|
35
|
+
]
|
|
36
|
+
header = "Branch monitor\n" + render_labeled_list(summary_rows) + "\n\n"
|
|
37
|
+
body = (
|
|
38
|
+
f"Local branches\n{_render_text_list(local_block)}\n\n"
|
|
39
|
+
f"Remote branches\n{_render_text_list(remote_block)}"
|
|
35
40
|
)
|
|
36
|
-
body = f"[Local]\n{local_block}\n\n[{remote} remote]\n{remote_block}"
|
|
37
41
|
text = header + body
|
|
38
42
|
if len(text) > max_len:
|
|
39
43
|
text = text[:max_len].rstrip() + "\n\n...(truncated for message length)"
|
|
@@ -87,17 +91,31 @@ def format_worktree_monitor(
|
|
|
87
91
|
if len(entries) > max_detail:
|
|
88
92
|
extra = f"\n...({len(entries) - max_detail} more omitted)"
|
|
89
93
|
|
|
94
|
+
summary_rows = [
|
|
95
|
+
("Project", project_name),
|
|
96
|
+
("root", str(root)),
|
|
97
|
+
("Managed base directory", str(managed_base)),
|
|
98
|
+
("Total worktrees", str(len(entries))),
|
|
99
|
+
("Detached worktrees", str(detached_n)),
|
|
100
|
+
("Managed candidates", str(managed_n)),
|
|
101
|
+
]
|
|
90
102
|
lines = [
|
|
91
103
|
"Worktree monitor",
|
|
92
|
-
|
|
93
|
-
f"root: {root}",
|
|
94
|
-
f"Managed base directory (worktree_base): {managed_base}",
|
|
95
|
-
f"Total worktrees: {len(entries)}",
|
|
96
|
-
f"Detached worktrees: {detached_n}",
|
|
97
|
-
f"Managed candidates (remote-*, base, _rebase_ops): {managed_n}",
|
|
104
|
+
render_labeled_list(summary_rows),
|
|
98
105
|
"",
|
|
99
|
-
"
|
|
106
|
+
"Worktree entries",
|
|
100
107
|
*detail_lines,
|
|
101
108
|
extra,
|
|
102
109
|
]
|
|
103
110
|
return "\n".join(lines).strip()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _render_text_list(text: str) -> str:
|
|
114
|
+
items: list[str] = []
|
|
115
|
+
for raw in text.splitlines():
|
|
116
|
+
item = raw.strip()
|
|
117
|
+
if item.startswith("* "):
|
|
118
|
+
item = item[2:]
|
|
119
|
+
if item:
|
|
120
|
+
items.append(f"- {item}")
|
|
121
|
+
return "\n".join(items)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from app.telegram.conversation import ConversationDbChatStats
|
|
4
|
+
from app.telegram.lists import render_labeled_list
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def format_memory_monitor(stats: ConversationDbChatStats, project: str, chat_id: int) -> str:
|
|
8
|
+
size_kb = stats.db_size_bytes / 1024.0 if stats.db_size_bytes else 0.0
|
|
9
|
+
summary_rows: list[tuple[str, str]] = [
|
|
10
|
+
("Project", project),
|
|
11
|
+
("chat_id", str(chat_id)),
|
|
12
|
+
("DB path", str(stats.db_path)),
|
|
13
|
+
("DB exists", "yes" if stats.db_exists else "no"),
|
|
14
|
+
("DB size", f"{size_kb:.2f} KiB ({stats.db_size_bytes} bytes)"),
|
|
15
|
+
("Rows for this chat", str(stats.total_rows)),
|
|
16
|
+
("Sessions", str(stats.session_count)),
|
|
17
|
+
]
|
|
18
|
+
if stats.rows_by_role:
|
|
19
|
+
role_rows = [(role, str(count)) for role, count in sorted(stats.rows_by_role.items())]
|
|
20
|
+
else:
|
|
21
|
+
role_rows = [("(none)", "0")]
|
|
22
|
+
return "\n".join(
|
|
23
|
+
[
|
|
24
|
+
"Memory (SQLite)",
|
|
25
|
+
render_labeled_list(summary_rows),
|
|
26
|
+
"",
|
|
27
|
+
"Rows by role",
|
|
28
|
+
render_labeled_list(role_rows),
|
|
29
|
+
]
|
|
30
|
+
)
|
|
@@ -12,6 +12,7 @@ from app.projects.registry import ProjectRegistry
|
|
|
12
12
|
from app.telegram.confirmations import InMemoryConfirmationStore, PendingConfirmation
|
|
13
13
|
from app.telegram.conversation import SQLiteConversationStore
|
|
14
14
|
from app.telegram.model_preferences import InMemoryModelPreferenceStore, ModelPreference
|
|
15
|
+
from app.telegram.lists import render_command_list
|
|
15
16
|
|
|
16
17
|
if TYPE_CHECKING:
|
|
17
18
|
from app.admin.advanced_settings import FileAdvancedSettingsStore
|
|
@@ -54,6 +55,7 @@ MODEL_USAGE = "<claude|codex|gemini>"
|
|
|
54
55
|
class InlineButton:
|
|
55
56
|
label: str
|
|
56
57
|
callback_data: str
|
|
58
|
+
style: str | None = None
|
|
57
59
|
|
|
58
60
|
|
|
59
61
|
@dataclass
|
|
@@ -69,13 +71,30 @@ def _help_response_skips_notifier_body_i18n(message_text: str) -> bool:
|
|
|
69
71
|
return False
|
|
70
72
|
|
|
71
73
|
|
|
74
|
+
_HELP_COMMAND_ROWS: tuple[tuple[str, str, str], ...] = (
|
|
75
|
+
("/model", "<claude|codex|gemini>", "Change the default model"),
|
|
76
|
+
("/status", "<job_id>", "Check job status"),
|
|
77
|
+
("/branch", "[name]", "Show or switch branches"),
|
|
78
|
+
("/pull", "", "Pull remote branch updates"),
|
|
79
|
+
("/rebase", "[branch]", "Rebase a branch"),
|
|
80
|
+
("/pr", "[branch]", "Open a GitHub PR"),
|
|
81
|
+
("/monitor", "<scope>", "Monitoring"),
|
|
82
|
+
("/clear", "<branch|worktrees|memory>", "Cleanup (confirmation)"),
|
|
83
|
+
("/reports", "[count]", "Conversation memory report"),
|
|
84
|
+
("/init", "", "Reset this chat's settings"),
|
|
85
|
+
("/stop", "<job_id>", "Stop a running job"),
|
|
86
|
+
("/fix", "", "Fix linked job commit"),
|
|
87
|
+
("/start", "", "Inline menu"),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
72
91
|
HELP_TEXT = "\n".join(
|
|
73
92
|
[
|
|
74
|
-
"Help",
|
|
93
|
+
"🧭 Help",
|
|
75
94
|
"",
|
|
76
95
|
"Send work requests as regular messages.",
|
|
77
96
|
"",
|
|
78
|
-
"Options",
|
|
97
|
+
"⚙️ Options",
|
|
79
98
|
"- model:",
|
|
80
99
|
"- branch:",
|
|
81
100
|
"- no commit",
|
|
@@ -84,31 +103,21 @@ HELP_TEXT = "\n".join(
|
|
|
84
103
|
"- fix: <natural language> or /fix - fix mode (reply to a job result; amends that commit)",
|
|
85
104
|
"- Korean aliases 계획:, 질문:, and 수정: instead of plan:/ask:/fix: (colons `:` or full-width `:` allowed)",
|
|
86
105
|
"",
|
|
87
|
-
"Commands
|
|
88
|
-
|
|
89
|
-
"
|
|
90
|
-
"
|
|
91
|
-
"- /pull: Pull all remote branch updates",
|
|
92
|
-
"- /rebase [branch]: Rebase a branch",
|
|
93
|
-
"- /pr [branch]: Open a GitHub PR for a branch",
|
|
94
|
-
"- /monitor <model|memory|branch|worktrees|code|project>: Monitoring",
|
|
95
|
-
"- /clear <branch|worktrees|memory>: Cleanup (confirmation required)",
|
|
96
|
-
"- /reports [count]: Conversation memory report",
|
|
97
|
-
"- /init: Reset this chat's settings",
|
|
98
|
-
"- /stop <job_id>: Stop a running job",
|
|
99
|
-
"- /fix: Fix the linked job commit (reply to a job result first)",
|
|
100
|
-
"- /start: Inline menu",
|
|
106
|
+
"📋 Commands",
|
|
107
|
+
render_command_list(_HELP_COMMAND_ROWS),
|
|
108
|
+
"",
|
|
109
|
+
"💡 Tip: Reply to a job result and send `fix: ...` to amend that commit.",
|
|
101
110
|
]
|
|
102
111
|
)
|
|
103
112
|
|
|
104
113
|
HELP_AGENT_TOPIC = "\n".join(
|
|
105
114
|
[
|
|
106
|
-
"AGENTS mode (agent)",
|
|
115
|
+
"🤖 AGENTS mode (agent)",
|
|
107
116
|
"",
|
|
108
117
|
"Natural-language coding tasks. The agent can modify code in the current project; when there are "
|
|
109
118
|
"changes it can create or update a branch, commit, and push.",
|
|
110
119
|
"",
|
|
111
|
-
"Examples",
|
|
120
|
+
"💡 Examples",
|
|
112
121
|
"- fix the login validation bug",
|
|
113
122
|
"- model: codex branch: remote-auth strengthen tests",
|
|
114
123
|
"- no commit just verify the doc wording",
|
|
@@ -119,12 +128,12 @@ HELP_AGENT_TOPIC = "\n".join(
|
|
|
119
128
|
|
|
120
129
|
HELP_PLAN_TOPIC = "\n".join(
|
|
121
130
|
[
|
|
122
|
-
"Plan mode (plan)",
|
|
131
|
+
"📐 Plan mode (plan)",
|
|
123
132
|
"",
|
|
124
133
|
"Receive change plans only; no code edits. Like agent mode, a job is accepted after confirmation "
|
|
125
134
|
"(`y`/`Y` or inline buttons).",
|
|
126
135
|
"",
|
|
127
|
-
"Examples",
|
|
136
|
+
"💡 Examples",
|
|
128
137
|
"- plan: summarize the login validation flow",
|
|
129
138
|
"- /plan model: codex list only API boundary risks",
|
|
130
139
|
"- 계획:refactor steps (full-width colon)",
|
|
@@ -135,12 +144,12 @@ HELP_PLAN_TOPIC = "\n".join(
|
|
|
135
144
|
|
|
136
145
|
HELP_ASK_TOPIC = "\n".join(
|
|
137
146
|
[
|
|
138
|
-
"Ask mode (ask)",
|
|
147
|
+
"❓ Ask mode (ask)",
|
|
139
148
|
"",
|
|
140
149
|
"Answer questions using the repository; no code edits, commits, or pushes. Jobs are accepted like "
|
|
141
150
|
"agent mode after confirmation (`y`/`Y` or inline buttons).",
|
|
142
151
|
"",
|
|
143
|
-
"Examples",
|
|
152
|
+
"💡 Examples",
|
|
144
153
|
"- ask: how do I run pytest in this project?",
|
|
145
154
|
"- /ask explain JobManager.run stages",
|
|
146
155
|
"- 질문:what this error line means",
|
|
@@ -151,12 +160,12 @@ HELP_ASK_TOPIC = "\n".join(
|
|
|
151
160
|
|
|
152
161
|
HELP_FIX_TOPIC = "\n".join(
|
|
153
162
|
[
|
|
154
|
-
"Fix mode (fix)",
|
|
163
|
+
"🔧 Fix mode (fix)",
|
|
155
164
|
"",
|
|
156
165
|
"Apply follow-up fixes on top of a previous succeeded job. Reply to a job result, then use "
|
|
157
166
|
"fix: or /fix. The agent amends the existing commit and pushes with --force-with-lease.",
|
|
158
167
|
"",
|
|
159
|
-
"Examples",
|
|
168
|
+
"💡 Examples",
|
|
160
169
|
"- (reply to job result) fix: add missing tests",
|
|
161
170
|
"- (reply to job result) /fix then send the fix instruction",
|
|
162
171
|
"- (reply to job result) 수정:로그인 검증 버그도 고쳐줘",
|
|
@@ -188,11 +188,11 @@ class StatusCommand(TelegramCommand):
|
|
|
188
188
|
rows: list[list[InlineButton]] = []
|
|
189
189
|
status = job.status.value
|
|
190
190
|
if status in ("running", "queued"):
|
|
191
|
-
rows.append([InlineButton("Stop", f"/stop {job.id}")])
|
|
191
|
+
rows.append([InlineButton("Stop", f"/stop {job.id}", style="danger")])
|
|
192
192
|
elif status == "succeeded" and job.branch:
|
|
193
193
|
actions: list[InlineButton] = []
|
|
194
194
|
if job.commit_hash:
|
|
195
|
-
actions.append(InlineButton("Open PR", f"/pr {job.branch}"))
|
|
195
|
+
actions.append(InlineButton("Open PR", f"/pr {job.branch}", style="primary"))
|
|
196
196
|
actions.append(InlineButton("Rebase", f"/rebase {job.branch}"))
|
|
197
197
|
rows.append(actions)
|
|
198
198
|
return with_nav_row(rows, back_to="/status")
|
|
@@ -11,7 +11,6 @@ from app.telegram.commands.base import (
|
|
|
11
11
|
InlineButton,
|
|
12
12
|
TelegramCommand,
|
|
13
13
|
TelegramMessage,
|
|
14
|
-
_button_rows,
|
|
15
14
|
_cmd_evt,
|
|
16
15
|
effective_model_for_chat,
|
|
17
16
|
effective_project_name_for_chat,
|
|
@@ -42,30 +41,35 @@ class StartCommand(TelegramCommand):
|
|
|
42
41
|
return topic_text
|
|
43
42
|
project_name = effective_project_name_for_chat(ctx, message.chat_id)
|
|
44
43
|
if not project_name:
|
|
45
|
-
return
|
|
44
|
+
return (
|
|
45
|
+
f"{self._ready_line()}\n\n"
|
|
46
|
+
"Welcome to Remote AI Coder.\n"
|
|
47
|
+
"Send a coding request or tap Help to start."
|
|
48
|
+
)
|
|
46
49
|
entry = ctx.project_registry.get(project_name)
|
|
47
50
|
if not entry:
|
|
48
51
|
return (
|
|
49
52
|
f"{self._ready_line()}\n\n"
|
|
50
|
-
"Welcome to Remote AI Coder.\n"
|
|
51
53
|
f"- Project: {project_name} (not registered)"
|
|
52
54
|
)
|
|
55
|
+
if not entry.enabled:
|
|
56
|
+
return (
|
|
57
|
+
f"{self._ready_line()}\n\n"
|
|
58
|
+
f"- Project: {entry.name} (disabled)"
|
|
59
|
+
)
|
|
53
60
|
try:
|
|
54
61
|
current_branch = ctx.git_service.get_current_branch(entry.root_path)
|
|
55
62
|
except RuntimeError:
|
|
56
63
|
current_branch = "(check failed)"
|
|
57
|
-
state = "enabled" if entry.enabled else "disabled"
|
|
58
64
|
return "\n".join(
|
|
59
65
|
[
|
|
60
66
|
self._ready_line(),
|
|
61
67
|
"",
|
|
62
|
-
"Welcome to Remote AI Coder.",
|
|
63
68
|
f"- Project: {entry.name}",
|
|
64
|
-
f"-
|
|
65
|
-
f"-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
f"- enabled: {state}",
|
|
69
|
+
f"- Model: {entry.default_model.value}",
|
|
70
|
+
f"- Branch: {current_branch}",
|
|
71
|
+
"",
|
|
72
|
+
"Send a coding request or tap Help to start.",
|
|
69
73
|
]
|
|
70
74
|
)
|
|
71
75
|
|
|
@@ -138,25 +142,7 @@ class HelpCommand(TelegramCommand):
|
|
|
138
142
|
message: TelegramMessage | None = None,
|
|
139
143
|
ctx: CommandContext | None = None,
|
|
140
144
|
) -> list[list[InlineButton]] | None:
|
|
141
|
-
|
|
142
|
-
return None
|
|
143
|
-
tokens = message.text.strip().split() if message else []
|
|
144
|
-
if len(tokens) >= 2:
|
|
145
|
-
topic = tokens[1].lower()
|
|
146
|
-
if topic in ("agent", "agents", "plan", "ask", "fix"):
|
|
147
|
-
return with_nav_row(None, back_to="/help")
|
|
148
|
-
subcmd = self._registry.get("/" + tokens[1])
|
|
149
|
-
if subcmd is not None:
|
|
150
|
-
sub_buttons = subcmd.get_inline_buttons(None, ctx) or []
|
|
151
|
-
return with_nav_row(sub_buttons, back_to="/help")
|
|
152
|
-
menu_cmds = [
|
|
153
|
-
cmd for name, cmd in self._registry.items()
|
|
154
|
-
if name not in ("/help", "/start") and cmd.menu_text
|
|
155
|
-
]
|
|
156
|
-
if not menu_cmds:
|
|
157
|
-
return None
|
|
158
|
-
buttons = [InlineButton(cmd.name[1:], f"/help {cmd.name[1:]}") for cmd in menu_cmds]
|
|
159
|
-
return with_nav_row(_button_rows(buttons, per_row=2))
|
|
145
|
+
return None
|
|
160
146
|
|
|
161
147
|
|
|
162
148
|
class InitCommand(TelegramCommand):
|
|
@@ -13,12 +13,26 @@ _SECTION_HEADINGS = frozenset(
|
|
|
13
13
|
{
|
|
14
14
|
"Options",
|
|
15
15
|
"옵션",
|
|
16
|
+
"⚙️ Options",
|
|
17
|
+
"⚙️ 옵션",
|
|
16
18
|
"Commands:",
|
|
17
19
|
"명령어 목록:",
|
|
20
|
+
"📋 Commands",
|
|
21
|
+
"📋 명령어 목록",
|
|
22
|
+
"Rows by role",
|
|
23
|
+
"역할별 행 수",
|
|
24
|
+
"Local branches",
|
|
25
|
+
"로컬 브랜치",
|
|
26
|
+
"Remote branches",
|
|
27
|
+
"원격 브랜치",
|
|
28
|
+
"Worktree entries",
|
|
29
|
+
"Worktree 항목",
|
|
18
30
|
"Usage",
|
|
19
31
|
"사용법",
|
|
20
32
|
"Examples",
|
|
21
33
|
"예시",
|
|
34
|
+
"💡 Examples",
|
|
35
|
+
"💡 예시",
|
|
22
36
|
"Prerequisites",
|
|
23
37
|
"전제조건",
|
|
24
38
|
"AI response:",
|
|
@@ -46,12 +60,17 @@ _BODY_MARKERS = frozenset(
|
|
|
46
60
|
_CODE_VALUE_LINE = re.compile(
|
|
47
61
|
r"^(?P<prefix>\s*-\s*(?:Job ID|Session ID|Branch|Commit|Log path|브랜치|커밋|로그 경로)\s*:\s*)(?P<value>\S.*?)\s*$"
|
|
48
62
|
)
|
|
63
|
+
_COMMAND_LIST_LINE = re.compile(r"^\s*-\s+(?P<command>/\S+(?:\s+\S.*)?)\s*$")
|
|
49
64
|
|
|
50
65
|
|
|
51
66
|
def _utf16_units(text: str) -> int:
|
|
52
67
|
return len(text.encode("utf-16-le")) // 2
|
|
53
68
|
|
|
54
69
|
|
|
70
|
+
def prepare_outgoing(text: str) -> tuple[str, list[dict[str, int | str]]]:
|
|
71
|
+
return text, build_message_entities(text)
|
|
72
|
+
|
|
73
|
+
|
|
55
74
|
def build_message_entities(text: str) -> list[dict[str, int | str]]:
|
|
56
75
|
"""Compute BotFather-like entities (bold title/headings, code values)."""
|
|
57
76
|
lines = text.split("\n")
|
|
@@ -77,14 +96,25 @@ def build_message_entities(text: str) -> list[dict[str, int | str]]:
|
|
|
77
96
|
if stripped in _BODY_MARKERS:
|
|
78
97
|
body_started = True
|
|
79
98
|
else:
|
|
80
|
-
|
|
81
|
-
if
|
|
99
|
+
command_match = _COMMAND_LIST_LINE.match(line)
|
|
100
|
+
if command_match is not None:
|
|
101
|
+
command = command_match.group("command")
|
|
82
102
|
entities.append(
|
|
83
103
|
{
|
|
84
104
|
"type": "code",
|
|
85
|
-
"offset": offset + _utf16_units(
|
|
86
|
-
"length": _utf16_units(
|
|
105
|
+
"offset": offset + _utf16_units(line[: command_match.start("command")]),
|
|
106
|
+
"length": _utf16_units(command),
|
|
87
107
|
}
|
|
88
108
|
)
|
|
109
|
+
else:
|
|
110
|
+
value_match = _CODE_VALUE_LINE.match(line)
|
|
111
|
+
if value_match is not None and not value_match.group("value").startswith("("):
|
|
112
|
+
entities.append(
|
|
113
|
+
{
|
|
114
|
+
"type": "code",
|
|
115
|
+
"offset": offset + _utf16_units(value_match.group("prefix")),
|
|
116
|
+
"length": _utf16_units(value_match.group("value")),
|
|
117
|
+
}
|
|
118
|
+
)
|
|
89
119
|
offset += line_units + 1
|
|
90
120
|
return entities
|