@pixelbyte-software/pixcode 1.53.4 → 1.53.6

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.
Files changed (31) hide show
  1. package/dist/assets/{index-DlwfTw9d.js → index-DP7jnMyc.js} +155 -155
  2. package/dist/index.html +1 -1
  3. package/dist-server/server/database/db.js +28 -0
  4. package/dist-server/server/database/db.js.map +1 -1
  5. package/dist-server/server/openai-codex.js +5 -0
  6. package/dist-server/server/openai-codex.js.map +1 -1
  7. package/dist-server/server/routes/agent.js +17 -7
  8. package/dist-server/server/routes/agent.js.map +1 -1
  9. package/dist-server/server/routes/telegram.js +16 -2
  10. package/dist-server/server/routes/telegram.js.map +1 -1
  11. package/dist-server/server/services/telegram/bot.js +9 -4
  12. package/dist-server/server/services/telegram/bot.js.map +1 -1
  13. package/dist-server/server/services/telegram/control-center.js +914 -98
  14. package/dist-server/server/services/telegram/control-center.js.map +1 -1
  15. package/dist-server/server/services/telegram/telegram-gateway.js +286 -0
  16. package/dist-server/server/services/telegram/telegram-gateway.js.map +1 -0
  17. package/dist-server/server/services/telegram/telegram-http-client.js +10 -8
  18. package/dist-server/server/services/telegram/telegram-http-client.js.map +1 -1
  19. package/dist-server/server/services/telegram/translations.js +72 -0
  20. package/dist-server/server/services/telegram/translations.js.map +1 -1
  21. package/package.json +1 -1
  22. package/scripts/smoke/telegram-control.mjs +50 -2
  23. package/server/database/db.js +28 -0
  24. package/server/openai-codex.js +5 -0
  25. package/server/routes/agent.js +17 -7
  26. package/server/routes/telegram.js +25 -2
  27. package/server/services/telegram/bot.js +8 -4
  28. package/server/services/telegram/control-center.js +927 -98
  29. package/server/services/telegram/telegram-gateway.js +314 -0
  30. package/server/services/telegram/telegram-http-client.js +7 -5
  31. package/server/services/telegram/translations.js +72 -0
@@ -4,13 +4,32 @@ import { apiKeysDb, telegramLinksDb } from '../../database/db.js';
4
4
  import { getProjects } from '../../projects.js';
5
5
  import { getStaticProviderModels } from '../model-registry.js';
6
6
 
7
+ import {
8
+ TELEGRAM_CONTROL_SCOPES,
9
+ TELEGRAM_PROVIDERS,
10
+ buildTelegramAgentPrompt,
11
+ buildTelegramIntentPrompt,
12
+ clearTelegramConfirmation,
13
+ consumeTelegramConfirmation,
14
+ createTelegramConfirmation,
15
+ enqueueTelegramJob,
16
+ parseTelegramAiIntentResponse,
17
+ resolveTelegramModel,
18
+ resolveTelegramProvider,
19
+ retryWithBackoff,
20
+ runTelegramTool,
21
+ splitTelegramText,
22
+ } from './telegram-gateway.js';
7
23
  import { SUPPORTED_LANGUAGES, t } from './translations.js';
8
24
 
9
- const PROVIDERS = ['claude', 'cursor', 'codex', 'gemini', 'qwen', 'opencode'];
25
+ const PROVIDERS = TELEGRAM_PROVIDERS;
10
26
  const TERMINAL_RUN_STATES = new Set(['completed', 'failed', 'canceled']);
11
27
  const CALLBACK_TTL_MS = 10 * 60 * 1000;
12
28
  const MAX_CALLBACK_ACTIONS = 1000;
13
29
  const MAX_TELEGRAM_TEXT = 3600;
30
+ const ACTIVITY_EDIT_THROTTLE_MS = 1200;
31
+ const ACTIVITY_HEARTBEAT_MS = 8000;
32
+ const INTENT_ROUTER_TIMEOUT_MS = 45_000;
14
33
  const callbackActions = new Map();
15
34
  const runMonitors = new Map();
16
35
 
@@ -130,7 +149,11 @@ function readAction(data) {
130
149
  callbackActions.delete(id);
131
150
  return null;
132
151
  }
133
- return entry;
152
+ return { id, ...entry };
153
+ }
154
+
155
+ function forgetAction(id) {
156
+ if (id) callbackActions.delete(id);
134
157
  }
135
158
 
136
159
  function button(text, action, payload = {}) {
@@ -150,27 +173,44 @@ async function send(bot, chatId, text, options = {}) {
150
173
  disable_web_page_preview: true,
151
174
  ...telegramOptions,
152
175
  };
153
- const messageText = truncate(text);
176
+ const chunks = splitTelegramText(text, MAX_TELEGRAM_TEXT);
177
+ const [firstChunk = ''] = chunks;
178
+ let startIndex = 0;
154
179
  if (editMessageId && typeof bot.editMessageText === 'function') {
155
180
  try {
156
- return await bot.editMessageText(messageText, {
181
+ const edited = await bot.editMessageText(firstChunk, {
157
182
  chat_id: chatId,
158
183
  message_id: editMessageId,
159
184
  ...extra,
160
185
  });
186
+ startIndex = 1;
187
+ for (const chunk of chunks.slice(startIndex)) {
188
+ await send(bot, chatId, chunk, telegramOptions);
189
+ }
190
+ return edited;
161
191
  } catch (err) {
162
192
  const description = err?.response?.body?.description || err?.message || '';
163
- if (/message is not modified/i.test(description)) return null;
193
+ if (/message is not modified/i.test(description)) {
194
+ startIndex = 1;
195
+ for (const chunk of chunks.slice(startIndex)) {
196
+ await send(bot, chatId, chunk, telegramOptions);
197
+ }
198
+ return null;
199
+ }
164
200
  console.warn('[telegram-control] editMessageText failed:', description || err);
165
201
  }
166
202
  }
167
- try {
168
- return await bot.sendMessage(chatId, messageText, extra);
169
- } catch {
170
- const fallback = { ...extra };
171
- delete fallback.parse_mode;
172
- return bot.sendMessage(chatId, messageText, fallback);
203
+ let result = null;
204
+ for (const chunk of chunks.slice(startIndex)) {
205
+ try {
206
+ result = await bot.sendMessage(chatId, chunk, extra);
207
+ } catch {
208
+ const fallback = { ...extra };
209
+ delete fallback.parse_mode;
210
+ result = await bot.sendMessage(chatId, chunk, fallback);
211
+ }
173
212
  }
213
+ return result;
174
214
  }
175
215
 
176
216
  function localApiBase() {
@@ -182,32 +222,242 @@ function getOrCreateTelegramApiKey(userId) {
182
222
  const existing = apiKeysDb
183
223
  .getApiKeys(userId)
184
224
  .find((key) => key.key_name === 'Telegram Control' && Boolean(key.is_active));
185
- if (existing?.api_key) return existing.api_key;
186
- return apiKeysDb.createApiKey(userId, 'Telegram Control').apiKey;
225
+ if (existing?.api_key) {
226
+ const existingScopes = apiKeysDb.normalizeScopes(existing.scopes || []);
227
+ const nextScopes = apiKeysDb.normalizeScopes([...existingScopes, ...TELEGRAM_CONTROL_SCOPES]);
228
+ const hasAllScopes = TELEGRAM_CONTROL_SCOPES.every((scope) => existingScopes.includes(scope));
229
+ if (!hasAllScopes || nextScopes.length !== existingScopes.length) {
230
+ apiKeysDb.updateApiKeyScopes(userId, existing.id, nextScopes);
231
+ }
232
+ return existing.api_key;
233
+ }
234
+ return apiKeysDb.createApiKey(userId, 'Telegram Control', TELEGRAM_CONTROL_SCOPES).apiKey;
187
235
  }
188
236
 
189
- async function localApi(userId, path, { method = 'GET', body } = {}) {
237
+ async function localApi(userId, path, { method = 'GET', body, timeoutMs = 0 } = {}) {
190
238
  const apiKey = getOrCreateTelegramApiKey(userId);
191
- const response = await fetch(`${localApiBase()}${path}`, {
192
- method,
239
+ const response = await retryWithBackoff(async () => {
240
+ const controller = timeoutMs > 0 ? new AbortController() : null;
241
+ const timeoutId = controller
242
+ ? setTimeout(() => controller.abort(new Error(`HTTP request timed out after ${timeoutMs}ms`)), timeoutMs)
243
+ : null;
244
+ const res = await fetch(`${localApiBase()}${path}`, {
245
+ method,
246
+ headers: {
247
+ 'Content-Type': 'application/json',
248
+ 'X-API-Key': apiKey,
249
+ },
250
+ signal: controller?.signal,
251
+ body: body === undefined ? undefined : JSON.stringify(body),
252
+ }).finally(() => {
253
+ if (timeoutId) clearTimeout(timeoutId);
254
+ });
255
+ if ([408, 429, 500, 502, 503, 504].includes(res.status)) {
256
+ const error = new Error(`HTTP ${res.status}`);
257
+ error.status = res.status;
258
+ throw error;
259
+ }
260
+ return res;
261
+ });
262
+ const data = await response.json().catch(() => ({}));
263
+ if (!response.ok) {
264
+ const error = data?.error?.message || data?.error || `HTTP ${response.status}`;
265
+ throw new Error(typeof error === 'string' ? error : JSON.stringify(error));
266
+ }
267
+ return data?.data ?? data;
268
+ }
269
+
270
+ async function localAgentStream(userId, body, onEvent) {
271
+ const apiKey = getOrCreateTelegramApiKey(userId);
272
+ const response = await fetch(`${localApiBase()}/api/agent`, {
273
+ method: 'POST',
193
274
  headers: {
194
275
  'Content-Type': 'application/json',
195
276
  'X-API-Key': apiKey,
196
277
  },
197
- body: body === undefined ? undefined : JSON.stringify(body),
278
+ body: JSON.stringify({
279
+ ...body,
280
+ stream: true,
281
+ }),
198
282
  });
199
- const data = await response.json().catch(() => ({}));
283
+
200
284
  if (!response.ok) {
285
+ const data = await response.json().catch(() => ({}));
201
286
  const error = data?.error?.message || data?.error || `HTTP ${response.status}`;
202
287
  throw new Error(typeof error === 'string' ? error : JSON.stringify(error));
203
288
  }
204
- return data?.data ?? data;
289
+
290
+ if (!response.body) return;
291
+
292
+ const reader = response.body.getReader();
293
+ const decoder = new TextDecoder();
294
+ let buffer = '';
295
+
296
+ const consumeBlock = async (block) => {
297
+ const lines = String(block || '').split('\n');
298
+ for (const line of lines) {
299
+ if (!line.startsWith('data:')) continue;
300
+ const payload = line.slice(5).trim();
301
+ if (!payload) continue;
302
+ let event;
303
+ try {
304
+ event = JSON.parse(payload);
305
+ } catch {
306
+ continue;
307
+ }
308
+ await onEvent?.(event);
309
+ }
310
+ };
311
+
312
+ for (;;) {
313
+ const { done, value } = await reader.read();
314
+ if (done) break;
315
+ buffer += decoder.decode(value, { stream: true });
316
+ let boundary = buffer.indexOf('\n\n');
317
+ while (boundary !== -1) {
318
+ const block = buffer.slice(0, boundary);
319
+ buffer = buffer.slice(boundary + 2);
320
+ await consumeBlock(block);
321
+ boundary = buffer.indexOf('\n\n');
322
+ }
323
+ }
324
+
325
+ const rest = `${buffer}${decoder.decode()}`;
326
+ if (rest.trim()) await consumeBlock(rest);
205
327
  }
206
328
 
207
329
  function checked(label) {
208
330
  return `${label} ✓`;
209
331
  }
210
332
 
333
+ function formatElapsed(startedAt) {
334
+ const seconds = Math.max(0, Math.round((Date.now() - startedAt) / 1000));
335
+ if (seconds < 60) return `${seconds}s`;
336
+ const minutes = Math.floor(seconds / 60);
337
+ const rest = seconds % 60;
338
+ return `${minutes}m ${rest}s`;
339
+ }
340
+
341
+ function projectLabel(state) {
342
+ return state.selectedProjectName || state.selectedProjectPath || '-';
343
+ }
344
+
345
+ function createActivityState({ lang, type = 'agent', provider, project, prompt, mode = 'final' }) {
346
+ return {
347
+ lang,
348
+ type,
349
+ status: 'starting',
350
+ phase: t(lang, 'control.activity.starting'),
351
+ provider,
352
+ project,
353
+ prompt: compact(prompt, 120),
354
+ mode,
355
+ startedAt: Date.now(),
356
+ lastEditAt: 0,
357
+ messageId: null,
358
+ sessionId: null,
359
+ runId: null,
360
+ workflowId: null,
361
+ events: [],
362
+ output: '',
363
+ error: null,
364
+ };
365
+ }
366
+
367
+ function pushActivityEvent(activity, text) {
368
+ const value = String(text || '').trim();
369
+ if (!value) return;
370
+ if (activity.events.at(-1) === value) return;
371
+ activity.events.push(value);
372
+ if (activity.events.length > 8) activity.events.splice(0, activity.events.length - 8);
373
+ }
374
+
375
+ function activityTitle(activity) {
376
+ if (activity.type === 'workflow') return t(activity.lang, 'control.activity.workflowTitle');
377
+ if (activity.type === 'router') return t(activity.lang, 'control.activity.routerTitle');
378
+ return t(activity.lang, 'control.activity.agentTitle');
379
+ }
380
+
381
+ function renderActivity(activity, { finalText = null } = {}) {
382
+ const output = finalText || activity.output;
383
+ const lines = [
384
+ `${activity.status === 'failed' ? '❌' : activity.status === 'done' ? '✅' : activity.status === 'running' ? '🔧' : '⏳'} ${activityTitle(activity)}`,
385
+ '',
386
+ `🤖 ${t(activity.lang, 'control.activity.provider')}: ${activity.provider || '-'}`,
387
+ `📁 ${t(activity.lang, 'control.activity.project')}: ${compact(activity.project, 90)}`,
388
+ ];
389
+
390
+ if (activity.sessionId) lines.push(`🧵 ${t(activity.lang, 'control.activity.session')}: ${activity.sessionId}`);
391
+ if (activity.runId) lines.push(`🧭 ${t(activity.lang, 'control.activity.run')}: ${activity.runId}`);
392
+ if (activity.workflowId) lines.push(`🧩 ${t(activity.lang, 'control.activity.workflow')}: ${activity.workflowId}`);
393
+ lines.push(`⏱ ${t(activity.lang, 'control.activity.elapsed')}: ${formatElapsed(activity.startedAt)}`);
394
+ lines.push(`📌 ${t(activity.lang, 'control.activity.status')}: ${activity.phase}`);
395
+
396
+ const visibleEvents = activity.mode === 'final'
397
+ ? activity.events.slice(-4)
398
+ : activity.events;
399
+ if (visibleEvents.length > 0) {
400
+ lines.push('', `🛠 ${t(activity.lang, 'control.activity.work')}:`);
401
+ for (const event of visibleEvents) lines.push(`• ${event}`);
402
+ }
403
+
404
+ if (activity.error) {
405
+ lines.push('', `⚠️ ${truncate(activity.error, 700)}`);
406
+ } else if (output) {
407
+ lines.push('', `💬 ${t(activity.lang, 'control.activity.output')}:`);
408
+ lines.push(truncate(output, 1800));
409
+ } else if (activity.prompt) {
410
+ lines.push('', `📝 ${compact(activity.prompt, 220)}`);
411
+ }
412
+
413
+ return truncate(lines.join('\n'), 3400);
414
+ }
415
+
416
+ async function createTelegramActivity({ bot, chatId, link, type = 'agent', prompt = '', phase = null }) {
417
+ const lang = languageFor(link);
418
+ const state = getState(link.user_id);
419
+ const activity = createActivityState({
420
+ lang,
421
+ type,
422
+ provider: resolveTelegramProvider(state),
423
+ project: projectLabel(state),
424
+ prompt,
425
+ mode: state.progressMode,
426
+ });
427
+ if (phase) activity.phase = phase;
428
+ const sent = await send(bot, chatId, renderActivity(activity), { parse_mode: undefined });
429
+ activity.messageId = sent?.message_id || sent?.message?.message_id || null;
430
+ activity.lastEditAt = Date.now();
431
+ return activity;
432
+ }
433
+
434
+ async function editTelegramActivity({ bot, chatId, activity, force = false, reply_markup }) {
435
+ if (!activity) return null;
436
+ const now = Date.now();
437
+ if (!force && now - activity.lastEditAt < ACTIVITY_EDIT_THROTTLE_MS) return null;
438
+ const sent = await send(bot, chatId, renderActivity(activity), {
439
+ editMessageId: activity.messageId,
440
+ parse_mode: undefined,
441
+ reply_markup,
442
+ });
443
+ activity.messageId = sent?.message_id || sent?.message?.message_id || activity.messageId;
444
+ activity.lastEditAt = now;
445
+ return sent;
446
+ }
447
+
448
+ function startActivityHeartbeat({ bot, chatId, activity }) {
449
+ return setInterval(() => {
450
+ if (!activity || activity.status === 'done' || activity.status === 'failed') return;
451
+ if (activity.status === 'starting') {
452
+ activity.status = 'running';
453
+ activity.phase = t(activity.lang, 'control.activity.thinking');
454
+ }
455
+ editTelegramActivity({ bot, chatId, activity }).catch((error) => {
456
+ console.warn('[telegram-control] activity heartbeat failed:', error?.message || error);
457
+ });
458
+ }, ACTIVITY_HEARTBEAT_MS);
459
+ }
460
+
211
461
  function stateSummary(lang, state) {
212
462
  return [
213
463
  `${t(lang, 'control.summary.project')}: ${state.selectedProjectName || t(lang, 'control.notSelected')}`,
@@ -258,11 +508,12 @@ async function listProjects() {
258
508
  return projects.slice(0, 20);
259
509
  }
260
510
 
261
- async function showProjectMenu({ bot, chatId, link, editMessageId }) {
511
+ async function showProjectMenu({ bot, chatId, link, editMessageId, notice }) {
262
512
  const lang = languageFor(link);
263
513
  const projects = await listProjects();
264
514
  if (projects.length === 0) {
265
- await send(bot, chatId, t(lang, 'control.noProjects'), { editMessageId });
515
+ const prefix = notice ? `${notice}\n\n` : '';
516
+ await send(bot, chatId, `${prefix}${t(lang, 'control.noProjects')}`, { editMessageId });
266
517
  return;
267
518
  }
268
519
 
@@ -275,7 +526,8 @@ async function showProjectMenu({ bot, chatId, link, editMessageId }) {
275
526
  displayName: project.displayName || project.name,
276
527
  },
277
528
  ));
278
- await send(bot, chatId, t(lang, 'control.pickProject'), {
529
+ const prefix = notice ? `${notice}\n\n` : '';
530
+ await send(bot, chatId, `${prefix}${t(lang, 'control.pickProject')}`, {
279
531
  editMessageId,
280
532
  reply_markup: { inline_keyboard: rows(buttons, 1) },
281
533
  });
@@ -487,89 +739,536 @@ function extractAssistantText(response) {
487
739
  return chunks.join('\n\n').trim();
488
740
  }
489
741
 
490
- async function runAgent({ bot, chatId, link, prompt }) {
742
+ function extractTextFromEvent(event) {
743
+ if (!event || typeof event !== 'object') return '';
744
+ if (typeof event.content === 'string') return event.content;
745
+ if (Array.isArray(event.content)) {
746
+ return event.content
747
+ .map((part) => (typeof part === 'string' ? part : part?.text || ''))
748
+ .join('');
749
+ }
750
+ const legacyContent = event.data?.message?.content || event.message?.content;
751
+ if (Array.isArray(legacyContent)) {
752
+ return legacyContent
753
+ .map((part) => (typeof part === 'string' ? part : part?.text || ''))
754
+ .join('');
755
+ }
756
+ if (typeof legacyContent === 'string') return legacyContent;
757
+ return '';
758
+ }
759
+
760
+ function describeToolInput(toolName, input) {
761
+ if (!input || typeof input !== 'object') return '';
762
+ if (toolName === 'Bash' && typeof input.command === 'string') {
763
+ return compact(input.command, 110);
764
+ }
765
+ if (toolName === 'WebSearch' && typeof input.query === 'string') {
766
+ return compact(input.query, 110);
767
+ }
768
+ if (toolName === 'FileChanges') {
769
+ return compact(JSON.stringify(input), 110);
770
+ }
771
+ const keys = Object.keys(input).slice(0, 3);
772
+ if (keys.length === 0) return '';
773
+ return compact(keys.map((key) => `${key}: ${String(input[key])}`).join(', '), 110);
774
+ }
775
+
776
+ function applyAgentStreamEvent(activity, event) {
777
+ if (!event || typeof event !== 'object') return;
778
+
779
+ const sessionId = event.actualSessionId || event.newSessionId || event.sessionId || event.threadId;
780
+ if (sessionId) activity.sessionId = sessionId;
781
+
782
+ if (event.type === 'status' && event.message) {
783
+ activity.status = 'running';
784
+ activity.phase = compact(event.message, 120);
785
+ pushActivityEvent(activity, `⏳ ${compact(event.message, 120)}`);
786
+ return;
787
+ }
788
+ if (event.type === 'done' || event.kind === 'complete' || event.kind === 'stream_end') {
789
+ activity.status = 'done';
790
+ activity.phase = t(activity.lang, 'control.activity.done');
791
+ return;
792
+ }
793
+ if (event.type === 'error' || event.kind === 'error') {
794
+ activity.status = 'failed';
795
+ activity.phase = t(activity.lang, 'control.activity.failed');
796
+ activity.error = event.error || event.message || event.content || t(activity.lang, 'error.generic');
797
+ pushActivityEvent(activity, `❌ ${compact(activity.error, 120)}`);
798
+ return;
799
+ }
800
+ if (event.kind === 'session_created') {
801
+ activity.status = 'running';
802
+ activity.phase = t(activity.lang, 'control.activity.sessionStarted');
803
+ pushActivityEvent(activity, `🧵 ${t(activity.lang, 'control.activity.sessionStarted')}`);
804
+ return;
805
+ }
806
+ if (event.kind === 'thinking') {
807
+ activity.status = 'running';
808
+ activity.phase = t(activity.lang, 'control.activity.thinking');
809
+ const thought = extractTextFromEvent(event);
810
+ if (thought) pushActivityEvent(activity, `🧠 ${compact(thought, 120)}`);
811
+ return;
812
+ }
813
+ if (event.kind === 'stream_delta' || event.kind === 'text') {
814
+ activity.status = 'running';
815
+ activity.phase = t(activity.lang, 'control.activity.responding');
816
+ activity.output = `${activity.output}${extractTextFromEvent(event)}`;
817
+ return;
818
+ }
819
+ if (event.type === 'claude-response' && event.data?.type === 'assistant') {
820
+ activity.status = 'running';
821
+ activity.phase = t(activity.lang, 'control.activity.responding');
822
+ activity.output = `${activity.output}${extractTextFromEvent(event)}`;
823
+ return;
824
+ }
825
+ if (event.kind === 'tool_use') {
826
+ activity.status = 'running';
827
+ activity.phase = t(activity.lang, 'control.activity.working');
828
+ const toolName = event.toolName || 'Tool';
829
+ const input = describeToolInput(toolName, event.toolInput);
830
+ const icon = toolName === 'Bash'
831
+ ? '💻'
832
+ : toolName === 'FileChanges'
833
+ ? '📝'
834
+ : toolName === 'WebSearch'
835
+ ? '🔎'
836
+ : '🔧';
837
+ pushActivityEvent(activity, `${icon} ${toolName}${input ? `: ${input}` : ''}`);
838
+ return;
839
+ }
840
+ if (event.kind === 'tool_result') {
841
+ activity.status = 'running';
842
+ activity.phase = t(activity.lang, 'control.activity.working');
843
+ const label = event.isError
844
+ ? t(activity.lang, 'control.activity.toolFailed')
845
+ : t(activity.lang, 'control.activity.toolDone');
846
+ pushActivityEvent(activity, `${event.isError ? '⚠️' : '✅'} ${label}`);
847
+ return;
848
+ }
849
+ if (event.kind === 'status' && event.text === 'token_budget') {
850
+ const used = event.tokenBudget?.used;
851
+ if (used) pushActivityEvent(activity, `📊 ${t(activity.lang, 'control.activity.tokens')}: ${used}`);
852
+ }
853
+ }
854
+
855
+ function confirmationLabel(lang, action, payload = {}) {
856
+ if (action === 'install_provider') {
857
+ return t(lang, 'control.confirmInstall', { provider: payload.provider || '' });
858
+ }
859
+ if (action === 'run_cancel') {
860
+ return t(lang, 'control.confirmCancelRun', { runId: payload.runId || '' });
861
+ }
862
+ return t(lang, 'control.confirmationRequired');
863
+ }
864
+
865
+ async function requestConfirmation({ bot, chatId, link, action, payload = {}, editMessageId }) {
866
+ const lang = languageFor(link);
867
+ const pending = createTelegramConfirmation(link.user_id, action, payload);
868
+ await send(bot, chatId, confirmationLabel(lang, action, payload), {
869
+ editMessageId,
870
+ reply_markup: {
871
+ inline_keyboard: [[
872
+ button(t(lang, 'control.button.confirm'), 'confirm_action', { id: pending.id }),
873
+ button(t(lang, 'control.button.cancel'), 'cancel_confirmation', { id: pending.id }),
874
+ ]],
875
+ },
876
+ });
877
+ return { ok: false, requiresConfirmation: true };
878
+ }
879
+
880
+ async function sendToolFailure({ bot, chatId, link, result, editMessageId }) {
881
+ const lang = languageFor(link);
882
+ const message = result?.message === 'REMOTE_CONTROL_DISABLED'
883
+ ? t(lang, 'control.disabled')
884
+ : (result?.message || t(lang, 'error.generic'));
885
+ await send(bot, chatId, message, { editMessageId });
886
+ }
887
+
888
+ async function runAgent({ bot, chatId, link, prompt, activity = null }) {
491
889
  const lang = languageFor(link);
492
890
  const state = getState(link.user_id);
493
891
  if (!state.remoteControlEnabled) {
494
- await send(bot, chatId, t(lang, 'control.disabled'));
892
+ await send(bot, chatId, t(lang, 'control.disabled'), { editMessageId: activity?.messageId });
495
893
  return;
496
894
  }
497
895
  if (!state.selectedProjectPath) {
498
- await send(bot, chatId, t(lang, 'control.selectProjectFirst'));
499
- await showProjectMenu({ bot, chatId, link });
896
+ await showProjectMenu({ bot, chatId, link, editMessageId: activity?.messageId, notice: t(lang, 'control.selectProjectFirst') });
500
897
  return;
501
898
  }
502
899
 
503
- if (state.progressMode !== 'final') {
504
- await send(bot, chatId, t(lang, 'control.agentStarted', {
505
- provider: state.selectedProvider,
506
- project: state.selectedProjectName || state.selectedProjectPath,
507
- }));
508
- }
509
-
510
- const response = await localApi(link.user_id, '/api/agent', {
511
- method: 'POST',
512
- body: {
900
+ const provider = resolveTelegramProvider(state);
901
+ const model = resolveTelegramModel(state);
902
+ const active = activity || await createTelegramActivity({
903
+ bot,
904
+ chatId,
905
+ link,
906
+ type: 'agent',
907
+ prompt,
908
+ phase: t(lang, 'control.activity.startingProvider', { provider }),
909
+ });
910
+ active.type = 'agent';
911
+ active.provider = provider;
912
+ active.project = projectLabel(state);
913
+ active.status = 'running';
914
+ active.phase = t(lang, 'control.activity.startingProvider', { provider });
915
+ await editTelegramActivity({ bot, chatId, activity: active, force: true });
916
+
917
+ const heartbeat = startActivityHeartbeat({ bot, chatId, activity: active });
918
+ let streamFailed = null;
919
+ try {
920
+ await localAgentStream(link.user_id, {
513
921
  projectPath: state.selectedProjectPath,
514
- provider: state.selectedProvider,
515
- model: state.selectedModel || undefined,
516
- message: prompt,
922
+ provider,
923
+ model: model || undefined,
924
+ message: buildTelegramAgentPrompt(prompt, state),
517
925
  cleanup: false,
518
- stream: false,
519
- },
520
- });
521
- const assistantText = extractAssistantText(response);
522
- const statusLine = response.success === false
523
- ? t(lang, 'control.agentFailed', { error: response.error || 'provider returned no answer' })
524
- : t(lang, 'control.agentDone', { provider: state.selectedProvider, session: response.sessionId ? ` (${response.sessionId})` : '' });
525
- await send(bot, chatId, `${statusLine}\n\n${assistantText || response.error || t(lang, 'control.noAssistantText')}`);
926
+ permissionMode: 'default',
927
+ }, async (event) => {
928
+ applyAgentStreamEvent(active, event);
929
+ await editTelegramActivity({ bot, chatId, activity: active });
930
+ });
931
+ } catch (error) {
932
+ streamFailed = error;
933
+ } finally {
934
+ clearInterval(heartbeat);
935
+ }
936
+
937
+ if (streamFailed) {
938
+ active.status = 'failed';
939
+ active.phase = t(lang, 'control.activity.failed');
940
+ active.error = streamFailed.message || t(lang, 'error.generic');
941
+ await editTelegramActivity({ bot, chatId, activity: active, force: true });
942
+ return;
943
+ }
944
+
945
+ active.status = active.error ? 'failed' : 'done';
946
+ active.phase = active.error ? t(lang, 'control.activity.failed') : t(lang, 'control.activity.done');
947
+ if (!active.output && !active.error) active.output = t(lang, 'control.noAssistantText');
948
+ await editTelegramActivity({ bot, chatId, activity: active, force: true });
526
949
  }
527
950
 
528
- export async function runWorkflow({ bot, chatId, link, input }) {
951
+ export async function runWorkflow({ bot, chatId, link, input, activity = null }) {
529
952
  const lang = languageFor(link);
530
953
  const state = getState(link.user_id);
531
954
  const workflowId = state.selectedWorkflowId;
955
+ if (!state.remoteControlEnabled) {
956
+ await send(bot, chatId, t(lang, 'control.disabled'), { editMessageId: activity?.messageId });
957
+ return;
958
+ }
532
959
  if (!workflowId) {
533
- await send(bot, chatId, t(lang, 'control.selectWorkflowFirst'));
534
- await showWorkflowMenu({ bot, chatId, link });
960
+ await send(bot, chatId, t(lang, 'control.selectWorkflowFirst'), { editMessageId: activity?.messageId });
961
+ await showWorkflowMenu({ bot, chatId, link, editMessageId: activity?.messageId });
535
962
  return;
536
963
  }
537
964
  if (!state.selectedProjectPath) {
538
- await send(bot, chatId, t(lang, 'control.selectProjectFirst'));
539
- await showProjectMenu({ bot, chatId, link });
965
+ await showProjectMenu({ bot, chatId, link, editMessageId: activity?.messageId, notice: t(lang, 'control.selectProjectFirst') });
540
966
  return;
541
967
  }
542
968
 
543
- const run = await localApi(link.user_id, `/api/orchestration/workflows/${workflowId}/runs`, {
544
- method: 'POST',
545
- body: {
546
- input,
547
- metadata: {
548
- projectId: state.selectedProjectName,
549
- projectName: state.selectedProjectName,
550
- projectPath: state.selectedProjectPath,
551
- workspaceTarget: 'selected_project',
552
- telegram: true,
553
- preferredProvider: state.selectedProvider,
554
- preferredModel: state.selectedModel,
969
+ const provider = resolveTelegramProvider(state);
970
+ const model = resolveTelegramModel(state);
971
+ const active = activity || await createTelegramActivity({
972
+ bot,
973
+ chatId,
974
+ link,
975
+ type: 'workflow',
976
+ prompt: input,
977
+ phase: t(lang, 'control.activity.startingWorkflow'),
978
+ });
979
+ active.type = 'workflow';
980
+ active.provider = provider;
981
+ active.project = projectLabel(state);
982
+ active.workflowId = workflowId;
983
+ active.status = 'running';
984
+ active.phase = t(lang, 'control.activity.startingWorkflow');
985
+ await editTelegramActivity({ bot, chatId, activity: active, force: true });
986
+
987
+ const result = await runTelegramTool({
988
+ userId: link.user_id,
989
+ action: 'run_workflow',
990
+ execute: () => localApi(link.user_id, `/api/orchestration/workflows/${workflowId}/runs`, {
991
+ method: 'POST',
992
+ body: {
993
+ input,
994
+ metadata: {
995
+ projectId: state.selectedProjectName,
996
+ projectName: state.selectedProjectName,
997
+ projectPath: state.selectedProjectPath,
998
+ workspaceTarget: 'selected_project',
999
+ telegram: true,
1000
+ preferredProvider: provider,
1001
+ preferredModel: model,
1002
+ },
555
1003
  },
556
- },
1004
+ }),
557
1005
  });
558
- await send(bot, chatId, t(lang, 'control.workflowStarted', { runId: run.id, workflowId }));
559
- monitorWorkflowRun({ bot, chatId, link, runId: run.id }).catch((error) => {
1006
+ if (!result.ok) {
1007
+ await sendToolFailure({ bot, chatId, link, result, editMessageId: active.messageId });
1008
+ return;
1009
+ }
1010
+ const run = result.data;
1011
+ active.runId = run.id;
1012
+ active.phase = t(lang, 'control.activity.workflowRunning');
1013
+ pushActivityEvent(active, `🧭 ${t(lang, 'control.workflowStarted', { runId: run.id, workflowId }).replace('\n', ' ')}`);
1014
+ await editTelegramActivity({ bot, chatId, activity: active, force: true });
1015
+ monitorWorkflowRun({ bot, chatId, link, runId: run.id, activity: active }).catch((error) => {
560
1016
  console.warn('[telegram-control] workflow monitor failed:', error?.message || error);
561
1017
  });
562
1018
  }
563
1019
 
1020
+ async function cancelRun({ bot, chatId, link, runId, editMessageId, confirmed = false }) {
1021
+ const lang = languageFor(link);
1022
+ if (!runId) {
1023
+ await send(bot, chatId, t(lang, 'control.cancelUsage'), { editMessageId });
1024
+ return;
1025
+ }
1026
+ const state = getState(link.user_id);
1027
+ if (state.remoteControlEnabled === false) {
1028
+ await send(bot, chatId, t(lang, 'control.disabled'), { editMessageId });
1029
+ return;
1030
+ }
1031
+ if (state.confirmationPolicy === 'strict' && !confirmed) {
1032
+ await requestConfirmation({
1033
+ bot,
1034
+ chatId,
1035
+ link,
1036
+ action: 'run_cancel',
1037
+ payload: { runId },
1038
+ editMessageId,
1039
+ });
1040
+ return;
1041
+ }
1042
+
1043
+ const result = await runTelegramTool({
1044
+ userId: link.user_id,
1045
+ action: 'run_cancel',
1046
+ execute: () => localApi(link.user_id, `/api/orchestration/workflows/runs/${encodeURIComponent(runId)}/cancel`, {
1047
+ method: 'POST',
1048
+ }),
1049
+ });
1050
+ if (!result.ok) {
1051
+ await sendToolFailure({ bot, chatId, link, result, editMessageId });
1052
+ return;
1053
+ }
1054
+ const run = result.data;
1055
+ await send(bot, chatId, t(lang, 'control.runStatus', { runId: run.id, status: run.status }), { editMessageId });
1056
+ }
1057
+
1058
+ async function executeConfirmedAction({ bot, chatId, link, confirmation, editMessageId }) {
1059
+ if (confirmation.action === 'install_provider') {
1060
+ await startCliInstall({
1061
+ bot,
1062
+ chatId,
1063
+ link,
1064
+ provider: confirmation.payload?.provider,
1065
+ editMessageId,
1066
+ confirmed: true,
1067
+ });
1068
+ return true;
1069
+ }
1070
+ if (confirmation.action === 'run_cancel') {
1071
+ await cancelRun({
1072
+ bot,
1073
+ chatId,
1074
+ link,
1075
+ runId: confirmation.payload?.runId,
1076
+ editMessageId,
1077
+ confirmed: true,
1078
+ });
1079
+ return true;
1080
+ }
1081
+ return false;
1082
+ }
1083
+
1084
+ async function findProjectByQuery(query) {
1085
+ const projects = await listProjects();
1086
+ const needle = String(query || '').trim().toLocaleLowerCase('tr');
1087
+ if (!needle) return null;
1088
+ return projects.find((project) => {
1089
+ const candidates = [project.name, project.displayName, project.fullPath, project.path]
1090
+ .filter(Boolean)
1091
+ .map((value) => String(value).toLocaleLowerCase('tr'));
1092
+ return candidates.some((candidate) => candidate === needle || candidate.includes(needle));
1093
+ }) || null;
1094
+ }
1095
+
1096
+ async function listWorkflowSummaries(userId) {
1097
+ try {
1098
+ const data = await localApi(userId, '/api/orchestration/workflows', { timeoutMs: 10_000 });
1099
+ if (Array.isArray(data)) return data;
1100
+ if (Array.isArray(data?.workflows)) return data.workflows;
1101
+ } catch {
1102
+ // Workflow context is helpful for routing but should never block chat input.
1103
+ }
1104
+ return [];
1105
+ }
1106
+
1107
+ async function resolveTelegramAiIntent({ bot, chatId, link, text, activity }) {
1108
+ const state = getState(link.user_id);
1109
+ if (state.routerEnabled === false || state.routerMode !== 'hybrid') {
1110
+ return { action: 'agent_prompt', prompt: text, confidence: 1 };
1111
+ }
1112
+
1113
+ const provider = resolveTelegramProvider(state);
1114
+ const model = resolveTelegramModel(state);
1115
+ if (activity) {
1116
+ activity.type = 'router';
1117
+ activity.provider = provider;
1118
+ activity.project = projectLabel(state);
1119
+ activity.status = 'running';
1120
+ activity.phase = t(activity.lang, 'control.activity.interpreting');
1121
+ pushActivityEvent(activity, `🧠 ${t(activity.lang, 'control.activity.interpreting')}`);
1122
+ await editTelegramActivity({ bot, chatId, activity, force: true });
1123
+ }
1124
+
1125
+ try {
1126
+ const projects = await listProjects().catch(() => []);
1127
+ const workflows = await listWorkflowSummaries(link.user_id);
1128
+ const response = await localApi(link.user_id, '/api/agent', {
1129
+ method: 'POST',
1130
+ timeoutMs: INTENT_ROUTER_TIMEOUT_MS,
1131
+ body: {
1132
+ projectPath: state.selectedProjectPath || process.cwd(),
1133
+ provider,
1134
+ model: model || undefined,
1135
+ message: buildTelegramIntentPrompt(text, state, { projects, workflows }),
1136
+ cleanup: false,
1137
+ stream: false,
1138
+ permissionMode: 'plan',
1139
+ },
1140
+ });
1141
+ const assistantText = extractAssistantText(response);
1142
+ const intent = parseTelegramAiIntentResponse(assistantText, text);
1143
+ if (activity) {
1144
+ pushActivityEvent(activity, `🧭 ${intent.action} (${Math.round(intent.confidence * 100)}%)`);
1145
+ await editTelegramActivity({ bot, chatId, activity });
1146
+ }
1147
+ return intent;
1148
+ } catch (error) {
1149
+ if (activity) {
1150
+ pushActivityEvent(activity, `⚠️ ${t(activity.lang, 'control.activity.routerFallback')}`);
1151
+ await editTelegramActivity({ bot, chatId, activity });
1152
+ }
1153
+ console.warn('[telegram-control] AI intent router fallback:', error?.message || error);
1154
+ return { action: 'agent_prompt', prompt: text, confidence: 0 };
1155
+ }
1156
+ }
1157
+
1158
+ async function handleRoutedIntent({ bot, chatId, link, text, activity }) {
1159
+ const intent = await resolveTelegramAiIntent({ bot, chatId, link, text, activity });
1160
+ const editMessageId = activity?.messageId;
1161
+
1162
+ if (intent.action === 'agent_prompt') {
1163
+ await runAgent({ bot, chatId, link, prompt: intent.prompt || text, activity });
1164
+ return true;
1165
+ }
1166
+ if (intent.action === 'show_menu') {
1167
+ await showMainMenu({ bot, chatId, link, editMessageId });
1168
+ return true;
1169
+ }
1170
+ if (intent.action === 'show_projects') {
1171
+ await showProjectMenu({ bot, chatId, link, editMessageId });
1172
+ return true;
1173
+ }
1174
+ if (intent.action === 'select_project') {
1175
+ const query = intent.projectQuery || intent.prompt || text;
1176
+ const project = await findProjectByQuery(query);
1177
+ const lang = languageFor(link);
1178
+ if (!project) {
1179
+ await showProjectMenu({
1180
+ bot,
1181
+ chatId,
1182
+ link,
1183
+ editMessageId,
1184
+ notice: t(lang, 'control.projectNotFound', { query }),
1185
+ });
1186
+ return true;
1187
+ }
1188
+ updateTelegramControlState(link.user_id, {
1189
+ selectedProjectName: project.name,
1190
+ selectedProjectPath: project.fullPath || project.path,
1191
+ });
1192
+ await showMainMenu({
1193
+ bot,
1194
+ chatId,
1195
+ link,
1196
+ editMessageId,
1197
+ notice: t(lang, 'control.projectSelected', {
1198
+ project: project.displayName || project.name,
1199
+ path: project.fullPath || project.path,
1200
+ }),
1201
+ });
1202
+ return true;
1203
+ }
1204
+ if (intent.action === 'show_provider_menu') {
1205
+ await showProviderMenu({ bot, chatId, link, editMessageId });
1206
+ return true;
1207
+ }
1208
+ if (intent.action === 'select_provider' && intent.provider) {
1209
+ updateTelegramControlState(link.user_id, { selectedProvider: intent.provider, selectedModel: null });
1210
+ await showModelMenu({ bot, chatId, link, editMessageId });
1211
+ return true;
1212
+ }
1213
+ if (intent.action === 'show_model_menu') {
1214
+ await showModelMenu({ bot, chatId, link, editMessageId });
1215
+ return true;
1216
+ }
1217
+ if (intent.action === 'select_model' && intent.model) {
1218
+ updateTelegramControlState(link.user_id, { selectedModel: intent.model });
1219
+ await showMainMenu({
1220
+ bot,
1221
+ chatId,
1222
+ link,
1223
+ editMessageId,
1224
+ notice: t(languageFor(link), 'control.modelSelected', { model: intent.model }),
1225
+ });
1226
+ return true;
1227
+ }
1228
+ if (intent.action === 'show_runs') {
1229
+ await showRuns({ bot, chatId, link, editMessageId });
1230
+ return true;
1231
+ }
1232
+ if (intent.action === 'show_approvals') {
1233
+ await showApprovalQueue({ bot, chatId, link, editMessageId });
1234
+ return true;
1235
+ }
1236
+ if (intent.action === 'show_workflows') {
1237
+ await showWorkflowMenu({ bot, chatId, link, editMessageId });
1238
+ return true;
1239
+ }
1240
+ if (intent.action === 'run_workflow') {
1241
+ await runWorkflow({ bot, chatId, link, input: intent.workflowInput || text, activity });
1242
+ return true;
1243
+ }
1244
+ if (intent.action === 'show_sessions') {
1245
+ await showSessions({ bot, chatId, link, editMessageId });
1246
+ return true;
1247
+ }
1248
+ if (intent.action === 'new_chat') {
1249
+ await startNewChat({ bot, chatId, link, editMessageId });
1250
+ return true;
1251
+ }
1252
+ await runAgent({ bot, chatId, link, prompt: text, activity });
1253
+ return true;
1254
+ }
1255
+
564
1256
  async function fetchRun(userId, runId) {
565
1257
  return localApi(userId, `/api/orchestration/workflows/runs/${runId}`);
566
1258
  }
567
1259
 
568
1260
  function summarizeRun(run, mode) {
1261
+ const statusIcon = run.status === 'completed'
1262
+ ? '✅'
1263
+ : run.status === 'failed'
1264
+ ? '❌'
1265
+ : run.status === 'canceled'
1266
+ ? '⏹'
1267
+ : '🔧';
569
1268
  const lines = [
570
- `Run ${run.id}`,
571
- `Workflow: ${run.workflowId}`,
572
- `Status: ${run.status}`,
1269
+ `${statusIcon} Run ${run.id}`,
1270
+ `🧩 Workflow: ${run.workflowId}`,
1271
+ `📌 Status: ${run.status}`,
573
1272
  ];
574
1273
  const nodeRuns = Array.isArray(run.nodeRuns) ? run.nodeRuns : [];
575
1274
  if (mode !== 'final') {
@@ -577,7 +1276,14 @@ function summarizeRun(run, mode) {
577
1276
  ? nodeRuns.filter((node) => node.error || node.status === 'failed')
578
1277
  : nodeRuns;
579
1278
  for (const node of visibleNodes) {
580
- lines.push(`- ${node.status}: ${node.agentLabel || node.nodeId}${node.error ? ` — ${node.error}` : ''}`);
1279
+ const nodeIcon = node.status === 'completed'
1280
+ ? '✅'
1281
+ : node.status === 'failed'
1282
+ ? '❌'
1283
+ : node.status === 'running'
1284
+ ? '🔧'
1285
+ : '⏳';
1286
+ lines.push(`${nodeIcon} ${node.status}: ${node.agentLabel || node.nodeId}${node.error ? ` - ${node.error}` : ''}`);
581
1287
  }
582
1288
  }
583
1289
  const outputs = nodeRuns
@@ -587,7 +1293,7 @@ function summarizeRun(run, mode) {
587
1293
  return truncate(`${lines.join('\n')}${outputs.join('\n')}`);
588
1294
  }
589
1295
 
590
- async function monitorWorkflowRun({ bot, chatId, link, runId }) {
1296
+ async function monitorWorkflowRun({ bot, chatId, link, runId, activity = null }) {
591
1297
  if (runMonitors.has(runId)) return;
592
1298
  runMonitors.set(runId, true);
593
1299
  const state = getState(link.user_id);
@@ -597,12 +1303,23 @@ async function monitorWorkflowRun({ bot, chatId, link, runId }) {
597
1303
  await new Promise((resolve) => setTimeout(resolve, 5000));
598
1304
  const run = await fetchRun(link.user_id, runId);
599
1305
  const nodeRuns = Array.isArray(run.nodeRuns) ? run.nodeRuns : [];
1306
+ if (activity && !TERMINAL_RUN_STATES.has(run.status)) {
1307
+ activity.status = 'running';
1308
+ activity.phase = `${t(activity.lang, 'control.activity.workflowRunning')} (${run.status})`;
1309
+ await editTelegramActivity({ bot, chatId, activity });
1310
+ }
600
1311
  if (state.progressMode === 'all') {
601
1312
  for (const node of nodeRuns) {
602
1313
  const key = `${node.nodeId}:${node.status}`;
603
1314
  if (!seenNodeStatus.has(key)) {
604
1315
  seenNodeStatus.set(key, true);
605
- await send(bot, chatId, `${node.agentLabel || node.nodeId}: ${node.status}${node.error ? `\n${node.error}` : ''}`);
1316
+ if (activity) {
1317
+ pushActivityEvent(activity, `${node.status === 'completed' ? '✅' : node.status === 'failed' ? '❌' : '🔧'} ${node.agentLabel || node.nodeId}: ${node.status}${node.error ? ` - ${node.error}` : ''}`);
1318
+ activity.phase = t(activity.lang, 'control.activity.workflowRunning');
1319
+ await editTelegramActivity({ bot, chatId, activity });
1320
+ } else {
1321
+ await send(bot, chatId, `${node.agentLabel || node.nodeId}: ${node.status}${node.error ? `\n${node.error}` : ''}`);
1322
+ }
606
1323
  }
607
1324
  }
608
1325
  }
@@ -611,12 +1328,27 @@ async function monitorWorkflowRun({ bot, chatId, link, runId }) {
611
1328
  const key = `${node.nodeId}:${node.status}:${node.error || ''}`;
612
1329
  if (!seenNodeStatus.has(key)) {
613
1330
  seenNodeStatus.set(key, true);
614
- await send(bot, chatId, `${node.agentLabel || node.nodeId}: ${node.status}\n${node.error || ''}`);
1331
+ if (activity) {
1332
+ pushActivityEvent(activity, `❌ ${node.agentLabel || node.nodeId}: ${node.status}${node.error ? ` - ${node.error}` : ''}`);
1333
+ activity.phase = t(activity.lang, 'control.activity.workflowRunning');
1334
+ await editTelegramActivity({ bot, chatId, activity });
1335
+ } else {
1336
+ await send(bot, chatId, `${node.agentLabel || node.nodeId}: ${node.status}\n${node.error || ''}`);
1337
+ }
615
1338
  }
616
1339
  }
617
1340
  }
618
1341
  if (TERMINAL_RUN_STATES.has(run.status)) {
619
- await send(bot, chatId, summarizeRun(run, state.progressMode));
1342
+ if (activity) {
1343
+ activity.status = run.status === 'completed' ? 'done' : 'failed';
1344
+ activity.phase = run.status === 'completed'
1345
+ ? t(activity.lang, 'control.activity.done')
1346
+ : t(activity.lang, 'control.activity.failed');
1347
+ activity.output = summarizeRun(run, state.progressMode);
1348
+ await editTelegramActivity({ bot, chatId, activity, force: true });
1349
+ } else {
1350
+ await send(bot, chatId, summarizeRun(run, state.progressMode));
1351
+ }
620
1352
  return;
621
1353
  }
622
1354
  }
@@ -625,18 +1357,49 @@ async function monitorWorkflowRun({ bot, chatId, link, runId }) {
625
1357
  }
626
1358
  }
627
1359
 
628
- export async function startCliInstall({ bot, chatId, link, provider }) {
1360
+ export async function startCliInstall({ bot, chatId, link, provider, editMessageId, confirmed = false }) {
629
1361
  const lang = languageFor(link);
630
- const data = await localApi(link.user_id, `/api/providers/${provider}/install`, { method: 'POST' });
1362
+ if (!PROVIDERS.includes(provider)) {
1363
+ await send(bot, chatId, t(lang, 'control.providerAuthFallback'), { editMessageId });
1364
+ return;
1365
+ }
1366
+ const state = getState(link.user_id);
1367
+ if (state.remoteControlEnabled === false) {
1368
+ await send(bot, chatId, t(lang, 'control.disabled'), { editMessageId });
1369
+ return;
1370
+ }
1371
+ if (state.confirmationPolicy === 'strict' && !confirmed) {
1372
+ await requestConfirmation({
1373
+ bot,
1374
+ chatId,
1375
+ link,
1376
+ action: 'install_provider',
1377
+ payload: { provider },
1378
+ editMessageId,
1379
+ });
1380
+ return;
1381
+ }
1382
+
1383
+ const result = await runTelegramTool({
1384
+ userId: link.user_id,
1385
+ action: 'install_provider',
1386
+ execute: () => localApi(link.user_id, `/api/providers/${provider}/install`, { method: 'POST' }),
1387
+ });
1388
+ if (!result.ok) {
1389
+ await sendToolFailure({ bot, chatId, link, result, editMessageId });
1390
+ return;
1391
+ }
1392
+
1393
+ const data = result.data;
631
1394
  if (data?.manual) {
632
- await send(bot, chatId, t(lang, 'control.manualInstall', { provider, manual: data.manual }));
1395
+ await send(bot, chatId, t(lang, 'control.manualInstall', { provider, manual: data.manual }), { editMessageId });
633
1396
  return;
634
1397
  }
635
1398
  await send(bot, chatId, t(lang, 'control.installStarted', {
636
1399
  provider,
637
1400
  jobId: data.jobId,
638
1401
  command: data.installCmd || 'internal installer',
639
- }));
1402
+ }), { editMessageId });
640
1403
  }
641
1404
 
642
1405
  async function showInstallMenu({ bot, chatId, link, editMessageId }) {
@@ -818,16 +1581,13 @@ async function handleCommand({ bot, chatId, link, text }) {
818
1581
  await send(bot, chatId, t(lang, 'control.cancelUsage'));
819
1582
  return true;
820
1583
  }
821
- const run = await localApi(link.user_id, `/api/orchestration/workflows/runs/${encodeURIComponent(argText)}/cancel`, {
822
- method: 'POST',
823
- });
824
- await send(bot, chatId, t(lang, 'control.runStatus', { runId: run.id, status: run.status }));
1584
+ await cancelRun({ bot, chatId, link, runId: argText });
825
1585
  return true;
826
1586
  }
827
1587
  return false;
828
1588
  }
829
1589
 
830
- export async function handleTelegramControlMessage({ bot, msg, link }) {
1590
+ async function handleTelegramControlMessageInternal({ bot, msg, link }) {
831
1591
  const chatId = msg.chat.id;
832
1592
  const text = String(msg.text || '').trim();
833
1593
  if (!text) return false;
@@ -845,13 +1605,35 @@ export async function handleTelegramControlMessage({ bot, msg, link }) {
845
1605
  if (await handleCommand({ bot, chatId, link, text })) return true;
846
1606
 
847
1607
  const state = getState(link.user_id);
1608
+ if (state.routerEnabled === false) return false;
848
1609
  if (!state.remoteControlEnabled) {
849
1610
  await send(bot, chatId, t(languageFor(link), 'control.disabled'));
850
1611
  return true;
851
1612
  }
852
1613
 
853
- await runAgent({ bot, chatId, link, prompt: text });
854
- return true;
1614
+ const activity = await createTelegramActivity({
1615
+ bot,
1616
+ chatId,
1617
+ link,
1618
+ type: 'router',
1619
+ prompt: text,
1620
+ phase: t(languageFor(link), 'control.activity.interpreting'),
1621
+ });
1622
+ return handleRoutedIntent({ bot, chatId, link, text, activity });
1623
+ }
1624
+
1625
+ export async function handleTelegramControlMessage(args) {
1626
+ const chatId = args?.msg?.chat?.id;
1627
+ if (!chatId) return false;
1628
+ return enqueueTelegramJob(chatId, async () => {
1629
+ try {
1630
+ return await handleTelegramControlMessageInternal(args);
1631
+ } catch (error) {
1632
+ console.error('[telegram-control] message handler failed:', error);
1633
+ await send(args.bot, chatId, t(languageFor(args.link), 'error.generic')).catch(() => {});
1634
+ return true;
1635
+ }
1636
+ });
855
1637
  }
856
1638
 
857
1639
  async function showRunDetail({ bot, chatId, link, runId, editMessageId }) {
@@ -868,7 +1650,7 @@ async function showRunDetail({ bot, chatId, link, runId, editMessageId }) {
868
1650
  });
869
1651
  }
870
1652
 
871
- export async function handleTelegramControlCallback({ bot, query, link }) {
1653
+ async function handleTelegramControlCallbackInternal({ bot, query, link }) {
872
1654
  const chatId = query.message?.chat?.id;
873
1655
  if (!chatId) return;
874
1656
  const editMessageId = query.message?.message_id;
@@ -882,6 +1664,32 @@ export async function handleTelegramControlCallback({ bot, query, link }) {
882
1664
  await bot.answerCallbackQuery(query.id).catch(() => {});
883
1665
 
884
1666
  const { action, payload } = entry;
1667
+ if (action === 'confirm_action') {
1668
+ forgetAction(entry.id);
1669
+ const lang = languageFor(link);
1670
+ const result = consumeTelegramConfirmation(link.user_id, payload.id);
1671
+ if (!result.ok) {
1672
+ await send(bot, chatId, t(lang, result.reason === 'expired'
1673
+ ? 'control.confirmationExpired'
1674
+ : 'control.confirmationMissing'), { editMessageId });
1675
+ return;
1676
+ }
1677
+ if (await executeConfirmedAction({
1678
+ bot,
1679
+ chatId,
1680
+ link,
1681
+ confirmation: result.confirmation,
1682
+ editMessageId,
1683
+ })) return;
1684
+ await send(bot, chatId, t(lang, 'error.generic'), { editMessageId });
1685
+ return;
1686
+ }
1687
+ if (action === 'cancel_confirmation') {
1688
+ forgetAction(entry.id);
1689
+ clearTelegramConfirmation(link.user_id, payload.id);
1690
+ await send(bot, chatId, t(languageFor(link), 'control.confirmationCanceled'), { editMessageId });
1691
+ return;
1692
+ }
885
1693
  if (action === 'menu') return showMainMenu({ bot, chatId, link, editMessageId });
886
1694
  if (action === 'projects') return showProjectMenu({ bot, chatId, link, editMessageId });
887
1695
  if (action === 'providers') return showProviderMenu({ bot, chatId, link, editMessageId });
@@ -947,20 +1755,26 @@ export async function handleTelegramControlCallback({ bot, query, link }) {
947
1755
  }
948
1756
  if (action === 'run_detail') return showRunDetail({ bot, chatId, link, runId: payload.runId, editMessageId });
949
1757
  if (action === 'run_cancel') {
950
- const run = await localApi(link.user_id, `/api/orchestration/workflows/runs/${encodeURIComponent(payload.runId)}/cancel`, {
951
- method: 'POST',
952
- });
953
- await send(bot, chatId, t(languageFor(link), 'control.runStatus', { runId: run.id, status: run.status }), { editMessageId });
1758
+ await cancelRun({ bot, chatId, link, runId: payload.runId, editMessageId });
954
1759
  return;
955
1760
  }
956
1761
  if (action === 'approval_decide') {
957
- const result = await localApi(link.user_id, `/api/orchestration/workflows/approvals/${encodeURIComponent(payload.approvalId)}`, {
958
- method: 'POST',
959
- body: {
960
- allow: payload.allow === true,
961
- source: 'telegram',
962
- },
1762
+ const toolResult = await runTelegramTool({
1763
+ userId: link.user_id,
1764
+ action: 'approval_decide',
1765
+ execute: () => localApi(link.user_id, `/api/orchestration/workflows/approvals/${encodeURIComponent(payload.approvalId)}`, {
1766
+ method: 'POST',
1767
+ body: {
1768
+ allow: payload.allow === true,
1769
+ source: 'telegram',
1770
+ },
1771
+ }),
963
1772
  });
1773
+ if (!toolResult.ok) {
1774
+ await sendToolFailure({ bot, chatId, link, result: toolResult, editMessageId });
1775
+ return;
1776
+ }
1777
+ const result = toolResult.data;
964
1778
  const lang = languageFor(link);
965
1779
  await send(bot, chatId, t(lang, 'control.approvalDecided', {
966
1780
  approvalId: payload.approvalId,
@@ -969,7 +1783,7 @@ export async function handleTelegramControlCallback({ bot, query, link }) {
969
1783
  }), { editMessageId });
970
1784
  return showApprovalQueue({ bot, chatId, link });
971
1785
  }
972
- if (action === 'install_provider') return startCliInstall({ bot, chatId, link, provider: payload.provider });
1786
+ if (action === 'install_provider') return startCliInstall({ bot, chatId, link, provider: payload.provider, editMessageId });
973
1787
  if (action === 'auth_provider') {
974
1788
  await send(bot, chatId, `${payload.provider} login:\n${AUTH_HELP[payload.provider] || t(languageFor(link), 'control.providerAuthFallback')}`, { editMessageId });
975
1789
  return;
@@ -994,3 +1808,18 @@ export async function handleTelegramControlCallback({ bot, query, link }) {
994
1808
  });
995
1809
  }
996
1810
  }
1811
+
1812
+ export async function handleTelegramControlCallback(args) {
1813
+ const chatId = args?.query?.message?.chat?.id;
1814
+ if (!chatId) return;
1815
+ return enqueueTelegramJob(chatId, async () => {
1816
+ try {
1817
+ await handleTelegramControlCallbackInternal(args);
1818
+ } catch (error) {
1819
+ console.error('[telegram-control] callback failed:', error);
1820
+ const lang = languageFor(args.link);
1821
+ await args.bot?.answerCallbackQuery(args.query?.id, { text: t(lang, 'error.generic') }).catch(() => {});
1822
+ await send(args.bot, chatId, t(lang, 'error.generic')).catch(() => {});
1823
+ }
1824
+ });
1825
+ }