@pixelbyte-software/pixcode 1.36.4 → 1.38.0

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 (56) hide show
  1. package/dist/assets/{index-D-YjltED.js → index-C-gVa0Gf.js} +163 -157
  2. package/dist/assets/index-CfHK8y_H.css +32 -0
  3. package/dist/index.html +2 -2
  4. package/dist-server/server/database/db.js +4 -2
  5. package/dist-server/server/database/db.js.map +1 -1
  6. package/dist-server/server/index.js +8 -0
  7. package/dist-server/server/index.js.map +1 -1
  8. package/dist-server/server/modules/orchestration/tasks/orchestration-task.routes.js +10 -1
  9. package/dist-server/server/modules/orchestration/tasks/orchestration-task.routes.js.map +1 -1
  10. package/dist-server/server/modules/orchestration/tasks/orchestration-task.service.js +7 -0
  11. package/dist-server/server/modules/orchestration/tasks/orchestration-task.service.js.map +1 -1
  12. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js +127 -24
  13. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js.map +1 -1
  14. package/dist-server/server/routes/diagnostics.js +12 -0
  15. package/dist-server/server/routes/diagnostics.js.map +1 -0
  16. package/dist-server/server/routes/taskmaster.js +194 -0
  17. package/dist-server/server/routes/taskmaster.js.map +1 -1
  18. package/dist-server/server/services/diagnostics.js +91 -0
  19. package/dist-server/server/services/diagnostics.js.map +1 -0
  20. package/dist-server/server/services/install-jobs.js +1 -0
  21. package/dist-server/server/services/install-jobs.js.map +1 -1
  22. package/dist-server/server/services/notification-orchestrator.js +66 -9
  23. package/dist-server/server/services/notification-orchestrator.js.map +1 -1
  24. package/dist-server/server/services/telegram/control-center.js +144 -2
  25. package/dist-server/server/services/telegram/control-center.js.map +1 -1
  26. package/dist-server/server/services/telegram/translations.js +14 -2
  27. package/dist-server/server/services/telegram/translations.js.map +1 -1
  28. package/package.json +5 -1
  29. package/scripts/github/create-v1.38-issues.mjs +351 -0
  30. package/scripts/smoke/chat-realtime-hydration.mjs +44 -0
  31. package/scripts/smoke/discord-release-workflow.mjs +24 -0
  32. package/scripts/smoke/multi-worker-slots.mjs +42 -0
  33. package/scripts/smoke/notification-center.mjs +63 -0
  34. package/scripts/smoke/orchestration-execution-dashboard.mjs +33 -0
  35. package/scripts/smoke/strict-handoff-compact.mjs +60 -0
  36. package/scripts/smoke/taskmaster-execution-telegram.mjs +52 -0
  37. package/scripts/smoke/taskmaster-onboarding.mjs +52 -0
  38. package/scripts/smoke/update-issue-progress.mjs +69 -0
  39. package/scripts/smoke/v138-desktop-release-hardening.mjs +69 -0
  40. package/scripts/smoke/v138-diagnostics.mjs +63 -0
  41. package/scripts/smoke/v138-issue-planner.mjs +33 -0
  42. package/server/database/db.js +4 -2
  43. package/server/index.js +9 -0
  44. package/server/modules/orchestration/tasks/orchestration-task.routes.ts +10 -1
  45. package/server/modules/orchestration/tasks/orchestration-task.service.ts +7 -0
  46. package/server/modules/orchestration/tasks/orchestration-task.types.ts +3 -0
  47. package/server/modules/orchestration/workflows/workflow-runner.ts +132 -24
  48. package/server/modules/orchestration/workflows/workflow.types.ts +2 -0
  49. package/server/routes/diagnostics.js +15 -0
  50. package/server/routes/taskmaster.js +201 -0
  51. package/server/services/diagnostics.js +105 -0
  52. package/server/services/install-jobs.js +1 -0
  53. package/server/services/notification-orchestrator.js +76 -8
  54. package/server/services/telegram/control-center.js +153 -2
  55. package/server/services/telegram/translations.js +14 -2
  56. package/dist/assets/index-CgF0-_6Z.css +0 -32
@@ -17,6 +17,12 @@ import express from 'express';
17
17
  import { orchestrationTaskService } from '@/modules/orchestration/tasks/orchestration-task.service.js';
18
18
 
19
19
  import { extractProjectDirectory } from '../projects.js';
20
+ import {
21
+ cancelInstallJob,
22
+ createInstallJob,
23
+ getInstallJob,
24
+ snapshotDonePayload
25
+ } from '../services/install-jobs.js';
20
26
  import { broadcastTaskMasterProjectUpdate, broadcastTaskMasterTasksUpdate } from '../utils/taskmaster-websocket.js';
21
27
  import { detectTaskMasterMCPServer } from '../utils/mcp-detector.js';
22
28
 
@@ -144,6 +150,17 @@ async function readTaskMasterTasks(projectName) {
144
150
  return { projectPath, transformedTasks, currentTag };
145
151
  }
146
152
 
153
+ function taskMasterExecutionDescription(task) {
154
+ return [
155
+ task.description ? `Description:\n${task.description}` : '',
156
+ task.details ? `Details:\n${task.details}` : '',
157
+ task.testStrategy ? `Test strategy:\n${task.testStrategy}` : '',
158
+ Array.isArray(task.dependencies) && task.dependencies.length
159
+ ? `Dependencies: ${task.dependencies.join(', ')}`
160
+ : '',
161
+ ].filter(Boolean).join('\n\n');
162
+ }
163
+
147
164
  // API Routes
148
165
 
149
166
  /**
@@ -181,6 +198,121 @@ router.get('/installation-status', async (req, res) => {
181
198
  }
182
199
  });
183
200
 
201
+ /**
202
+ * POST /api/taskmaster/install
203
+ * Install TaskMaster CLI into Pixcode's sandboxed CLI bin.
204
+ */
205
+ router.post('/install', async (req, res) => {
206
+ try {
207
+ const job = createInstallJob({
208
+ provider: 'taskmaster',
209
+ installCmd: 'npm install -g task-master',
210
+ packageName: 'task-master'
211
+ });
212
+
213
+ res.json({
214
+ success: true,
215
+ jobId: job.id,
216
+ provider: 'taskmaster',
217
+ packageName: 'task-master',
218
+ startedAt: job.startedAt
219
+ });
220
+ } catch (error) {
221
+ console.error('TaskMaster install start error:', error);
222
+ res.status(500).json({
223
+ success: false,
224
+ error: 'Failed to start TaskMaster install',
225
+ message: error.message
226
+ });
227
+ }
228
+ });
229
+
230
+ /**
231
+ * GET /api/taskmaster/install/:jobId/stream
232
+ * Replay and stream TaskMaster install output.
233
+ */
234
+ router.get('/install/:jobId/stream', async (req, res) => {
235
+ const job = getInstallJob(req.params.jobId);
236
+ if (!job || job.provider !== 'taskmaster') {
237
+ return res.status(404).json({
238
+ success: false,
239
+ error: 'Install job not found or already expired'
240
+ });
241
+ }
242
+
243
+ res.setHeader('Content-Type', 'text/event-stream');
244
+ res.setHeader('Cache-Control', 'no-cache, no-transform');
245
+ res.setHeader('Connection', 'keep-alive');
246
+ res.setHeader('X-Accel-Buffering', 'no');
247
+ if (typeof res.flushHeaders === 'function') res.flushHeaders();
248
+
249
+ let closed = false;
250
+ const write = (event, payload) => {
251
+ if (closed) return;
252
+ try {
253
+ res.write(`event: ${event}\n`);
254
+ res.write(`data: ${JSON.stringify(payload)}\n\n`);
255
+ } catch {
256
+ // Socket is gone.
257
+ }
258
+ };
259
+
260
+ try { res.write(': start\n\n'); } catch { /* noop */ }
261
+ const heartbeat = setInterval(() => {
262
+ if (!closed) {
263
+ try { res.write(': ping\n\n'); } catch { /* noop */ }
264
+ }
265
+ }, 5000);
266
+
267
+ for (const entry of job.logs) {
268
+ write('log', { stream: entry.stream, chunk: entry.chunk });
269
+ }
270
+
271
+ const cleanup = () => {
272
+ if (closed) return;
273
+ closed = true;
274
+ clearInterval(heartbeat);
275
+ job.emitter.off('log', onLog);
276
+ job.emitter.off('done', onDone);
277
+ };
278
+ const onLog = (entry) => {
279
+ write('log', { stream: entry.stream, chunk: entry.chunk });
280
+ };
281
+ const onDone = (payload) => {
282
+ write('done', payload);
283
+ cleanup();
284
+ try { res.end(); } catch { /* noop */ }
285
+ };
286
+
287
+ if (job.status !== 'running') {
288
+ write('done', snapshotDonePayload(job));
289
+ cleanup();
290
+ try { res.end(); } catch { /* noop */ }
291
+ return;
292
+ }
293
+
294
+ job.emitter.on('log', onLog);
295
+ job.emitter.once('done', onDone);
296
+
297
+ req.on('close', cleanup);
298
+ });
299
+
300
+ /**
301
+ * DELETE /api/taskmaster/install/:jobId
302
+ * Cancel a running TaskMaster install job.
303
+ */
304
+ router.delete('/install/:jobId', async (req, res) => {
305
+ const job = getInstallJob(req.params.jobId);
306
+ if (!job || job.provider !== 'taskmaster') {
307
+ return res.status(404).json({
308
+ success: false,
309
+ error: 'Install job not found'
310
+ });
311
+ }
312
+
313
+ res.json({ success: true, cancelled: cancelInstallJob(req.params.jobId) });
314
+ });
315
+
184
316
  /**
185
317
  * GET /api/taskmaster/tasks/:projectName
186
318
  * Load actual tasks from .taskmaster/tasks/tasks.json
@@ -230,6 +362,75 @@ router.get('/tasks/:projectName', async (req, res) => {
230
362
  }
231
363
  });
232
364
 
365
+ /**
366
+ * POST /api/taskmaster/execute/:projectName/:taskId
367
+ * Import a TaskMaster task into orchestration and dispatch it to a CLI agent.
368
+ */
369
+ router.post('/execute/:projectName/:taskId', async (req, res) => {
370
+ try {
371
+ const { projectName, taskId } = req.params;
372
+ const adapterId = typeof req.body?.adapterId === 'string'
373
+ ? req.body.adapterId
374
+ : typeof req.body?.provider === 'string'
375
+ ? req.body.provider
376
+ : '';
377
+ const model = typeof req.body?.model === 'string' ? req.body.model : undefined;
378
+ const permissionMode = typeof req.body?.permissionMode === 'string' ? req.body.permissionMode : undefined;
379
+ const isolation = ['host', 'worktree', 'docker'].includes(req.body?.isolation)
380
+ ? req.body.isolation
381
+ : 'worktree';
382
+ const projectId = typeof req.body?.projectId === 'string' ? req.body.projectId : projectName;
383
+
384
+ if (!adapterId) {
385
+ return res.status(400).json({
386
+ success: false,
387
+ error: 'Missing adapterId',
388
+ message: 'adapterId or provider is required'
389
+ });
390
+ }
391
+
392
+ const { projectPath, transformedTasks } = await readTaskMasterTasks(projectName);
393
+ const task = transformedTasks.find((candidate) => String(candidate.id) === String(taskId));
394
+ if (!task) {
395
+ return res.status(404).json({
396
+ success: false,
397
+ error: 'TaskMaster task not found',
398
+ message: `Task "${taskId}" was not found in project "${projectName}"`
399
+ });
400
+ }
401
+
402
+ const orchestrationTask = orchestrationTaskService.upsertFromTaskMaster({
403
+ projectId,
404
+ taskmasterId: String(task.id),
405
+ title: `TaskMaster #${task.id}: ${task.title}`,
406
+ description: taskMasterExecutionDescription(task)
407
+ });
408
+
409
+ const dispatchedTask = await orchestrationTaskService.dispatch(orchestrationTask.id, {
410
+ adapterId,
411
+ isolation,
412
+ projectPath,
413
+ model,
414
+ permissionMode
415
+ });
416
+
417
+ res.json({
418
+ success: true,
419
+ projectName,
420
+ projectPath,
421
+ taskmasterTask: task,
422
+ task: dispatchedTask
423
+ });
424
+ } catch (error) {
425
+ console.error('TaskMaster execute error:', error);
426
+ res.status(500).json({
427
+ success: false,
428
+ error: 'Failed to execute TaskMaster task',
429
+ message: error.message
430
+ });
431
+ }
432
+ });
433
+
233
434
  /**
234
435
  * POST /api/taskmaster/sync-orchestration/:projectName
235
436
  * One-way sync: TaskMaster -> Orchestration tasks
@@ -0,0 +1,105 @@
1
+ const SAFE_ENV_KEYS = [
2
+ 'NODE_ENV',
3
+ 'SERVER_PORT',
4
+ 'VITE_PORT',
5
+ 'HOST',
6
+ 'PORT',
7
+ 'DATABASE_PATH',
8
+ 'PIXCODE_NO_DAEMON',
9
+ 'PIXCODE_DISABLE_UPDATE_CHECK',
10
+ 'GITHUB_TOKEN',
11
+ 'NPM_TOKEN',
12
+ 'TELEGRAM_BOT_TOKEN',
13
+ 'ANTHROPIC_API_KEY',
14
+ 'OPENAI_API_KEY',
15
+ 'GEMINI_API_KEY',
16
+ ];
17
+
18
+ const SENSITIVE_KEY_PATTERN = /(authorization|cookie|credential|password|secret|token|api[_-]?key)/i;
19
+
20
+ function isSensitiveKey(key) {
21
+ return SENSITIVE_KEY_PATTERN.test(key);
22
+ }
23
+
24
+ function redactEnv(env = process.env) {
25
+ return SAFE_ENV_KEYS.reduce((acc, key) => {
26
+ if (!(key in env)) {
27
+ return acc;
28
+ }
29
+ acc[key] = isSensitiveKey(key) ? '[redacted]' : String(env[key]);
30
+ return acc;
31
+ }, {});
32
+ }
33
+
34
+ function providerCredentialState(env = process.env) {
35
+ return {
36
+ claude: Boolean(env.ANTHROPIC_API_KEY || env.CLAUDE_API_KEY),
37
+ codex: Boolean(env.OPENAI_API_KEY),
38
+ gemini: Boolean(env.GEMINI_API_KEY || env.GOOGLE_API_KEY),
39
+ telegram: Boolean(env.TELEGRAM_BOT_TOKEN),
40
+ github: Boolean(env.GITHUB_TOKEN),
41
+ npm: Boolean(env.NPM_TOKEN),
42
+ };
43
+ }
44
+
45
+ function normalizeMemory(memory) {
46
+ return Object.fromEntries(
47
+ Object.entries(memory).map(([key, value]) => [key, Number.isFinite(value) ? value : 0])
48
+ );
49
+ }
50
+
51
+ function resolveWebSocketClientCount(options) {
52
+ if (Number.isInteger(options.wsClientCount)) {
53
+ return options.wsClientCount;
54
+ }
55
+ return options.wss?.clients?.size || 0;
56
+ }
57
+
58
+ export function collectDiagnostics(options = {}) {
59
+ const now = options.now || new Date();
60
+ const env = options.env || process.env;
61
+ const versions = options.versions || process.versions;
62
+ const memoryUsage = options.memoryUsage || process.memoryUsage;
63
+ const uptime = options.uptime ?? process.uptime();
64
+
65
+ return {
66
+ status: 'ok',
67
+ timestamp: now.toISOString(),
68
+ version: options.serverVersion || '0.0.0',
69
+ installMode: options.installMode || 'unknown',
70
+ runtime: {
71
+ node: versions.node,
72
+ v8: versions.v8,
73
+ platform: options.platform || process.platform,
74
+ arch: options.arch || process.arch,
75
+ uptimeSeconds: Math.round(uptime),
76
+ },
77
+ memory: normalizeMemory(memoryUsage()),
78
+ websocket: {
79
+ clients: resolveWebSocketClientCount(options),
80
+ },
81
+ environment: redactEnv(env),
82
+ credentials: providerCredentialState(env),
83
+ notifications: {
84
+ telegramConfigured: Boolean(env.TELEGRAM_BOT_TOKEN),
85
+ webPushConfigured: Boolean(env.VAPID_PUBLIC_KEY && env.VAPID_PRIVATE_KEY),
86
+ },
87
+ };
88
+ }
89
+
90
+ export function redactDiagnostics(input) {
91
+ if (Array.isArray(input)) {
92
+ return input.map(item => redactDiagnostics(item));
93
+ }
94
+
95
+ if (!input || typeof input !== 'object') {
96
+ return input;
97
+ }
98
+
99
+ return Object.fromEntries(
100
+ Object.entries(input).map(([key, value]) => [
101
+ key,
102
+ isSensitiveKey(key) ? '[redacted]' : redactDiagnostics(value),
103
+ ])
104
+ );
105
+ }
@@ -63,6 +63,7 @@ const PACKAGE_BINARIES = {
63
63
  '@google/gemini-cli': 'gemini',
64
64
  '@qwen-code/qwen-code': 'qwen',
65
65
  'opencode-ai': 'opencode',
66
+ 'task-master': 'task-master',
66
67
  };
67
68
 
68
69
  /**
@@ -23,6 +23,11 @@ const PROVIDER_LABELS = {
23
23
 
24
24
  const recentEventKeys = new Map();
25
25
  const DEDUPE_WINDOW_MS = 20000;
26
+ let notificationWebSocketServer = null;
27
+
28
+ function setNotificationWebSocketServer(wss) {
29
+ notificationWebSocketServer = wss;
30
+ }
26
31
 
27
32
  const cleanupOldEventKeys = () => {
28
33
  const now = Date.now();
@@ -41,6 +46,22 @@ function shouldSendPush(preferences, event) {
41
46
  return webPushEnabled && eventEnabled;
42
47
  }
43
48
 
49
+ function shouldSendInApp(preferences, event) {
50
+ const inAppEnabled = preferences?.channels?.inApp !== false;
51
+ const prefEventKey = KIND_TO_PREF_KEY[event.kind];
52
+ const eventEnabled = prefEventKey ? preferences?.events?.[prefEventKey] !== false : true;
53
+
54
+ return inAppEnabled && eventEnabled;
55
+ }
56
+
57
+ function shouldSendTelegram(preferences, event) {
58
+ const telegramEnabled = preferences?.channels?.telegram !== false;
59
+ const prefEventKey = KIND_TO_PREF_KEY[event.kind];
60
+ const eventEnabled = prefEventKey ? preferences?.events?.[prefEventKey] !== false : true;
61
+
62
+ return telegramEnabled && eventEnabled;
63
+ }
64
+
44
65
  function isDuplicate(event) {
45
66
  cleanupOldEventKeys();
46
67
  const key = event.dedupeKey || `${event.provider}:${event.kind || 'info'}:${event.code || 'generic'}:${event.sessionId || 'none'}`;
@@ -149,6 +170,41 @@ function buildPushBody(event) {
149
170
  };
150
171
  }
151
172
 
173
+ function buildNotificationPayload(event) {
174
+ const pushBody = buildPushBody(event);
175
+ return {
176
+ id: event.dedupeKey || `${event.provider || 'system'}:${event.kind || 'info'}:${event.code || 'generic'}:${event.sessionId || 'none'}:${event.createdAt}`,
177
+ title: pushBody.title,
178
+ body: pushBody.body,
179
+ kind: event.kind || 'info',
180
+ code: event.code || 'generic.info',
181
+ severity: event.severity || 'info',
182
+ provider: event.provider || null,
183
+ sessionId: event.sessionId || null,
184
+ createdAt: event.createdAt,
185
+ requiresUserAction: Boolean(event.requiresUserAction),
186
+ data: pushBody.data
187
+ };
188
+ }
189
+
190
+ function broadcastInAppNotification(userId, event) {
191
+ if (!notificationWebSocketServer || !userId || !event) {
192
+ return;
193
+ }
194
+
195
+ const message = JSON.stringify({
196
+ type: 'notification:event',
197
+ notification: buildNotificationPayload(event)
198
+ });
199
+ const normalizedUserId = String(userId);
200
+
201
+ notificationWebSocketServer.clients.forEach((client) => {
202
+ if (client.readyState === 1 && String(client.userId || '') === normalizedUserId) {
203
+ client.send(message);
204
+ }
205
+ });
206
+ }
207
+
152
208
  async function sendWebPush(userId, event) {
153
209
  const subscriptions = pushSubscriptionsDb.getSubscriptions(userId);
154
210
  if (!subscriptions.length) return;
@@ -191,6 +247,14 @@ function notifyUserIfEnabled({ userId, event }) {
191
247
  return;
192
248
  }
193
249
 
250
+ if (shouldSendInApp(preferences, event)) {
251
+ try {
252
+ broadcastInAppNotification(userId, event);
253
+ } catch (err) {
254
+ console.error('In-app notification send error:', err);
255
+ }
256
+ }
257
+
194
258
  if (shouldSendPush(preferences, event)) {
195
259
  sendWebPush(userId, event).catch((err) => {
196
260
  console.error('Web push send error:', err);
@@ -203,14 +267,16 @@ function notifyUserIfEnabled({ userId, event }) {
203
267
  const providerLabel = PROVIDER_LABELS[event.provider] || event.provider || 'Session';
204
268
  const sessionTitle = event.meta?.sessionName || providerLabel;
205
269
  const errorText = event.meta?.error || '';
206
- notifyTelegramUser({
207
- userId,
208
- kind: event.kind,
209
- title: sessionTitle,
210
- error: errorText,
211
- }).catch((err) => {
212
- console.warn('[telegram] notify failed:', err?.message || err);
213
- });
270
+ if (shouldSendTelegram(preferences, event)) {
271
+ notifyTelegramUser({
272
+ userId,
273
+ kind: event.kind,
274
+ title: sessionTitle,
275
+ error: errorText,
276
+ }).catch((err) => {
277
+ console.warn('[telegram] notify failed:', err?.message || err);
278
+ });
279
+ }
214
280
  }
215
281
 
216
282
  function notifyRunStopped({ userId, provider, sessionId = null, stopReason = 'completed', sessionName = null }) {
@@ -247,6 +313,8 @@ function notifyRunFailed({ userId, provider, sessionId = null, error, sessionNam
247
313
 
248
314
  export {
249
315
  createNotificationEvent,
316
+ setNotificationWebSocketServer,
317
+ broadcastInAppNotification,
250
318
  notifyUserIfEnabled,
251
319
  notifyRunStopped,
252
320
  notifyRunFailed
@@ -49,6 +49,8 @@ const CONTROL_COMMANDS = new Set([
49
49
  '/workflows',
50
50
  '/orchestration',
51
51
  '/runs',
52
+ '/tasks',
53
+ '/task',
52
54
  '/settings',
53
55
  '/install',
54
56
  '/auth',
@@ -210,8 +212,9 @@ function mainMenuKeyboard(lang) {
210
212
  return [
211
213
  [button(t(lang, 'control.button.projects'), 'projects'), button(t(lang, 'control.button.provider'), 'providers')],
212
214
  [button(t(lang, 'control.button.models'), 'models'), button(t(lang, 'control.button.workflows'), 'workflows')],
213
- [button(t(lang, 'control.button.runs'), 'runs'), button(t(lang, 'control.button.install'), 'install_menu')],
214
- [button(t(lang, 'control.button.auth'), 'auth_menu'), button(t(lang, 'control.button.settings'), 'settings')],
215
+ [button(t(lang, 'control.button.tasks'), 'tasks'), button(t(lang, 'control.button.runs'), 'runs')],
216
+ [button(t(lang, 'control.button.install'), 'install_menu'), button(t(lang, 'control.button.auth'), 'auth_menu')],
217
+ [button(t(lang, 'control.button.settings'), 'settings')],
215
218
  ];
216
219
  }
217
220
 
@@ -344,6 +347,37 @@ async function showRuns({ bot, chatId, link, editMessageId }) {
344
347
  });
345
348
  }
346
349
 
350
+ async function showTaskMasterTasks({ bot, chatId, link, editMessageId }) {
351
+ const lang = languageFor(link);
352
+ const state = getState(link.user_id);
353
+ if (!state.selectedProjectName) {
354
+ await send(bot, chatId, t(lang, 'control.selectProjectFirst'), { editMessageId });
355
+ await showProjectMenu({ bot, chatId, link });
356
+ return;
357
+ }
358
+
359
+ const data = await localApi(link.user_id, `/api/taskmaster/tasks/${encodeURIComponent(state.selectedProjectName)}`);
360
+ const tasks = Array.isArray(data?.tasks) ? data.tasks : [];
361
+ const activeTasks = tasks.filter((task) => !['done', 'completed', 'cancelled', 'canceled'].includes(String(task.status || '').toLowerCase()));
362
+ if (activeTasks.length === 0) {
363
+ await send(bot, chatId, t(lang, 'control.noTaskMasterTasks'), {
364
+ editMessageId,
365
+ reply_markup: { inline_keyboard: [[button(t(lang, 'control.button.mainMenu'), 'menu')]] },
366
+ });
367
+ return;
368
+ }
369
+
370
+ const buttons = activeTasks.slice(0, 12).map((task) => button(
371
+ `#${task.id} ${compact(task.title, 38)}`,
372
+ 'task_run',
373
+ { taskId: String(task.id) },
374
+ ));
375
+ await send(bot, chatId, t(lang, 'control.pickTaskMasterTask'), {
376
+ editMessageId,
377
+ reply_markup: { inline_keyboard: rows(buttons, 1) },
378
+ });
379
+ }
380
+
347
381
  function extractAssistantText(response) {
348
382
  const messages = Array.isArray(response?.messages) ? response.messages : [];
349
383
  const chunks = [];
@@ -437,6 +471,10 @@ async function fetchRun(userId, runId) {
437
471
  return localApi(userId, `/api/orchestration/workflows/runs/${runId}`);
438
472
  }
439
473
 
474
+ async function fetchA2ATask(userId, taskId) {
475
+ return localApi(userId, `/a2a/tasks/${encodeURIComponent(taskId)}`);
476
+ }
477
+
440
478
  function summarizeRun(run, mode) {
441
479
  const lines = [
442
480
  `Run ${run.id}`,
@@ -485,6 +523,100 @@ async function monitorWorkflowRun({ bot, chatId, link, runId }) {
485
523
  }
486
524
  }
487
525
 
526
+ function extractA2ATaskText(task) {
527
+ const chunks = [];
528
+ const artifacts = Array.isArray(task?.artifacts) ? task.artifacts : [];
529
+ for (const artifact of artifacts) {
530
+ for (const part of Array.isArray(artifact?.parts) ? artifact.parts : []) {
531
+ if (part?.kind === 'text' && typeof part.text === 'string') chunks.push(part.text);
532
+ if (part?.type === 'text' && typeof part.text === 'string') chunks.push(part.text);
533
+ }
534
+ }
535
+ const history = Array.isArray(task?.history) ? task.history : [];
536
+ for (const message of history) {
537
+ if (message?.role !== 'assistant') continue;
538
+ for (const part of Array.isArray(message?.parts) ? message.parts : []) {
539
+ if (part?.kind === 'text' && typeof part.text === 'string') chunks.push(part.text);
540
+ if (part?.type === 'text' && typeof part.text === 'string') chunks.push(part.text);
541
+ }
542
+ }
543
+ return chunks.join('\n\n').trim();
544
+ }
545
+
546
+ function summarizeA2ATask(task) {
547
+ const text = extractA2ATaskText(task);
548
+ const error = task?.error?.message || task?.metadata?.error?.message || '';
549
+ return truncate([
550
+ `Task ${task?.id || ''}`,
551
+ `Status: ${task?.state || 'unknown'}`,
552
+ error ? `Error: ${error}` : '',
553
+ text,
554
+ ].filter(Boolean).join('\n\n'));
555
+ }
556
+
557
+ async function monitorA2ATask({ bot, chatId, link, taskId }) {
558
+ const key = `a2a:${taskId}`;
559
+ if (runMonitors.has(key)) return;
560
+ runMonitors.set(key, true);
561
+ const lang = languageFor(link);
562
+ try {
563
+ for (;;) {
564
+ await new Promise((resolve) => setTimeout(resolve, 5000));
565
+ const task = await fetchA2ATask(link.user_id, taskId);
566
+ if (TERMINAL_RUN_STATES.has(task?.state)) {
567
+ await send(bot, chatId, t(lang, 'control.taskFinished', {
568
+ taskId,
569
+ status: task.state,
570
+ summary: summarizeA2ATask(task),
571
+ }));
572
+ return;
573
+ }
574
+ }
575
+ } finally {
576
+ runMonitors.delete(key);
577
+ }
578
+ }
579
+
580
+ async function runTaskMasterTask({ bot, chatId, link, taskId }) {
581
+ const lang = languageFor(link);
582
+ const state = getState(link.user_id);
583
+ if (!state.remoteControlEnabled) {
584
+ await send(bot, chatId, t(lang, 'control.disabled'));
585
+ return;
586
+ }
587
+ if (!state.selectedProjectName) {
588
+ await send(bot, chatId, t(lang, 'control.selectProjectFirst'));
589
+ await showProjectMenu({ bot, chatId, link });
590
+ return;
591
+ }
592
+
593
+ const result = await localApi(
594
+ link.user_id,
595
+ `/api/taskmaster/execute/${encodeURIComponent(state.selectedProjectName)}/${encodeURIComponent(taskId)}`,
596
+ {
597
+ method: 'POST',
598
+ body: {
599
+ projectId: state.selectedProjectName,
600
+ adapterId: state.selectedProvider,
601
+ model: state.selectedModel || undefined,
602
+ isolation: 'worktree',
603
+ },
604
+ },
605
+ );
606
+ const a2aTaskId = result?.task?.a2aTaskId;
607
+ await send(bot, chatId, t(lang, 'control.taskStarted', {
608
+ taskId,
609
+ provider: state.selectedProvider,
610
+ a2aTaskId: a2aTaskId || result?.task?.id || 'unknown',
611
+ }));
612
+
613
+ if (a2aTaskId) {
614
+ monitorA2ATask({ bot, chatId, link, taskId: a2aTaskId }).catch((error) => {
615
+ console.warn('[telegram-control] task monitor failed:', error?.message || error);
616
+ });
617
+ }
618
+ }
619
+
488
620
  export async function startCliInstall({ bot, chatId, link, provider }) {
489
621
  const lang = languageFor(link);
490
622
  const data = await localApi(link.user_id, `/api/providers/${provider}/install`, { method: 'POST' });
@@ -568,6 +700,10 @@ async function handleAwaitingInput({ bot, chatId, link, text }) {
568
700
  await runWorkflow({ bot, chatId, link, input: text });
569
701
  return true;
570
702
  }
703
+ if (awaiting.type === 'task_id') {
704
+ await runTaskMasterTask({ bot, chatId, link, taskId: text });
705
+ return true;
706
+ }
571
707
  return false;
572
708
  }
573
709
 
@@ -605,6 +741,19 @@ async function handleCommand({ bot, chatId, link, text }) {
605
741
  await showRuns({ bot, chatId, link });
606
742
  return true;
607
743
  }
744
+ if (command === '/tasks') {
745
+ await showTaskMasterTasks({ bot, chatId, link });
746
+ return true;
747
+ }
748
+ if (command === '/task') {
749
+ if (!argText) {
750
+ updateTelegramControlState(link.user_id, { awaiting: { type: 'task_id' } });
751
+ await send(bot, chatId, t(lang, 'control.sendTaskId'));
752
+ return true;
753
+ }
754
+ await runTaskMasterTask({ bot, chatId, link, taskId: argText });
755
+ return true;
756
+ }
608
757
  if (command === '/settings') {
609
758
  await showSettings({ bot, chatId, link });
610
759
  return true;
@@ -728,6 +877,7 @@ export async function handleTelegramControlCallback({ bot, query, link }) {
728
877
  if (action === 'models_refresh') return showModelMenu({ bot, chatId, link, refresh: true, editMessageId });
729
878
  if (action === 'workflows') return showWorkflowMenu({ bot, chatId, link, editMessageId });
730
879
  if (action === 'runs') return showRuns({ bot, chatId, link, editMessageId });
880
+ if (action === 'tasks') return showTaskMasterTasks({ bot, chatId, link, editMessageId });
731
881
  if (action === 'install_menu') return showInstallMenu({ bot, chatId, link, editMessageId });
732
882
  if (action === 'auth_menu') return showAuthMenu({ bot, chatId, link, editMessageId });
733
883
  if (action === 'settings') return showSettings({ bot, chatId, link, editMessageId });
@@ -787,6 +937,7 @@ export async function handleTelegramControlCallback({ bot, query, link }) {
787
937
  await send(bot, chatId, t(languageFor(link), 'control.runStatus', { runId: run.id, status: run.status }), { editMessageId });
788
938
  return;
789
939
  }
940
+ if (action === 'task_run') return runTaskMasterTask({ bot, chatId, link, taskId: payload.taskId });
790
941
  if (action === 'install_provider') return startCliInstall({ bot, chatId, link, provider: payload.provider });
791
942
  if (action === 'auth_provider') {
792
943
  await send(bot, chatId, `${payload.provider} login:\n${AUTH_HELP[payload.provider] || t(languageFor(link), 'control.providerAuthFallback')}`, { editMessageId });