@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
@@ -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,214 @@ 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 trimTelegramOutput(text, max, suffix = '') {
346
+ const value = String(text || '').trim();
347
+ const ending = String(suffix || '').trim();
348
+ if (value.length <= max)
349
+ return value;
350
+ const room = Math.max(300, max - ending.length - 4);
351
+ return `${value.slice(0, room).trim()}\n\n${ending}`;
352
+ }
353
+ function renderActivity(activity, { finalText = null } = {}) {
354
+ const output = finalText || activity.output;
355
+ if (activity.type === 'agent' && output && !activity.error) {
356
+ if (activity.status === 'done') {
357
+ return trimTelegramOutput(output, 3400, t(activity.lang, 'control.activity.outputTooLong'));
358
+ }
359
+ const footer = `⏳ ${t(activity.lang, 'control.activity.liveFooter', { elapsed: formatElapsed(activity.startedAt) })}`;
360
+ const body = trimTelegramOutput(output, 3200, t(activity.lang, 'control.activity.outputShortened'));
361
+ return truncate(`${body}\n\n${footer}`, 3400);
362
+ }
363
+ const lines = [
364
+ `${activity.status === 'failed' ? '❌' : activity.status === 'done' ? '✅' : activity.status === 'running' ? '🔧' : '⏳'} ${activityTitle(activity)}`,
365
+ '',
366
+ `🤖 ${t(activity.lang, 'control.activity.provider')}: ${activity.provider || '-'}`,
367
+ `📁 ${t(activity.lang, 'control.activity.project')}: ${compact(activity.project, 90)}`,
368
+ ];
369
+ if (activity.sessionId)
370
+ lines.push(`🧵 ${t(activity.lang, 'control.activity.session')}: ${activity.sessionId}`);
371
+ if (activity.runId)
372
+ lines.push(`🧭 ${t(activity.lang, 'control.activity.run')}: ${activity.runId}`);
373
+ if (activity.workflowId)
374
+ lines.push(`🧩 ${t(activity.lang, 'control.activity.workflow')}: ${activity.workflowId}`);
375
+ lines.push(`⏱ ${t(activity.lang, 'control.activity.elapsed')}: ${formatElapsed(activity.startedAt)}`);
376
+ lines.push(`📌 ${t(activity.lang, 'control.activity.status')}: ${activity.phase}`);
377
+ const visibleEvents = activity.mode === 'final'
378
+ ? activity.events.slice(-4)
379
+ : activity.events;
380
+ if (visibleEvents.length > 0) {
381
+ lines.push('', `🛠 ${t(activity.lang, 'control.activity.work')}:`);
382
+ for (const event of visibleEvents)
383
+ lines.push(`• ${event}`);
384
+ }
385
+ if (activity.error) {
386
+ lines.push('', `⚠️ ${truncate(activity.error, 700)}`);
387
+ }
388
+ else if (output) {
389
+ lines.push('', `💬 ${t(activity.lang, 'control.activity.output')}:`);
390
+ lines.push(truncate(output, 1800));
391
+ }
392
+ else if (activity.prompt) {
393
+ lines.push('', `📝 ${compact(activity.prompt, 220)}`);
394
+ }
395
+ return truncate(lines.join('\n'), 3400);
396
+ }
397
+ async function createTelegramActivity({ bot, chatId, link, type = 'agent', prompt = '', phase = null }) {
398
+ const lang = languageFor(link);
399
+ const state = getState(link.user_id);
400
+ const activity = createActivityState({
401
+ lang,
402
+ type,
403
+ provider: resolveTelegramProvider(state),
404
+ project: projectLabel(state),
405
+ prompt,
406
+ mode: state.progressMode,
407
+ });
408
+ if (phase)
409
+ activity.phase = phase;
410
+ const sent = await send(bot, chatId, renderActivity(activity), { parse_mode: undefined });
411
+ activity.messageId = sent?.message_id || sent?.message?.message_id || null;
412
+ activity.lastEditAt = Date.now();
413
+ return activity;
414
+ }
415
+ async function editTelegramActivity({ bot, chatId, activity, force = false, reply_markup }) {
416
+ if (!activity)
417
+ return null;
418
+ const now = Date.now();
419
+ if (!force && now - activity.lastEditAt < ACTIVITY_EDIT_THROTTLE_MS)
420
+ return null;
421
+ const sent = await send(bot, chatId, renderActivity(activity), {
422
+ editMessageId: activity.messageId,
423
+ parse_mode: undefined,
424
+ reply_markup,
425
+ });
426
+ activity.messageId = sent?.message_id || sent?.message?.message_id || activity.messageId;
427
+ activity.lastEditAt = now;
428
+ return sent;
429
+ }
430
+ function startActivityHeartbeat({ bot, chatId, activity }) {
431
+ return setInterval(() => {
432
+ if (!activity || activity.status === 'done' || activity.status === 'failed')
433
+ return;
434
+ if (activity.status === 'starting') {
435
+ activity.status = 'running';
436
+ activity.phase = t(activity.lang, 'control.activity.thinking');
437
+ }
438
+ editTelegramActivity({ bot, chatId, activity }).catch((error) => {
439
+ console.warn('[telegram-control] activity heartbeat failed:', error?.message || error);
440
+ });
441
+ }, ACTIVITY_HEARTBEAT_MS);
442
+ }
227
443
  function stateSummary(lang, state) {
228
444
  return [
229
445
  `${t(lang, 'control.summary.project')}: ${state.selectedProjectName || t(lang, 'control.notSelected')}`,
@@ -268,11 +484,12 @@ async function listProjects() {
268
484
  const projects = await getProjects();
269
485
  return projects.slice(0, 20);
270
486
  }
271
- async function showProjectMenu({ bot, chatId, link, editMessageId }) {
487
+ async function showProjectMenu({ bot, chatId, link, editMessageId, notice }) {
272
488
  const lang = languageFor(link);
273
489
  const projects = await listProjects();
274
490
  if (projects.length === 0) {
275
- await send(bot, chatId, t(lang, 'control.noProjects'), { editMessageId });
491
+ const prefix = notice ? `${notice}\n\n` : '';
492
+ await send(bot, chatId, `${prefix}${t(lang, 'control.noProjects')}`, { editMessageId });
276
493
  return;
277
494
  }
278
495
  const buttons = projects.map((project, index) => button(`${index + 1}. ${compact(project.displayName || project.name, 34)}`, 'project_select', {
@@ -280,7 +497,8 @@ async function showProjectMenu({ bot, chatId, link, editMessageId }) {
280
497
  path: project.fullPath || project.path,
281
498
  displayName: project.displayName || project.name,
282
499
  }));
283
- await send(bot, chatId, t(lang, 'control.pickProject'), {
500
+ const prefix = notice ? `${notice}\n\n` : '';
501
+ await send(bot, chatId, `${prefix}${t(lang, 'control.pickProject')}`, {
284
502
  editMessageId,
285
503
  reply_markup: { inline_keyboard: rows(buttons, 1) },
286
504
  });
@@ -464,6 +682,123 @@ function extractAssistantText(response) {
464
682
  }
465
683
  return chunks.join('\n\n').trim();
466
684
  }
685
+ function extractTextFromEvent(event) {
686
+ if (!event || typeof event !== 'object')
687
+ return '';
688
+ if (typeof event.content === 'string')
689
+ return event.content;
690
+ if (Array.isArray(event.content)) {
691
+ return event.content
692
+ .map((part) => (typeof part === 'string' ? part : part?.text || ''))
693
+ .join('');
694
+ }
695
+ const legacyContent = event.data?.message?.content || event.message?.content;
696
+ if (Array.isArray(legacyContent)) {
697
+ return legacyContent
698
+ .map((part) => (typeof part === 'string' ? part : part?.text || ''))
699
+ .join('');
700
+ }
701
+ if (typeof legacyContent === 'string')
702
+ return legacyContent;
703
+ return '';
704
+ }
705
+ function describeToolInput(toolName, input) {
706
+ if (!input || typeof input !== 'object')
707
+ return '';
708
+ if (toolName === 'Bash' && typeof input.command === 'string') {
709
+ return compact(input.command, 110);
710
+ }
711
+ if (toolName === 'WebSearch' && typeof input.query === 'string') {
712
+ return compact(input.query, 110);
713
+ }
714
+ if (toolName === 'FileChanges') {
715
+ return compact(JSON.stringify(input), 110);
716
+ }
717
+ const keys = Object.keys(input).slice(0, 3);
718
+ if (keys.length === 0)
719
+ return '';
720
+ return compact(keys.map((key) => `${key}: ${String(input[key])}`).join(', '), 110);
721
+ }
722
+ function applyAgentStreamEvent(activity, event) {
723
+ if (!event || typeof event !== 'object')
724
+ return;
725
+ const sessionId = event.actualSessionId || event.newSessionId || event.sessionId || event.threadId;
726
+ if (sessionId)
727
+ activity.sessionId = sessionId;
728
+ if (event.type === 'status' && event.message) {
729
+ activity.status = 'running';
730
+ activity.phase = compact(event.message, 120);
731
+ pushActivityEvent(activity, `⏳ ${compact(event.message, 120)}`);
732
+ return;
733
+ }
734
+ if (event.type === 'done' || event.kind === 'complete' || event.kind === 'stream_end') {
735
+ activity.status = 'done';
736
+ activity.phase = t(activity.lang, 'control.activity.done');
737
+ return;
738
+ }
739
+ if (event.type === 'error' || event.kind === 'error') {
740
+ activity.status = 'failed';
741
+ activity.phase = t(activity.lang, 'control.activity.failed');
742
+ activity.error = event.error || event.message || event.content || t(activity.lang, 'error.generic');
743
+ pushActivityEvent(activity, `❌ ${compact(activity.error, 120)}`);
744
+ return;
745
+ }
746
+ if (event.kind === 'session_created') {
747
+ activity.status = 'running';
748
+ activity.phase = t(activity.lang, 'control.activity.sessionStarted');
749
+ pushActivityEvent(activity, `🧵 ${t(activity.lang, 'control.activity.sessionStarted')}`);
750
+ return;
751
+ }
752
+ if (event.kind === 'thinking') {
753
+ activity.status = 'running';
754
+ activity.phase = t(activity.lang, 'control.activity.thinking');
755
+ const thought = extractTextFromEvent(event);
756
+ if (thought)
757
+ pushActivityEvent(activity, `🧠 ${compact(thought, 120)}`);
758
+ return;
759
+ }
760
+ if (event.kind === 'stream_delta' || event.kind === 'text') {
761
+ activity.status = 'running';
762
+ activity.phase = t(activity.lang, 'control.activity.responding');
763
+ activity.output = `${activity.output}${extractTextFromEvent(event)}`;
764
+ return;
765
+ }
766
+ if (event.type === 'claude-response' && event.data?.type === 'assistant') {
767
+ activity.status = 'running';
768
+ activity.phase = t(activity.lang, 'control.activity.responding');
769
+ activity.output = `${activity.output}${extractTextFromEvent(event)}`;
770
+ return;
771
+ }
772
+ if (event.kind === 'tool_use') {
773
+ activity.status = 'running';
774
+ activity.phase = t(activity.lang, 'control.activity.working');
775
+ const toolName = event.toolName || 'Tool';
776
+ const input = describeToolInput(toolName, event.toolInput);
777
+ const icon = toolName === 'Bash'
778
+ ? '💻'
779
+ : toolName === 'FileChanges'
780
+ ? '📝'
781
+ : toolName === 'WebSearch'
782
+ ? '🔎'
783
+ : '🔧';
784
+ pushActivityEvent(activity, `${icon} ${toolName}${input ? `: ${input}` : ''}`);
785
+ return;
786
+ }
787
+ if (event.kind === 'tool_result') {
788
+ activity.status = 'running';
789
+ activity.phase = t(activity.lang, 'control.activity.working');
790
+ const label = event.isError
791
+ ? t(activity.lang, 'control.activity.toolFailed')
792
+ : t(activity.lang, 'control.activity.toolDone');
793
+ pushActivityEvent(activity, `${event.isError ? '⚠️' : '✅'} ${label}`);
794
+ return;
795
+ }
796
+ if (event.kind === 'status' && event.text === 'token_budget') {
797
+ const used = event.tokenBudget?.used;
798
+ if (used)
799
+ pushActivityEvent(activity, `📊 ${t(activity.lang, 'control.activity.tokens')}: ${used}`);
800
+ }
801
+ }
467
802
  function confirmationLabel(lang, action, payload = {}) {
468
803
  if (action === 'install_provider') {
469
804
  return t(lang, 'control.confirmInstall', { provider: payload.provider || '' });
@@ -494,73 +829,102 @@ async function sendToolFailure({ bot, chatId, link, result, editMessageId }) {
494
829
  : (result?.message || t(lang, 'error.generic'));
495
830
  await send(bot, chatId, message, { editMessageId });
496
831
  }
497
- async function runAgent({ bot, chatId, link, prompt }) {
832
+ async function runAgent({ bot, chatId, link, prompt, activity = null }) {
498
833
  const lang = languageFor(link);
499
834
  const state = getState(link.user_id);
500
835
  if (!state.remoteControlEnabled) {
501
- await send(bot, chatId, t(lang, 'control.disabled'));
836
+ await send(bot, chatId, t(lang, 'control.disabled'), { editMessageId: activity?.messageId });
502
837
  return;
503
838
  }
504
839
  if (!state.selectedProjectPath) {
505
- await send(bot, chatId, t(lang, 'control.selectProjectFirst'));
506
- await showProjectMenu({ bot, chatId, link });
840
+ await showProjectMenu({ bot, chatId, link, editMessageId: activity?.messageId, notice: t(lang, 'control.selectProjectFirst') });
507
841
  return;
508
842
  }
509
843
  const provider = resolveTelegramProvider(state);
510
844
  const model = resolveTelegramModel(state);
511
- if (state.progressMode !== 'final') {
512
- await send(bot, chatId, t(lang, 'control.agentStarted', {
845
+ const active = activity || await createTelegramActivity({
846
+ bot,
847
+ chatId,
848
+ link,
849
+ type: 'agent',
850
+ prompt,
851
+ phase: t(lang, 'control.activity.startingProvider', { provider }),
852
+ });
853
+ active.type = 'agent';
854
+ active.provider = provider;
855
+ active.project = projectLabel(state);
856
+ active.status = 'running';
857
+ active.phase = t(lang, 'control.activity.startingProvider', { provider });
858
+ await editTelegramActivity({ bot, chatId, activity: active, force: true });
859
+ const heartbeat = startActivityHeartbeat({ bot, chatId, activity: active });
860
+ let streamFailed = null;
861
+ try {
862
+ await localAgentStream(link.user_id, {
863
+ projectPath: state.selectedProjectPath,
513
864
  provider,
514
- project: state.selectedProjectName || state.selectedProjectPath,
515
- }));
865
+ model: model || undefined,
866
+ message: buildTelegramAgentPrompt(prompt, state),
867
+ cleanup: false,
868
+ permissionMode: 'default',
869
+ suppressNotifications: true,
870
+ }, async (event) => {
871
+ applyAgentStreamEvent(active, event);
872
+ await editTelegramActivity({ bot, chatId, activity: active });
873
+ });
516
874
  }
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 });
875
+ catch (error) {
876
+ streamFailed = error;
877
+ }
878
+ finally {
879
+ clearInterval(heartbeat);
880
+ }
881
+ if (streamFailed) {
882
+ active.status = 'failed';
883
+ active.phase = t(lang, 'control.activity.failed');
884
+ active.error = streamFailed.message || t(lang, 'error.generic');
885
+ await editTelegramActivity({ bot, chatId, activity: active, force: true });
535
886
  return;
536
887
  }
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')}`);
888
+ active.status = active.error ? 'failed' : 'done';
889
+ active.phase = active.error ? t(lang, 'control.activity.failed') : t(lang, 'control.activity.done');
890
+ if (!active.output && !active.error)
891
+ active.output = t(lang, 'control.noAssistantText');
892
+ await editTelegramActivity({ bot, chatId, activity: active, force: true });
543
893
  }
544
- export async function runWorkflow({ bot, chatId, link, input }) {
894
+ export async function runWorkflow({ bot, chatId, link, input, activity = null }) {
545
895
  const lang = languageFor(link);
546
896
  const state = getState(link.user_id);
547
897
  const workflowId = state.selectedWorkflowId;
548
898
  if (!state.remoteControlEnabled) {
549
- await send(bot, chatId, t(lang, 'control.disabled'));
899
+ await send(bot, chatId, t(lang, 'control.disabled'), { editMessageId: activity?.messageId });
550
900
  return;
551
901
  }
552
902
  if (!workflowId) {
553
- await send(bot, chatId, t(lang, 'control.selectWorkflowFirst'));
554
- await showWorkflowMenu({ bot, chatId, link });
903
+ await send(bot, chatId, t(lang, 'control.selectWorkflowFirst'), { editMessageId: activity?.messageId });
904
+ await showWorkflowMenu({ bot, chatId, link, editMessageId: activity?.messageId });
555
905
  return;
556
906
  }
557
907
  if (!state.selectedProjectPath) {
558
- await send(bot, chatId, t(lang, 'control.selectProjectFirst'));
559
- await showProjectMenu({ bot, chatId, link });
908
+ await showProjectMenu({ bot, chatId, link, editMessageId: activity?.messageId, notice: t(lang, 'control.selectProjectFirst') });
560
909
  return;
561
910
  }
562
911
  const provider = resolveTelegramProvider(state);
563
912
  const model = resolveTelegramModel(state);
913
+ const active = activity || await createTelegramActivity({
914
+ bot,
915
+ chatId,
916
+ link,
917
+ type: 'workflow',
918
+ prompt: input,
919
+ phase: t(lang, 'control.activity.startingWorkflow'),
920
+ });
921
+ active.type = 'workflow';
922
+ active.provider = provider;
923
+ active.project = projectLabel(state);
924
+ active.workflowId = workflowId;
925
+ active.status = 'running';
926
+ active.phase = t(lang, 'control.activity.startingWorkflow');
927
+ await editTelegramActivity({ bot, chatId, activity: active, force: true });
564
928
  const result = await runTelegramTool({
565
929
  userId: link.user_id,
566
930
  action: 'run_workflow',
@@ -581,12 +945,15 @@ export async function runWorkflow({ bot, chatId, link, input }) {
581
945
  }),
582
946
  });
583
947
  if (!result.ok) {
584
- await sendToolFailure({ bot, chatId, link, result });
948
+ await sendToolFailure({ bot, chatId, link, result, editMessageId: active.messageId });
585
949
  return;
586
950
  }
587
951
  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) => {
952
+ active.runId = run.id;
953
+ active.phase = t(lang, 'control.activity.workflowRunning');
954
+ pushActivityEvent(active, `🧭 ${t(lang, 'control.workflowStarted', { runId: run.id, workflowId }).replace('\n', ' ')}`);
955
+ await editTelegramActivity({ bot, chatId, activity: active, force: true });
956
+ monitorWorkflowRun({ bot, chatId, link, runId: run.id, activity: active }).catch((error) => {
590
957
  console.warn('[telegram-control] workflow monitor failed:', error?.message || error);
591
958
  });
592
959
  }
@@ -663,21 +1030,96 @@ async function findProjectByQuery(query) {
663
1030
  return candidates.some((candidate) => candidate === needle || candidate.includes(needle));
664
1031
  }) || null;
665
1032
  }
666
- async function handleRoutedIntent({ bot, chatId, link, text }) {
1033
+ async function listWorkflowSummaries(userId) {
1034
+ try {
1035
+ const data = await localApi(userId, '/api/orchestration/workflows', { timeoutMs: 10_000 });
1036
+ if (Array.isArray(data))
1037
+ return data;
1038
+ if (Array.isArray(data?.workflows))
1039
+ return data.workflows;
1040
+ }
1041
+ catch {
1042
+ // Workflow context is helpful for routing but should never block chat input.
1043
+ }
1044
+ return [];
1045
+ }
1046
+ async function resolveTelegramAiIntent({ bot, chatId, link, text, activity }) {
667
1047
  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 });
1048
+ if (state.routerEnabled === false || state.routerMode !== 'hybrid') {
1049
+ return { action: 'agent_prompt', prompt: text, confidence: 1 };
1050
+ }
1051
+ const provider = resolveTelegramProvider(state);
1052
+ const model = resolveTelegramModel(state);
1053
+ if (activity) {
1054
+ activity.type = 'router';
1055
+ activity.provider = provider;
1056
+ activity.project = projectLabel(state);
1057
+ activity.status = 'running';
1058
+ activity.phase = t(activity.lang, 'control.activity.interpreting');
1059
+ pushActivityEvent(activity, `🧠 ${t(activity.lang, 'control.activity.interpreting')}`);
1060
+ await editTelegramActivity({ bot, chatId, activity, force: true });
1061
+ }
1062
+ try {
1063
+ const projects = await listProjects().catch(() => []);
1064
+ const workflows = await listWorkflowSummaries(link.user_id);
1065
+ const response = await localApi(link.user_id, '/api/agent', {
1066
+ method: 'POST',
1067
+ timeoutMs: INTENT_ROUTER_TIMEOUT_MS,
1068
+ body: {
1069
+ projectPath: state.selectedProjectPath || process.cwd(),
1070
+ provider,
1071
+ model: model || undefined,
1072
+ message: buildTelegramIntentPrompt(text, state, { projects, workflows }),
1073
+ cleanup: false,
1074
+ stream: false,
1075
+ permissionMode: 'plan',
1076
+ suppressNotifications: true,
1077
+ },
1078
+ });
1079
+ const assistantText = extractAssistantText(response);
1080
+ const intent = parseTelegramAiIntentResponse(assistantText, text);
1081
+ if (activity) {
1082
+ pushActivityEvent(activity, `🧭 ${intent.action} (${Math.round(intent.confidence * 100)}%)`);
1083
+ await editTelegramActivity({ bot, chatId, activity });
1084
+ }
1085
+ return intent;
1086
+ }
1087
+ catch (error) {
1088
+ if (activity) {
1089
+ pushActivityEvent(activity, `⚠️ ${t(activity.lang, 'control.activity.routerFallback')}`);
1090
+ await editTelegramActivity({ bot, chatId, activity });
1091
+ }
1092
+ console.warn('[telegram-control] AI intent router fallback:', error?.message || error);
1093
+ return { action: 'agent_prompt', prompt: text, confidence: 0 };
1094
+ }
1095
+ }
1096
+ async function handleRoutedIntent({ bot, chatId, link, text, activity }) {
1097
+ const intent = await resolveTelegramAiIntent({ bot, chatId, link, text, activity });
1098
+ const editMessageId = activity?.messageId;
1099
+ if (intent.action === 'agent_prompt') {
1100
+ await runAgent({ bot, chatId, link, prompt: intent.prompt || text, activity });
1101
+ return true;
1102
+ }
1103
+ if (intent.action === 'show_menu') {
1104
+ await showMainMenu({ bot, chatId, link, editMessageId });
1105
+ return true;
1106
+ }
1107
+ if (intent.action === 'show_projects') {
1108
+ await showProjectMenu({ bot, chatId, link, editMessageId });
673
1109
  return true;
674
1110
  }
675
- if (intent.action === 'project_select_query') {
676
- const project = await findProjectByQuery(intent.query);
1111
+ if (intent.action === 'select_project') {
1112
+ const query = intent.projectQuery || intent.prompt || text;
1113
+ const project = await findProjectByQuery(query);
677
1114
  const lang = languageFor(link);
678
1115
  if (!project) {
679
- await send(bot, chatId, t(lang, 'control.projectNotFound', { query: intent.query }));
680
- await showProjectMenu({ bot, chatId, link });
1116
+ await showProjectMenu({
1117
+ bot,
1118
+ chatId,
1119
+ link,
1120
+ editMessageId,
1121
+ notice: t(lang, 'control.projectNotFound', { query }),
1122
+ });
681
1123
  return true;
682
1124
  }
683
1125
  updateTelegramControlState(link.user_id, {
@@ -688,6 +1130,7 @@ async function handleRoutedIntent({ bot, chatId, link, text }) {
688
1130
  bot,
689
1131
  chatId,
690
1132
  link,
1133
+ editMessageId,
691
1134
  notice: t(lang, 'control.projectSelected', {
692
1135
  project: project.displayName || project.name,
693
1136
  path: project.fullPath || project.path,
@@ -695,45 +1138,72 @@ async function handleRoutedIntent({ bot, chatId, link, text }) {
695
1138
  });
696
1139
  return true;
697
1140
  }
698
- if (intent.action === 'provider_menu') {
699
- await showProviderMenu({ bot, chatId, link });
1141
+ if (intent.action === 'show_provider_menu') {
1142
+ await showProviderMenu({ bot, chatId, link, editMessageId });
700
1143
  return true;
701
1144
  }
702
- if (intent.action === 'provider_select') {
1145
+ if (intent.action === 'select_provider' && intent.provider) {
703
1146
  updateTelegramControlState(link.user_id, { selectedProvider: intent.provider, selectedModel: null });
704
- await showModelMenu({ bot, chatId, link });
1147
+ await showModelMenu({ bot, chatId, link, editMessageId });
705
1148
  return true;
706
1149
  }
707
- if (intent.action === 'model_menu') {
708
- await showModelMenu({ bot, chatId, link });
1150
+ if (intent.action === 'show_model_menu') {
1151
+ await showModelMenu({ bot, chatId, link, editMessageId });
709
1152
  return true;
710
1153
  }
711
- if (intent.action === 'runs') {
712
- await showRuns({ bot, chatId, link });
1154
+ if (intent.action === 'select_model' && intent.model) {
1155
+ updateTelegramControlState(link.user_id, { selectedModel: intent.model });
1156
+ await showMainMenu({
1157
+ bot,
1158
+ chatId,
1159
+ link,
1160
+ editMessageId,
1161
+ notice: t(languageFor(link), 'control.modelSelected', { model: intent.model }),
1162
+ });
713
1163
  return true;
714
1164
  }
715
- if (intent.action === 'approvals') {
716
- await showApprovalQueue({ bot, chatId, link });
1165
+ if (intent.action === 'show_runs') {
1166
+ await showRuns({ bot, chatId, link, editMessageId });
717
1167
  return true;
718
1168
  }
719
- if (intent.action === 'workflows') {
720
- await showWorkflowMenu({ bot, chatId, link });
1169
+ if (intent.action === 'show_approvals') {
1170
+ await showApprovalQueue({ bot, chatId, link, editMessageId });
1171
+ return true;
1172
+ }
1173
+ if (intent.action === 'show_workflows') {
1174
+ await showWorkflowMenu({ bot, chatId, link, editMessageId });
1175
+ return true;
1176
+ }
1177
+ if (intent.action === 'run_workflow') {
1178
+ await runWorkflow({ bot, chatId, link, input: intent.workflowInput || text, activity });
1179
+ return true;
1180
+ }
1181
+ if (intent.action === 'show_sessions') {
1182
+ await showSessions({ bot, chatId, link, editMessageId });
721
1183
  return true;
722
1184
  }
723
1185
  if (intent.action === 'new_chat') {
724
- await startNewChat({ bot, chatId, link });
1186
+ await startNewChat({ bot, chatId, link, editMessageId });
725
1187
  return true;
726
1188
  }
727
- return false;
1189
+ await runAgent({ bot, chatId, link, prompt: text, activity });
1190
+ return true;
728
1191
  }
729
1192
  async function fetchRun(userId, runId) {
730
1193
  return localApi(userId, `/api/orchestration/workflows/runs/${runId}`);
731
1194
  }
732
1195
  function summarizeRun(run, mode) {
1196
+ const statusIcon = run.status === 'completed'
1197
+ ? '✅'
1198
+ : run.status === 'failed'
1199
+ ? '❌'
1200
+ : run.status === 'canceled'
1201
+ ? '⏹'
1202
+ : '🔧';
733
1203
  const lines = [
734
- `Run ${run.id}`,
735
- `Workflow: ${run.workflowId}`,
736
- `Status: ${run.status}`,
1204
+ `${statusIcon} Run ${run.id}`,
1205
+ `🧩 Workflow: ${run.workflowId}`,
1206
+ `📌 Status: ${run.status}`,
737
1207
  ];
738
1208
  const nodeRuns = Array.isArray(run.nodeRuns) ? run.nodeRuns : [];
739
1209
  if (mode !== 'final') {
@@ -741,7 +1211,14 @@ function summarizeRun(run, mode) {
741
1211
  ? nodeRuns.filter((node) => node.error || node.status === 'failed')
742
1212
  : nodeRuns;
743
1213
  for (const node of visibleNodes) {
744
- lines.push(`- ${node.status}: ${node.agentLabel || node.nodeId}${node.error ? ` — ${node.error}` : ''}`);
1214
+ const nodeIcon = node.status === 'completed'
1215
+ ? '✅'
1216
+ : node.status === 'failed'
1217
+ ? '❌'
1218
+ : node.status === 'running'
1219
+ ? '🔧'
1220
+ : '⏳';
1221
+ lines.push(`${nodeIcon} ${node.status}: ${node.agentLabel || node.nodeId}${node.error ? ` - ${node.error}` : ''}`);
745
1222
  }
746
1223
  }
747
1224
  const outputs = nodeRuns
@@ -750,7 +1227,7 @@ function summarizeRun(run, mode) {
750
1227
  .map((node) => `\n${node.agentLabel || node.nodeId}:\n${node.outputText}`);
751
1228
  return truncate(`${lines.join('\n')}${outputs.join('\n')}`);
752
1229
  }
753
- async function monitorWorkflowRun({ bot, chatId, link, runId }) {
1230
+ async function monitorWorkflowRun({ bot, chatId, link, runId, activity = null }) {
754
1231
  if (runMonitors.has(runId))
755
1232
  return;
756
1233
  runMonitors.set(runId, true);
@@ -761,12 +1238,24 @@ async function monitorWorkflowRun({ bot, chatId, link, runId }) {
761
1238
  await new Promise((resolve) => setTimeout(resolve, 5000));
762
1239
  const run = await fetchRun(link.user_id, runId);
763
1240
  const nodeRuns = Array.isArray(run.nodeRuns) ? run.nodeRuns : [];
1241
+ if (activity && !TERMINAL_RUN_STATES.has(run.status)) {
1242
+ activity.status = 'running';
1243
+ activity.phase = `${t(activity.lang, 'control.activity.workflowRunning')} (${run.status})`;
1244
+ await editTelegramActivity({ bot, chatId, activity });
1245
+ }
764
1246
  if (state.progressMode === 'all') {
765
1247
  for (const node of nodeRuns) {
766
1248
  const key = `${node.nodeId}:${node.status}`;
767
1249
  if (!seenNodeStatus.has(key)) {
768
1250
  seenNodeStatus.set(key, true);
769
- await send(bot, chatId, `${node.agentLabel || node.nodeId}: ${node.status}${node.error ? `\n${node.error}` : ''}`);
1251
+ if (activity) {
1252
+ pushActivityEvent(activity, `${node.status === 'completed' ? '✅' : node.status === 'failed' ? '❌' : '🔧'} ${node.agentLabel || node.nodeId}: ${node.status}${node.error ? ` - ${node.error}` : ''}`);
1253
+ activity.phase = t(activity.lang, 'control.activity.workflowRunning');
1254
+ await editTelegramActivity({ bot, chatId, activity });
1255
+ }
1256
+ else {
1257
+ await send(bot, chatId, `${node.agentLabel || node.nodeId}: ${node.status}${node.error ? `\n${node.error}` : ''}`);
1258
+ }
770
1259
  }
771
1260
  }
772
1261
  }
@@ -775,12 +1264,29 @@ async function monitorWorkflowRun({ bot, chatId, link, runId }) {
775
1264
  const key = `${node.nodeId}:${node.status}:${node.error || ''}`;
776
1265
  if (!seenNodeStatus.has(key)) {
777
1266
  seenNodeStatus.set(key, true);
778
- await send(bot, chatId, `${node.agentLabel || node.nodeId}: ${node.status}\n${node.error || ''}`);
1267
+ if (activity) {
1268
+ pushActivityEvent(activity, `❌ ${node.agentLabel || node.nodeId}: ${node.status}${node.error ? ` - ${node.error}` : ''}`);
1269
+ activity.phase = t(activity.lang, 'control.activity.workflowRunning');
1270
+ await editTelegramActivity({ bot, chatId, activity });
1271
+ }
1272
+ else {
1273
+ await send(bot, chatId, `${node.agentLabel || node.nodeId}: ${node.status}\n${node.error || ''}`);
1274
+ }
779
1275
  }
780
1276
  }
781
1277
  }
782
1278
  if (TERMINAL_RUN_STATES.has(run.status)) {
783
- await send(bot, chatId, summarizeRun(run, state.progressMode));
1279
+ if (activity) {
1280
+ activity.status = run.status === 'completed' ? 'done' : 'failed';
1281
+ activity.phase = run.status === 'completed'
1282
+ ? t(activity.lang, 'control.activity.done')
1283
+ : t(activity.lang, 'control.activity.failed');
1284
+ activity.output = summarizeRun(run, state.progressMode);
1285
+ await editTelegramActivity({ bot, chatId, activity, force: true });
1286
+ }
1287
+ else {
1288
+ await send(bot, chatId, summarizeRun(run, state.progressMode));
1289
+ }
784
1290
  return;
785
1291
  }
786
1292
  }
@@ -1027,8 +1533,6 @@ async function handleTelegramControlMessageInternal({ bot, msg, link }) {
1027
1533
  return true;
1028
1534
  if (await handleCommand({ bot, chatId, link, text }))
1029
1535
  return true;
1030
- if (await handleRoutedIntent({ bot, chatId, link, text }))
1031
- return true;
1032
1536
  const state = getState(link.user_id);
1033
1537
  if (state.routerEnabled === false)
1034
1538
  return false;
@@ -1036,8 +1540,15 @@ async function handleTelegramControlMessageInternal({ bot, msg, link }) {
1036
1540
  await send(bot, chatId, t(languageFor(link), 'control.disabled'));
1037
1541
  return true;
1038
1542
  }
1039
- await runAgent({ bot, chatId, link, prompt: text });
1040
- return true;
1543
+ const activity = await createTelegramActivity({
1544
+ bot,
1545
+ chatId,
1546
+ link,
1547
+ type: 'router',
1548
+ prompt: text,
1549
+ phase: t(languageFor(link), 'control.activity.interpreting'),
1550
+ });
1551
+ return handleRoutedIntent({ bot, chatId, link, text, activity });
1041
1552
  }
1042
1553
  export async function handleTelegramControlMessage(args) {
1043
1554
  const chatId = args?.msg?.chat?.id;