remote-coder 0.5.0__tar.gz → 0.5.2__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.2}/PKG-INFO +3 -2
- {remote_coder-0.5.0 → remote_coder-0.5.2}/README.md +2 -1
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/__init__.py +1 -1
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/admin/router.py +2 -1
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/admin/static/i18n.js +5 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/admin/templates/admin.html +4 -1
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/ai/base.py +15 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/ai/claude.py +10 -3
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/ai/codex.py +3 -3
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/ai/gemini.py +2 -2
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/cli.py +5 -1
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/diagnostics.py +12 -1
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/jobs/execution_pipeline.py +4 -5
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/jobs/fix_pipeline.py +1 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/jobs/heartbeat.py +19 -10
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/jobs/manager.py +32 -2
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/jobs/schemas.py +8 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/jobs/worktree_planner.py +2 -2
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/main.py +25 -7
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/monitoring/git.py +34 -16
- remote_coder-0.5.2/app/monitoring/memory.py +30 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/commands/base.py +54 -28
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/commands/branch.py +24 -4
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/commands/registry.py +8 -1
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/commands/status.py +2 -2
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/commands/system.py +28 -30
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/formatting.py +34 -4
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/handlers/job_submission.py +2 -2
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/handlers/natural_flow.py +6 -1
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/handlers/presenters.py +9 -6
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/handlers/update_handler.py +7 -2
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/i18n.py +80 -91
- remote_coder-0.5.2/app/telegram/lists.py +20 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/messages.py +31 -7
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/notifier.py +65 -20
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/notifier_protocol.py +2 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/parser.py +14 -12
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/plan_decisions_flow.py +1 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/webhook_registration.py +32 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/pyproject.toml +1 -1
- {remote_coder-0.5.0 → remote_coder-0.5.2}/remote_coder.egg-info/PKG-INFO +3 -2
- {remote_coder-0.5.0 → remote_coder-0.5.2}/remote_coder.egg-info/SOURCES.txt +3 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_admin_router.py +2 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_ai_base.py +8 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_claude_runner.py +50 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_cli.py +2 -1
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_codex_runner.py +23 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_command_parser.py +25 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_commands.py +99 -20
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_diagnostics.py +14 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_gemini_runner.py +26 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_i18n.py +25 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_job_manager.py +156 -1
- remote_coder-0.5.2/tests/test_lists.py +26 -0
- remote_coder-0.5.2/tests/test_main_lifespan.py +67 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_monitoring.py +26 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_notifier.py +193 -5
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_telegram_formatting.py +31 -1
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_webhook.py +114 -10
- {remote_coder-0.5.0 → remote_coder-0.5.2}/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.2}/LICENSE +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/admin/__init__.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/admin/advanced_settings.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/admin/database_browser.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/admin/static/admin.css +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/admin/static/icons/advanced.svg +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/admin/static/icons/database.svg +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/admin/static/icons/download.svg +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/admin/static/icons/home.svg +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/admin/static/icons/logs.svg +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/admin/static/icons/projects.svg +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/admin/static/summary.js +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/admin/templates/advanced.html +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/admin/templates/database.html +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/admin/templates/logs.html +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/admin/templates/projects.html +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/ai/__init__.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/ai/factory.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/ai/model_catalog.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/ai/usage.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/config.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/git/__init__.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/git/ai_commit.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/git/branch_naming.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/git/commit_message.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/git/service.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/git/worktree_service.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/jobs/__init__.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/jobs/fix_support.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/jobs/plan_decisions.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/jobs/result_writer.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/jobs/store.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/models.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/monitoring/__init__.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/monitoring/code.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/monitoring/events.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/monitoring/log_buffer.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/monitoring/model.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/projects/__init__.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/projects/registry.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/security/__init__.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/security/auth.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/system_startup.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/__init__.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/bot_instances.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/commands/__init__.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/commands/clear_stop.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/commands/fix.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/commands/model.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/commands/monitor.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/confirmations.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/conversation/__init__.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/conversation/collaborators.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/conversation/context.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/conversation/models.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/conversation/protocols.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/conversation/sqlite_rows.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/conversation/sqlite_store.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/conversation/store.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/handlers/__init__.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/handlers/callback_dispatcher.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/handlers/command_flow.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/handlers/fix_flow.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/handlers/plan_flow.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/handlers/recent_updates.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/handlers/request.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/handlers/session_binding.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/model_preferences.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/telegram/webhook.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/app/tunnel.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/remote_coder.egg-info/dependency_links.txt +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/remote_coder.egg-info/entry_points.txt +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/remote_coder.egg-info/requires.txt +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/remote_coder.egg-info/top_level.txt +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/setup.cfg +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_ai_commit.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_ai_factory.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_auth.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_bot_instance_manager.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_branch_naming.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_commit_message_formatter.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_config.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_conversation_store.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_database_browser.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_event_logger.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_git_service.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_job_status.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_job_store.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_log_buffer.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_plan_decisions.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_project_registry.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_project_scoped_state.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_system_startup.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_tunnel.py +0 -0
- {remote_coder-0.5.0 → remote_coder-0.5.2}/tests/test_webhook_multibot.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: remote-coder
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.2
|
|
4
4
|
Summary: Telegram-based remote AI coding automation server
|
|
5
5
|
Author: Remote AI Coder contributors
|
|
6
6
|
License: Apache License
|
|
@@ -236,7 +236,7 @@ Run Claude Code, Codex, or Gemini on your local development machine by sending a
|
|
|
236
236
|
- Claude, Codex, and Gemini runners behind the same job flow.
|
|
237
237
|
- Reply-linked jobs continue the same AI CLI session, so follow-ups build on prior context.
|
|
238
238
|
- Local admin UI for project setup, advanced settings, logs, and conversation memory.
|
|
239
|
-
- Read-only `plan
|
|
239
|
+
- Read-only `plan:`, `ask:`, and `research:` modes when you want analysis without commits. PLAN mode asks open decisions through inline buttons first, then finalizes the plan from your answers; RESEARCH mode asks the selected AI CLI to use internet search when useful.
|
|
240
240
|
|
|
241
241
|
## Quick Start
|
|
242
242
|
|
|
@@ -311,6 +311,7 @@ Natural-language examples:
|
|
|
311
311
|
Fix the login validation bug with model: codex
|
|
312
312
|
plan: outline the migration before changing code
|
|
313
313
|
/ask what test command does this repo use?
|
|
314
|
+
/research compare current Telegram webhook security guidance
|
|
314
315
|
수정: 방금 작업에서 README 문구만 더 간결하게 바꿔줘
|
|
315
316
|
```
|
|
316
317
|
|
|
@@ -15,7 +15,7 @@ Run Claude Code, Codex, or Gemini on your local development machine by sending a
|
|
|
15
15
|
- Claude, Codex, and Gemini runners behind the same job flow.
|
|
16
16
|
- Reply-linked jobs continue the same AI CLI session, so follow-ups build on prior context.
|
|
17
17
|
- Local admin UI for project setup, advanced settings, logs, and conversation memory.
|
|
18
|
-
- Read-only `plan
|
|
18
|
+
- Read-only `plan:`, `ask:`, and `research:` modes when you want analysis without commits. PLAN mode asks open decisions through inline buttons first, then finalizes the plan from your answers; RESEARCH mode asks the selected AI CLI to use internet search when useful.
|
|
19
19
|
|
|
20
20
|
## Quick Start
|
|
21
21
|
|
|
@@ -90,6 +90,7 @@ Natural-language examples:
|
|
|
90
90
|
Fix the login validation bug with model: codex
|
|
91
91
|
plan: outline the migration before changing code
|
|
92
92
|
/ask what test command does this repo use?
|
|
93
|
+
/research compare current Telegram webhook security guidance
|
|
93
94
|
수정: 방금 작업에서 README 문구만 더 간결하게 바꿔줘
|
|
94
95
|
```
|
|
95
96
|
|
|
@@ -378,9 +378,10 @@ def create_admin_router(
|
|
|
378
378
|
report = check_prerequisites()
|
|
379
379
|
installed = [cli.name for cli in report.ai_clis if cli.installed]
|
|
380
380
|
_adminlog.info(
|
|
381
|
-
"prerequisites queried ngrok_ok=%s ai_clis=%s",
|
|
381
|
+
"prerequisites queried ngrok_ok=%s ai_clis=%s github_cli=%s",
|
|
382
382
|
report.ngrok_ok,
|
|
383
383
|
",".join(installed) or "-",
|
|
384
|
+
report.github_cli.installed,
|
|
384
385
|
)
|
|
385
386
|
return report.model_dump()
|
|
386
387
|
|
|
@@ -146,6 +146,11 @@
|
|
|
146
146
|
en: "None found (install at least one: claude / codex / gemini)",
|
|
147
147
|
ko: "설치된 것 없음 (claude / codex / gemini 중 최소 1개 설치)",
|
|
148
148
|
},
|
|
149
|
+
"setup.githubCli": { en: "GitHub CLI", ko: "GitHub CLI" },
|
|
150
|
+
"setup.githubCliNone": {
|
|
151
|
+
en: "Not found (install gh before using /pr)",
|
|
152
|
+
ko: "설치되지 않음 (/pr 사용 전 gh 설치 필요)",
|
|
153
|
+
},
|
|
149
154
|
"setup.checking": { en: "Checking…", ko: "확인 중…" },
|
|
150
155
|
"setup.recheck": { en: "Re-check", ko: "다시 확인" },
|
|
151
156
|
"setup.cta": { en: "Add your first project", ko: "첫 프로젝트 추가" },
|
|
@@ -248,9 +248,12 @@
|
|
|
248
248
|
const installed = (report.ai_clis || []).filter((c) => c.installed).map((c) => c.name);
|
|
249
249
|
const aiOk = installed.length > 0;
|
|
250
250
|
const aiDetail = aiOk ? installed.join(", ") : i18n.t("setup.aiCliNone");
|
|
251
|
+
const ghOk = !!(report.github_cli && report.github_cli.installed);
|
|
252
|
+
const ghDetail = ghOk ? "gh" : i18n.t("setup.githubCliNone");
|
|
251
253
|
$("#setup-prereq").innerHTML =
|
|
252
254
|
renderPrereqRow(!!report.ngrok_ok, i18n.t("setup.ngrok"), ngrokDetail) +
|
|
253
|
-
renderPrereqRow(aiOk, i18n.t("setup.aiCli"), aiDetail)
|
|
255
|
+
renderPrereqRow(aiOk, i18n.t("setup.aiCli"), aiDetail) +
|
|
256
|
+
renderPrereqRow(ghOk, i18n.t("setup.githubCli"), ghDetail);
|
|
254
257
|
}
|
|
255
258
|
|
|
256
259
|
async function loadPrerequisites() {
|
|
@@ -57,6 +57,18 @@ _PLAN_DECISIONS_INSTRUCTION = (
|
|
|
57
57
|
)
|
|
58
58
|
|
|
59
59
|
|
|
60
|
+
_RESEARCH_INSTRUCTION = (
|
|
61
|
+
"You are in RESEARCH mode. Read the repository context and answer the user's research "
|
|
62
|
+
"question. Do not modify files.\n\n"
|
|
63
|
+
"Use internet search when it is useful or necessary for the question, similar to a deep "
|
|
64
|
+
"research workflow. Compare multiple perspectives or sources when possible, and clearly "
|
|
65
|
+
"separate repository-derived facts from external findings. Include citations or source "
|
|
66
|
+
"links for external claims, call out uncertainty or limitations, and finish with a direct "
|
|
67
|
+
"answer to the user's problem.\n\n"
|
|
68
|
+
"User research request:\n"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
60
72
|
def instruction_for_runner_mode(instruction: str, mode: JobMode) -> str:
|
|
61
73
|
if mode == JobMode.PLAN:
|
|
62
74
|
return f"{_PLAN_DECISIONS_INSTRUCTION}{instruction}"
|
|
@@ -66,6 +78,8 @@ def instruction_for_runner_mode(instruction: str, mode: JobMode) -> str:
|
|
|
66
78
|
"Do not modify files.\n\n"
|
|
67
79
|
f"User question:\n{instruction}"
|
|
68
80
|
)
|
|
81
|
+
if mode == JobMode.RESEARCH:
|
|
82
|
+
return f"{_RESEARCH_INSTRUCTION}{instruction}"
|
|
69
83
|
return instruction
|
|
70
84
|
|
|
71
85
|
|
|
@@ -80,6 +94,7 @@ class RunnerInput:
|
|
|
80
94
|
mode: JobMode = JobMode.AGENT
|
|
81
95
|
session_id: str | None = None
|
|
82
96
|
resume_token: str | None = None
|
|
97
|
+
native_resume_cwd_stable: bool = True
|
|
83
98
|
|
|
84
99
|
|
|
85
100
|
@dataclass
|
|
@@ -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:
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
5
|
from app.ai.base import BaseCliRunner, RunnerInput, instruction_for_runner_mode
|
|
6
|
-
from app.jobs.schemas import
|
|
6
|
+
from app.jobs.schemas import is_read_only_job_mode
|
|
7
7
|
from app.models import CodexSandboxMode
|
|
8
8
|
from app.monitoring.events import EventLogger
|
|
9
9
|
|
|
@@ -16,7 +16,7 @@ class CodexRunner(BaseCliRunner):
|
|
|
16
16
|
self._sandbox = sandbox
|
|
17
17
|
|
|
18
18
|
def _resolve_sandbox(self, runner_input: RunnerInput) -> CodexSandboxMode:
|
|
19
|
-
if runner_input.mode
|
|
19
|
+
if is_read_only_job_mode(runner_input.mode):
|
|
20
20
|
return CodexSandboxMode.READ_ONLY
|
|
21
21
|
return self._sandbox
|
|
22
22
|
|
|
@@ -29,7 +29,7 @@ class CodexRunner(BaseCliRunner):
|
|
|
29
29
|
|
|
30
30
|
def build_argv(self, runner_input: RunnerInput) -> list[str]:
|
|
31
31
|
sandbox = self._resolve_sandbox(runner_input)
|
|
32
|
-
if runner_input.mode
|
|
32
|
+
if is_read_only_job_mode(runner_input.mode):
|
|
33
33
|
instruction = instruction_for_runner_mode(runner_input.instruction, runner_input.mode)
|
|
34
34
|
else:
|
|
35
35
|
instruction = runner_input.instruction
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
5
|
from app.ai.base import BaseCliRunner, RunnerInput, instruction_for_runner_mode
|
|
6
|
-
from app.jobs.schemas import
|
|
6
|
+
from app.jobs.schemas import is_read_only_job_mode
|
|
7
7
|
from app.monitoring.events import EventLogger
|
|
8
8
|
|
|
9
9
|
|
|
@@ -17,7 +17,7 @@ class GeminiRunner(BaseCliRunner):
|
|
|
17
17
|
return Path.home() / ".gemini" / "sessions"
|
|
18
18
|
|
|
19
19
|
def build_argv(self, runner_input: RunnerInput) -> list[str]:
|
|
20
|
-
if runner_input.mode
|
|
20
|
+
if is_read_only_job_mode(runner_input.mode):
|
|
21
21
|
prompt = instruction_for_runner_mode(runner_input.instruction, runner_input.mode)
|
|
22
22
|
argv = ["gemini", "--skip-trust", "-p", prompt]
|
|
23
23
|
else:
|
|
@@ -32,7 +32,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
32
32
|
help="Run the server only (skip ngrok and webhook registration)",
|
|
33
33
|
)
|
|
34
34
|
|
|
35
|
-
subparsers.add_parser("doctor", help="Check prerequisites (ngrok, AI CLIs)")
|
|
35
|
+
subparsers.add_parser("doctor", help="Check prerequisites (ngrok, AI CLIs, GitHub CLI)")
|
|
36
36
|
return parser
|
|
37
37
|
|
|
38
38
|
|
|
@@ -128,6 +128,10 @@ def run_doctor() -> None:
|
|
|
128
128
|
" ⚠️ AI CLI (claude/codex/gemini) not found. Install at least one. "
|
|
129
129
|
"(e.g. npm install -g @anthropic-ai/claude-code)"
|
|
130
130
|
)
|
|
131
|
+
if report.github_cli.installed:
|
|
132
|
+
print(" ✅ GitHub CLI: gh")
|
|
133
|
+
else:
|
|
134
|
+
print(" ⚠️ GitHub CLI (gh) not found. Install it before using /pr.")
|
|
131
135
|
|
|
132
136
|
|
|
133
137
|
if __name__ == "__main__":
|
|
@@ -7,6 +7,7 @@ from pydantic import BaseModel
|
|
|
7
7
|
from app import tunnel
|
|
8
8
|
|
|
9
9
|
AI_CLI_TOOLS = ("claude", "codex", "gemini")
|
|
10
|
+
GITHUB_CLI_TOOL = "gh"
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
class AiCliStatus(BaseModel):
|
|
@@ -18,6 +19,7 @@ class PrerequisitesReport(BaseModel):
|
|
|
18
19
|
ngrok_ok: bool
|
|
19
20
|
ngrok_detail: str
|
|
20
21
|
ai_clis: list[AiCliStatus]
|
|
22
|
+
github_cli: AiCliStatus
|
|
21
23
|
|
|
22
24
|
|
|
23
25
|
def check_prerequisites() -> PrerequisitesReport:
|
|
@@ -34,4 +36,13 @@ def check_prerequisites() -> PrerequisitesReport:
|
|
|
34
36
|
AiCliStatus(name=tool, installed=shutil.which(tool) is not None)
|
|
35
37
|
for tool in AI_CLI_TOOLS
|
|
36
38
|
]
|
|
37
|
-
|
|
39
|
+
github_cli = AiCliStatus(
|
|
40
|
+
name=GITHUB_CLI_TOOL,
|
|
41
|
+
installed=shutil.which(GITHUB_CLI_TOOL) is not None,
|
|
42
|
+
)
|
|
43
|
+
return PrerequisitesReport(
|
|
44
|
+
ngrok_ok=ngrok_ok,
|
|
45
|
+
ngrok_detail=ngrok_detail,
|
|
46
|
+
ai_clis=ai_clis,
|
|
47
|
+
github_cli=github_cli,
|
|
48
|
+
)
|
|
@@ -6,7 +6,7 @@ from pathlib import Path
|
|
|
6
6
|
from app.ai.base import RunnerInput
|
|
7
7
|
from app.git.commit_message import CommitMessageFormatter
|
|
8
8
|
from app.jobs.plan_decisions import PlanDecisionQuestion, parse_plan_decisions
|
|
9
|
-
from app.jobs.schemas import Job, JobMode
|
|
9
|
+
from app.jobs.schemas import Job, JobMode, is_read_only_job_mode
|
|
10
10
|
from app.monitoring.events import EventLogger
|
|
11
11
|
|
|
12
12
|
_joblog = EventLogger("app.jobs.lifecycle", "job.lifecycle")
|
|
@@ -50,7 +50,7 @@ def run_job(manager, job_id: str) -> Job:
|
|
|
50
50
|
created_worktree_for_job = False
|
|
51
51
|
failed_stage: str | None = None
|
|
52
52
|
remote = manager._effective_git_remote_name()
|
|
53
|
-
read_only_job = job.request.mode
|
|
53
|
+
read_only_job = is_read_only_job_mode(job.request.mode)
|
|
54
54
|
plan_decision_questions: list[PlanDecisionQuestion] | None = None
|
|
55
55
|
try:
|
|
56
56
|
job.mark_running()
|
|
@@ -96,6 +96,7 @@ def run_job(manager, job_id: str) -> Job:
|
|
|
96
96
|
mode=job.request.mode,
|
|
97
97
|
session_id=job.request.session_id,
|
|
98
98
|
resume_token=job.request.resume_session_token,
|
|
99
|
+
native_resume_cwd_stable=not created_worktree_for_job,
|
|
99
100
|
)
|
|
100
101
|
)
|
|
101
102
|
finally:
|
|
@@ -237,9 +238,7 @@ def run_job(manager, job_id: str) -> Job:
|
|
|
237
238
|
finally:
|
|
238
239
|
manager._cancel_events.pop(job_id, None)
|
|
239
240
|
manager._cancelled_job_ids.discard(job_id)
|
|
240
|
-
read_only_succeeded = (
|
|
241
|
-
job.request.mode in (JobMode.PLAN, JobMode.ASK) and job.status.value == "succeeded"
|
|
242
|
-
)
|
|
241
|
+
read_only_succeeded = is_read_only_job_mode(job.request.mode) and job.status.value == "succeeded"
|
|
243
242
|
cleanup_on_success = read_only_succeeded or not manager._effective_keep_worktree_on_success()
|
|
244
243
|
_joblog.info(
|
|
245
244
|
"job finalizing status=%s created_worktree=%s cleanup_on_success=%s",
|
|
@@ -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
|
|
|
@@ -26,9 +26,17 @@ class JobMode(StrEnum):
|
|
|
26
26
|
AGENT = "agent"
|
|
27
27
|
PLAN = "plan"
|
|
28
28
|
ASK = "ask"
|
|
29
|
+
RESEARCH = "research"
|
|
29
30
|
AGENT_FIX = "agent_fix"
|
|
30
31
|
|
|
31
32
|
|
|
33
|
+
READ_ONLY_JOB_MODES = frozenset({JobMode.PLAN, JobMode.ASK, JobMode.RESEARCH})
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def is_read_only_job_mode(mode: JobMode) -> bool:
|
|
37
|
+
return mode in READ_ONLY_JOB_MODES
|
|
38
|
+
|
|
39
|
+
|
|
32
40
|
class FixKind(StrEnum):
|
|
33
41
|
SOURCE = "source"
|
|
34
42
|
|
|
@@ -4,7 +4,7 @@ from dataclasses import dataclass
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
6
|
from app.git.service import GitWorktreeService
|
|
7
|
-
from app.jobs.schemas import Job,
|
|
7
|
+
from app.jobs.schemas import Job, is_read_only_job_mode
|
|
8
8
|
from app.monitoring.events import EventLogger
|
|
9
9
|
|
|
10
10
|
_joblog = EventLogger("app.jobs.lifecycle", "job.lifecycle")
|
|
@@ -27,7 +27,7 @@ def prepare_worktree_plan(
|
|
|
27
27
|
job_ctx: dict[str, object],
|
|
28
28
|
) -> WorktreePlan:
|
|
29
29
|
requested_branch = job.request.branch
|
|
30
|
-
if job.request.mode
|
|
30
|
+
if is_read_only_job_mode(job.request.mode):
|
|
31
31
|
path = git_service.prepare_detached_worktree(
|
|
32
32
|
project_path, job.id, worktree_base_dir=worktree_base
|
|
33
33
|
)
|
|
@@ -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
|
+
)
|