remote-coder 0.4.1__py3-none-any.whl
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.
- app/__init__.py +3 -0
- app/admin/__init__.py +0 -0
- app/admin/advanced_settings.py +88 -0
- app/admin/database_browser.py +301 -0
- app/admin/router.py +528 -0
- app/admin/static/i18n.js +401 -0
- app/admin/static/icons/advanced.svg +8 -0
- app/admin/static/icons/database.svg +5 -0
- app/admin/static/icons/download.svg +3 -0
- app/admin/static/icons/home.svg +4 -0
- app/admin/static/icons/logs.svg +3 -0
- app/admin/static/icons/projects.svg +5 -0
- app/admin/static/summary.js +73 -0
- app/admin/templates/admin.html +511 -0
- app/admin/templates/advanced.html +635 -0
- app/admin/templates/database.html +880 -0
- app/admin/templates/logs.html +686 -0
- app/admin/templates/projects.html +878 -0
- app/ai/__init__.py +0 -0
- app/ai/base.py +129 -0
- app/ai/claude.py +20 -0
- app/ai/codex.py +34 -0
- app/ai/factory.py +27 -0
- app/ai/gemini.py +20 -0
- app/ai/model_catalog.py +47 -0
- app/ai/usage.py +134 -0
- app/cli.py +238 -0
- app/config.py +130 -0
- app/git/__init__.py +0 -0
- app/git/ai_commit.py +88 -0
- app/git/branch_naming.py +21 -0
- app/git/commit_message.py +279 -0
- app/git/service.py +669 -0
- app/jobs/__init__.py +0 -0
- app/jobs/manager.py +770 -0
- app/jobs/schemas.py +116 -0
- app/jobs/store.py +334 -0
- app/main.py +265 -0
- app/models.py +20 -0
- app/monitoring/__init__.py +10 -0
- app/monitoring/code.py +161 -0
- app/monitoring/events.py +33 -0
- app/monitoring/git.py +103 -0
- app/monitoring/log_buffer.py +245 -0
- app/monitoring/memory.py +19 -0
- app/monitoring/model.py +598 -0
- app/projects/__init__.py +19 -0
- app/projects/registry.py +384 -0
- app/security/__init__.py +0 -0
- app/security/auth.py +19 -0
- app/system_startup.py +34 -0
- app/telegram/__init__.py +0 -0
- app/telegram/bot_instances.py +67 -0
- app/telegram/commands/__init__.py +64 -0
- app/telegram/commands/base.py +222 -0
- app/telegram/commands/branch.py +366 -0
- app/telegram/commands/clear_stop.py +221 -0
- app/telegram/commands/fix.py +219 -0
- app/telegram/commands/model.py +93 -0
- app/telegram/commands/monitor.py +185 -0
- app/telegram/commands/registry.py +110 -0
- app/telegram/commands/status.py +243 -0
- app/telegram/commands/system.py +201 -0
- app/telegram/confirmations.py +36 -0
- app/telegram/conversation.py +789 -0
- app/telegram/i18n.py +742 -0
- app/telegram/model_preferences.py +53 -0
- app/telegram/notifier.py +387 -0
- app/telegram/parser.py +267 -0
- app/telegram/webhook.py +988 -0
- app/telegram/webhook_registration.py +172 -0
- app/tunnel.py +104 -0
- remote_coder-0.4.1.dist-info/METADATA +520 -0
- remote_coder-0.4.1.dist-info/RECORD +78 -0
- remote_coder-0.4.1.dist-info/WHEEL +5 -0
- remote_coder-0.4.1.dist-info/entry_points.txt +2 -0
- remote_coder-0.4.1.dist-info/licenses/LICENSE +201 -0
- remote_coder-0.4.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from app.jobs.schemas import Job, JobStatus
|
|
4
|
+
from app.projects.registry import ProjectRecord
|
|
5
|
+
from app.telegram.commands.base import (
|
|
6
|
+
CommandContext,
|
|
7
|
+
ConfirmableCommand,
|
|
8
|
+
InlineButton,
|
|
9
|
+
TelegramCommand,
|
|
10
|
+
TelegramMessage,
|
|
11
|
+
_button_rows,
|
|
12
|
+
_cmd_evt,
|
|
13
|
+
_confirmation_buttons_enabled,
|
|
14
|
+
_job_button_label,
|
|
15
|
+
effective_project_name_for_chat,
|
|
16
|
+
)
|
|
17
|
+
from app.telegram.confirmations import PendingConfirmation
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ClearCommand(ConfirmableCommand):
|
|
21
|
+
name = "/clear"
|
|
22
|
+
description = "Clean branches, worktrees, or conversation memory after confirmation"
|
|
23
|
+
|
|
24
|
+
def execute(self, message: TelegramMessage, ctx: CommandContext) -> str:
|
|
25
|
+
tokens = message.text.strip().split()
|
|
26
|
+
if len(tokens) == 1:
|
|
27
|
+
return "Choose what to clear. Confirmation with y/Y is required before running."
|
|
28
|
+
if len(tokens) != 2 or tokens[1] not in {"branch", "memory", "worktrees"}:
|
|
29
|
+
return "Usage: /clear branch or /clear worktrees or /clear memory"
|
|
30
|
+
|
|
31
|
+
action = tokens[1]
|
|
32
|
+
if action == "memory" and ctx.conversation_store is None:
|
|
33
|
+
return "Memory store is not configured."
|
|
34
|
+
|
|
35
|
+
ctx.confirmation_store.set(
|
|
36
|
+
effective_project_name_for_chat(ctx, message.chat_id),
|
|
37
|
+
message.chat_id,
|
|
38
|
+
PendingConfirmation(command_name=self.name, action=action),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if action == "branch":
|
|
42
|
+
summary = "Delete remote-* branches and their linked worktrees in this bot project."
|
|
43
|
+
elif action == "worktrees":
|
|
44
|
+
summary = "Clean managed worktrees and prune stale entries in this bot project."
|
|
45
|
+
else:
|
|
46
|
+
summary = "Delete only this chat's conversation memory in this bot project."
|
|
47
|
+
if _confirmation_buttons_enabled(ctx):
|
|
48
|
+
return f"Pending action: {summary}\nChoose whether to run it."
|
|
49
|
+
return (
|
|
50
|
+
f"Pending action: {summary}\n"
|
|
51
|
+
"Send y or Y to run it. Any other response cancels it."
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def get_inline_buttons(
|
|
55
|
+
self,
|
|
56
|
+
message: TelegramMessage | None = None,
|
|
57
|
+
ctx: CommandContext | None = None,
|
|
58
|
+
) -> list[list[InlineButton]] | None:
|
|
59
|
+
if message is None or ctx is None:
|
|
60
|
+
return None
|
|
61
|
+
tokens = message.text.strip().split()
|
|
62
|
+
if len(tokens) == 1:
|
|
63
|
+
return [
|
|
64
|
+
[
|
|
65
|
+
InlineButton("branch", "/clear branch"),
|
|
66
|
+
InlineButton("worktrees", "/clear worktrees"),
|
|
67
|
+
InlineButton("memory", "/clear memory"),
|
|
68
|
+
],
|
|
69
|
+
]
|
|
70
|
+
if not _confirmation_buttons_enabled(ctx):
|
|
71
|
+
return None
|
|
72
|
+
pending = ctx.confirmation_store.get(
|
|
73
|
+
effective_project_name_for_chat(ctx, message.chat_id),
|
|
74
|
+
message.chat_id,
|
|
75
|
+
)
|
|
76
|
+
if pending is None or pending.command_name != self.name:
|
|
77
|
+
return None
|
|
78
|
+
return [[InlineButton("Yes", "Y"), InlineButton("No", "n")]]
|
|
79
|
+
|
|
80
|
+
def confirm(
|
|
81
|
+
self,
|
|
82
|
+
message: TelegramMessage,
|
|
83
|
+
ctx: CommandContext,
|
|
84
|
+
pending: PendingConfirmation,
|
|
85
|
+
) -> str:
|
|
86
|
+
if message.text.strip() not in {"y", "Y"}:
|
|
87
|
+
if pending.action == "branch":
|
|
88
|
+
target = "Branch cleanup"
|
|
89
|
+
elif pending.action == "worktrees":
|
|
90
|
+
target = "Worktree cleanup"
|
|
91
|
+
else:
|
|
92
|
+
target = "Memory cleanup"
|
|
93
|
+
return f"{target} was cancelled."
|
|
94
|
+
|
|
95
|
+
_cmd_evt.info("clear confirmed action=%s", pending.action, chat_id=message.chat_id)
|
|
96
|
+
if pending.action == "branch":
|
|
97
|
+
return self._clear_branches(ctx)
|
|
98
|
+
if pending.action == "worktrees":
|
|
99
|
+
return self._clear_worktrees(ctx)
|
|
100
|
+
if pending.action == "memory":
|
|
101
|
+
return self._clear_memory(ctx, message.chat_id)
|
|
102
|
+
return "Unknown clear action."
|
|
103
|
+
|
|
104
|
+
def _bound_project_record(self, ctx: CommandContext) -> ProjectRecord | None:
|
|
105
|
+
name = ctx.project_name
|
|
106
|
+
if not name:
|
|
107
|
+
return None
|
|
108
|
+
return ctx.project_registry.get(name)
|
|
109
|
+
|
|
110
|
+
def _clear_branches(self, ctx: CommandContext) -> str:
|
|
111
|
+
p = self._bound_project_record(ctx)
|
|
112
|
+
if p is None:
|
|
113
|
+
return "No project is bound to this bot or the project was not found in the registry."
|
|
114
|
+
if not p.enabled:
|
|
115
|
+
return f"Project is disabled: {p.name}"
|
|
116
|
+
|
|
117
|
+
try:
|
|
118
|
+
ctx.git_service.checkout_integrate_branch(p.root_path)
|
|
119
|
+
remote_branches = ctx.git_service.list_remote_branches_matching(
|
|
120
|
+
p.root_path, ctx.git_remote_name, "remote-"
|
|
121
|
+
)
|
|
122
|
+
local_branches = ctx.git_service.list_local_branches_matching(p.root_path, "remote-")
|
|
123
|
+
if remote_branches:
|
|
124
|
+
ctx.git_service.delete_remote_branches(p.root_path, ctx.git_remote_name, remote_branches)
|
|
125
|
+
if local_branches:
|
|
126
|
+
ctx.git_service.remove_linked_worktrees_for_branches(p.root_path, local_branches)
|
|
127
|
+
ctx.git_service.delete_local_branches(p.root_path, local_branches)
|
|
128
|
+
return (
|
|
129
|
+
f"{p.name}: remote {len(remote_branches)}, local {len(local_branches)} deleted "
|
|
130
|
+
f"({ctx.git_remote_name})"
|
|
131
|
+
)
|
|
132
|
+
except RuntimeError as exc:
|
|
133
|
+
return f"{p.name}: failed - {exc}"
|
|
134
|
+
|
|
135
|
+
def _clear_memory(self, ctx: CommandContext, chat_id: int) -> str:
|
|
136
|
+
if ctx.conversation_store is None:
|
|
137
|
+
return "Memory store is not configured."
|
|
138
|
+
project_name = ctx.project_name
|
|
139
|
+
if not project_name:
|
|
140
|
+
return "No project is bound to this bot."
|
|
141
|
+
entries_removed, links_removed = ctx.conversation_store.delete_chat_memory(
|
|
142
|
+
project=project_name, chat_id=chat_id
|
|
143
|
+
)
|
|
144
|
+
return (
|
|
145
|
+
f"Deleted this chat's conversation memory. "
|
|
146
|
+
f"(project={project_name}, entries {entries_removed}, branch links {links_removed})"
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
def _clear_worktrees(self, ctx: CommandContext) -> str:
|
|
150
|
+
p = self._bound_project_record(ctx)
|
|
151
|
+
if p is None:
|
|
152
|
+
return "No project is bound to this bot or the project was not found in the registry."
|
|
153
|
+
if not p.enabled:
|
|
154
|
+
return f"Project is disabled: {p.name}"
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
removed_count = ctx.git_service.cleanup_managed_worktrees(
|
|
158
|
+
p.root_path,
|
|
159
|
+
p.worktree_base_dir,
|
|
160
|
+
branch_prefix="remote-",
|
|
161
|
+
)
|
|
162
|
+
return f"{p.name}: {removed_count} worktrees deleted, stale entries pruned"
|
|
163
|
+
except RuntimeError as exc:
|
|
164
|
+
return f"{p.name}: failed - {exc}"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
class StopCommand(TelegramCommand):
|
|
168
|
+
name = "/stop"
|
|
169
|
+
description = "Choose and stop a running job"
|
|
170
|
+
|
|
171
|
+
def execute(self, message: TelegramMessage, ctx: CommandContext) -> str:
|
|
172
|
+
tokens = message.text.strip().split(maxsplit=1)
|
|
173
|
+
if len(tokens) < 2:
|
|
174
|
+
jobs = self._list_cancellable_jobs(message, ctx)
|
|
175
|
+
if not jobs:
|
|
176
|
+
return "No running job can be stopped."
|
|
177
|
+
return "Choose a job to stop."
|
|
178
|
+
job_id = tokens[1].strip()
|
|
179
|
+
project_name = effective_project_name_for_chat(ctx, message.chat_id)
|
|
180
|
+
existing = ctx.job_store.get(job_id)
|
|
181
|
+
if existing is not None and project_name and existing.request.project != project_name:
|
|
182
|
+
return f"Job not found: {job_id}"
|
|
183
|
+
if ctx.job_manager is None:
|
|
184
|
+
return "Job cancellation is not available."
|
|
185
|
+
success = ctx.job_manager.cancel(job_id)
|
|
186
|
+
if success:
|
|
187
|
+
return f"Stop requested: {job_id}"
|
|
188
|
+
job = ctx.job_store.get(job_id)
|
|
189
|
+
if not job:
|
|
190
|
+
return f"Job not found: {job_id}"
|
|
191
|
+
return f"Cannot stop job: {job_id} (current status: {job.status.value})"
|
|
192
|
+
|
|
193
|
+
def get_inline_buttons(
|
|
194
|
+
self,
|
|
195
|
+
message: TelegramMessage | None = None,
|
|
196
|
+
ctx: CommandContext | None = None,
|
|
197
|
+
) -> list[list[InlineButton]] | None:
|
|
198
|
+
if message is None or ctx is None:
|
|
199
|
+
return None
|
|
200
|
+
if len(message.text.strip().split()) != 1:
|
|
201
|
+
return None
|
|
202
|
+
jobs = self._list_cancellable_jobs(message, ctx)
|
|
203
|
+
if not jobs:
|
|
204
|
+
return None
|
|
205
|
+
return _button_rows(
|
|
206
|
+
[InlineButton(_job_button_label(job), f"/stop {job.id}") for job in jobs],
|
|
207
|
+
per_row=1,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
@staticmethod
|
|
211
|
+
def _list_cancellable_jobs(message: TelegramMessage, ctx: CommandContext) -> list[Job]:
|
|
212
|
+
project_name = effective_project_name_for_chat(ctx, message.chat_id)
|
|
213
|
+
if not project_name:
|
|
214
|
+
return []
|
|
215
|
+
return [
|
|
216
|
+
job
|
|
217
|
+
for job in ctx.job_store.list_recent_for_project_chat(
|
|
218
|
+
project_name, message.chat_id, 20
|
|
219
|
+
)
|
|
220
|
+
if job.status in {JobStatus.QUEUED, JobStatus.RUNNING}
|
|
221
|
+
]
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from app.jobs.schemas import FixKind, Job, JobMode, JobRequest
|
|
4
|
+
from app.telegram.commands.base import (
|
|
5
|
+
CommandContext,
|
|
6
|
+
ConfirmableCommand,
|
|
7
|
+
InlineButton,
|
|
8
|
+
TelegramMessage,
|
|
9
|
+
_button_rows,
|
|
10
|
+
_confirmation_buttons_enabled,
|
|
11
|
+
effective_project_name_for_chat,
|
|
12
|
+
format_usage,
|
|
13
|
+
)
|
|
14
|
+
from app.telegram.confirmations import PendingConfirmation
|
|
15
|
+
|
|
16
|
+
FIX_SOURCE_AWAIT_ACTION = "fix_source_await_instruction"
|
|
17
|
+
FIX_COMMIT_PENDING_ACTION = "fix_commit"
|
|
18
|
+
FIX_SOURCE_PENDING_ACTION = "fix_source"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _fix_job_button_label(job: Job) -> str:
|
|
22
|
+
short_hash = (job.commit_hash or "")[:8]
|
|
23
|
+
branch = job.branch or "-"
|
|
24
|
+
return f"{job.id} ({branch}) [{short_hash}]"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class FixCommand(ConfirmableCommand):
|
|
28
|
+
name = "/fix"
|
|
29
|
+
menu_text = "Choose what to fix."
|
|
30
|
+
description = "Re-do the commit or source of a previous job"
|
|
31
|
+
|
|
32
|
+
_MAX_CANDIDATES = 8
|
|
33
|
+
|
|
34
|
+
def execute(self, message: TelegramMessage, ctx: CommandContext) -> str:
|
|
35
|
+
tokens = message.text.strip().split()
|
|
36
|
+
if ctx.job_manager is None:
|
|
37
|
+
return "Fix feature is not available."
|
|
38
|
+
if len(tokens) == 1:
|
|
39
|
+
return "Choose what to fix."
|
|
40
|
+
if len(tokens) == 2:
|
|
41
|
+
kind = tokens[1].lower()
|
|
42
|
+
if kind not in {"commit", "source"}:
|
|
43
|
+
return format_usage("/fix", "/fix commit", "/fix source")
|
|
44
|
+
candidates = self._list_candidates(message, ctx)
|
|
45
|
+
if not candidates:
|
|
46
|
+
return "No job is available to fix."
|
|
47
|
+
return "Choose a job to fix."
|
|
48
|
+
if len(tokens) >= 3:
|
|
49
|
+
kind = tokens[1].lower()
|
|
50
|
+
if kind not in {"commit", "source"}:
|
|
51
|
+
return format_usage("/fix", "/fix commit", "/fix source")
|
|
52
|
+
job_id = tokens[2].strip()
|
|
53
|
+
project_name = effective_project_name_for_chat(ctx, message.chat_id)
|
|
54
|
+
if not project_name:
|
|
55
|
+
return "No project is registered."
|
|
56
|
+
target_job = ctx.job_store.get(job_id)
|
|
57
|
+
if target_job is None or not ctx.job_manager.is_fix_candidate(
|
|
58
|
+
target_job, project_name, message.chat_id
|
|
59
|
+
):
|
|
60
|
+
return f"Job cannot be used as a fix target: {job_id}"
|
|
61
|
+
if kind == "commit":
|
|
62
|
+
return self._start_commit_fix(message, ctx, target_job)
|
|
63
|
+
return self._start_source_fix(message, ctx, target_job)
|
|
64
|
+
return format_usage("/fix", "/fix commit", "/fix source")
|
|
65
|
+
|
|
66
|
+
def _start_commit_fix(
|
|
67
|
+
self, message: TelegramMessage, ctx: CommandContext, target_job: Job
|
|
68
|
+
) -> str:
|
|
69
|
+
assert ctx.job_manager is not None
|
|
70
|
+
prepared_message = ctx.job_manager.build_fix_commit_preview(target_job)
|
|
71
|
+
ctx.confirmation_store.set(
|
|
72
|
+
effective_project_name_for_chat(ctx, message.chat_id),
|
|
73
|
+
message.chat_id,
|
|
74
|
+
PendingConfirmation(
|
|
75
|
+
command_name=self.name,
|
|
76
|
+
action=FIX_COMMIT_PENDING_ACTION,
|
|
77
|
+
target_job_id=target_job.id,
|
|
78
|
+
prepared_payload=prepared_message,
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
lines = [
|
|
82
|
+
f"Commit message preview (Job {target_job.id}, branch {target_job.branch})",
|
|
83
|
+
"",
|
|
84
|
+
prepared_message,
|
|
85
|
+
"",
|
|
86
|
+
"Send y/Y to apply, or n/N to cancel (or use buttons).",
|
|
87
|
+
]
|
|
88
|
+
return "\n".join(lines)
|
|
89
|
+
|
|
90
|
+
def _start_source_fix(
|
|
91
|
+
self, message: TelegramMessage, ctx: CommandContext, target_job: Job
|
|
92
|
+
) -> str:
|
|
93
|
+
ctx.confirmation_store.set(
|
|
94
|
+
effective_project_name_for_chat(ctx, message.chat_id),
|
|
95
|
+
message.chat_id,
|
|
96
|
+
PendingConfirmation(
|
|
97
|
+
command_name=self.name,
|
|
98
|
+
action=FIX_SOURCE_AWAIT_ACTION,
|
|
99
|
+
target_job_id=target_job.id,
|
|
100
|
+
),
|
|
101
|
+
)
|
|
102
|
+
return (
|
|
103
|
+
f"Send the fix instruction for Job {target_job.id}. "
|
|
104
|
+
"The next message will be used as the instruction."
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def confirm(
|
|
108
|
+
self,
|
|
109
|
+
message: TelegramMessage,
|
|
110
|
+
ctx: CommandContext,
|
|
111
|
+
pending: PendingConfirmation,
|
|
112
|
+
) -> str:
|
|
113
|
+
if pending.action == FIX_COMMIT_PENDING_ACTION:
|
|
114
|
+
return self._confirm_commit(message, ctx, pending)
|
|
115
|
+
if pending.action == FIX_SOURCE_PENDING_ACTION:
|
|
116
|
+
return self._confirm_source(message, ctx, pending)
|
|
117
|
+
return "Could not process the pending confirmation."
|
|
118
|
+
|
|
119
|
+
def _confirm_commit(
|
|
120
|
+
self,
|
|
121
|
+
message: TelegramMessage,
|
|
122
|
+
ctx: CommandContext,
|
|
123
|
+
pending: PendingConfirmation,
|
|
124
|
+
) -> str:
|
|
125
|
+
if message.text.strip() not in {"y", "Y"}:
|
|
126
|
+
return "Cancelled the commit message fix."
|
|
127
|
+
if ctx.job_manager is None or not pending.target_job_id or not pending.prepared_payload:
|
|
128
|
+
return "Could not process the pending confirmation."
|
|
129
|
+
target_job = ctx.job_store.get(pending.target_job_id)
|
|
130
|
+
project_name = effective_project_name_for_chat(ctx, message.chat_id)
|
|
131
|
+
if target_job is None or not project_name or not ctx.job_manager.is_fix_candidate(
|
|
132
|
+
target_job, project_name, message.chat_id
|
|
133
|
+
):
|
|
134
|
+
return "Fix target job is no longer available."
|
|
135
|
+
request = JobRequest(
|
|
136
|
+
project=project_name,
|
|
137
|
+
model=target_job.request.model,
|
|
138
|
+
model_id=target_job.request.model_id,
|
|
139
|
+
instruction=target_job.request.instruction,
|
|
140
|
+
mode=JobMode.AGENT_FIX,
|
|
141
|
+
fix_kind=FixKind.COMMIT,
|
|
142
|
+
parent_job_id=target_job.id,
|
|
143
|
+
branch=target_job.branch,
|
|
144
|
+
chat_id=message.chat_id,
|
|
145
|
+
requested_by=message.user_id,
|
|
146
|
+
)
|
|
147
|
+
result_job = ctx.job_manager.execute_fix_job(
|
|
148
|
+
request, prepared_message=pending.prepared_payload
|
|
149
|
+
)
|
|
150
|
+
if result_job.status.value == "succeeded":
|
|
151
|
+
return (
|
|
152
|
+
f"Commit message updated.\n"
|
|
153
|
+
f"- Job: {result_job.id}\n"
|
|
154
|
+
f"- Branch: {result_job.branch}\n"
|
|
155
|
+
f"- New commit: {result_job.commit_hash}"
|
|
156
|
+
)
|
|
157
|
+
return f"Commit message fix failed: {result_job.error or 'unknown'}"
|
|
158
|
+
|
|
159
|
+
def _confirm_source(
|
|
160
|
+
self,
|
|
161
|
+
message: TelegramMessage,
|
|
162
|
+
ctx: CommandContext,
|
|
163
|
+
pending: PendingConfirmation,
|
|
164
|
+
) -> str:
|
|
165
|
+
# Source-mode confirmation is routed by the webhook (background task);
|
|
166
|
+
# see app/telegram/webhook.py for the actual execution.
|
|
167
|
+
_ = (message, ctx, pending)
|
|
168
|
+
return "Started the fix job in the background."
|
|
169
|
+
|
|
170
|
+
def get_inline_buttons(
|
|
171
|
+
self,
|
|
172
|
+
message: TelegramMessage | None = None,
|
|
173
|
+
ctx: CommandContext | None = None,
|
|
174
|
+
) -> list[list[InlineButton]] | None:
|
|
175
|
+
if message is None or ctx is None:
|
|
176
|
+
return None
|
|
177
|
+
tokens = message.text.strip().split()
|
|
178
|
+
if len(tokens) == 1:
|
|
179
|
+
return [
|
|
180
|
+
[
|
|
181
|
+
InlineButton("Fix commit", "/fix commit"),
|
|
182
|
+
InlineButton("Fix source", "/fix source"),
|
|
183
|
+
],
|
|
184
|
+
]
|
|
185
|
+
if len(tokens) == 2:
|
|
186
|
+
kind = tokens[1].lower()
|
|
187
|
+
if kind not in {"commit", "source"}:
|
|
188
|
+
return None
|
|
189
|
+
candidates = self._list_candidates(message, ctx)
|
|
190
|
+
if not candidates:
|
|
191
|
+
return None
|
|
192
|
+
return _button_rows(
|
|
193
|
+
[
|
|
194
|
+
InlineButton(_fix_job_button_label(job), f"/fix {kind} {job.id}")
|
|
195
|
+
for job in candidates
|
|
196
|
+
],
|
|
197
|
+
per_row=1,
|
|
198
|
+
)
|
|
199
|
+
if not _confirmation_buttons_enabled(ctx):
|
|
200
|
+
return None
|
|
201
|
+
pending = ctx.confirmation_store.get(
|
|
202
|
+
effective_project_name_for_chat(ctx, message.chat_id),
|
|
203
|
+
message.chat_id,
|
|
204
|
+
)
|
|
205
|
+
if pending is None or pending.command_name != self.name:
|
|
206
|
+
return None
|
|
207
|
+
if pending.action != FIX_COMMIT_PENDING_ACTION:
|
|
208
|
+
return None
|
|
209
|
+
return [[InlineButton("Yes", "Y"), InlineButton("No", "n")]]
|
|
210
|
+
|
|
211
|
+
def _list_candidates(self, message: TelegramMessage, ctx: CommandContext) -> list[Job]:
|
|
212
|
+
if ctx.job_manager is None:
|
|
213
|
+
return []
|
|
214
|
+
project_name = effective_project_name_for_chat(ctx, message.chat_id)
|
|
215
|
+
if not project_name:
|
|
216
|
+
return []
|
|
217
|
+
return ctx.job_manager.list_fix_candidates(
|
|
218
|
+
project_name, message.chat_id, limit=self._MAX_CANDIDATES
|
|
219
|
+
)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from app.ai.model_catalog import format_model_selection, get_model_options, is_valid_model_id
|
|
4
|
+
from app.models import ModelName
|
|
5
|
+
from app.telegram.commands.base import (
|
|
6
|
+
MODEL_USAGE,
|
|
7
|
+
CommandContext,
|
|
8
|
+
InlineButton,
|
|
9
|
+
TelegramCommand,
|
|
10
|
+
TelegramMessage,
|
|
11
|
+
_button_rows,
|
|
12
|
+
effective_model_selection_for_chat,
|
|
13
|
+
effective_project_name_for_chat,
|
|
14
|
+
format_usage,
|
|
15
|
+
)
|
|
16
|
+
from app.telegram.i18n import ui_message
|
|
17
|
+
from app.telegram.model_preferences import ModelPreference
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ModelCommand(TelegramCommand):
|
|
21
|
+
name = "/model"
|
|
22
|
+
menu_text = ui_message("model.menu", "Choose a model.")
|
|
23
|
+
description = "Show or change this chat's default AI model"
|
|
24
|
+
|
|
25
|
+
def execute(self, message: TelegramMessage, ctx: CommandContext) -> str:
|
|
26
|
+
tokens = message.text.strip().split()
|
|
27
|
+
project_name = effective_project_name_for_chat(ctx, message.chat_id)
|
|
28
|
+
current = effective_model_selection_for_chat(ctx, message.chat_id, project_name)
|
|
29
|
+
if len(tokens) == 1:
|
|
30
|
+
return ui_message(
|
|
31
|
+
"model.settings",
|
|
32
|
+
"Model settings\n\n- Current default model: {selection}",
|
|
33
|
+
selection=format_model_selection(current.provider, current.model_id),
|
|
34
|
+
)
|
|
35
|
+
if len(tokens) == 2 and tokens[1] in {model.value for model in ModelName}:
|
|
36
|
+
selected = ModelName(tokens[1])
|
|
37
|
+
ctx.model_preferences.set(project_name, message.chat_id, selected)
|
|
38
|
+
return ui_message(
|
|
39
|
+
"model.provider_selected",
|
|
40
|
+
"Model provider selected.\n\n- Default model: {provider}\n- Choose a specific model.",
|
|
41
|
+
provider=selected.value,
|
|
42
|
+
)
|
|
43
|
+
if len(tokens) == 3 and tokens[1] in {model.value for model in ModelName}:
|
|
44
|
+
selected = ModelName(tokens[1])
|
|
45
|
+
model_id = tokens[2]
|
|
46
|
+
if not is_valid_model_id(selected, model_id):
|
|
47
|
+
usage = format_usage(
|
|
48
|
+
"/model",
|
|
49
|
+
f"/model {MODEL_USAGE}",
|
|
50
|
+
f"/model {MODEL_USAGE} <model_id>",
|
|
51
|
+
)
|
|
52
|
+
return ui_message(
|
|
53
|
+
"model.unknown_specific",
|
|
54
|
+
"Unknown specific model: {model_id}\n\n{usage}",
|
|
55
|
+
model_id=model_id,
|
|
56
|
+
usage=usage,
|
|
57
|
+
)
|
|
58
|
+
ctx.model_preferences.set_selection(
|
|
59
|
+
project_name,
|
|
60
|
+
message.chat_id,
|
|
61
|
+
ModelPreference(selected, model_id),
|
|
62
|
+
)
|
|
63
|
+
return ui_message(
|
|
64
|
+
"model.updated",
|
|
65
|
+
"Model setting updated.\n\n- Default model: {selection}",
|
|
66
|
+
selection=format_model_selection(selected, model_id),
|
|
67
|
+
)
|
|
68
|
+
return format_usage("/model", f"/model {MODEL_USAGE}", f"/model {MODEL_USAGE} <model_id>")
|
|
69
|
+
|
|
70
|
+
def get_inline_buttons(
|
|
71
|
+
self,
|
|
72
|
+
message: TelegramMessage | None = None,
|
|
73
|
+
ctx: CommandContext | None = None,
|
|
74
|
+
) -> list[list[InlineButton]] | None:
|
|
75
|
+
tokens = message.text.strip().split() if message is not None else []
|
|
76
|
+
if len(tokens) <= 1:
|
|
77
|
+
return [
|
|
78
|
+
[
|
|
79
|
+
InlineButton("claude", "/model claude"),
|
|
80
|
+
InlineButton("codex", "/model codex"),
|
|
81
|
+
InlineButton("gemini", "/model gemini"),
|
|
82
|
+
]
|
|
83
|
+
]
|
|
84
|
+
if len(tokens) == 2 and tokens[1] in {model.value for model in ModelName}:
|
|
85
|
+
provider = ModelName(tokens[1])
|
|
86
|
+
return _button_rows(
|
|
87
|
+
[
|
|
88
|
+
InlineButton(option.label, f"/model {provider.value} {option.value}")
|
|
89
|
+
for option in get_model_options(provider)
|
|
90
|
+
],
|
|
91
|
+
per_row=1,
|
|
92
|
+
)
|
|
93
|
+
return None
|