@pixelbyte-software/pixcode 1.53.5 → 1.53.7

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 (34) hide show
  1. package/dist/assets/{index-CyeODv14.js → index-DM-KfWAm.js} +3 -3
  2. package/dist/index.html +1 -1
  3. package/dist-server/server/claude-sdk.js +17 -15
  4. package/dist-server/server/claude-sdk.js.map +1 -1
  5. package/dist-server/server/cursor-cli.js +14 -12
  6. package/dist-server/server/cursor-cli.js.map +1 -1
  7. package/dist-server/server/gemini-cli.js +14 -12
  8. package/dist-server/server/gemini-cli.js.map +1 -1
  9. package/dist-server/server/openai-codex.js +30 -22
  10. package/dist-server/server/openai-codex.js.map +1 -1
  11. package/dist-server/server/opencode-cli.js +14 -12
  12. package/dist-server/server/opencode-cli.js.map +1 -1
  13. package/dist-server/server/qwen-code-cli.js +14 -12
  14. package/dist-server/server/qwen-code-cli.js.map +1 -1
  15. package/dist-server/server/routes/agent.js +24 -7
  16. package/dist-server/server/routes/agent.js.map +1 -1
  17. package/dist-server/server/services/telegram/control-center.js +593 -82
  18. package/dist-server/server/services/telegram/control-center.js.map +1 -1
  19. package/dist-server/server/services/telegram/telegram-gateway.js +111 -45
  20. package/dist-server/server/services/telegram/telegram-gateway.js.map +1 -1
  21. package/dist-server/server/services/telegram/translations.js +60 -0
  22. package/dist-server/server/services/telegram/translations.js.map +1 -1
  23. package/package.json +1 -1
  24. package/scripts/smoke/telegram-control.mjs +40 -1
  25. package/server/claude-sdk.js +3 -3
  26. package/server/cursor-cli.js +3 -3
  27. package/server/gemini-cli.js +3 -3
  28. package/server/openai-codex.js +10 -4
  29. package/server/opencode-cli.js +3 -3
  30. package/server/qwen-code-cli.js +3 -3
  31. package/server/routes/agent.js +24 -7
  32. package/server/services/telegram/control-center.js +593 -80
  33. package/server/services/telegram/telegram-gateway.js +115 -51
  34. package/server/services/telegram/translations.js +60 -0
@@ -8,11 +8,12 @@ import {
8
8
  TELEGRAM_CONTROL_SCOPES,
9
9
  TELEGRAM_PROVIDERS,
10
10
  buildTelegramAgentPrompt,
11
- classifyTelegramIntent,
11
+ buildTelegramIntentPrompt,
12
12
  clearTelegramConfirmation,
13
13
  consumeTelegramConfirmation,
14
14
  createTelegramConfirmation,
15
15
  enqueueTelegramJob,
16
+ parseTelegramAiIntentResponse,
16
17
  resolveTelegramModel,
17
18
  resolveTelegramProvider,
18
19
  retryWithBackoff,
@@ -26,6 +27,9 @@ const TERMINAL_RUN_STATES = new Set(['completed', 'failed', 'canceled']);
26
27
  const CALLBACK_TTL_MS = 10 * 60 * 1000;
27
28
  const MAX_CALLBACK_ACTIONS = 1000;
28
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;
29
33
  const callbackActions = new Map();
30
34
  const runMonitors = new Map();
31
35
 
@@ -230,16 +234,23 @@ function getOrCreateTelegramApiKey(userId) {
230
234
  return apiKeysDb.createApiKey(userId, 'Telegram Control', TELEGRAM_CONTROL_SCOPES).apiKey;
231
235
  }
232
236
 
233
- async function localApi(userId, path, { method = 'GET', body } = {}) {
237
+ async function localApi(userId, path, { method = 'GET', body, timeoutMs = 0 } = {}) {
234
238
  const apiKey = getOrCreateTelegramApiKey(userId);
235
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;
236
244
  const res = await fetch(`${localApiBase()}${path}`, {
237
245
  method,
238
246
  headers: {
239
247
  'Content-Type': 'application/json',
240
248
  'X-API-Key': apiKey,
241
249
  },
250
+ signal: controller?.signal,
242
251
  body: body === undefined ? undefined : JSON.stringify(body),
252
+ }).finally(() => {
253
+ if (timeoutId) clearTimeout(timeoutId);
243
254
  });
244
255
  if ([408, 429, 500, 502, 503, 504].includes(res.status)) {
245
256
  const error = new Error(`HTTP ${res.status}`);
@@ -256,10 +267,223 @@ async function localApi(userId, path, { method = 'GET', body } = {}) {
256
267
  return data?.data ?? data;
257
268
  }
258
269
 
270
+ async function localAgentStream(userId, body, onEvent) {
271
+ const apiKey = getOrCreateTelegramApiKey(userId);
272
+ const response = await fetch(`${localApiBase()}/api/agent`, {
273
+ method: 'POST',
274
+ headers: {
275
+ 'Content-Type': 'application/json',
276
+ 'X-API-Key': apiKey,
277
+ },
278
+ body: JSON.stringify({
279
+ ...body,
280
+ stream: true,
281
+ }),
282
+ });
283
+
284
+ if (!response.ok) {
285
+ const data = await response.json().catch(() => ({}));
286
+ const error = data?.error?.message || data?.error || `HTTP ${response.status}`;
287
+ throw new Error(typeof error === 'string' ? error : JSON.stringify(error));
288
+ }
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);
327
+ }
328
+
259
329
  function checked(label) {
260
330
  return `${label} ✓`;
261
331
  }
262
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 trimTelegramOutput(text, max, suffix = '') {
382
+ const value = String(text || '').trim();
383
+ const ending = String(suffix || '').trim();
384
+ if (value.length <= max) return value;
385
+ const room = Math.max(300, max - ending.length - 4);
386
+ return `${value.slice(0, room).trim()}\n\n${ending}`;
387
+ }
388
+
389
+ function renderActivity(activity, { finalText = null } = {}) {
390
+ const output = finalText || activity.output;
391
+ if (activity.type === 'agent' && output && !activity.error) {
392
+ if (activity.status === 'done') {
393
+ return trimTelegramOutput(
394
+ output,
395
+ 3400,
396
+ t(activity.lang, 'control.activity.outputTooLong'),
397
+ );
398
+ }
399
+
400
+ const footer = `⏳ ${t(activity.lang, 'control.activity.liveFooter', { elapsed: formatElapsed(activity.startedAt) })}`;
401
+ const body = trimTelegramOutput(
402
+ output,
403
+ 3200,
404
+ t(activity.lang, 'control.activity.outputShortened'),
405
+ );
406
+ return truncate(`${body}\n\n${footer}`, 3400);
407
+ }
408
+
409
+ const lines = [
410
+ `${activity.status === 'failed' ? '❌' : activity.status === 'done' ? '✅' : activity.status === 'running' ? '🔧' : '⏳'} ${activityTitle(activity)}`,
411
+ '',
412
+ `🤖 ${t(activity.lang, 'control.activity.provider')}: ${activity.provider || '-'}`,
413
+ `📁 ${t(activity.lang, 'control.activity.project')}: ${compact(activity.project, 90)}`,
414
+ ];
415
+
416
+ if (activity.sessionId) lines.push(`🧵 ${t(activity.lang, 'control.activity.session')}: ${activity.sessionId}`);
417
+ if (activity.runId) lines.push(`🧭 ${t(activity.lang, 'control.activity.run')}: ${activity.runId}`);
418
+ if (activity.workflowId) lines.push(`🧩 ${t(activity.lang, 'control.activity.workflow')}: ${activity.workflowId}`);
419
+ lines.push(`⏱ ${t(activity.lang, 'control.activity.elapsed')}: ${formatElapsed(activity.startedAt)}`);
420
+ lines.push(`📌 ${t(activity.lang, 'control.activity.status')}: ${activity.phase}`);
421
+
422
+ const visibleEvents = activity.mode === 'final'
423
+ ? activity.events.slice(-4)
424
+ : activity.events;
425
+ if (visibleEvents.length > 0) {
426
+ lines.push('', `🛠 ${t(activity.lang, 'control.activity.work')}:`);
427
+ for (const event of visibleEvents) lines.push(`• ${event}`);
428
+ }
429
+
430
+ if (activity.error) {
431
+ lines.push('', `⚠️ ${truncate(activity.error, 700)}`);
432
+ } else if (output) {
433
+ lines.push('', `💬 ${t(activity.lang, 'control.activity.output')}:`);
434
+ lines.push(truncate(output, 1800));
435
+ } else if (activity.prompt) {
436
+ lines.push('', `📝 ${compact(activity.prompt, 220)}`);
437
+ }
438
+
439
+ return truncate(lines.join('\n'), 3400);
440
+ }
441
+
442
+ async function createTelegramActivity({ bot, chatId, link, type = 'agent', prompt = '', phase = null }) {
443
+ const lang = languageFor(link);
444
+ const state = getState(link.user_id);
445
+ const activity = createActivityState({
446
+ lang,
447
+ type,
448
+ provider: resolveTelegramProvider(state),
449
+ project: projectLabel(state),
450
+ prompt,
451
+ mode: state.progressMode,
452
+ });
453
+ if (phase) activity.phase = phase;
454
+ const sent = await send(bot, chatId, renderActivity(activity), { parse_mode: undefined });
455
+ activity.messageId = sent?.message_id || sent?.message?.message_id || null;
456
+ activity.lastEditAt = Date.now();
457
+ return activity;
458
+ }
459
+
460
+ async function editTelegramActivity({ bot, chatId, activity, force = false, reply_markup }) {
461
+ if (!activity) return null;
462
+ const now = Date.now();
463
+ if (!force && now - activity.lastEditAt < ACTIVITY_EDIT_THROTTLE_MS) return null;
464
+ const sent = await send(bot, chatId, renderActivity(activity), {
465
+ editMessageId: activity.messageId,
466
+ parse_mode: undefined,
467
+ reply_markup,
468
+ });
469
+ activity.messageId = sent?.message_id || sent?.message?.message_id || activity.messageId;
470
+ activity.lastEditAt = now;
471
+ return sent;
472
+ }
473
+
474
+ function startActivityHeartbeat({ bot, chatId, activity }) {
475
+ return setInterval(() => {
476
+ if (!activity || activity.status === 'done' || activity.status === 'failed') return;
477
+ if (activity.status === 'starting') {
478
+ activity.status = 'running';
479
+ activity.phase = t(activity.lang, 'control.activity.thinking');
480
+ }
481
+ editTelegramActivity({ bot, chatId, activity }).catch((error) => {
482
+ console.warn('[telegram-control] activity heartbeat failed:', error?.message || error);
483
+ });
484
+ }, ACTIVITY_HEARTBEAT_MS);
485
+ }
486
+
263
487
  function stateSummary(lang, state) {
264
488
  return [
265
489
  `${t(lang, 'control.summary.project')}: ${state.selectedProjectName || t(lang, 'control.notSelected')}`,
@@ -310,11 +534,12 @@ async function listProjects() {
310
534
  return projects.slice(0, 20);
311
535
  }
312
536
 
313
- async function showProjectMenu({ bot, chatId, link, editMessageId }) {
537
+ async function showProjectMenu({ bot, chatId, link, editMessageId, notice }) {
314
538
  const lang = languageFor(link);
315
539
  const projects = await listProjects();
316
540
  if (projects.length === 0) {
317
- await send(bot, chatId, t(lang, 'control.noProjects'), { editMessageId });
541
+ const prefix = notice ? `${notice}\n\n` : '';
542
+ await send(bot, chatId, `${prefix}${t(lang, 'control.noProjects')}`, { editMessageId });
318
543
  return;
319
544
  }
320
545
 
@@ -327,7 +552,8 @@ async function showProjectMenu({ bot, chatId, link, editMessageId }) {
327
552
  displayName: project.displayName || project.name,
328
553
  },
329
554
  ));
330
- await send(bot, chatId, t(lang, 'control.pickProject'), {
555
+ const prefix = notice ? `${notice}\n\n` : '';
556
+ await send(bot, chatId, `${prefix}${t(lang, 'control.pickProject')}`, {
331
557
  editMessageId,
332
558
  reply_markup: { inline_keyboard: rows(buttons, 1) },
333
559
  });
@@ -539,6 +765,119 @@ function extractAssistantText(response) {
539
765
  return chunks.join('\n\n').trim();
540
766
  }
541
767
 
768
+ function extractTextFromEvent(event) {
769
+ if (!event || typeof event !== 'object') return '';
770
+ if (typeof event.content === 'string') return event.content;
771
+ if (Array.isArray(event.content)) {
772
+ return event.content
773
+ .map((part) => (typeof part === 'string' ? part : part?.text || ''))
774
+ .join('');
775
+ }
776
+ const legacyContent = event.data?.message?.content || event.message?.content;
777
+ if (Array.isArray(legacyContent)) {
778
+ return legacyContent
779
+ .map((part) => (typeof part === 'string' ? part : part?.text || ''))
780
+ .join('');
781
+ }
782
+ if (typeof legacyContent === 'string') return legacyContent;
783
+ return '';
784
+ }
785
+
786
+ function describeToolInput(toolName, input) {
787
+ if (!input || typeof input !== 'object') return '';
788
+ if (toolName === 'Bash' && typeof input.command === 'string') {
789
+ return compact(input.command, 110);
790
+ }
791
+ if (toolName === 'WebSearch' && typeof input.query === 'string') {
792
+ return compact(input.query, 110);
793
+ }
794
+ if (toolName === 'FileChanges') {
795
+ return compact(JSON.stringify(input), 110);
796
+ }
797
+ const keys = Object.keys(input).slice(0, 3);
798
+ if (keys.length === 0) return '';
799
+ return compact(keys.map((key) => `${key}: ${String(input[key])}`).join(', '), 110);
800
+ }
801
+
802
+ function applyAgentStreamEvent(activity, event) {
803
+ if (!event || typeof event !== 'object') return;
804
+
805
+ const sessionId = event.actualSessionId || event.newSessionId || event.sessionId || event.threadId;
806
+ if (sessionId) activity.sessionId = sessionId;
807
+
808
+ if (event.type === 'status' && event.message) {
809
+ activity.status = 'running';
810
+ activity.phase = compact(event.message, 120);
811
+ pushActivityEvent(activity, `⏳ ${compact(event.message, 120)}`);
812
+ return;
813
+ }
814
+ if (event.type === 'done' || event.kind === 'complete' || event.kind === 'stream_end') {
815
+ activity.status = 'done';
816
+ activity.phase = t(activity.lang, 'control.activity.done');
817
+ return;
818
+ }
819
+ if (event.type === 'error' || event.kind === 'error') {
820
+ activity.status = 'failed';
821
+ activity.phase = t(activity.lang, 'control.activity.failed');
822
+ activity.error = event.error || event.message || event.content || t(activity.lang, 'error.generic');
823
+ pushActivityEvent(activity, `❌ ${compact(activity.error, 120)}`);
824
+ return;
825
+ }
826
+ if (event.kind === 'session_created') {
827
+ activity.status = 'running';
828
+ activity.phase = t(activity.lang, 'control.activity.sessionStarted');
829
+ pushActivityEvent(activity, `🧵 ${t(activity.lang, 'control.activity.sessionStarted')}`);
830
+ return;
831
+ }
832
+ if (event.kind === 'thinking') {
833
+ activity.status = 'running';
834
+ activity.phase = t(activity.lang, 'control.activity.thinking');
835
+ const thought = extractTextFromEvent(event);
836
+ if (thought) pushActivityEvent(activity, `🧠 ${compact(thought, 120)}`);
837
+ return;
838
+ }
839
+ if (event.kind === 'stream_delta' || event.kind === 'text') {
840
+ activity.status = 'running';
841
+ activity.phase = t(activity.lang, 'control.activity.responding');
842
+ activity.output = `${activity.output}${extractTextFromEvent(event)}`;
843
+ return;
844
+ }
845
+ if (event.type === 'claude-response' && event.data?.type === 'assistant') {
846
+ activity.status = 'running';
847
+ activity.phase = t(activity.lang, 'control.activity.responding');
848
+ activity.output = `${activity.output}${extractTextFromEvent(event)}`;
849
+ return;
850
+ }
851
+ if (event.kind === 'tool_use') {
852
+ activity.status = 'running';
853
+ activity.phase = t(activity.lang, 'control.activity.working');
854
+ const toolName = event.toolName || 'Tool';
855
+ const input = describeToolInput(toolName, event.toolInput);
856
+ const icon = toolName === 'Bash'
857
+ ? '💻'
858
+ : toolName === 'FileChanges'
859
+ ? '📝'
860
+ : toolName === 'WebSearch'
861
+ ? '🔎'
862
+ : '🔧';
863
+ pushActivityEvent(activity, `${icon} ${toolName}${input ? `: ${input}` : ''}`);
864
+ return;
865
+ }
866
+ if (event.kind === 'tool_result') {
867
+ activity.status = 'running';
868
+ activity.phase = t(activity.lang, 'control.activity.working');
869
+ const label = event.isError
870
+ ? t(activity.lang, 'control.activity.toolFailed')
871
+ : t(activity.lang, 'control.activity.toolDone');
872
+ pushActivityEvent(activity, `${event.isError ? '⚠️' : '✅'} ${label}`);
873
+ return;
874
+ }
875
+ if (event.kind === 'status' && event.text === 'token_budget') {
876
+ const used = event.tokenBudget?.used;
877
+ if (used) pushActivityEvent(activity, `📊 ${t(activity.lang, 'control.activity.tokens')}: ${used}`);
878
+ }
879
+ }
880
+
542
881
  function confirmationLabel(lang, action, payload = {}) {
543
882
  if (action === 'install_provider') {
544
883
  return t(lang, 'control.confirmInstall', { provider: payload.provider || '' });
@@ -572,78 +911,106 @@ async function sendToolFailure({ bot, chatId, link, result, editMessageId }) {
572
911
  await send(bot, chatId, message, { editMessageId });
573
912
  }
574
913
 
575
- async function runAgent({ bot, chatId, link, prompt }) {
914
+ async function runAgent({ bot, chatId, link, prompt, activity = null }) {
576
915
  const lang = languageFor(link);
577
916
  const state = getState(link.user_id);
578
917
  if (!state.remoteControlEnabled) {
579
- await send(bot, chatId, t(lang, 'control.disabled'));
918
+ await send(bot, chatId, t(lang, 'control.disabled'), { editMessageId: activity?.messageId });
580
919
  return;
581
920
  }
582
921
  if (!state.selectedProjectPath) {
583
- await send(bot, chatId, t(lang, 'control.selectProjectFirst'));
584
- await showProjectMenu({ bot, chatId, link });
922
+ await showProjectMenu({ bot, chatId, link, editMessageId: activity?.messageId, notice: t(lang, 'control.selectProjectFirst') });
585
923
  return;
586
924
  }
587
925
 
588
926
  const provider = resolveTelegramProvider(state);
589
927
  const model = resolveTelegramModel(state);
590
- if (state.progressMode !== 'final') {
591
- await send(bot, chatId, t(lang, 'control.agentStarted', {
928
+ const active = activity || await createTelegramActivity({
929
+ bot,
930
+ chatId,
931
+ link,
932
+ type: 'agent',
933
+ prompt,
934
+ phase: t(lang, 'control.activity.startingProvider', { provider }),
935
+ });
936
+ active.type = 'agent';
937
+ active.provider = provider;
938
+ active.project = projectLabel(state);
939
+ active.status = 'running';
940
+ active.phase = t(lang, 'control.activity.startingProvider', { provider });
941
+ await editTelegramActivity({ bot, chatId, activity: active, force: true });
942
+
943
+ const heartbeat = startActivityHeartbeat({ bot, chatId, activity: active });
944
+ let streamFailed = null;
945
+ try {
946
+ await localAgentStream(link.user_id, {
947
+ projectPath: state.selectedProjectPath,
592
948
  provider,
593
- project: state.selectedProjectName || state.selectedProjectPath,
594
- }));
949
+ model: model || undefined,
950
+ message: buildTelegramAgentPrompt(prompt, state),
951
+ cleanup: false,
952
+ permissionMode: 'default',
953
+ suppressNotifications: true,
954
+ }, async (event) => {
955
+ applyAgentStreamEvent(active, event);
956
+ await editTelegramActivity({ bot, chatId, activity: active });
957
+ });
958
+ } catch (error) {
959
+ streamFailed = error;
960
+ } finally {
961
+ clearInterval(heartbeat);
595
962
  }
596
963
 
597
- const result = await runTelegramTool({
598
- userId: link.user_id,
599
- action: 'run_agent',
600
- retries: 0,
601
- execute: () => localApi(link.user_id, '/api/agent', {
602
- method: 'POST',
603
- body: {
604
- projectPath: state.selectedProjectPath,
605
- provider,
606
- model: model || undefined,
607
- message: buildTelegramAgentPrompt(prompt, state),
608
- cleanup: false,
609
- stream: false,
610
- },
611
- }),
612
- });
613
- if (!result.ok) {
614
- await sendToolFailure({ bot, chatId, link, result });
964
+ if (streamFailed) {
965
+ active.status = 'failed';
966
+ active.phase = t(lang, 'control.activity.failed');
967
+ active.error = streamFailed.message || t(lang, 'error.generic');
968
+ await editTelegramActivity({ bot, chatId, activity: active, force: true });
615
969
  return;
616
970
  }
617
971
 
618
- const response = result.data;
619
- const assistantText = extractAssistantText(response);
620
- const statusLine = response.success === false
621
- ? t(lang, 'control.agentFailed', { error: response.error || 'provider returned no answer' })
622
- : t(lang, 'control.agentDone', { provider, session: response.sessionId ? ` (${response.sessionId})` : '' });
623
- await send(bot, chatId, `${statusLine}\n\n${assistantText || response.error || t(lang, 'control.noAssistantText')}`);
972
+ active.status = active.error ? 'failed' : 'done';
973
+ active.phase = active.error ? t(lang, 'control.activity.failed') : t(lang, 'control.activity.done');
974
+ if (!active.output && !active.error) active.output = t(lang, 'control.noAssistantText');
975
+ await editTelegramActivity({ bot, chatId, activity: active, force: true });
624
976
  }
625
977
 
626
- export async function runWorkflow({ bot, chatId, link, input }) {
978
+ export async function runWorkflow({ bot, chatId, link, input, activity = null }) {
627
979
  const lang = languageFor(link);
628
980
  const state = getState(link.user_id);
629
981
  const workflowId = state.selectedWorkflowId;
630
982
  if (!state.remoteControlEnabled) {
631
- await send(bot, chatId, t(lang, 'control.disabled'));
983
+ await send(bot, chatId, t(lang, 'control.disabled'), { editMessageId: activity?.messageId });
632
984
  return;
633
985
  }
634
986
  if (!workflowId) {
635
- await send(bot, chatId, t(lang, 'control.selectWorkflowFirst'));
636
- await showWorkflowMenu({ bot, chatId, link });
987
+ await send(bot, chatId, t(lang, 'control.selectWorkflowFirst'), { editMessageId: activity?.messageId });
988
+ await showWorkflowMenu({ bot, chatId, link, editMessageId: activity?.messageId });
637
989
  return;
638
990
  }
639
991
  if (!state.selectedProjectPath) {
640
- await send(bot, chatId, t(lang, 'control.selectProjectFirst'));
641
- await showProjectMenu({ bot, chatId, link });
992
+ await showProjectMenu({ bot, chatId, link, editMessageId: activity?.messageId, notice: t(lang, 'control.selectProjectFirst') });
642
993
  return;
643
994
  }
644
995
 
645
996
  const provider = resolveTelegramProvider(state);
646
997
  const model = resolveTelegramModel(state);
998
+ const active = activity || await createTelegramActivity({
999
+ bot,
1000
+ chatId,
1001
+ link,
1002
+ type: 'workflow',
1003
+ prompt: input,
1004
+ phase: t(lang, 'control.activity.startingWorkflow'),
1005
+ });
1006
+ active.type = 'workflow';
1007
+ active.provider = provider;
1008
+ active.project = projectLabel(state);
1009
+ active.workflowId = workflowId;
1010
+ active.status = 'running';
1011
+ active.phase = t(lang, 'control.activity.startingWorkflow');
1012
+ await editTelegramActivity({ bot, chatId, activity: active, force: true });
1013
+
647
1014
  const result = await runTelegramTool({
648
1015
  userId: link.user_id,
649
1016
  action: 'run_workflow',
@@ -664,12 +1031,15 @@ export async function runWorkflow({ bot, chatId, link, input }) {
664
1031
  }),
665
1032
  });
666
1033
  if (!result.ok) {
667
- await sendToolFailure({ bot, chatId, link, result });
1034
+ await sendToolFailure({ bot, chatId, link, result, editMessageId: active.messageId });
668
1035
  return;
669
1036
  }
670
1037
  const run = result.data;
671
- await send(bot, chatId, t(lang, 'control.workflowStarted', { runId: run.id, workflowId }));
672
- monitorWorkflowRun({ bot, chatId, link, runId: run.id }).catch((error) => {
1038
+ active.runId = run.id;
1039
+ active.phase = t(lang, 'control.activity.workflowRunning');
1040
+ pushActivityEvent(active, `🧭 ${t(lang, 'control.workflowStarted', { runId: run.id, workflowId }).replace('\n', ' ')}`);
1041
+ await editTelegramActivity({ bot, chatId, activity: active, force: true });
1042
+ monitorWorkflowRun({ bot, chatId, link, runId: run.id, activity: active }).catch((error) => {
673
1043
  console.warn('[telegram-control] workflow monitor failed:', error?.message || error);
674
1044
  });
675
1045
  }
@@ -750,21 +1120,97 @@ async function findProjectByQuery(query) {
750
1120
  }) || null;
751
1121
  }
752
1122
 
753
- async function handleRoutedIntent({ bot, chatId, link, text }) {
1123
+ async function listWorkflowSummaries(userId) {
1124
+ try {
1125
+ const data = await localApi(userId, '/api/orchestration/workflows', { timeoutMs: 10_000 });
1126
+ if (Array.isArray(data)) return data;
1127
+ if (Array.isArray(data?.workflows)) return data.workflows;
1128
+ } catch {
1129
+ // Workflow context is helpful for routing but should never block chat input.
1130
+ }
1131
+ return [];
1132
+ }
1133
+
1134
+ async function resolveTelegramAiIntent({ bot, chatId, link, text, activity }) {
754
1135
  const state = getState(link.user_id);
755
- const intent = classifyTelegramIntent(text, state);
756
- if (!intent) return false;
1136
+ if (state.routerEnabled === false || state.routerMode !== 'hybrid') {
1137
+ return { action: 'agent_prompt', prompt: text, confidence: 1 };
1138
+ }
757
1139
 
758
- if (intent.action === 'projects') {
759
- await showProjectMenu({ bot, chatId, link });
1140
+ const provider = resolveTelegramProvider(state);
1141
+ const model = resolveTelegramModel(state);
1142
+ if (activity) {
1143
+ activity.type = 'router';
1144
+ activity.provider = provider;
1145
+ activity.project = projectLabel(state);
1146
+ activity.status = 'running';
1147
+ activity.phase = t(activity.lang, 'control.activity.interpreting');
1148
+ pushActivityEvent(activity, `🧠 ${t(activity.lang, 'control.activity.interpreting')}`);
1149
+ await editTelegramActivity({ bot, chatId, activity, force: true });
1150
+ }
1151
+
1152
+ try {
1153
+ const projects = await listProjects().catch(() => []);
1154
+ const workflows = await listWorkflowSummaries(link.user_id);
1155
+ const response = await localApi(link.user_id, '/api/agent', {
1156
+ method: 'POST',
1157
+ timeoutMs: INTENT_ROUTER_TIMEOUT_MS,
1158
+ body: {
1159
+ projectPath: state.selectedProjectPath || process.cwd(),
1160
+ provider,
1161
+ model: model || undefined,
1162
+ message: buildTelegramIntentPrompt(text, state, { projects, workflows }),
1163
+ cleanup: false,
1164
+ stream: false,
1165
+ permissionMode: 'plan',
1166
+ suppressNotifications: true,
1167
+ },
1168
+ });
1169
+ const assistantText = extractAssistantText(response);
1170
+ const intent = parseTelegramAiIntentResponse(assistantText, text);
1171
+ if (activity) {
1172
+ pushActivityEvent(activity, `🧭 ${intent.action} (${Math.round(intent.confidence * 100)}%)`);
1173
+ await editTelegramActivity({ bot, chatId, activity });
1174
+ }
1175
+ return intent;
1176
+ } catch (error) {
1177
+ if (activity) {
1178
+ pushActivityEvent(activity, `⚠️ ${t(activity.lang, 'control.activity.routerFallback')}`);
1179
+ await editTelegramActivity({ bot, chatId, activity });
1180
+ }
1181
+ console.warn('[telegram-control] AI intent router fallback:', error?.message || error);
1182
+ return { action: 'agent_prompt', prompt: text, confidence: 0 };
1183
+ }
1184
+ }
1185
+
1186
+ async function handleRoutedIntent({ bot, chatId, link, text, activity }) {
1187
+ const intent = await resolveTelegramAiIntent({ bot, chatId, link, text, activity });
1188
+ const editMessageId = activity?.messageId;
1189
+
1190
+ if (intent.action === 'agent_prompt') {
1191
+ await runAgent({ bot, chatId, link, prompt: intent.prompt || text, activity });
760
1192
  return true;
761
1193
  }
762
- if (intent.action === 'project_select_query') {
763
- const project = await findProjectByQuery(intent.query);
1194
+ if (intent.action === 'show_menu') {
1195
+ await showMainMenu({ bot, chatId, link, editMessageId });
1196
+ return true;
1197
+ }
1198
+ if (intent.action === 'show_projects') {
1199
+ await showProjectMenu({ bot, chatId, link, editMessageId });
1200
+ return true;
1201
+ }
1202
+ if (intent.action === 'select_project') {
1203
+ const query = intent.projectQuery || intent.prompt || text;
1204
+ const project = await findProjectByQuery(query);
764
1205
  const lang = languageFor(link);
765
1206
  if (!project) {
766
- await send(bot, chatId, t(lang, 'control.projectNotFound', { query: intent.query }));
767
- await showProjectMenu({ bot, chatId, link });
1207
+ await showProjectMenu({
1208
+ bot,
1209
+ chatId,
1210
+ link,
1211
+ editMessageId,
1212
+ notice: t(lang, 'control.projectNotFound', { query }),
1213
+ });
768
1214
  return true;
769
1215
  }
770
1216
  updateTelegramControlState(link.user_id, {
@@ -775,6 +1221,7 @@ async function handleRoutedIntent({ bot, chatId, link, text }) {
775
1221
  bot,
776
1222
  chatId,
777
1223
  link,
1224
+ editMessageId,
778
1225
  notice: t(lang, 'control.projectSelected', {
779
1226
  project: project.displayName || project.name,
780
1227
  path: project.fullPath || project.path,
@@ -782,36 +1229,56 @@ async function handleRoutedIntent({ bot, chatId, link, text }) {
782
1229
  });
783
1230
  return true;
784
1231
  }
785
- if (intent.action === 'provider_menu') {
786
- await showProviderMenu({ bot, chatId, link });
1232
+ if (intent.action === 'show_provider_menu') {
1233
+ await showProviderMenu({ bot, chatId, link, editMessageId });
787
1234
  return true;
788
1235
  }
789
- if (intent.action === 'provider_select') {
1236
+ if (intent.action === 'select_provider' && intent.provider) {
790
1237
  updateTelegramControlState(link.user_id, { selectedProvider: intent.provider, selectedModel: null });
791
- await showModelMenu({ bot, chatId, link });
1238
+ await showModelMenu({ bot, chatId, link, editMessageId });
792
1239
  return true;
793
1240
  }
794
- if (intent.action === 'model_menu') {
795
- await showModelMenu({ bot, chatId, link });
1241
+ if (intent.action === 'show_model_menu') {
1242
+ await showModelMenu({ bot, chatId, link, editMessageId });
796
1243
  return true;
797
1244
  }
798
- if (intent.action === 'runs') {
799
- await showRuns({ bot, chatId, link });
1245
+ if (intent.action === 'select_model' && intent.model) {
1246
+ updateTelegramControlState(link.user_id, { selectedModel: intent.model });
1247
+ await showMainMenu({
1248
+ bot,
1249
+ chatId,
1250
+ link,
1251
+ editMessageId,
1252
+ notice: t(languageFor(link), 'control.modelSelected', { model: intent.model }),
1253
+ });
800
1254
  return true;
801
1255
  }
802
- if (intent.action === 'approvals') {
803
- await showApprovalQueue({ bot, chatId, link });
1256
+ if (intent.action === 'show_runs') {
1257
+ await showRuns({ bot, chatId, link, editMessageId });
804
1258
  return true;
805
1259
  }
806
- if (intent.action === 'workflows') {
807
- await showWorkflowMenu({ bot, chatId, link });
1260
+ if (intent.action === 'show_approvals') {
1261
+ await showApprovalQueue({ bot, chatId, link, editMessageId });
1262
+ return true;
1263
+ }
1264
+ if (intent.action === 'show_workflows') {
1265
+ await showWorkflowMenu({ bot, chatId, link, editMessageId });
1266
+ return true;
1267
+ }
1268
+ if (intent.action === 'run_workflow') {
1269
+ await runWorkflow({ bot, chatId, link, input: intent.workflowInput || text, activity });
1270
+ return true;
1271
+ }
1272
+ if (intent.action === 'show_sessions') {
1273
+ await showSessions({ bot, chatId, link, editMessageId });
808
1274
  return true;
809
1275
  }
810
1276
  if (intent.action === 'new_chat') {
811
- await startNewChat({ bot, chatId, link });
1277
+ await startNewChat({ bot, chatId, link, editMessageId });
812
1278
  return true;
813
1279
  }
814
- return false;
1280
+ await runAgent({ bot, chatId, link, prompt: text, activity });
1281
+ return true;
815
1282
  }
816
1283
 
817
1284
  async function fetchRun(userId, runId) {
@@ -819,10 +1286,17 @@ async function fetchRun(userId, runId) {
819
1286
  }
820
1287
 
821
1288
  function summarizeRun(run, mode) {
1289
+ const statusIcon = run.status === 'completed'
1290
+ ? '✅'
1291
+ : run.status === 'failed'
1292
+ ? '❌'
1293
+ : run.status === 'canceled'
1294
+ ? '⏹'
1295
+ : '🔧';
822
1296
  const lines = [
823
- `Run ${run.id}`,
824
- `Workflow: ${run.workflowId}`,
825
- `Status: ${run.status}`,
1297
+ `${statusIcon} Run ${run.id}`,
1298
+ `🧩 Workflow: ${run.workflowId}`,
1299
+ `📌 Status: ${run.status}`,
826
1300
  ];
827
1301
  const nodeRuns = Array.isArray(run.nodeRuns) ? run.nodeRuns : [];
828
1302
  if (mode !== 'final') {
@@ -830,7 +1304,14 @@ function summarizeRun(run, mode) {
830
1304
  ? nodeRuns.filter((node) => node.error || node.status === 'failed')
831
1305
  : nodeRuns;
832
1306
  for (const node of visibleNodes) {
833
- lines.push(`- ${node.status}: ${node.agentLabel || node.nodeId}${node.error ? ` — ${node.error}` : ''}`);
1307
+ const nodeIcon = node.status === 'completed'
1308
+ ? '✅'
1309
+ : node.status === 'failed'
1310
+ ? '❌'
1311
+ : node.status === 'running'
1312
+ ? '🔧'
1313
+ : '⏳';
1314
+ lines.push(`${nodeIcon} ${node.status}: ${node.agentLabel || node.nodeId}${node.error ? ` - ${node.error}` : ''}`);
834
1315
  }
835
1316
  }
836
1317
  const outputs = nodeRuns
@@ -840,7 +1321,7 @@ function summarizeRun(run, mode) {
840
1321
  return truncate(`${lines.join('\n')}${outputs.join('\n')}`);
841
1322
  }
842
1323
 
843
- async function monitorWorkflowRun({ bot, chatId, link, runId }) {
1324
+ async function monitorWorkflowRun({ bot, chatId, link, runId, activity = null }) {
844
1325
  if (runMonitors.has(runId)) return;
845
1326
  runMonitors.set(runId, true);
846
1327
  const state = getState(link.user_id);
@@ -850,12 +1331,23 @@ async function monitorWorkflowRun({ bot, chatId, link, runId }) {
850
1331
  await new Promise((resolve) => setTimeout(resolve, 5000));
851
1332
  const run = await fetchRun(link.user_id, runId);
852
1333
  const nodeRuns = Array.isArray(run.nodeRuns) ? run.nodeRuns : [];
1334
+ if (activity && !TERMINAL_RUN_STATES.has(run.status)) {
1335
+ activity.status = 'running';
1336
+ activity.phase = `${t(activity.lang, 'control.activity.workflowRunning')} (${run.status})`;
1337
+ await editTelegramActivity({ bot, chatId, activity });
1338
+ }
853
1339
  if (state.progressMode === 'all') {
854
1340
  for (const node of nodeRuns) {
855
1341
  const key = `${node.nodeId}:${node.status}`;
856
1342
  if (!seenNodeStatus.has(key)) {
857
1343
  seenNodeStatus.set(key, true);
858
- await send(bot, chatId, `${node.agentLabel || node.nodeId}: ${node.status}${node.error ? `\n${node.error}` : ''}`);
1344
+ if (activity) {
1345
+ pushActivityEvent(activity, `${node.status === 'completed' ? '✅' : node.status === 'failed' ? '❌' : '🔧'} ${node.agentLabel || node.nodeId}: ${node.status}${node.error ? ` - ${node.error}` : ''}`);
1346
+ activity.phase = t(activity.lang, 'control.activity.workflowRunning');
1347
+ await editTelegramActivity({ bot, chatId, activity });
1348
+ } else {
1349
+ await send(bot, chatId, `${node.agentLabel || node.nodeId}: ${node.status}${node.error ? `\n${node.error}` : ''}`);
1350
+ }
859
1351
  }
860
1352
  }
861
1353
  }
@@ -864,12 +1356,27 @@ async function monitorWorkflowRun({ bot, chatId, link, runId }) {
864
1356
  const key = `${node.nodeId}:${node.status}:${node.error || ''}`;
865
1357
  if (!seenNodeStatus.has(key)) {
866
1358
  seenNodeStatus.set(key, true);
867
- await send(bot, chatId, `${node.agentLabel || node.nodeId}: ${node.status}\n${node.error || ''}`);
1359
+ if (activity) {
1360
+ pushActivityEvent(activity, `❌ ${node.agentLabel || node.nodeId}: ${node.status}${node.error ? ` - ${node.error}` : ''}`);
1361
+ activity.phase = t(activity.lang, 'control.activity.workflowRunning');
1362
+ await editTelegramActivity({ bot, chatId, activity });
1363
+ } else {
1364
+ await send(bot, chatId, `${node.agentLabel || node.nodeId}: ${node.status}\n${node.error || ''}`);
1365
+ }
868
1366
  }
869
1367
  }
870
1368
  }
871
1369
  if (TERMINAL_RUN_STATES.has(run.status)) {
872
- await send(bot, chatId, summarizeRun(run, state.progressMode));
1370
+ if (activity) {
1371
+ activity.status = run.status === 'completed' ? 'done' : 'failed';
1372
+ activity.phase = run.status === 'completed'
1373
+ ? t(activity.lang, 'control.activity.done')
1374
+ : t(activity.lang, 'control.activity.failed');
1375
+ activity.output = summarizeRun(run, state.progressMode);
1376
+ await editTelegramActivity({ bot, chatId, activity, force: true });
1377
+ } else {
1378
+ await send(bot, chatId, summarizeRun(run, state.progressMode));
1379
+ }
873
1380
  return;
874
1381
  }
875
1382
  }
@@ -1124,7 +1631,6 @@ async function handleTelegramControlMessageInternal({ bot, msg, link }) {
1124
1631
 
1125
1632
  if (await handleAwaitingInput({ bot, chatId, link, text })) return true;
1126
1633
  if (await handleCommand({ bot, chatId, link, text })) return true;
1127
- if (await handleRoutedIntent({ bot, chatId, link, text })) return true;
1128
1634
 
1129
1635
  const state = getState(link.user_id);
1130
1636
  if (state.routerEnabled === false) return false;
@@ -1133,8 +1639,15 @@ async function handleTelegramControlMessageInternal({ bot, msg, link }) {
1133
1639
  return true;
1134
1640
  }
1135
1641
 
1136
- await runAgent({ bot, chatId, link, prompt: text });
1137
- return true;
1642
+ const activity = await createTelegramActivity({
1643
+ bot,
1644
+ chatId,
1645
+ link,
1646
+ type: 'router',
1647
+ prompt: text,
1648
+ phase: t(languageFor(link), 'control.activity.interpreting'),
1649
+ });
1650
+ return handleRoutedIntent({ bot, chatId, link, text, activity });
1138
1651
  }
1139
1652
 
1140
1653
  export async function handleTelegramControlMessage(args) {