@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
@@ -2,12 +2,16 @@ import crypto from 'node:crypto';
2
2
  import { apiKeysDb, telegramLinksDb } from '../../database/db.js';
3
3
  import { getProjects } from '../../projects.js';
4
4
  import { getStaticProviderModels } from '../model-registry.js';
5
+ import { TELEGRAM_CONTROL_SCOPES, TELEGRAM_PROVIDERS, buildTelegramAgentPrompt, buildTelegramIntentPrompt, clearTelegramConfirmation, consumeTelegramConfirmation, createTelegramConfirmation, enqueueTelegramJob, parseTelegramAiIntentResponse, resolveTelegramModel, resolveTelegramProvider, retryWithBackoff, runTelegramTool, splitTelegramText, } from './telegram-gateway.js';
5
6
  import { SUPPORTED_LANGUAGES, t } from './translations.js';
6
- const PROVIDERS = ['claude', 'cursor', 'codex', 'gemini', 'qwen', 'opencode'];
7
+ const PROVIDERS = TELEGRAM_PROVIDERS;
7
8
  const TERMINAL_RUN_STATES = new Set(['completed', 'failed', 'canceled']);
8
9
  const CALLBACK_TTL_MS = 10 * 60 * 1000;
9
10
  const MAX_CALLBACK_ACTIONS = 1000;
10
11
  const MAX_TELEGRAM_TEXT = 3600;
12
+ const ACTIVITY_EDIT_THROTTLE_MS = 1200;
13
+ const ACTIVITY_HEARTBEAT_MS = 8000;
14
+ const INTENT_ROUTER_TIMEOUT_MS = 45_000;
11
15
  const callbackActions = new Map();
12
16
  const runMonitors = new Map();
13
17
  const MODEL_FALLBACKS = Object.fromEntries(PROVIDERS.map((provider) => [provider, getStaticProviderModels(provider)]));
@@ -113,7 +117,11 @@ function readAction(data) {
113
117
  callbackActions.delete(id);
114
118
  return null;
115
119
  }
116
- return entry;
120
+ return { id, ...entry };
121
+ }
122
+ function forgetAction(id) {
123
+ if (id)
124
+ callbackActions.delete(id);
117
125
  }
118
126
  function button(text, action, payload = {}) {
119
127
  return { text, callback_data: registerAction(action, payload) };
@@ -131,30 +139,46 @@ async function send(bot, chatId, text, options = {}) {
131
139
  disable_web_page_preview: true,
132
140
  ...telegramOptions,
133
141
  };
134
- const messageText = truncate(text);
142
+ const chunks = splitTelegramText(text, MAX_TELEGRAM_TEXT);
143
+ const [firstChunk = ''] = chunks;
144
+ let startIndex = 0;
135
145
  if (editMessageId && typeof bot.editMessageText === 'function') {
136
146
  try {
137
- return await bot.editMessageText(messageText, {
147
+ const edited = await bot.editMessageText(firstChunk, {
138
148
  chat_id: chatId,
139
149
  message_id: editMessageId,
140
150
  ...extra,
141
151
  });
152
+ startIndex = 1;
153
+ for (const chunk of chunks.slice(startIndex)) {
154
+ await send(bot, chatId, chunk, telegramOptions);
155
+ }
156
+ return edited;
142
157
  }
143
158
  catch (err) {
144
159
  const description = err?.response?.body?.description || err?.message || '';
145
- if (/message is not modified/i.test(description))
160
+ if (/message is not modified/i.test(description)) {
161
+ startIndex = 1;
162
+ for (const chunk of chunks.slice(startIndex)) {
163
+ await send(bot, chatId, chunk, telegramOptions);
164
+ }
146
165
  return null;
166
+ }
147
167
  console.warn('[telegram-control] editMessageText failed:', description || err);
148
168
  }
149
169
  }
150
- try {
151
- return await bot.sendMessage(chatId, messageText, extra);
152
- }
153
- catch {
154
- const fallback = { ...extra };
155
- delete fallback.parse_mode;
156
- return bot.sendMessage(chatId, messageText, fallback);
170
+ let result = null;
171
+ for (const chunk of chunks.slice(startIndex)) {
172
+ try {
173
+ result = await bot.sendMessage(chatId, chunk, extra);
174
+ }
175
+ catch {
176
+ const fallback = { ...extra };
177
+ delete fallback.parse_mode;
178
+ result = await bot.sendMessage(chatId, chunk, fallback);
179
+ }
157
180
  }
181
+ return result;
158
182
  }
159
183
  function localApiBase() {
160
184
  const port = process.env.SERVER_PORT || process.env.PORT || '3001';
@@ -164,30 +188,242 @@ function getOrCreateTelegramApiKey(userId) {
164
188
  const existing = apiKeysDb
165
189
  .getApiKeys(userId)
166
190
  .find((key) => key.key_name === 'Telegram Control' && Boolean(key.is_active));
167
- if (existing?.api_key)
191
+ if (existing?.api_key) {
192
+ const existingScopes = apiKeysDb.normalizeScopes(existing.scopes || []);
193
+ const nextScopes = apiKeysDb.normalizeScopes([...existingScopes, ...TELEGRAM_CONTROL_SCOPES]);
194
+ const hasAllScopes = TELEGRAM_CONTROL_SCOPES.every((scope) => existingScopes.includes(scope));
195
+ if (!hasAllScopes || nextScopes.length !== existingScopes.length) {
196
+ apiKeysDb.updateApiKeyScopes(userId, existing.id, nextScopes);
197
+ }
168
198
  return existing.api_key;
169
- return apiKeysDb.createApiKey(userId, 'Telegram Control').apiKey;
199
+ }
200
+ return apiKeysDb.createApiKey(userId, 'Telegram Control', TELEGRAM_CONTROL_SCOPES).apiKey;
170
201
  }
171
- async function localApi(userId, path, { method = 'GET', body } = {}) {
202
+ async function localApi(userId, path, { method = 'GET', body, timeoutMs = 0 } = {}) {
172
203
  const apiKey = getOrCreateTelegramApiKey(userId);
173
- const response = await fetch(`${localApiBase()}${path}`, {
174
- method,
204
+ const response = await retryWithBackoff(async () => {
205
+ const controller = timeoutMs > 0 ? new AbortController() : null;
206
+ const timeoutId = controller
207
+ ? setTimeout(() => controller.abort(new Error(`HTTP request timed out after ${timeoutMs}ms`)), timeoutMs)
208
+ : null;
209
+ const res = await fetch(`${localApiBase()}${path}`, {
210
+ method,
211
+ headers: {
212
+ 'Content-Type': 'application/json',
213
+ 'X-API-Key': apiKey,
214
+ },
215
+ signal: controller?.signal,
216
+ body: body === undefined ? undefined : JSON.stringify(body),
217
+ }).finally(() => {
218
+ if (timeoutId)
219
+ clearTimeout(timeoutId);
220
+ });
221
+ if ([408, 429, 500, 502, 503, 504].includes(res.status)) {
222
+ const error = new Error(`HTTP ${res.status}`);
223
+ error.status = res.status;
224
+ throw error;
225
+ }
226
+ return res;
227
+ });
228
+ const data = await response.json().catch(() => ({}));
229
+ if (!response.ok) {
230
+ const error = data?.error?.message || data?.error || `HTTP ${response.status}`;
231
+ throw new Error(typeof error === 'string' ? error : JSON.stringify(error));
232
+ }
233
+ return data?.data ?? data;
234
+ }
235
+ async function localAgentStream(userId, body, onEvent) {
236
+ const apiKey = getOrCreateTelegramApiKey(userId);
237
+ const response = await fetch(`${localApiBase()}/api/agent`, {
238
+ method: 'POST',
175
239
  headers: {
176
240
  'Content-Type': 'application/json',
177
241
  'X-API-Key': apiKey,
178
242
  },
179
- body: body === undefined ? undefined : JSON.stringify(body),
243
+ body: JSON.stringify({
244
+ ...body,
245
+ stream: true,
246
+ }),
180
247
  });
181
- const data = await response.json().catch(() => ({}));
182
248
  if (!response.ok) {
249
+ const data = await response.json().catch(() => ({}));
183
250
  const error = data?.error?.message || data?.error || `HTTP ${response.status}`;
184
251
  throw new Error(typeof error === 'string' ? error : JSON.stringify(error));
185
252
  }
186
- return data?.data ?? data;
253
+ if (!response.body)
254
+ return;
255
+ const reader = response.body.getReader();
256
+ const decoder = new TextDecoder();
257
+ let buffer = '';
258
+ const consumeBlock = async (block) => {
259
+ const lines = String(block || '').split('\n');
260
+ for (const line of lines) {
261
+ if (!line.startsWith('data:'))
262
+ continue;
263
+ const payload = line.slice(5).trim();
264
+ if (!payload)
265
+ continue;
266
+ let event;
267
+ try {
268
+ event = JSON.parse(payload);
269
+ }
270
+ catch {
271
+ continue;
272
+ }
273
+ await onEvent?.(event);
274
+ }
275
+ };
276
+ for (;;) {
277
+ const { done, value } = await reader.read();
278
+ if (done)
279
+ break;
280
+ buffer += decoder.decode(value, { stream: true });
281
+ let boundary = buffer.indexOf('\n\n');
282
+ while (boundary !== -1) {
283
+ const block = buffer.slice(0, boundary);
284
+ buffer = buffer.slice(boundary + 2);
285
+ await consumeBlock(block);
286
+ boundary = buffer.indexOf('\n\n');
287
+ }
288
+ }
289
+ const rest = `${buffer}${decoder.decode()}`;
290
+ if (rest.trim())
291
+ await consumeBlock(rest);
187
292
  }
188
293
  function checked(label) {
189
294
  return `${label} ✓`;
190
295
  }
296
+ function formatElapsed(startedAt) {
297
+ const seconds = Math.max(0, Math.round((Date.now() - startedAt) / 1000));
298
+ if (seconds < 60)
299
+ return `${seconds}s`;
300
+ const minutes = Math.floor(seconds / 60);
301
+ const rest = seconds % 60;
302
+ return `${minutes}m ${rest}s`;
303
+ }
304
+ function projectLabel(state) {
305
+ return state.selectedProjectName || state.selectedProjectPath || '-';
306
+ }
307
+ function createActivityState({ lang, type = 'agent', provider, project, prompt, mode = 'final' }) {
308
+ return {
309
+ lang,
310
+ type,
311
+ status: 'starting',
312
+ phase: t(lang, 'control.activity.starting'),
313
+ provider,
314
+ project,
315
+ prompt: compact(prompt, 120),
316
+ mode,
317
+ startedAt: Date.now(),
318
+ lastEditAt: 0,
319
+ messageId: null,
320
+ sessionId: null,
321
+ runId: null,
322
+ workflowId: null,
323
+ events: [],
324
+ output: '',
325
+ error: null,
326
+ };
327
+ }
328
+ function pushActivityEvent(activity, text) {
329
+ const value = String(text || '').trim();
330
+ if (!value)
331
+ return;
332
+ if (activity.events.at(-1) === value)
333
+ return;
334
+ activity.events.push(value);
335
+ if (activity.events.length > 8)
336
+ activity.events.splice(0, activity.events.length - 8);
337
+ }
338
+ function activityTitle(activity) {
339
+ if (activity.type === 'workflow')
340
+ return t(activity.lang, 'control.activity.workflowTitle');
341
+ if (activity.type === 'router')
342
+ return t(activity.lang, 'control.activity.routerTitle');
343
+ return t(activity.lang, 'control.activity.agentTitle');
344
+ }
345
+ function renderActivity(activity, { finalText = null } = {}) {
346
+ const output = finalText || activity.output;
347
+ const lines = [
348
+ `${activity.status === 'failed' ? '❌' : activity.status === 'done' ? '✅' : activity.status === 'running' ? '🔧' : '⏳'} ${activityTitle(activity)}`,
349
+ '',
350
+ `🤖 ${t(activity.lang, 'control.activity.provider')}: ${activity.provider || '-'}`,
351
+ `📁 ${t(activity.lang, 'control.activity.project')}: ${compact(activity.project, 90)}`,
352
+ ];
353
+ if (activity.sessionId)
354
+ lines.push(`🧵 ${t(activity.lang, 'control.activity.session')}: ${activity.sessionId}`);
355
+ if (activity.runId)
356
+ lines.push(`🧭 ${t(activity.lang, 'control.activity.run')}: ${activity.runId}`);
357
+ if (activity.workflowId)
358
+ lines.push(`🧩 ${t(activity.lang, 'control.activity.workflow')}: ${activity.workflowId}`);
359
+ lines.push(`⏱ ${t(activity.lang, 'control.activity.elapsed')}: ${formatElapsed(activity.startedAt)}`);
360
+ lines.push(`📌 ${t(activity.lang, 'control.activity.status')}: ${activity.phase}`);
361
+ const visibleEvents = activity.mode === 'final'
362
+ ? activity.events.slice(-4)
363
+ : activity.events;
364
+ if (visibleEvents.length > 0) {
365
+ lines.push('', `🛠 ${t(activity.lang, 'control.activity.work')}:`);
366
+ for (const event of visibleEvents)
367
+ lines.push(`• ${event}`);
368
+ }
369
+ if (activity.error) {
370
+ lines.push('', `⚠️ ${truncate(activity.error, 700)}`);
371
+ }
372
+ else if (output) {
373
+ lines.push('', `💬 ${t(activity.lang, 'control.activity.output')}:`);
374
+ lines.push(truncate(output, 1800));
375
+ }
376
+ else if (activity.prompt) {
377
+ lines.push('', `📝 ${compact(activity.prompt, 220)}`);
378
+ }
379
+ return truncate(lines.join('\n'), 3400);
380
+ }
381
+ async function createTelegramActivity({ bot, chatId, link, type = 'agent', prompt = '', phase = null }) {
382
+ const lang = languageFor(link);
383
+ const state = getState(link.user_id);
384
+ const activity = createActivityState({
385
+ lang,
386
+ type,
387
+ provider: resolveTelegramProvider(state),
388
+ project: projectLabel(state),
389
+ prompt,
390
+ mode: state.progressMode,
391
+ });
392
+ if (phase)
393
+ activity.phase = phase;
394
+ const sent = await send(bot, chatId, renderActivity(activity), { parse_mode: undefined });
395
+ activity.messageId = sent?.message_id || sent?.message?.message_id || null;
396
+ activity.lastEditAt = Date.now();
397
+ return activity;
398
+ }
399
+ async function editTelegramActivity({ bot, chatId, activity, force = false, reply_markup }) {
400
+ if (!activity)
401
+ return null;
402
+ const now = Date.now();
403
+ if (!force && now - activity.lastEditAt < ACTIVITY_EDIT_THROTTLE_MS)
404
+ return null;
405
+ const sent = await send(bot, chatId, renderActivity(activity), {
406
+ editMessageId: activity.messageId,
407
+ parse_mode: undefined,
408
+ reply_markup,
409
+ });
410
+ activity.messageId = sent?.message_id || sent?.message?.message_id || activity.messageId;
411
+ activity.lastEditAt = now;
412
+ return sent;
413
+ }
414
+ function startActivityHeartbeat({ bot, chatId, activity }) {
415
+ return setInterval(() => {
416
+ if (!activity || activity.status === 'done' || activity.status === 'failed')
417
+ return;
418
+ if (activity.status === 'starting') {
419
+ activity.status = 'running';
420
+ activity.phase = t(activity.lang, 'control.activity.thinking');
421
+ }
422
+ editTelegramActivity({ bot, chatId, activity }).catch((error) => {
423
+ console.warn('[telegram-control] activity heartbeat failed:', error?.message || error);
424
+ });
425
+ }, ACTIVITY_HEARTBEAT_MS);
426
+ }
191
427
  function stateSummary(lang, state) {
192
428
  return [
193
429
  `${t(lang, 'control.summary.project')}: ${state.selectedProjectName || t(lang, 'control.notSelected')}`,
@@ -232,11 +468,12 @@ async function listProjects() {
232
468
  const projects = await getProjects();
233
469
  return projects.slice(0, 20);
234
470
  }
235
- async function showProjectMenu({ bot, chatId, link, editMessageId }) {
471
+ async function showProjectMenu({ bot, chatId, link, editMessageId, notice }) {
236
472
  const lang = languageFor(link);
237
473
  const projects = await listProjects();
238
474
  if (projects.length === 0) {
239
- await send(bot, chatId, t(lang, 'control.noProjects'), { editMessageId });
475
+ const prefix = notice ? `${notice}\n\n` : '';
476
+ await send(bot, chatId, `${prefix}${t(lang, 'control.noProjects')}`, { editMessageId });
240
477
  return;
241
478
  }
242
479
  const buttons = projects.map((project, index) => button(`${index + 1}. ${compact(project.displayName || project.name, 34)}`, 'project_select', {
@@ -244,7 +481,8 @@ async function showProjectMenu({ bot, chatId, link, editMessageId }) {
244
481
  path: project.fullPath || project.path,
245
482
  displayName: project.displayName || project.name,
246
483
  }));
247
- await send(bot, chatId, t(lang, 'control.pickProject'), {
484
+ const prefix = notice ? `${notice}\n\n` : '';
485
+ await send(bot, chatId, `${prefix}${t(lang, 'control.pickProject')}`, {
248
486
  editMessageId,
249
487
  reply_markup: { inline_keyboard: rows(buttons, 1) },
250
488
  });
@@ -428,83 +666,526 @@ function extractAssistantText(response) {
428
666
  }
429
667
  return chunks.join('\n\n').trim();
430
668
  }
431
- async function runAgent({ bot, chatId, link, prompt }) {
669
+ function extractTextFromEvent(event) {
670
+ if (!event || typeof event !== 'object')
671
+ return '';
672
+ if (typeof event.content === 'string')
673
+ return event.content;
674
+ if (Array.isArray(event.content)) {
675
+ return event.content
676
+ .map((part) => (typeof part === 'string' ? part : part?.text || ''))
677
+ .join('');
678
+ }
679
+ const legacyContent = event.data?.message?.content || event.message?.content;
680
+ if (Array.isArray(legacyContent)) {
681
+ return legacyContent
682
+ .map((part) => (typeof part === 'string' ? part : part?.text || ''))
683
+ .join('');
684
+ }
685
+ if (typeof legacyContent === 'string')
686
+ return legacyContent;
687
+ return '';
688
+ }
689
+ function describeToolInput(toolName, input) {
690
+ if (!input || typeof input !== 'object')
691
+ return '';
692
+ if (toolName === 'Bash' && typeof input.command === 'string') {
693
+ return compact(input.command, 110);
694
+ }
695
+ if (toolName === 'WebSearch' && typeof input.query === 'string') {
696
+ return compact(input.query, 110);
697
+ }
698
+ if (toolName === 'FileChanges') {
699
+ return compact(JSON.stringify(input), 110);
700
+ }
701
+ const keys = Object.keys(input).slice(0, 3);
702
+ if (keys.length === 0)
703
+ return '';
704
+ return compact(keys.map((key) => `${key}: ${String(input[key])}`).join(', '), 110);
705
+ }
706
+ function applyAgentStreamEvent(activity, event) {
707
+ if (!event || typeof event !== 'object')
708
+ return;
709
+ const sessionId = event.actualSessionId || event.newSessionId || event.sessionId || event.threadId;
710
+ if (sessionId)
711
+ activity.sessionId = sessionId;
712
+ if (event.type === 'status' && event.message) {
713
+ activity.status = 'running';
714
+ activity.phase = compact(event.message, 120);
715
+ pushActivityEvent(activity, `⏳ ${compact(event.message, 120)}`);
716
+ return;
717
+ }
718
+ if (event.type === 'done' || event.kind === 'complete' || event.kind === 'stream_end') {
719
+ activity.status = 'done';
720
+ activity.phase = t(activity.lang, 'control.activity.done');
721
+ return;
722
+ }
723
+ if (event.type === 'error' || event.kind === 'error') {
724
+ activity.status = 'failed';
725
+ activity.phase = t(activity.lang, 'control.activity.failed');
726
+ activity.error = event.error || event.message || event.content || t(activity.lang, 'error.generic');
727
+ pushActivityEvent(activity, `❌ ${compact(activity.error, 120)}`);
728
+ return;
729
+ }
730
+ if (event.kind === 'session_created') {
731
+ activity.status = 'running';
732
+ activity.phase = t(activity.lang, 'control.activity.sessionStarted');
733
+ pushActivityEvent(activity, `🧵 ${t(activity.lang, 'control.activity.sessionStarted')}`);
734
+ return;
735
+ }
736
+ if (event.kind === 'thinking') {
737
+ activity.status = 'running';
738
+ activity.phase = t(activity.lang, 'control.activity.thinking');
739
+ const thought = extractTextFromEvent(event);
740
+ if (thought)
741
+ pushActivityEvent(activity, `🧠 ${compact(thought, 120)}`);
742
+ return;
743
+ }
744
+ if (event.kind === 'stream_delta' || event.kind === 'text') {
745
+ activity.status = 'running';
746
+ activity.phase = t(activity.lang, 'control.activity.responding');
747
+ activity.output = `${activity.output}${extractTextFromEvent(event)}`;
748
+ return;
749
+ }
750
+ if (event.type === 'claude-response' && event.data?.type === 'assistant') {
751
+ activity.status = 'running';
752
+ activity.phase = t(activity.lang, 'control.activity.responding');
753
+ activity.output = `${activity.output}${extractTextFromEvent(event)}`;
754
+ return;
755
+ }
756
+ if (event.kind === 'tool_use') {
757
+ activity.status = 'running';
758
+ activity.phase = t(activity.lang, 'control.activity.working');
759
+ const toolName = event.toolName || 'Tool';
760
+ const input = describeToolInput(toolName, event.toolInput);
761
+ const icon = toolName === 'Bash'
762
+ ? '💻'
763
+ : toolName === 'FileChanges'
764
+ ? '📝'
765
+ : toolName === 'WebSearch'
766
+ ? '🔎'
767
+ : '🔧';
768
+ pushActivityEvent(activity, `${icon} ${toolName}${input ? `: ${input}` : ''}`);
769
+ return;
770
+ }
771
+ if (event.kind === 'tool_result') {
772
+ activity.status = 'running';
773
+ activity.phase = t(activity.lang, 'control.activity.working');
774
+ const label = event.isError
775
+ ? t(activity.lang, 'control.activity.toolFailed')
776
+ : t(activity.lang, 'control.activity.toolDone');
777
+ pushActivityEvent(activity, `${event.isError ? '⚠️' : '✅'} ${label}`);
778
+ return;
779
+ }
780
+ if (event.kind === 'status' && event.text === 'token_budget') {
781
+ const used = event.tokenBudget?.used;
782
+ if (used)
783
+ pushActivityEvent(activity, `📊 ${t(activity.lang, 'control.activity.tokens')}: ${used}`);
784
+ }
785
+ }
786
+ function confirmationLabel(lang, action, payload = {}) {
787
+ if (action === 'install_provider') {
788
+ return t(lang, 'control.confirmInstall', { provider: payload.provider || '' });
789
+ }
790
+ if (action === 'run_cancel') {
791
+ return t(lang, 'control.confirmCancelRun', { runId: payload.runId || '' });
792
+ }
793
+ return t(lang, 'control.confirmationRequired');
794
+ }
795
+ async function requestConfirmation({ bot, chatId, link, action, payload = {}, editMessageId }) {
796
+ const lang = languageFor(link);
797
+ const pending = createTelegramConfirmation(link.user_id, action, payload);
798
+ await send(bot, chatId, confirmationLabel(lang, action, payload), {
799
+ editMessageId,
800
+ reply_markup: {
801
+ inline_keyboard: [[
802
+ button(t(lang, 'control.button.confirm'), 'confirm_action', { id: pending.id }),
803
+ button(t(lang, 'control.button.cancel'), 'cancel_confirmation', { id: pending.id }),
804
+ ]],
805
+ },
806
+ });
807
+ return { ok: false, requiresConfirmation: true };
808
+ }
809
+ async function sendToolFailure({ bot, chatId, link, result, editMessageId }) {
810
+ const lang = languageFor(link);
811
+ const message = result?.message === 'REMOTE_CONTROL_DISABLED'
812
+ ? t(lang, 'control.disabled')
813
+ : (result?.message || t(lang, 'error.generic'));
814
+ await send(bot, chatId, message, { editMessageId });
815
+ }
816
+ async function runAgent({ bot, chatId, link, prompt, activity = null }) {
432
817
  const lang = languageFor(link);
433
818
  const state = getState(link.user_id);
434
819
  if (!state.remoteControlEnabled) {
435
- await send(bot, chatId, t(lang, 'control.disabled'));
820
+ await send(bot, chatId, t(lang, 'control.disabled'), { editMessageId: activity?.messageId });
436
821
  return;
437
822
  }
438
823
  if (!state.selectedProjectPath) {
439
- await send(bot, chatId, t(lang, 'control.selectProjectFirst'));
440
- await showProjectMenu({ bot, chatId, link });
824
+ await showProjectMenu({ bot, chatId, link, editMessageId: activity?.messageId, notice: t(lang, 'control.selectProjectFirst') });
441
825
  return;
442
826
  }
443
- if (state.progressMode !== 'final') {
444
- await send(bot, chatId, t(lang, 'control.agentStarted', {
445
- provider: state.selectedProvider,
446
- project: state.selectedProjectName || state.selectedProjectPath,
447
- }));
448
- }
449
- const response = await localApi(link.user_id, '/api/agent', {
450
- method: 'POST',
451
- body: {
827
+ const provider = resolveTelegramProvider(state);
828
+ const model = resolveTelegramModel(state);
829
+ const active = activity || await createTelegramActivity({
830
+ bot,
831
+ chatId,
832
+ link,
833
+ type: 'agent',
834
+ prompt,
835
+ phase: t(lang, 'control.activity.startingProvider', { provider }),
836
+ });
837
+ active.type = 'agent';
838
+ active.provider = provider;
839
+ active.project = projectLabel(state);
840
+ active.status = 'running';
841
+ active.phase = t(lang, 'control.activity.startingProvider', { provider });
842
+ await editTelegramActivity({ bot, chatId, activity: active, force: true });
843
+ const heartbeat = startActivityHeartbeat({ bot, chatId, activity: active });
844
+ let streamFailed = null;
845
+ try {
846
+ await localAgentStream(link.user_id, {
452
847
  projectPath: state.selectedProjectPath,
453
- provider: state.selectedProvider,
454
- model: state.selectedModel || undefined,
455
- message: prompt,
848
+ provider,
849
+ model: model || undefined,
850
+ message: buildTelegramAgentPrompt(prompt, state),
456
851
  cleanup: false,
457
- stream: false,
458
- },
459
- });
460
- const assistantText = extractAssistantText(response);
461
- const statusLine = response.success === false
462
- ? t(lang, 'control.agentFailed', { error: response.error || 'provider returned no answer' })
463
- : t(lang, 'control.agentDone', { provider: state.selectedProvider, session: response.sessionId ? ` (${response.sessionId})` : '' });
464
- await send(bot, chatId, `${statusLine}\n\n${assistantText || response.error || t(lang, 'control.noAssistantText')}`);
852
+ permissionMode: 'default',
853
+ }, async (event) => {
854
+ applyAgentStreamEvent(active, event);
855
+ await editTelegramActivity({ bot, chatId, activity: active });
856
+ });
857
+ }
858
+ catch (error) {
859
+ streamFailed = error;
860
+ }
861
+ finally {
862
+ clearInterval(heartbeat);
863
+ }
864
+ if (streamFailed) {
865
+ active.status = 'failed';
866
+ active.phase = t(lang, 'control.activity.failed');
867
+ active.error = streamFailed.message || t(lang, 'error.generic');
868
+ await editTelegramActivity({ bot, chatId, activity: active, force: true });
869
+ return;
870
+ }
871
+ active.status = active.error ? 'failed' : 'done';
872
+ active.phase = active.error ? t(lang, 'control.activity.failed') : t(lang, 'control.activity.done');
873
+ if (!active.output && !active.error)
874
+ active.output = t(lang, 'control.noAssistantText');
875
+ await editTelegramActivity({ bot, chatId, activity: active, force: true });
465
876
  }
466
- export async function runWorkflow({ bot, chatId, link, input }) {
877
+ export async function runWorkflow({ bot, chatId, link, input, activity = null }) {
467
878
  const lang = languageFor(link);
468
879
  const state = getState(link.user_id);
469
880
  const workflowId = state.selectedWorkflowId;
881
+ if (!state.remoteControlEnabled) {
882
+ await send(bot, chatId, t(lang, 'control.disabled'), { editMessageId: activity?.messageId });
883
+ return;
884
+ }
470
885
  if (!workflowId) {
471
- await send(bot, chatId, t(lang, 'control.selectWorkflowFirst'));
472
- await showWorkflowMenu({ bot, chatId, link });
886
+ await send(bot, chatId, t(lang, 'control.selectWorkflowFirst'), { editMessageId: activity?.messageId });
887
+ await showWorkflowMenu({ bot, chatId, link, editMessageId: activity?.messageId });
473
888
  return;
474
889
  }
475
890
  if (!state.selectedProjectPath) {
476
- await send(bot, chatId, t(lang, 'control.selectProjectFirst'));
477
- await showProjectMenu({ bot, chatId, link });
891
+ await showProjectMenu({ bot, chatId, link, editMessageId: activity?.messageId, notice: t(lang, 'control.selectProjectFirst') });
478
892
  return;
479
893
  }
480
- const run = await localApi(link.user_id, `/api/orchestration/workflows/${workflowId}/runs`, {
481
- method: 'POST',
482
- body: {
483
- input,
484
- metadata: {
485
- projectId: state.selectedProjectName,
486
- projectName: state.selectedProjectName,
487
- projectPath: state.selectedProjectPath,
488
- workspaceTarget: 'selected_project',
489
- telegram: true,
490
- preferredProvider: state.selectedProvider,
491
- preferredModel: state.selectedModel,
894
+ const provider = resolveTelegramProvider(state);
895
+ const model = resolveTelegramModel(state);
896
+ const active = activity || await createTelegramActivity({
897
+ bot,
898
+ chatId,
899
+ link,
900
+ type: 'workflow',
901
+ prompt: input,
902
+ phase: t(lang, 'control.activity.startingWorkflow'),
903
+ });
904
+ active.type = 'workflow';
905
+ active.provider = provider;
906
+ active.project = projectLabel(state);
907
+ active.workflowId = workflowId;
908
+ active.status = 'running';
909
+ active.phase = t(lang, 'control.activity.startingWorkflow');
910
+ await editTelegramActivity({ bot, chatId, activity: active, force: true });
911
+ const result = await runTelegramTool({
912
+ userId: link.user_id,
913
+ action: 'run_workflow',
914
+ execute: () => localApi(link.user_id, `/api/orchestration/workflows/${workflowId}/runs`, {
915
+ method: 'POST',
916
+ body: {
917
+ input,
918
+ metadata: {
919
+ projectId: state.selectedProjectName,
920
+ projectName: state.selectedProjectName,
921
+ projectPath: state.selectedProjectPath,
922
+ workspaceTarget: 'selected_project',
923
+ telegram: true,
924
+ preferredProvider: provider,
925
+ preferredModel: model,
926
+ },
492
927
  },
493
- },
928
+ }),
494
929
  });
495
- await send(bot, chatId, t(lang, 'control.workflowStarted', { runId: run.id, workflowId }));
496
- monitorWorkflowRun({ bot, chatId, link, runId: run.id }).catch((error) => {
930
+ if (!result.ok) {
931
+ await sendToolFailure({ bot, chatId, link, result, editMessageId: active.messageId });
932
+ return;
933
+ }
934
+ const run = result.data;
935
+ active.runId = run.id;
936
+ active.phase = t(lang, 'control.activity.workflowRunning');
937
+ pushActivityEvent(active, `🧭 ${t(lang, 'control.workflowStarted', { runId: run.id, workflowId }).replace('\n', ' ')}`);
938
+ await editTelegramActivity({ bot, chatId, activity: active, force: true });
939
+ monitorWorkflowRun({ bot, chatId, link, runId: run.id, activity: active }).catch((error) => {
497
940
  console.warn('[telegram-control] workflow monitor failed:', error?.message || error);
498
941
  });
499
942
  }
943
+ async function cancelRun({ bot, chatId, link, runId, editMessageId, confirmed = false }) {
944
+ const lang = languageFor(link);
945
+ if (!runId) {
946
+ await send(bot, chatId, t(lang, 'control.cancelUsage'), { editMessageId });
947
+ return;
948
+ }
949
+ const state = getState(link.user_id);
950
+ if (state.remoteControlEnabled === false) {
951
+ await send(bot, chatId, t(lang, 'control.disabled'), { editMessageId });
952
+ return;
953
+ }
954
+ if (state.confirmationPolicy === 'strict' && !confirmed) {
955
+ await requestConfirmation({
956
+ bot,
957
+ chatId,
958
+ link,
959
+ action: 'run_cancel',
960
+ payload: { runId },
961
+ editMessageId,
962
+ });
963
+ return;
964
+ }
965
+ const result = await runTelegramTool({
966
+ userId: link.user_id,
967
+ action: 'run_cancel',
968
+ execute: () => localApi(link.user_id, `/api/orchestration/workflows/runs/${encodeURIComponent(runId)}/cancel`, {
969
+ method: 'POST',
970
+ }),
971
+ });
972
+ if (!result.ok) {
973
+ await sendToolFailure({ bot, chatId, link, result, editMessageId });
974
+ return;
975
+ }
976
+ const run = result.data;
977
+ await send(bot, chatId, t(lang, 'control.runStatus', { runId: run.id, status: run.status }), { editMessageId });
978
+ }
979
+ async function executeConfirmedAction({ bot, chatId, link, confirmation, editMessageId }) {
980
+ if (confirmation.action === 'install_provider') {
981
+ await startCliInstall({
982
+ bot,
983
+ chatId,
984
+ link,
985
+ provider: confirmation.payload?.provider,
986
+ editMessageId,
987
+ confirmed: true,
988
+ });
989
+ return true;
990
+ }
991
+ if (confirmation.action === 'run_cancel') {
992
+ await cancelRun({
993
+ bot,
994
+ chatId,
995
+ link,
996
+ runId: confirmation.payload?.runId,
997
+ editMessageId,
998
+ confirmed: true,
999
+ });
1000
+ return true;
1001
+ }
1002
+ return false;
1003
+ }
1004
+ async function findProjectByQuery(query) {
1005
+ const projects = await listProjects();
1006
+ const needle = String(query || '').trim().toLocaleLowerCase('tr');
1007
+ if (!needle)
1008
+ return null;
1009
+ return projects.find((project) => {
1010
+ const candidates = [project.name, project.displayName, project.fullPath, project.path]
1011
+ .filter(Boolean)
1012
+ .map((value) => String(value).toLocaleLowerCase('tr'));
1013
+ return candidates.some((candidate) => candidate === needle || candidate.includes(needle));
1014
+ }) || null;
1015
+ }
1016
+ async function listWorkflowSummaries(userId) {
1017
+ try {
1018
+ const data = await localApi(userId, '/api/orchestration/workflows', { timeoutMs: 10_000 });
1019
+ if (Array.isArray(data))
1020
+ return data;
1021
+ if (Array.isArray(data?.workflows))
1022
+ return data.workflows;
1023
+ }
1024
+ catch {
1025
+ // Workflow context is helpful for routing but should never block chat input.
1026
+ }
1027
+ return [];
1028
+ }
1029
+ async function resolveTelegramAiIntent({ bot, chatId, link, text, activity }) {
1030
+ const state = getState(link.user_id);
1031
+ if (state.routerEnabled === false || state.routerMode !== 'hybrid') {
1032
+ return { action: 'agent_prompt', prompt: text, confidence: 1 };
1033
+ }
1034
+ const provider = resolveTelegramProvider(state);
1035
+ const model = resolveTelegramModel(state);
1036
+ if (activity) {
1037
+ activity.type = 'router';
1038
+ activity.provider = provider;
1039
+ activity.project = projectLabel(state);
1040
+ activity.status = 'running';
1041
+ activity.phase = t(activity.lang, 'control.activity.interpreting');
1042
+ pushActivityEvent(activity, `🧠 ${t(activity.lang, 'control.activity.interpreting')}`);
1043
+ await editTelegramActivity({ bot, chatId, activity, force: true });
1044
+ }
1045
+ try {
1046
+ const projects = await listProjects().catch(() => []);
1047
+ const workflows = await listWorkflowSummaries(link.user_id);
1048
+ const response = await localApi(link.user_id, '/api/agent', {
1049
+ method: 'POST',
1050
+ timeoutMs: INTENT_ROUTER_TIMEOUT_MS,
1051
+ body: {
1052
+ projectPath: state.selectedProjectPath || process.cwd(),
1053
+ provider,
1054
+ model: model || undefined,
1055
+ message: buildTelegramIntentPrompt(text, state, { projects, workflows }),
1056
+ cleanup: false,
1057
+ stream: false,
1058
+ permissionMode: 'plan',
1059
+ },
1060
+ });
1061
+ const assistantText = extractAssistantText(response);
1062
+ const intent = parseTelegramAiIntentResponse(assistantText, text);
1063
+ if (activity) {
1064
+ pushActivityEvent(activity, `🧭 ${intent.action} (${Math.round(intent.confidence * 100)}%)`);
1065
+ await editTelegramActivity({ bot, chatId, activity });
1066
+ }
1067
+ return intent;
1068
+ }
1069
+ catch (error) {
1070
+ if (activity) {
1071
+ pushActivityEvent(activity, `⚠️ ${t(activity.lang, 'control.activity.routerFallback')}`);
1072
+ await editTelegramActivity({ bot, chatId, activity });
1073
+ }
1074
+ console.warn('[telegram-control] AI intent router fallback:', error?.message || error);
1075
+ return { action: 'agent_prompt', prompt: text, confidence: 0 };
1076
+ }
1077
+ }
1078
+ async function handleRoutedIntent({ bot, chatId, link, text, activity }) {
1079
+ const intent = await resolveTelegramAiIntent({ bot, chatId, link, text, activity });
1080
+ const editMessageId = activity?.messageId;
1081
+ if (intent.action === 'agent_prompt') {
1082
+ await runAgent({ bot, chatId, link, prompt: intent.prompt || text, activity });
1083
+ return true;
1084
+ }
1085
+ if (intent.action === 'show_menu') {
1086
+ await showMainMenu({ bot, chatId, link, editMessageId });
1087
+ return true;
1088
+ }
1089
+ if (intent.action === 'show_projects') {
1090
+ await showProjectMenu({ bot, chatId, link, editMessageId });
1091
+ return true;
1092
+ }
1093
+ if (intent.action === 'select_project') {
1094
+ const query = intent.projectQuery || intent.prompt || text;
1095
+ const project = await findProjectByQuery(query);
1096
+ const lang = languageFor(link);
1097
+ if (!project) {
1098
+ await showProjectMenu({
1099
+ bot,
1100
+ chatId,
1101
+ link,
1102
+ editMessageId,
1103
+ notice: t(lang, 'control.projectNotFound', { query }),
1104
+ });
1105
+ return true;
1106
+ }
1107
+ updateTelegramControlState(link.user_id, {
1108
+ selectedProjectName: project.name,
1109
+ selectedProjectPath: project.fullPath || project.path,
1110
+ });
1111
+ await showMainMenu({
1112
+ bot,
1113
+ chatId,
1114
+ link,
1115
+ editMessageId,
1116
+ notice: t(lang, 'control.projectSelected', {
1117
+ project: project.displayName || project.name,
1118
+ path: project.fullPath || project.path,
1119
+ }),
1120
+ });
1121
+ return true;
1122
+ }
1123
+ if (intent.action === 'show_provider_menu') {
1124
+ await showProviderMenu({ bot, chatId, link, editMessageId });
1125
+ return true;
1126
+ }
1127
+ if (intent.action === 'select_provider' && intent.provider) {
1128
+ updateTelegramControlState(link.user_id, { selectedProvider: intent.provider, selectedModel: null });
1129
+ await showModelMenu({ bot, chatId, link, editMessageId });
1130
+ return true;
1131
+ }
1132
+ if (intent.action === 'show_model_menu') {
1133
+ await showModelMenu({ bot, chatId, link, editMessageId });
1134
+ return true;
1135
+ }
1136
+ if (intent.action === 'select_model' && intent.model) {
1137
+ updateTelegramControlState(link.user_id, { selectedModel: intent.model });
1138
+ await showMainMenu({
1139
+ bot,
1140
+ chatId,
1141
+ link,
1142
+ editMessageId,
1143
+ notice: t(languageFor(link), 'control.modelSelected', { model: intent.model }),
1144
+ });
1145
+ return true;
1146
+ }
1147
+ if (intent.action === 'show_runs') {
1148
+ await showRuns({ bot, chatId, link, editMessageId });
1149
+ return true;
1150
+ }
1151
+ if (intent.action === 'show_approvals') {
1152
+ await showApprovalQueue({ bot, chatId, link, editMessageId });
1153
+ return true;
1154
+ }
1155
+ if (intent.action === 'show_workflows') {
1156
+ await showWorkflowMenu({ bot, chatId, link, editMessageId });
1157
+ return true;
1158
+ }
1159
+ if (intent.action === 'run_workflow') {
1160
+ await runWorkflow({ bot, chatId, link, input: intent.workflowInput || text, activity });
1161
+ return true;
1162
+ }
1163
+ if (intent.action === 'show_sessions') {
1164
+ await showSessions({ bot, chatId, link, editMessageId });
1165
+ return true;
1166
+ }
1167
+ if (intent.action === 'new_chat') {
1168
+ await startNewChat({ bot, chatId, link, editMessageId });
1169
+ return true;
1170
+ }
1171
+ await runAgent({ bot, chatId, link, prompt: text, activity });
1172
+ return true;
1173
+ }
500
1174
  async function fetchRun(userId, runId) {
501
1175
  return localApi(userId, `/api/orchestration/workflows/runs/${runId}`);
502
1176
  }
503
1177
  function summarizeRun(run, mode) {
1178
+ const statusIcon = run.status === 'completed'
1179
+ ? '✅'
1180
+ : run.status === 'failed'
1181
+ ? '❌'
1182
+ : run.status === 'canceled'
1183
+ ? '⏹'
1184
+ : '🔧';
504
1185
  const lines = [
505
- `Run ${run.id}`,
506
- `Workflow: ${run.workflowId}`,
507
- `Status: ${run.status}`,
1186
+ `${statusIcon} Run ${run.id}`,
1187
+ `🧩 Workflow: ${run.workflowId}`,
1188
+ `📌 Status: ${run.status}`,
508
1189
  ];
509
1190
  const nodeRuns = Array.isArray(run.nodeRuns) ? run.nodeRuns : [];
510
1191
  if (mode !== 'final') {
@@ -512,7 +1193,14 @@ function summarizeRun(run, mode) {
512
1193
  ? nodeRuns.filter((node) => node.error || node.status === 'failed')
513
1194
  : nodeRuns;
514
1195
  for (const node of visibleNodes) {
515
- lines.push(`- ${node.status}: ${node.agentLabel || node.nodeId}${node.error ? ` — ${node.error}` : ''}`);
1196
+ const nodeIcon = node.status === 'completed'
1197
+ ? '✅'
1198
+ : node.status === 'failed'
1199
+ ? '❌'
1200
+ : node.status === 'running'
1201
+ ? '🔧'
1202
+ : '⏳';
1203
+ lines.push(`${nodeIcon} ${node.status}: ${node.agentLabel || node.nodeId}${node.error ? ` - ${node.error}` : ''}`);
516
1204
  }
517
1205
  }
518
1206
  const outputs = nodeRuns
@@ -521,7 +1209,7 @@ function summarizeRun(run, mode) {
521
1209
  .map((node) => `\n${node.agentLabel || node.nodeId}:\n${node.outputText}`);
522
1210
  return truncate(`${lines.join('\n')}${outputs.join('\n')}`);
523
1211
  }
524
- async function monitorWorkflowRun({ bot, chatId, link, runId }) {
1212
+ async function monitorWorkflowRun({ bot, chatId, link, runId, activity = null }) {
525
1213
  if (runMonitors.has(runId))
526
1214
  return;
527
1215
  runMonitors.set(runId, true);
@@ -532,12 +1220,24 @@ async function monitorWorkflowRun({ bot, chatId, link, runId }) {
532
1220
  await new Promise((resolve) => setTimeout(resolve, 5000));
533
1221
  const run = await fetchRun(link.user_id, runId);
534
1222
  const nodeRuns = Array.isArray(run.nodeRuns) ? run.nodeRuns : [];
1223
+ if (activity && !TERMINAL_RUN_STATES.has(run.status)) {
1224
+ activity.status = 'running';
1225
+ activity.phase = `${t(activity.lang, 'control.activity.workflowRunning')} (${run.status})`;
1226
+ await editTelegramActivity({ bot, chatId, activity });
1227
+ }
535
1228
  if (state.progressMode === 'all') {
536
1229
  for (const node of nodeRuns) {
537
1230
  const key = `${node.nodeId}:${node.status}`;
538
1231
  if (!seenNodeStatus.has(key)) {
539
1232
  seenNodeStatus.set(key, true);
540
- await send(bot, chatId, `${node.agentLabel || node.nodeId}: ${node.status}${node.error ? `\n${node.error}` : ''}`);
1233
+ if (activity) {
1234
+ pushActivityEvent(activity, `${node.status === 'completed' ? '✅' : node.status === 'failed' ? '❌' : '🔧'} ${node.agentLabel || node.nodeId}: ${node.status}${node.error ? ` - ${node.error}` : ''}`);
1235
+ activity.phase = t(activity.lang, 'control.activity.workflowRunning');
1236
+ await editTelegramActivity({ bot, chatId, activity });
1237
+ }
1238
+ else {
1239
+ await send(bot, chatId, `${node.agentLabel || node.nodeId}: ${node.status}${node.error ? `\n${node.error}` : ''}`);
1240
+ }
541
1241
  }
542
1242
  }
543
1243
  }
@@ -546,12 +1246,29 @@ async function monitorWorkflowRun({ bot, chatId, link, runId }) {
546
1246
  const key = `${node.nodeId}:${node.status}:${node.error || ''}`;
547
1247
  if (!seenNodeStatus.has(key)) {
548
1248
  seenNodeStatus.set(key, true);
549
- await send(bot, chatId, `${node.agentLabel || node.nodeId}: ${node.status}\n${node.error || ''}`);
1249
+ if (activity) {
1250
+ pushActivityEvent(activity, `❌ ${node.agentLabel || node.nodeId}: ${node.status}${node.error ? ` - ${node.error}` : ''}`);
1251
+ activity.phase = t(activity.lang, 'control.activity.workflowRunning');
1252
+ await editTelegramActivity({ bot, chatId, activity });
1253
+ }
1254
+ else {
1255
+ await send(bot, chatId, `${node.agentLabel || node.nodeId}: ${node.status}\n${node.error || ''}`);
1256
+ }
550
1257
  }
551
1258
  }
552
1259
  }
553
1260
  if (TERMINAL_RUN_STATES.has(run.status)) {
554
- await send(bot, chatId, summarizeRun(run, state.progressMode));
1261
+ if (activity) {
1262
+ activity.status = run.status === 'completed' ? 'done' : 'failed';
1263
+ activity.phase = run.status === 'completed'
1264
+ ? t(activity.lang, 'control.activity.done')
1265
+ : t(activity.lang, 'control.activity.failed');
1266
+ activity.output = summarizeRun(run, state.progressMode);
1267
+ await editTelegramActivity({ bot, chatId, activity, force: true });
1268
+ }
1269
+ else {
1270
+ await send(bot, chatId, summarizeRun(run, state.progressMode));
1271
+ }
555
1272
  return;
556
1273
  }
557
1274
  }
@@ -560,18 +1277,47 @@ async function monitorWorkflowRun({ bot, chatId, link, runId }) {
560
1277
  runMonitors.delete(runId);
561
1278
  }
562
1279
  }
563
- export async function startCliInstall({ bot, chatId, link, provider }) {
1280
+ export async function startCliInstall({ bot, chatId, link, provider, editMessageId, confirmed = false }) {
564
1281
  const lang = languageFor(link);
565
- const data = await localApi(link.user_id, `/api/providers/${provider}/install`, { method: 'POST' });
1282
+ if (!PROVIDERS.includes(provider)) {
1283
+ await send(bot, chatId, t(lang, 'control.providerAuthFallback'), { editMessageId });
1284
+ return;
1285
+ }
1286
+ const state = getState(link.user_id);
1287
+ if (state.remoteControlEnabled === false) {
1288
+ await send(bot, chatId, t(lang, 'control.disabled'), { editMessageId });
1289
+ return;
1290
+ }
1291
+ if (state.confirmationPolicy === 'strict' && !confirmed) {
1292
+ await requestConfirmation({
1293
+ bot,
1294
+ chatId,
1295
+ link,
1296
+ action: 'install_provider',
1297
+ payload: { provider },
1298
+ editMessageId,
1299
+ });
1300
+ return;
1301
+ }
1302
+ const result = await runTelegramTool({
1303
+ userId: link.user_id,
1304
+ action: 'install_provider',
1305
+ execute: () => localApi(link.user_id, `/api/providers/${provider}/install`, { method: 'POST' }),
1306
+ });
1307
+ if (!result.ok) {
1308
+ await sendToolFailure({ bot, chatId, link, result, editMessageId });
1309
+ return;
1310
+ }
1311
+ const data = result.data;
566
1312
  if (data?.manual) {
567
- await send(bot, chatId, t(lang, 'control.manualInstall', { provider, manual: data.manual }));
1313
+ await send(bot, chatId, t(lang, 'control.manualInstall', { provider, manual: data.manual }), { editMessageId });
568
1314
  return;
569
1315
  }
570
1316
  await send(bot, chatId, t(lang, 'control.installStarted', {
571
1317
  provider,
572
1318
  jobId: data.jobId,
573
1319
  command: data.installCmd || 'internal installer',
574
- }));
1320
+ }), { editMessageId });
575
1321
  }
576
1322
  async function showInstallMenu({ bot, chatId, link, editMessageId }) {
577
1323
  const lang = languageFor(link);
@@ -747,15 +1493,12 @@ async function handleCommand({ bot, chatId, link, text }) {
747
1493
  await send(bot, chatId, t(lang, 'control.cancelUsage'));
748
1494
  return true;
749
1495
  }
750
- const run = await localApi(link.user_id, `/api/orchestration/workflows/runs/${encodeURIComponent(argText)}/cancel`, {
751
- method: 'POST',
752
- });
753
- await send(bot, chatId, t(lang, 'control.runStatus', { runId: run.id, status: run.status }));
1496
+ await cancelRun({ bot, chatId, link, runId: argText });
754
1497
  return true;
755
1498
  }
756
1499
  return false;
757
1500
  }
758
- export async function handleTelegramControlMessage({ bot, msg, link }) {
1501
+ async function handleTelegramControlMessageInternal({ bot, msg, link }) {
759
1502
  const chatId = msg.chat.id;
760
1503
  const text = String(msg.text || '').trim();
761
1504
  if (!text)
@@ -773,12 +1516,36 @@ export async function handleTelegramControlMessage({ bot, msg, link }) {
773
1516
  if (await handleCommand({ bot, chatId, link, text }))
774
1517
  return true;
775
1518
  const state = getState(link.user_id);
1519
+ if (state.routerEnabled === false)
1520
+ return false;
776
1521
  if (!state.remoteControlEnabled) {
777
1522
  await send(bot, chatId, t(languageFor(link), 'control.disabled'));
778
1523
  return true;
779
1524
  }
780
- await runAgent({ bot, chatId, link, prompt: text });
781
- return true;
1525
+ const activity = await createTelegramActivity({
1526
+ bot,
1527
+ chatId,
1528
+ link,
1529
+ type: 'router',
1530
+ prompt: text,
1531
+ phase: t(languageFor(link), 'control.activity.interpreting'),
1532
+ });
1533
+ return handleRoutedIntent({ bot, chatId, link, text, activity });
1534
+ }
1535
+ export async function handleTelegramControlMessage(args) {
1536
+ const chatId = args?.msg?.chat?.id;
1537
+ if (!chatId)
1538
+ return false;
1539
+ return enqueueTelegramJob(chatId, async () => {
1540
+ try {
1541
+ return await handleTelegramControlMessageInternal(args);
1542
+ }
1543
+ catch (error) {
1544
+ console.error('[telegram-control] message handler failed:', error);
1545
+ await send(args.bot, chatId, t(languageFor(args.link), 'error.generic')).catch(() => { });
1546
+ return true;
1547
+ }
1548
+ });
782
1549
  }
783
1550
  async function showRunDetail({ bot, chatId, link, runId, editMessageId }) {
784
1551
  const lang = languageFor(link);
@@ -793,7 +1560,7 @@ async function showRunDetail({ bot, chatId, link, runId, editMessageId }) {
793
1560
  },
794
1561
  });
795
1562
  }
796
- export async function handleTelegramControlCallback({ bot, query, link }) {
1563
+ async function handleTelegramControlCallbackInternal({ bot, query, link }) {
797
1564
  const chatId = query.message?.chat?.id;
798
1565
  if (!chatId)
799
1566
  return;
@@ -807,6 +1574,33 @@ export async function handleTelegramControlCallback({ bot, query, link }) {
807
1574
  }
808
1575
  await bot.answerCallbackQuery(query.id).catch(() => { });
809
1576
  const { action, payload } = entry;
1577
+ if (action === 'confirm_action') {
1578
+ forgetAction(entry.id);
1579
+ const lang = languageFor(link);
1580
+ const result = consumeTelegramConfirmation(link.user_id, payload.id);
1581
+ if (!result.ok) {
1582
+ await send(bot, chatId, t(lang, result.reason === 'expired'
1583
+ ? 'control.confirmationExpired'
1584
+ : 'control.confirmationMissing'), { editMessageId });
1585
+ return;
1586
+ }
1587
+ if (await executeConfirmedAction({
1588
+ bot,
1589
+ chatId,
1590
+ link,
1591
+ confirmation: result.confirmation,
1592
+ editMessageId,
1593
+ }))
1594
+ return;
1595
+ await send(bot, chatId, t(lang, 'error.generic'), { editMessageId });
1596
+ return;
1597
+ }
1598
+ if (action === 'cancel_confirmation') {
1599
+ forgetAction(entry.id);
1600
+ clearTelegramConfirmation(link.user_id, payload.id);
1601
+ await send(bot, chatId, t(languageFor(link), 'control.confirmationCanceled'), { editMessageId });
1602
+ return;
1603
+ }
810
1604
  if (action === 'menu')
811
1605
  return showMainMenu({ bot, chatId, link, editMessageId });
812
1606
  if (action === 'projects')
@@ -889,20 +1683,26 @@ export async function handleTelegramControlCallback({ bot, query, link }) {
889
1683
  if (action === 'run_detail')
890
1684
  return showRunDetail({ bot, chatId, link, runId: payload.runId, editMessageId });
891
1685
  if (action === 'run_cancel') {
892
- const run = await localApi(link.user_id, `/api/orchestration/workflows/runs/${encodeURIComponent(payload.runId)}/cancel`, {
893
- method: 'POST',
894
- });
895
- await send(bot, chatId, t(languageFor(link), 'control.runStatus', { runId: run.id, status: run.status }), { editMessageId });
1686
+ await cancelRun({ bot, chatId, link, runId: payload.runId, editMessageId });
896
1687
  return;
897
1688
  }
898
1689
  if (action === 'approval_decide') {
899
- const result = await localApi(link.user_id, `/api/orchestration/workflows/approvals/${encodeURIComponent(payload.approvalId)}`, {
900
- method: 'POST',
901
- body: {
902
- allow: payload.allow === true,
903
- source: 'telegram',
904
- },
1690
+ const toolResult = await runTelegramTool({
1691
+ userId: link.user_id,
1692
+ action: 'approval_decide',
1693
+ execute: () => localApi(link.user_id, `/api/orchestration/workflows/approvals/${encodeURIComponent(payload.approvalId)}`, {
1694
+ method: 'POST',
1695
+ body: {
1696
+ allow: payload.allow === true,
1697
+ source: 'telegram',
1698
+ },
1699
+ }),
905
1700
  });
1701
+ if (!toolResult.ok) {
1702
+ await sendToolFailure({ bot, chatId, link, result: toolResult, editMessageId });
1703
+ return;
1704
+ }
1705
+ const result = toolResult.data;
906
1706
  const lang = languageFor(link);
907
1707
  await send(bot, chatId, t(lang, 'control.approvalDecided', {
908
1708
  approvalId: payload.approvalId,
@@ -912,7 +1712,7 @@ export async function handleTelegramControlCallback({ bot, query, link }) {
912
1712
  return showApprovalQueue({ bot, chatId, link });
913
1713
  }
914
1714
  if (action === 'install_provider')
915
- return startCliInstall({ bot, chatId, link, provider: payload.provider });
1715
+ return startCliInstall({ bot, chatId, link, provider: payload.provider, editMessageId });
916
1716
  if (action === 'auth_provider') {
917
1717
  await send(bot, chatId, `${payload.provider} login:\n${AUTH_HELP[payload.provider] || t(languageFor(link), 'control.providerAuthFallback')}`, { editMessageId });
918
1718
  return;
@@ -937,4 +1737,20 @@ export async function handleTelegramControlCallback({ bot, query, link }) {
937
1737
  });
938
1738
  }
939
1739
  }
1740
+ export async function handleTelegramControlCallback(args) {
1741
+ const chatId = args?.query?.message?.chat?.id;
1742
+ if (!chatId)
1743
+ return;
1744
+ return enqueueTelegramJob(chatId, async () => {
1745
+ try {
1746
+ await handleTelegramControlCallbackInternal(args);
1747
+ }
1748
+ catch (error) {
1749
+ console.error('[telegram-control] callback failed:', error);
1750
+ const lang = languageFor(args.link);
1751
+ await args.bot?.answerCallbackQuery(args.query?.id, { text: t(lang, 'error.generic') }).catch(() => { });
1752
+ await send(args.bot, chatId, t(lang, 'error.generic')).catch(() => { });
1753
+ }
1754
+ });
1755
+ }
940
1756
  //# sourceMappingURL=control-center.js.map