@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.
@@ -2,13 +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, classifyTelegramIntent, clearTelegramConfirmation, consumeTelegramConfirmation, createTelegramConfirmation, enqueueTelegramJob, resolveTelegramModel, resolveTelegramProvider, retryWithBackoff, runTelegramTool, splitTelegramText, } from './telegram-gateway.js';
5
+ import { TELEGRAM_CONTROL_SCOPES, TELEGRAM_PROVIDERS, buildTelegramAgentPrompt, buildTelegramIntentPrompt, clearTelegramConfirmation, consumeTelegramConfirmation, createTelegramConfirmation, enqueueTelegramJob, parseTelegramAiIntentResponse, resolveTelegramModel, resolveTelegramProvider, retryWithBackoff, runTelegramTool, splitTelegramText, } from './telegram-gateway.js';
6
6
  import { SUPPORTED_LANGUAGES, t } from './translations.js';
7
7
  const PROVIDERS = TELEGRAM_PROVIDERS;
8
8
  const TERMINAL_RUN_STATES = new Set(['completed', 'failed', 'canceled']);
9
9
  const CALLBACK_TTL_MS = 10 * 60 * 1000;
10
10
  const MAX_CALLBACK_ACTIONS = 1000;
11
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;
12
15
  const callbackActions = new Map();
13
16
  const runMonitors = new Map();
14
17
  const MODEL_FALLBACKS = Object.fromEntries(PROVIDERS.map((provider) => [provider, getStaticProviderModels(provider)]));
@@ -196,16 +199,24 @@ function getOrCreateTelegramApiKey(userId) {
196
199
  }
197
200
  return apiKeysDb.createApiKey(userId, 'Telegram Control', TELEGRAM_CONTROL_SCOPES).apiKey;
198
201
  }
199
- async function localApi(userId, path, { method = 'GET', body } = {}) {
202
+ async function localApi(userId, path, { method = 'GET', body, timeoutMs = 0 } = {}) {
200
203
  const apiKey = getOrCreateTelegramApiKey(userId);
201
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;
202
209
  const res = await fetch(`${localApiBase()}${path}`, {
203
210
  method,
204
211
  headers: {
205
212
  'Content-Type': 'application/json',
206
213
  'X-API-Key': apiKey,
207
214
  },
215
+ signal: controller?.signal,
208
216
  body: body === undefined ? undefined : JSON.stringify(body),
217
+ }).finally(() => {
218
+ if (timeoutId)
219
+ clearTimeout(timeoutId);
209
220
  });
210
221
  if ([408, 429, 500, 502, 503, 504].includes(res.status)) {
211
222
  const error = new Error(`HTTP ${res.status}`);
@@ -221,9 +232,198 @@ async function localApi(userId, path, { method = 'GET', body } = {}) {
221
232
  }
222
233
  return data?.data ?? data;
223
234
  }
235
+ async function localAgentStream(userId, body, onEvent) {
236
+ const apiKey = getOrCreateTelegramApiKey(userId);
237
+ const response = await fetch(`${localApiBase()}/api/agent`, {
238
+ method: 'POST',
239
+ headers: {
240
+ 'Content-Type': 'application/json',
241
+ 'X-API-Key': apiKey,
242
+ },
243
+ body: JSON.stringify({
244
+ ...body,
245
+ stream: true,
246
+ }),
247
+ });
248
+ if (!response.ok) {
249
+ const data = await response.json().catch(() => ({}));
250
+ const error = data?.error?.message || data?.error || `HTTP ${response.status}`;
251
+ throw new Error(typeof error === 'string' ? error : JSON.stringify(error));
252
+ }
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);
292
+ }
224
293
  function checked(label) {
225
294
  return `${label} ✓`;
226
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
+ }
227
427
  function stateSummary(lang, state) {
228
428
  return [
229
429
  `${t(lang, 'control.summary.project')}: ${state.selectedProjectName || t(lang, 'control.notSelected')}`,
@@ -268,11 +468,12 @@ async function listProjects() {
268
468
  const projects = await getProjects();
269
469
  return projects.slice(0, 20);
270
470
  }
271
- async function showProjectMenu({ bot, chatId, link, editMessageId }) {
471
+ async function showProjectMenu({ bot, chatId, link, editMessageId, notice }) {
272
472
  const lang = languageFor(link);
273
473
  const projects = await listProjects();
274
474
  if (projects.length === 0) {
275
- 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 });
276
477
  return;
277
478
  }
278
479
  const buttons = projects.map((project, index) => button(`${index + 1}. ${compact(project.displayName || project.name, 34)}`, 'project_select', {
@@ -280,7 +481,8 @@ async function showProjectMenu({ bot, chatId, link, editMessageId }) {
280
481
  path: project.fullPath || project.path,
281
482
  displayName: project.displayName || project.name,
282
483
  }));
283
- 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')}`, {
284
486
  editMessageId,
285
487
  reply_markup: { inline_keyboard: rows(buttons, 1) },
286
488
  });
@@ -464,6 +666,123 @@ function extractAssistantText(response) {
464
666
  }
465
667
  return chunks.join('\n\n').trim();
466
668
  }
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
+ }
467
786
  function confirmationLabel(lang, action, payload = {}) {
468
787
  if (action === 'install_provider') {
469
788
  return t(lang, 'control.confirmInstall', { provider: payload.provider || '' });
@@ -494,73 +813,101 @@ async function sendToolFailure({ bot, chatId, link, result, editMessageId }) {
494
813
  : (result?.message || t(lang, 'error.generic'));
495
814
  await send(bot, chatId, message, { editMessageId });
496
815
  }
497
- async function runAgent({ bot, chatId, link, prompt }) {
816
+ async function runAgent({ bot, chatId, link, prompt, activity = null }) {
498
817
  const lang = languageFor(link);
499
818
  const state = getState(link.user_id);
500
819
  if (!state.remoteControlEnabled) {
501
- await send(bot, chatId, t(lang, 'control.disabled'));
820
+ await send(bot, chatId, t(lang, 'control.disabled'), { editMessageId: activity?.messageId });
502
821
  return;
503
822
  }
504
823
  if (!state.selectedProjectPath) {
505
- await send(bot, chatId, t(lang, 'control.selectProjectFirst'));
506
- await showProjectMenu({ bot, chatId, link });
824
+ await showProjectMenu({ bot, chatId, link, editMessageId: activity?.messageId, notice: t(lang, 'control.selectProjectFirst') });
507
825
  return;
508
826
  }
509
827
  const provider = resolveTelegramProvider(state);
510
828
  const model = resolveTelegramModel(state);
511
- if (state.progressMode !== 'final') {
512
- await send(bot, chatId, t(lang, 'control.agentStarted', {
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, {
847
+ projectPath: state.selectedProjectPath,
513
848
  provider,
514
- project: state.selectedProjectName || state.selectedProjectPath,
515
- }));
849
+ model: model || undefined,
850
+ message: buildTelegramAgentPrompt(prompt, state),
851
+ cleanup: false,
852
+ permissionMode: 'default',
853
+ }, async (event) => {
854
+ applyAgentStreamEvent(active, event);
855
+ await editTelegramActivity({ bot, chatId, activity: active });
856
+ });
516
857
  }
517
- const result = await runTelegramTool({
518
- userId: link.user_id,
519
- action: 'run_agent',
520
- retries: 0,
521
- execute: () => localApi(link.user_id, '/api/agent', {
522
- method: 'POST',
523
- body: {
524
- projectPath: state.selectedProjectPath,
525
- provider,
526
- model: model || undefined,
527
- message: buildTelegramAgentPrompt(prompt, state),
528
- cleanup: false,
529
- stream: false,
530
- },
531
- }),
532
- });
533
- if (!result.ok) {
534
- await sendToolFailure({ bot, chatId, link, result });
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 });
535
869
  return;
536
870
  }
537
- const response = result.data;
538
- const assistantText = extractAssistantText(response);
539
- const statusLine = response.success === false
540
- ? t(lang, 'control.agentFailed', { error: response.error || 'provider returned no answer' })
541
- : t(lang, 'control.agentDone', { provider, session: response.sessionId ? ` (${response.sessionId})` : '' });
542
- await send(bot, chatId, `${statusLine}\n\n${assistantText || response.error || t(lang, 'control.noAssistantText')}`);
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 });
543
876
  }
544
- export async function runWorkflow({ bot, chatId, link, input }) {
877
+ export async function runWorkflow({ bot, chatId, link, input, activity = null }) {
545
878
  const lang = languageFor(link);
546
879
  const state = getState(link.user_id);
547
880
  const workflowId = state.selectedWorkflowId;
548
881
  if (!state.remoteControlEnabled) {
549
- await send(bot, chatId, t(lang, 'control.disabled'));
882
+ await send(bot, chatId, t(lang, 'control.disabled'), { editMessageId: activity?.messageId });
550
883
  return;
551
884
  }
552
885
  if (!workflowId) {
553
- await send(bot, chatId, t(lang, 'control.selectWorkflowFirst'));
554
- 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 });
555
888
  return;
556
889
  }
557
890
  if (!state.selectedProjectPath) {
558
- await send(bot, chatId, t(lang, 'control.selectProjectFirst'));
559
- await showProjectMenu({ bot, chatId, link });
891
+ await showProjectMenu({ bot, chatId, link, editMessageId: activity?.messageId, notice: t(lang, 'control.selectProjectFirst') });
560
892
  return;
561
893
  }
562
894
  const provider = resolveTelegramProvider(state);
563
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 });
564
911
  const result = await runTelegramTool({
565
912
  userId: link.user_id,
566
913
  action: 'run_workflow',
@@ -581,12 +928,15 @@ export async function runWorkflow({ bot, chatId, link, input }) {
581
928
  }),
582
929
  });
583
930
  if (!result.ok) {
584
- await sendToolFailure({ bot, chatId, link, result });
931
+ await sendToolFailure({ bot, chatId, link, result, editMessageId: active.messageId });
585
932
  return;
586
933
  }
587
934
  const run = result.data;
588
- await send(bot, chatId, t(lang, 'control.workflowStarted', { runId: run.id, workflowId }));
589
- monitorWorkflowRun({ bot, chatId, link, runId: run.id }).catch((error) => {
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) => {
590
940
  console.warn('[telegram-control] workflow monitor failed:', error?.message || error);
591
941
  });
592
942
  }
@@ -663,21 +1013,95 @@ async function findProjectByQuery(query) {
663
1013
  return candidates.some((candidate) => candidate === needle || candidate.includes(needle));
664
1014
  }) || null;
665
1015
  }
666
- async function handleRoutedIntent({ bot, chatId, link, text }) {
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 }) {
667
1030
  const state = getState(link.user_id);
668
- const intent = classifyTelegramIntent(text, state);
669
- if (!intent)
670
- return false;
671
- if (intent.action === 'projects') {
672
- await showProjectMenu({ bot, chatId, link });
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 });
673
1091
  return true;
674
1092
  }
675
- if (intent.action === 'project_select_query') {
676
- const project = await findProjectByQuery(intent.query);
1093
+ if (intent.action === 'select_project') {
1094
+ const query = intent.projectQuery || intent.prompt || text;
1095
+ const project = await findProjectByQuery(query);
677
1096
  const lang = languageFor(link);
678
1097
  if (!project) {
679
- await send(bot, chatId, t(lang, 'control.projectNotFound', { query: intent.query }));
680
- await showProjectMenu({ bot, chatId, link });
1098
+ await showProjectMenu({
1099
+ bot,
1100
+ chatId,
1101
+ link,
1102
+ editMessageId,
1103
+ notice: t(lang, 'control.projectNotFound', { query }),
1104
+ });
681
1105
  return true;
682
1106
  }
683
1107
  updateTelegramControlState(link.user_id, {
@@ -688,6 +1112,7 @@ async function handleRoutedIntent({ bot, chatId, link, text }) {
688
1112
  bot,
689
1113
  chatId,
690
1114
  link,
1115
+ editMessageId,
691
1116
  notice: t(lang, 'control.projectSelected', {
692
1117
  project: project.displayName || project.name,
693
1118
  path: project.fullPath || project.path,
@@ -695,45 +1120,72 @@ async function handleRoutedIntent({ bot, chatId, link, text }) {
695
1120
  });
696
1121
  return true;
697
1122
  }
698
- if (intent.action === 'provider_menu') {
699
- await showProviderMenu({ bot, chatId, link });
1123
+ if (intent.action === 'show_provider_menu') {
1124
+ await showProviderMenu({ bot, chatId, link, editMessageId });
700
1125
  return true;
701
1126
  }
702
- if (intent.action === 'provider_select') {
1127
+ if (intent.action === 'select_provider' && intent.provider) {
703
1128
  updateTelegramControlState(link.user_id, { selectedProvider: intent.provider, selectedModel: null });
704
- await showModelMenu({ bot, chatId, link });
1129
+ await showModelMenu({ bot, chatId, link, editMessageId });
705
1130
  return true;
706
1131
  }
707
- if (intent.action === 'model_menu') {
708
- await showModelMenu({ bot, chatId, link });
1132
+ if (intent.action === 'show_model_menu') {
1133
+ await showModelMenu({ bot, chatId, link, editMessageId });
709
1134
  return true;
710
1135
  }
711
- if (intent.action === 'runs') {
712
- await showRuns({ bot, chatId, link });
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
+ });
713
1145
  return true;
714
1146
  }
715
- if (intent.action === 'approvals') {
716
- await showApprovalQueue({ bot, chatId, link });
1147
+ if (intent.action === 'show_runs') {
1148
+ await showRuns({ bot, chatId, link, editMessageId });
717
1149
  return true;
718
1150
  }
719
- if (intent.action === 'workflows') {
720
- await showWorkflowMenu({ bot, chatId, link });
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 });
721
1165
  return true;
722
1166
  }
723
1167
  if (intent.action === 'new_chat') {
724
- await startNewChat({ bot, chatId, link });
1168
+ await startNewChat({ bot, chatId, link, editMessageId });
725
1169
  return true;
726
1170
  }
727
- return false;
1171
+ await runAgent({ bot, chatId, link, prompt: text, activity });
1172
+ return true;
728
1173
  }
729
1174
  async function fetchRun(userId, runId) {
730
1175
  return localApi(userId, `/api/orchestration/workflows/runs/${runId}`);
731
1176
  }
732
1177
  function summarizeRun(run, mode) {
1178
+ const statusIcon = run.status === 'completed'
1179
+ ? '✅'
1180
+ : run.status === 'failed'
1181
+ ? '❌'
1182
+ : run.status === 'canceled'
1183
+ ? '⏹'
1184
+ : '🔧';
733
1185
  const lines = [
734
- `Run ${run.id}`,
735
- `Workflow: ${run.workflowId}`,
736
- `Status: ${run.status}`,
1186
+ `${statusIcon} Run ${run.id}`,
1187
+ `🧩 Workflow: ${run.workflowId}`,
1188
+ `📌 Status: ${run.status}`,
737
1189
  ];
738
1190
  const nodeRuns = Array.isArray(run.nodeRuns) ? run.nodeRuns : [];
739
1191
  if (mode !== 'final') {
@@ -741,7 +1193,14 @@ function summarizeRun(run, mode) {
741
1193
  ? nodeRuns.filter((node) => node.error || node.status === 'failed')
742
1194
  : nodeRuns;
743
1195
  for (const node of visibleNodes) {
744
- 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}` : ''}`);
745
1204
  }
746
1205
  }
747
1206
  const outputs = nodeRuns
@@ -750,7 +1209,7 @@ function summarizeRun(run, mode) {
750
1209
  .map((node) => `\n${node.agentLabel || node.nodeId}:\n${node.outputText}`);
751
1210
  return truncate(`${lines.join('\n')}${outputs.join('\n')}`);
752
1211
  }
753
- async function monitorWorkflowRun({ bot, chatId, link, runId }) {
1212
+ async function monitorWorkflowRun({ bot, chatId, link, runId, activity = null }) {
754
1213
  if (runMonitors.has(runId))
755
1214
  return;
756
1215
  runMonitors.set(runId, true);
@@ -761,12 +1220,24 @@ async function monitorWorkflowRun({ bot, chatId, link, runId }) {
761
1220
  await new Promise((resolve) => setTimeout(resolve, 5000));
762
1221
  const run = await fetchRun(link.user_id, runId);
763
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
+ }
764
1228
  if (state.progressMode === 'all') {
765
1229
  for (const node of nodeRuns) {
766
1230
  const key = `${node.nodeId}:${node.status}`;
767
1231
  if (!seenNodeStatus.has(key)) {
768
1232
  seenNodeStatus.set(key, true);
769
- 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
+ }
770
1241
  }
771
1242
  }
772
1243
  }
@@ -775,12 +1246,29 @@ async function monitorWorkflowRun({ bot, chatId, link, runId }) {
775
1246
  const key = `${node.nodeId}:${node.status}:${node.error || ''}`;
776
1247
  if (!seenNodeStatus.has(key)) {
777
1248
  seenNodeStatus.set(key, true);
778
- 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
+ }
779
1257
  }
780
1258
  }
781
1259
  }
782
1260
  if (TERMINAL_RUN_STATES.has(run.status)) {
783
- 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
+ }
784
1272
  return;
785
1273
  }
786
1274
  }
@@ -1027,8 +1515,6 @@ async function handleTelegramControlMessageInternal({ bot, msg, link }) {
1027
1515
  return true;
1028
1516
  if (await handleCommand({ bot, chatId, link, text }))
1029
1517
  return true;
1030
- if (await handleRoutedIntent({ bot, chatId, link, text }))
1031
- return true;
1032
1518
  const state = getState(link.user_id);
1033
1519
  if (state.routerEnabled === false)
1034
1520
  return false;
@@ -1036,8 +1522,15 @@ async function handleTelegramControlMessageInternal({ bot, msg, link }) {
1036
1522
  await send(bot, chatId, t(languageFor(link), 'control.disabled'));
1037
1523
  return true;
1038
1524
  }
1039
- await runAgent({ bot, chatId, link, prompt: text });
1040
- 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 });
1041
1534
  }
1042
1535
  export async function handleTelegramControlMessage(args) {
1043
1536
  const chatId = args?.msg?.chat?.id;