@pixelbyte-software/pixcode 1.53.5 → 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.
@@ -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,197 @@ 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 renderActivity(activity, { finalText = null } = {}) {
382
+ const output = finalText || activity.output;
383
+ const lines = [
384
+ `${activity.status === 'failed' ? '❌' : activity.status === 'done' ? '✅' : activity.status === 'running' ? '🔧' : '⏳'} ${activityTitle(activity)}`,
385
+ '',
386
+ `🤖 ${t(activity.lang, 'control.activity.provider')}: ${activity.provider || '-'}`,
387
+ `📁 ${t(activity.lang, 'control.activity.project')}: ${compact(activity.project, 90)}`,
388
+ ];
389
+
390
+ if (activity.sessionId) lines.push(`🧵 ${t(activity.lang, 'control.activity.session')}: ${activity.sessionId}`);
391
+ if (activity.runId) lines.push(`🧭 ${t(activity.lang, 'control.activity.run')}: ${activity.runId}`);
392
+ if (activity.workflowId) lines.push(`🧩 ${t(activity.lang, 'control.activity.workflow')}: ${activity.workflowId}`);
393
+ lines.push(`⏱ ${t(activity.lang, 'control.activity.elapsed')}: ${formatElapsed(activity.startedAt)}`);
394
+ lines.push(`📌 ${t(activity.lang, 'control.activity.status')}: ${activity.phase}`);
395
+
396
+ const visibleEvents = activity.mode === 'final'
397
+ ? activity.events.slice(-4)
398
+ : activity.events;
399
+ if (visibleEvents.length > 0) {
400
+ lines.push('', `🛠 ${t(activity.lang, 'control.activity.work')}:`);
401
+ for (const event of visibleEvents) lines.push(`• ${event}`);
402
+ }
403
+
404
+ if (activity.error) {
405
+ lines.push('', `⚠️ ${truncate(activity.error, 700)}`);
406
+ } else if (output) {
407
+ lines.push('', `💬 ${t(activity.lang, 'control.activity.output')}:`);
408
+ lines.push(truncate(output, 1800));
409
+ } else if (activity.prompt) {
410
+ lines.push('', `📝 ${compact(activity.prompt, 220)}`);
411
+ }
412
+
413
+ return truncate(lines.join('\n'), 3400);
414
+ }
415
+
416
+ async function createTelegramActivity({ bot, chatId, link, type = 'agent', prompt = '', phase = null }) {
417
+ const lang = languageFor(link);
418
+ const state = getState(link.user_id);
419
+ const activity = createActivityState({
420
+ lang,
421
+ type,
422
+ provider: resolveTelegramProvider(state),
423
+ project: projectLabel(state),
424
+ prompt,
425
+ mode: state.progressMode,
426
+ });
427
+ if (phase) activity.phase = phase;
428
+ const sent = await send(bot, chatId, renderActivity(activity), { parse_mode: undefined });
429
+ activity.messageId = sent?.message_id || sent?.message?.message_id || null;
430
+ activity.lastEditAt = Date.now();
431
+ return activity;
432
+ }
433
+
434
+ async function editTelegramActivity({ bot, chatId, activity, force = false, reply_markup }) {
435
+ if (!activity) return null;
436
+ const now = Date.now();
437
+ if (!force && now - activity.lastEditAt < ACTIVITY_EDIT_THROTTLE_MS) return null;
438
+ const sent = await send(bot, chatId, renderActivity(activity), {
439
+ editMessageId: activity.messageId,
440
+ parse_mode: undefined,
441
+ reply_markup,
442
+ });
443
+ activity.messageId = sent?.message_id || sent?.message?.message_id || activity.messageId;
444
+ activity.lastEditAt = now;
445
+ return sent;
446
+ }
447
+
448
+ function startActivityHeartbeat({ bot, chatId, activity }) {
449
+ return setInterval(() => {
450
+ if (!activity || activity.status === 'done' || activity.status === 'failed') return;
451
+ if (activity.status === 'starting') {
452
+ activity.status = 'running';
453
+ activity.phase = t(activity.lang, 'control.activity.thinking');
454
+ }
455
+ editTelegramActivity({ bot, chatId, activity }).catch((error) => {
456
+ console.warn('[telegram-control] activity heartbeat failed:', error?.message || error);
457
+ });
458
+ }, ACTIVITY_HEARTBEAT_MS);
459
+ }
460
+
263
461
  function stateSummary(lang, state) {
264
462
  return [
265
463
  `${t(lang, 'control.summary.project')}: ${state.selectedProjectName || t(lang, 'control.notSelected')}`,
@@ -310,11 +508,12 @@ async function listProjects() {
310
508
  return projects.slice(0, 20);
311
509
  }
312
510
 
313
- async function showProjectMenu({ bot, chatId, link, editMessageId }) {
511
+ async function showProjectMenu({ bot, chatId, link, editMessageId, notice }) {
314
512
  const lang = languageFor(link);
315
513
  const projects = await listProjects();
316
514
  if (projects.length === 0) {
317
- await send(bot, chatId, t(lang, 'control.noProjects'), { editMessageId });
515
+ const prefix = notice ? `${notice}\n\n` : '';
516
+ await send(bot, chatId, `${prefix}${t(lang, 'control.noProjects')}`, { editMessageId });
318
517
  return;
319
518
  }
320
519
 
@@ -327,7 +526,8 @@ async function showProjectMenu({ bot, chatId, link, editMessageId }) {
327
526
  displayName: project.displayName || project.name,
328
527
  },
329
528
  ));
330
- await send(bot, chatId, t(lang, 'control.pickProject'), {
529
+ const prefix = notice ? `${notice}\n\n` : '';
530
+ await send(bot, chatId, `${prefix}${t(lang, 'control.pickProject')}`, {
331
531
  editMessageId,
332
532
  reply_markup: { inline_keyboard: rows(buttons, 1) },
333
533
  });
@@ -539,6 +739,119 @@ function extractAssistantText(response) {
539
739
  return chunks.join('\n\n').trim();
540
740
  }
541
741
 
742
+ function extractTextFromEvent(event) {
743
+ if (!event || typeof event !== 'object') return '';
744
+ if (typeof event.content === 'string') return event.content;
745
+ if (Array.isArray(event.content)) {
746
+ return event.content
747
+ .map((part) => (typeof part === 'string' ? part : part?.text || ''))
748
+ .join('');
749
+ }
750
+ const legacyContent = event.data?.message?.content || event.message?.content;
751
+ if (Array.isArray(legacyContent)) {
752
+ return legacyContent
753
+ .map((part) => (typeof part === 'string' ? part : part?.text || ''))
754
+ .join('');
755
+ }
756
+ if (typeof legacyContent === 'string') return legacyContent;
757
+ return '';
758
+ }
759
+
760
+ function describeToolInput(toolName, input) {
761
+ if (!input || typeof input !== 'object') return '';
762
+ if (toolName === 'Bash' && typeof input.command === 'string') {
763
+ return compact(input.command, 110);
764
+ }
765
+ if (toolName === 'WebSearch' && typeof input.query === 'string') {
766
+ return compact(input.query, 110);
767
+ }
768
+ if (toolName === 'FileChanges') {
769
+ return compact(JSON.stringify(input), 110);
770
+ }
771
+ const keys = Object.keys(input).slice(0, 3);
772
+ if (keys.length === 0) return '';
773
+ return compact(keys.map((key) => `${key}: ${String(input[key])}`).join(', '), 110);
774
+ }
775
+
776
+ function applyAgentStreamEvent(activity, event) {
777
+ if (!event || typeof event !== 'object') return;
778
+
779
+ const sessionId = event.actualSessionId || event.newSessionId || event.sessionId || event.threadId;
780
+ if (sessionId) activity.sessionId = sessionId;
781
+
782
+ if (event.type === 'status' && event.message) {
783
+ activity.status = 'running';
784
+ activity.phase = compact(event.message, 120);
785
+ pushActivityEvent(activity, `⏳ ${compact(event.message, 120)}`);
786
+ return;
787
+ }
788
+ if (event.type === 'done' || event.kind === 'complete' || event.kind === 'stream_end') {
789
+ activity.status = 'done';
790
+ activity.phase = t(activity.lang, 'control.activity.done');
791
+ return;
792
+ }
793
+ if (event.type === 'error' || event.kind === 'error') {
794
+ activity.status = 'failed';
795
+ activity.phase = t(activity.lang, 'control.activity.failed');
796
+ activity.error = event.error || event.message || event.content || t(activity.lang, 'error.generic');
797
+ pushActivityEvent(activity, `❌ ${compact(activity.error, 120)}`);
798
+ return;
799
+ }
800
+ if (event.kind === 'session_created') {
801
+ activity.status = 'running';
802
+ activity.phase = t(activity.lang, 'control.activity.sessionStarted');
803
+ pushActivityEvent(activity, `🧵 ${t(activity.lang, 'control.activity.sessionStarted')}`);
804
+ return;
805
+ }
806
+ if (event.kind === 'thinking') {
807
+ activity.status = 'running';
808
+ activity.phase = t(activity.lang, 'control.activity.thinking');
809
+ const thought = extractTextFromEvent(event);
810
+ if (thought) pushActivityEvent(activity, `🧠 ${compact(thought, 120)}`);
811
+ return;
812
+ }
813
+ if (event.kind === 'stream_delta' || event.kind === 'text') {
814
+ activity.status = 'running';
815
+ activity.phase = t(activity.lang, 'control.activity.responding');
816
+ activity.output = `${activity.output}${extractTextFromEvent(event)}`;
817
+ return;
818
+ }
819
+ if (event.type === 'claude-response' && event.data?.type === 'assistant') {
820
+ activity.status = 'running';
821
+ activity.phase = t(activity.lang, 'control.activity.responding');
822
+ activity.output = `${activity.output}${extractTextFromEvent(event)}`;
823
+ return;
824
+ }
825
+ if (event.kind === 'tool_use') {
826
+ activity.status = 'running';
827
+ activity.phase = t(activity.lang, 'control.activity.working');
828
+ const toolName = event.toolName || 'Tool';
829
+ const input = describeToolInput(toolName, event.toolInput);
830
+ const icon = toolName === 'Bash'
831
+ ? '💻'
832
+ : toolName === 'FileChanges'
833
+ ? '📝'
834
+ : toolName === 'WebSearch'
835
+ ? '🔎'
836
+ : '🔧';
837
+ pushActivityEvent(activity, `${icon} ${toolName}${input ? `: ${input}` : ''}`);
838
+ return;
839
+ }
840
+ if (event.kind === 'tool_result') {
841
+ activity.status = 'running';
842
+ activity.phase = t(activity.lang, 'control.activity.working');
843
+ const label = event.isError
844
+ ? t(activity.lang, 'control.activity.toolFailed')
845
+ : t(activity.lang, 'control.activity.toolDone');
846
+ pushActivityEvent(activity, `${event.isError ? '⚠️' : '✅'} ${label}`);
847
+ return;
848
+ }
849
+ if (event.kind === 'status' && event.text === 'token_budget') {
850
+ const used = event.tokenBudget?.used;
851
+ if (used) pushActivityEvent(activity, `📊 ${t(activity.lang, 'control.activity.tokens')}: ${used}`);
852
+ }
853
+ }
854
+
542
855
  function confirmationLabel(lang, action, payload = {}) {
543
856
  if (action === 'install_provider') {
544
857
  return t(lang, 'control.confirmInstall', { provider: payload.provider || '' });
@@ -572,78 +885,105 @@ async function sendToolFailure({ bot, chatId, link, result, editMessageId }) {
572
885
  await send(bot, chatId, message, { editMessageId });
573
886
  }
574
887
 
575
- async function runAgent({ bot, chatId, link, prompt }) {
888
+ async function runAgent({ bot, chatId, link, prompt, activity = null }) {
576
889
  const lang = languageFor(link);
577
890
  const state = getState(link.user_id);
578
891
  if (!state.remoteControlEnabled) {
579
- await send(bot, chatId, t(lang, 'control.disabled'));
892
+ await send(bot, chatId, t(lang, 'control.disabled'), { editMessageId: activity?.messageId });
580
893
  return;
581
894
  }
582
895
  if (!state.selectedProjectPath) {
583
- await send(bot, chatId, t(lang, 'control.selectProjectFirst'));
584
- await showProjectMenu({ bot, chatId, link });
896
+ await showProjectMenu({ bot, chatId, link, editMessageId: activity?.messageId, notice: t(lang, 'control.selectProjectFirst') });
585
897
  return;
586
898
  }
587
899
 
588
900
  const provider = resolveTelegramProvider(state);
589
901
  const model = resolveTelegramModel(state);
590
- if (state.progressMode !== 'final') {
591
- await send(bot, chatId, t(lang, 'control.agentStarted', {
902
+ const active = activity || await createTelegramActivity({
903
+ bot,
904
+ chatId,
905
+ link,
906
+ type: 'agent',
907
+ prompt,
908
+ phase: t(lang, 'control.activity.startingProvider', { provider }),
909
+ });
910
+ active.type = 'agent';
911
+ active.provider = provider;
912
+ active.project = projectLabel(state);
913
+ active.status = 'running';
914
+ active.phase = t(lang, 'control.activity.startingProvider', { provider });
915
+ await editTelegramActivity({ bot, chatId, activity: active, force: true });
916
+
917
+ const heartbeat = startActivityHeartbeat({ bot, chatId, activity: active });
918
+ let streamFailed = null;
919
+ try {
920
+ await localAgentStream(link.user_id, {
921
+ projectPath: state.selectedProjectPath,
592
922
  provider,
593
- project: state.selectedProjectName || state.selectedProjectPath,
594
- }));
923
+ model: model || undefined,
924
+ message: buildTelegramAgentPrompt(prompt, state),
925
+ cleanup: false,
926
+ permissionMode: 'default',
927
+ }, async (event) => {
928
+ applyAgentStreamEvent(active, event);
929
+ await editTelegramActivity({ bot, chatId, activity: active });
930
+ });
931
+ } catch (error) {
932
+ streamFailed = error;
933
+ } finally {
934
+ clearInterval(heartbeat);
595
935
  }
596
936
 
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 });
937
+ if (streamFailed) {
938
+ active.status = 'failed';
939
+ active.phase = t(lang, 'control.activity.failed');
940
+ active.error = streamFailed.message || t(lang, 'error.generic');
941
+ await editTelegramActivity({ bot, chatId, activity: active, force: true });
615
942
  return;
616
943
  }
617
944
 
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')}`);
945
+ active.status = active.error ? 'failed' : 'done';
946
+ active.phase = active.error ? t(lang, 'control.activity.failed') : t(lang, 'control.activity.done');
947
+ if (!active.output && !active.error) active.output = t(lang, 'control.noAssistantText');
948
+ await editTelegramActivity({ bot, chatId, activity: active, force: true });
624
949
  }
625
950
 
626
- export async function runWorkflow({ bot, chatId, link, input }) {
951
+ export async function runWorkflow({ bot, chatId, link, input, activity = null }) {
627
952
  const lang = languageFor(link);
628
953
  const state = getState(link.user_id);
629
954
  const workflowId = state.selectedWorkflowId;
630
955
  if (!state.remoteControlEnabled) {
631
- await send(bot, chatId, t(lang, 'control.disabled'));
956
+ await send(bot, chatId, t(lang, 'control.disabled'), { editMessageId: activity?.messageId });
632
957
  return;
633
958
  }
634
959
  if (!workflowId) {
635
- await send(bot, chatId, t(lang, 'control.selectWorkflowFirst'));
636
- await showWorkflowMenu({ bot, chatId, link });
960
+ await send(bot, chatId, t(lang, 'control.selectWorkflowFirst'), { editMessageId: activity?.messageId });
961
+ await showWorkflowMenu({ bot, chatId, link, editMessageId: activity?.messageId });
637
962
  return;
638
963
  }
639
964
  if (!state.selectedProjectPath) {
640
- await send(bot, chatId, t(lang, 'control.selectProjectFirst'));
641
- await showProjectMenu({ bot, chatId, link });
965
+ await showProjectMenu({ bot, chatId, link, editMessageId: activity?.messageId, notice: t(lang, 'control.selectProjectFirst') });
642
966
  return;
643
967
  }
644
968
 
645
969
  const provider = resolveTelegramProvider(state);
646
970
  const model = resolveTelegramModel(state);
971
+ const active = activity || await createTelegramActivity({
972
+ bot,
973
+ chatId,
974
+ link,
975
+ type: 'workflow',
976
+ prompt: input,
977
+ phase: t(lang, 'control.activity.startingWorkflow'),
978
+ });
979
+ active.type = 'workflow';
980
+ active.provider = provider;
981
+ active.project = projectLabel(state);
982
+ active.workflowId = workflowId;
983
+ active.status = 'running';
984
+ active.phase = t(lang, 'control.activity.startingWorkflow');
985
+ await editTelegramActivity({ bot, chatId, activity: active, force: true });
986
+
647
987
  const result = await runTelegramTool({
648
988
  userId: link.user_id,
649
989
  action: 'run_workflow',
@@ -664,12 +1004,15 @@ export async function runWorkflow({ bot, chatId, link, input }) {
664
1004
  }),
665
1005
  });
666
1006
  if (!result.ok) {
667
- await sendToolFailure({ bot, chatId, link, result });
1007
+ await sendToolFailure({ bot, chatId, link, result, editMessageId: active.messageId });
668
1008
  return;
669
1009
  }
670
1010
  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) => {
1011
+ active.runId = run.id;
1012
+ active.phase = t(lang, 'control.activity.workflowRunning');
1013
+ pushActivityEvent(active, `🧭 ${t(lang, 'control.workflowStarted', { runId: run.id, workflowId }).replace('\n', ' ')}`);
1014
+ await editTelegramActivity({ bot, chatId, activity: active, force: true });
1015
+ monitorWorkflowRun({ bot, chatId, link, runId: run.id, activity: active }).catch((error) => {
673
1016
  console.warn('[telegram-control] workflow monitor failed:', error?.message || error);
674
1017
  });
675
1018
  }
@@ -750,21 +1093,96 @@ async function findProjectByQuery(query) {
750
1093
  }) || null;
751
1094
  }
752
1095
 
753
- async function handleRoutedIntent({ bot, chatId, link, text }) {
1096
+ async function listWorkflowSummaries(userId) {
1097
+ try {
1098
+ const data = await localApi(userId, '/api/orchestration/workflows', { timeoutMs: 10_000 });
1099
+ if (Array.isArray(data)) return data;
1100
+ if (Array.isArray(data?.workflows)) return data.workflows;
1101
+ } catch {
1102
+ // Workflow context is helpful for routing but should never block chat input.
1103
+ }
1104
+ return [];
1105
+ }
1106
+
1107
+ async function resolveTelegramAiIntent({ bot, chatId, link, text, activity }) {
754
1108
  const state = getState(link.user_id);
755
- const intent = classifyTelegramIntent(text, state);
756
- if (!intent) return false;
1109
+ if (state.routerEnabled === false || state.routerMode !== 'hybrid') {
1110
+ return { action: 'agent_prompt', prompt: text, confidence: 1 };
1111
+ }
757
1112
 
758
- if (intent.action === 'projects') {
759
- await showProjectMenu({ bot, chatId, link });
1113
+ const provider = resolveTelegramProvider(state);
1114
+ const model = resolveTelegramModel(state);
1115
+ if (activity) {
1116
+ activity.type = 'router';
1117
+ activity.provider = provider;
1118
+ activity.project = projectLabel(state);
1119
+ activity.status = 'running';
1120
+ activity.phase = t(activity.lang, 'control.activity.interpreting');
1121
+ pushActivityEvent(activity, `🧠 ${t(activity.lang, 'control.activity.interpreting')}`);
1122
+ await editTelegramActivity({ bot, chatId, activity, force: true });
1123
+ }
1124
+
1125
+ try {
1126
+ const projects = await listProjects().catch(() => []);
1127
+ const workflows = await listWorkflowSummaries(link.user_id);
1128
+ const response = await localApi(link.user_id, '/api/agent', {
1129
+ method: 'POST',
1130
+ timeoutMs: INTENT_ROUTER_TIMEOUT_MS,
1131
+ body: {
1132
+ projectPath: state.selectedProjectPath || process.cwd(),
1133
+ provider,
1134
+ model: model || undefined,
1135
+ message: buildTelegramIntentPrompt(text, state, { projects, workflows }),
1136
+ cleanup: false,
1137
+ stream: false,
1138
+ permissionMode: 'plan',
1139
+ },
1140
+ });
1141
+ const assistantText = extractAssistantText(response);
1142
+ const intent = parseTelegramAiIntentResponse(assistantText, text);
1143
+ if (activity) {
1144
+ pushActivityEvent(activity, `🧭 ${intent.action} (${Math.round(intent.confidence * 100)}%)`);
1145
+ await editTelegramActivity({ bot, chatId, activity });
1146
+ }
1147
+ return intent;
1148
+ } catch (error) {
1149
+ if (activity) {
1150
+ pushActivityEvent(activity, `⚠️ ${t(activity.lang, 'control.activity.routerFallback')}`);
1151
+ await editTelegramActivity({ bot, chatId, activity });
1152
+ }
1153
+ console.warn('[telegram-control] AI intent router fallback:', error?.message || error);
1154
+ return { action: 'agent_prompt', prompt: text, confidence: 0 };
1155
+ }
1156
+ }
1157
+
1158
+ async function handleRoutedIntent({ bot, chatId, link, text, activity }) {
1159
+ const intent = await resolveTelegramAiIntent({ bot, chatId, link, text, activity });
1160
+ const editMessageId = activity?.messageId;
1161
+
1162
+ if (intent.action === 'agent_prompt') {
1163
+ await runAgent({ bot, chatId, link, prompt: intent.prompt || text, activity });
1164
+ return true;
1165
+ }
1166
+ if (intent.action === 'show_menu') {
1167
+ await showMainMenu({ bot, chatId, link, editMessageId });
1168
+ return true;
1169
+ }
1170
+ if (intent.action === 'show_projects') {
1171
+ await showProjectMenu({ bot, chatId, link, editMessageId });
760
1172
  return true;
761
1173
  }
762
- if (intent.action === 'project_select_query') {
763
- const project = await findProjectByQuery(intent.query);
1174
+ if (intent.action === 'select_project') {
1175
+ const query = intent.projectQuery || intent.prompt || text;
1176
+ const project = await findProjectByQuery(query);
764
1177
  const lang = languageFor(link);
765
1178
  if (!project) {
766
- await send(bot, chatId, t(lang, 'control.projectNotFound', { query: intent.query }));
767
- await showProjectMenu({ bot, chatId, link });
1179
+ await showProjectMenu({
1180
+ bot,
1181
+ chatId,
1182
+ link,
1183
+ editMessageId,
1184
+ notice: t(lang, 'control.projectNotFound', { query }),
1185
+ });
768
1186
  return true;
769
1187
  }
770
1188
  updateTelegramControlState(link.user_id, {
@@ -775,6 +1193,7 @@ async function handleRoutedIntent({ bot, chatId, link, text }) {
775
1193
  bot,
776
1194
  chatId,
777
1195
  link,
1196
+ editMessageId,
778
1197
  notice: t(lang, 'control.projectSelected', {
779
1198
  project: project.displayName || project.name,
780
1199
  path: project.fullPath || project.path,
@@ -782,36 +1201,56 @@ async function handleRoutedIntent({ bot, chatId, link, text }) {
782
1201
  });
783
1202
  return true;
784
1203
  }
785
- if (intent.action === 'provider_menu') {
786
- await showProviderMenu({ bot, chatId, link });
1204
+ if (intent.action === 'show_provider_menu') {
1205
+ await showProviderMenu({ bot, chatId, link, editMessageId });
787
1206
  return true;
788
1207
  }
789
- if (intent.action === 'provider_select') {
1208
+ if (intent.action === 'select_provider' && intent.provider) {
790
1209
  updateTelegramControlState(link.user_id, { selectedProvider: intent.provider, selectedModel: null });
791
- await showModelMenu({ bot, chatId, link });
1210
+ await showModelMenu({ bot, chatId, link, editMessageId });
792
1211
  return true;
793
1212
  }
794
- if (intent.action === 'model_menu') {
795
- await showModelMenu({ bot, chatId, link });
1213
+ if (intent.action === 'show_model_menu') {
1214
+ await showModelMenu({ bot, chatId, link, editMessageId });
796
1215
  return true;
797
1216
  }
798
- if (intent.action === 'runs') {
799
- await showRuns({ bot, chatId, link });
1217
+ if (intent.action === 'select_model' && intent.model) {
1218
+ updateTelegramControlState(link.user_id, { selectedModel: intent.model });
1219
+ await showMainMenu({
1220
+ bot,
1221
+ chatId,
1222
+ link,
1223
+ editMessageId,
1224
+ notice: t(languageFor(link), 'control.modelSelected', { model: intent.model }),
1225
+ });
800
1226
  return true;
801
1227
  }
802
- if (intent.action === 'approvals') {
803
- await showApprovalQueue({ bot, chatId, link });
1228
+ if (intent.action === 'show_runs') {
1229
+ await showRuns({ bot, chatId, link, editMessageId });
804
1230
  return true;
805
1231
  }
806
- if (intent.action === 'workflows') {
807
- await showWorkflowMenu({ bot, chatId, link });
1232
+ if (intent.action === 'show_approvals') {
1233
+ await showApprovalQueue({ bot, chatId, link, editMessageId });
1234
+ return true;
1235
+ }
1236
+ if (intent.action === 'show_workflows') {
1237
+ await showWorkflowMenu({ bot, chatId, link, editMessageId });
1238
+ return true;
1239
+ }
1240
+ if (intent.action === 'run_workflow') {
1241
+ await runWorkflow({ bot, chatId, link, input: intent.workflowInput || text, activity });
1242
+ return true;
1243
+ }
1244
+ if (intent.action === 'show_sessions') {
1245
+ await showSessions({ bot, chatId, link, editMessageId });
808
1246
  return true;
809
1247
  }
810
1248
  if (intent.action === 'new_chat') {
811
- await startNewChat({ bot, chatId, link });
1249
+ await startNewChat({ bot, chatId, link, editMessageId });
812
1250
  return true;
813
1251
  }
814
- return false;
1252
+ await runAgent({ bot, chatId, link, prompt: text, activity });
1253
+ return true;
815
1254
  }
816
1255
 
817
1256
  async function fetchRun(userId, runId) {
@@ -819,10 +1258,17 @@ async function fetchRun(userId, runId) {
819
1258
  }
820
1259
 
821
1260
  function summarizeRun(run, mode) {
1261
+ const statusIcon = run.status === 'completed'
1262
+ ? '✅'
1263
+ : run.status === 'failed'
1264
+ ? '❌'
1265
+ : run.status === 'canceled'
1266
+ ? '⏹'
1267
+ : '🔧';
822
1268
  const lines = [
823
- `Run ${run.id}`,
824
- `Workflow: ${run.workflowId}`,
825
- `Status: ${run.status}`,
1269
+ `${statusIcon} Run ${run.id}`,
1270
+ `🧩 Workflow: ${run.workflowId}`,
1271
+ `📌 Status: ${run.status}`,
826
1272
  ];
827
1273
  const nodeRuns = Array.isArray(run.nodeRuns) ? run.nodeRuns : [];
828
1274
  if (mode !== 'final') {
@@ -830,7 +1276,14 @@ function summarizeRun(run, mode) {
830
1276
  ? nodeRuns.filter((node) => node.error || node.status === 'failed')
831
1277
  : nodeRuns;
832
1278
  for (const node of visibleNodes) {
833
- lines.push(`- ${node.status}: ${node.agentLabel || node.nodeId}${node.error ? ` — ${node.error}` : ''}`);
1279
+ const nodeIcon = node.status === 'completed'
1280
+ ? '✅'
1281
+ : node.status === 'failed'
1282
+ ? '❌'
1283
+ : node.status === 'running'
1284
+ ? '🔧'
1285
+ : '⏳';
1286
+ lines.push(`${nodeIcon} ${node.status}: ${node.agentLabel || node.nodeId}${node.error ? ` - ${node.error}` : ''}`);
834
1287
  }
835
1288
  }
836
1289
  const outputs = nodeRuns
@@ -840,7 +1293,7 @@ function summarizeRun(run, mode) {
840
1293
  return truncate(`${lines.join('\n')}${outputs.join('\n')}`);
841
1294
  }
842
1295
 
843
- async function monitorWorkflowRun({ bot, chatId, link, runId }) {
1296
+ async function monitorWorkflowRun({ bot, chatId, link, runId, activity = null }) {
844
1297
  if (runMonitors.has(runId)) return;
845
1298
  runMonitors.set(runId, true);
846
1299
  const state = getState(link.user_id);
@@ -850,12 +1303,23 @@ async function monitorWorkflowRun({ bot, chatId, link, runId }) {
850
1303
  await new Promise((resolve) => setTimeout(resolve, 5000));
851
1304
  const run = await fetchRun(link.user_id, runId);
852
1305
  const nodeRuns = Array.isArray(run.nodeRuns) ? run.nodeRuns : [];
1306
+ if (activity && !TERMINAL_RUN_STATES.has(run.status)) {
1307
+ activity.status = 'running';
1308
+ activity.phase = `${t(activity.lang, 'control.activity.workflowRunning')} (${run.status})`;
1309
+ await editTelegramActivity({ bot, chatId, activity });
1310
+ }
853
1311
  if (state.progressMode === 'all') {
854
1312
  for (const node of nodeRuns) {
855
1313
  const key = `${node.nodeId}:${node.status}`;
856
1314
  if (!seenNodeStatus.has(key)) {
857
1315
  seenNodeStatus.set(key, true);
858
- await send(bot, chatId, `${node.agentLabel || node.nodeId}: ${node.status}${node.error ? `\n${node.error}` : ''}`);
1316
+ if (activity) {
1317
+ pushActivityEvent(activity, `${node.status === 'completed' ? '✅' : node.status === 'failed' ? '❌' : '🔧'} ${node.agentLabel || node.nodeId}: ${node.status}${node.error ? ` - ${node.error}` : ''}`);
1318
+ activity.phase = t(activity.lang, 'control.activity.workflowRunning');
1319
+ await editTelegramActivity({ bot, chatId, activity });
1320
+ } else {
1321
+ await send(bot, chatId, `${node.agentLabel || node.nodeId}: ${node.status}${node.error ? `\n${node.error}` : ''}`);
1322
+ }
859
1323
  }
860
1324
  }
861
1325
  }
@@ -864,12 +1328,27 @@ async function monitorWorkflowRun({ bot, chatId, link, runId }) {
864
1328
  const key = `${node.nodeId}:${node.status}:${node.error || ''}`;
865
1329
  if (!seenNodeStatus.has(key)) {
866
1330
  seenNodeStatus.set(key, true);
867
- await send(bot, chatId, `${node.agentLabel || node.nodeId}: ${node.status}\n${node.error || ''}`);
1331
+ if (activity) {
1332
+ pushActivityEvent(activity, `❌ ${node.agentLabel || node.nodeId}: ${node.status}${node.error ? ` - ${node.error}` : ''}`);
1333
+ activity.phase = t(activity.lang, 'control.activity.workflowRunning');
1334
+ await editTelegramActivity({ bot, chatId, activity });
1335
+ } else {
1336
+ await send(bot, chatId, `${node.agentLabel || node.nodeId}: ${node.status}\n${node.error || ''}`);
1337
+ }
868
1338
  }
869
1339
  }
870
1340
  }
871
1341
  if (TERMINAL_RUN_STATES.has(run.status)) {
872
- await send(bot, chatId, summarizeRun(run, state.progressMode));
1342
+ if (activity) {
1343
+ activity.status = run.status === 'completed' ? 'done' : 'failed';
1344
+ activity.phase = run.status === 'completed'
1345
+ ? t(activity.lang, 'control.activity.done')
1346
+ : t(activity.lang, 'control.activity.failed');
1347
+ activity.output = summarizeRun(run, state.progressMode);
1348
+ await editTelegramActivity({ bot, chatId, activity, force: true });
1349
+ } else {
1350
+ await send(bot, chatId, summarizeRun(run, state.progressMode));
1351
+ }
873
1352
  return;
874
1353
  }
875
1354
  }
@@ -1124,7 +1603,6 @@ async function handleTelegramControlMessageInternal({ bot, msg, link }) {
1124
1603
 
1125
1604
  if (await handleAwaitingInput({ bot, chatId, link, text })) return true;
1126
1605
  if (await handleCommand({ bot, chatId, link, text })) return true;
1127
- if (await handleRoutedIntent({ bot, chatId, link, text })) return true;
1128
1606
 
1129
1607
  const state = getState(link.user_id);
1130
1608
  if (state.routerEnabled === false) return false;
@@ -1133,8 +1611,15 @@ async function handleTelegramControlMessageInternal({ bot, msg, link }) {
1133
1611
  return true;
1134
1612
  }
1135
1613
 
1136
- await runAgent({ bot, chatId, link, prompt: text });
1137
- return true;
1614
+ const activity = await createTelegramActivity({
1615
+ bot,
1616
+ chatId,
1617
+ link,
1618
+ type: 'router',
1619
+ prompt: text,
1620
+ phase: t(languageFor(link), 'control.activity.interpreting'),
1621
+ });
1622
+ return handleRoutedIntent({ bot, chatId, link, text, activity });
1138
1623
  }
1139
1624
 
1140
1625
  export async function handleTelegramControlMessage(args) {