@pixelbyte-software/pixcode 1.53.10 → 1.53.12

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.
@@ -39,22 +39,47 @@ const DAEMON_COMMAND_CONTEXT = {
39
39
  function resolveMonacoAssetsPath() {
40
40
  const appParent = path.dirname(APP_ROOT);
41
41
  const appGrandparent = path.dirname(appParent);
42
+ const nodePathRoots = String(process.env.NODE_PATH || '')
43
+ .split(path.delimiter)
44
+ .map((entry) => entry.trim())
45
+ .filter(Boolean);
42
46
  const candidates = [
43
47
  path.join(APP_ROOT, 'node_modules', 'monaco-editor', 'min', 'vs'),
44
48
  path.join(appParent, 'node_modules', 'monaco-editor', 'min', 'vs'),
45
49
  path.join(appParent, 'monaco-editor', 'min', 'vs'),
46
50
  path.join(appGrandparent, 'node_modules', 'monaco-editor', 'min', 'vs'),
47
51
  path.join(appGrandparent, 'monaco-editor', 'min', 'vs'),
52
+ ...nodePathRoots.flatMap((nodePathRoot) => [
53
+ path.join(nodePathRoot, 'monaco-editor', 'min', 'vs'),
54
+ path.join(nodePathRoot, 'node_modules', 'monaco-editor', 'min', 'vs'),
55
+ ]),
56
+ ];
57
+ const resolutionPaths = [
58
+ APP_ROOT,
59
+ appParent,
60
+ appGrandparent,
61
+ __dirname,
62
+ process.cwd(),
63
+ ...nodePathRoots,
48
64
  ];
49
65
  try {
50
66
  const monacoPackagePath = require.resolve('monaco-editor/package.json', {
51
- paths: [APP_ROOT, appParent, appGrandparent, __dirname, process.cwd()],
67
+ paths: resolutionPaths,
52
68
  });
53
69
  candidates.push(path.join(path.dirname(monacoPackagePath), 'min', 'vs'));
54
70
  }
55
71
  catch {
56
72
  // The editor will show its normal load failure if the dependency is unavailable.
57
73
  }
74
+ try {
75
+ const monacoLoaderPath = require.resolve('monaco-editor/min/vs/loader.js', {
76
+ paths: resolutionPaths,
77
+ });
78
+ candidates.push(path.dirname(monacoLoaderPath));
79
+ }
80
+ catch {
81
+ // Some package managers only expose the package root; candidates above cover that case.
82
+ }
58
83
  return candidates.find((candidate) => fs.existsSync(path.join(candidate, 'loader.js'))) || null;
59
84
  }
60
85
  import { c } from './utils/colors.js';
@@ -100,7 +125,7 @@ import { ensurePortOpen } from './utils/port-access.js';
100
125
  import { applyAllStoredCredentialsToEnv, } from './services/provider-credentials.js';
101
126
  import { primeCliBinPath } from './services/install-jobs.js';
102
127
  import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js';
103
- import { initializeDatabase, sessionNamesDb, applyCustomSessionNames, appConfigDb } from './database/db.js';
128
+ import { initializeDatabase, sessionNamesDb, applyCustomSessionNames, appConfigDb, telegramLinksDb } from './database/db.js';
104
129
  import { setNotificationWebSocketServer } from './services/notification-orchestrator.js';
105
130
  import { configureWebPush } from './services/vapid-keys.js';
106
131
  import { validateApiKey, authenticateToken, authenticateWebSocket, requireAdmin, requireApiScope } from './middleware/auth.js';
@@ -553,7 +578,7 @@ const WATCHER_IGNORED_PATTERNS = [
553
578
  // every open tab, which shows up as mouse/UI stutter. 1500ms collapses
554
579
  // a full chat reply into ~1 scan while still feeling responsive when
555
580
  // the user flips to the projects list.
556
- const WATCHER_DEBOUNCE_MS = 1500;
581
+ const WATCHER_DEBOUNCE_MS = Number.parseInt(process.env.PIXCODE_PROJECT_WATCH_DEBOUNCE_MS || '', 10) || 10000;
557
582
  let projectsWatchers = [];
558
583
  let projectsWatcherDebounceTimer = null;
559
584
  const connectedClients = new Set();
@@ -599,8 +624,9 @@ async function setupProjectsWatcher() {
599
624
  }
600
625
  try {
601
626
  isGetProjectsRunning = true;
602
- // Clear project directory cache when files change
603
- clearProjectDirectoryCache();
627
+ if (eventType === 'addDir' || eventType === 'unlinkDir') {
628
+ clearProjectDirectoryCache();
629
+ }
604
630
  // Get updated projects list
605
631
  const updatedProjects = await getProjects(broadcastProgress);
606
632
  const updatePayload = {
@@ -681,9 +707,39 @@ async function setupProjectsWatcher() {
681
707
  // `project_files_updated` events to subscribed clients only, letting the
682
708
  // explorer refresh automatically without HTTP polling.
683
709
  const WORKSPACE_WATCHER_DEBOUNCE_MS = 800;
684
- const WORKSPACE_DIFF_SNAPSHOT_MAX_BYTES = 512 * 1024;
685
- const WORKSPACE_DIFF_SNAPSHOT_MAX_FILES = 800;
710
+ const WORKSPACE_DIFF_SNAPSHOT_MAX_BYTES = 128 * 1024;
711
+ const WORKSPACE_DIFF_SNAPSHOT_MAX_FILES = 80;
712
+ const WORKSPACE_DIFF_SNAPSHOT_MAX_TOTAL_BYTES = 2 * 1024 * 1024;
686
713
  const workspaceWatchers = new Map(); // projectName -> { watcher, subscribers, debounceTimer, pendingEvent, rootPath, fileSnapshots }
714
+ function getWorkspaceSnapshotBytes(content) {
715
+ return Buffer.byteLength(String(content || ''), 'utf8');
716
+ }
717
+ function deleteWorkspaceSnapshot(entry, relativePath) {
718
+ if (!entry?.fileSnapshots?.has(relativePath))
719
+ return;
720
+ const previous = entry.fileSnapshots.get(relativePath);
721
+ entry.snapshotBytes = Math.max(0, (entry.snapshotBytes || 0) - getWorkspaceSnapshotBytes(previous));
722
+ entry.fileSnapshots.delete(relativePath);
723
+ }
724
+ function setWorkspaceSnapshot(entry, relativePath, content) {
725
+ if (!entry || !relativePath || typeof content !== 'string')
726
+ return;
727
+ const nextBytes = getWorkspaceSnapshotBytes(content);
728
+ if (nextBytes > WORKSPACE_DIFF_SNAPSHOT_MAX_BYTES) {
729
+ deleteWorkspaceSnapshot(entry, relativePath);
730
+ return;
731
+ }
732
+ deleteWorkspaceSnapshot(entry, relativePath);
733
+ entry.fileSnapshots.set(relativePath, content);
734
+ entry.snapshotBytes = (entry.snapshotBytes || 0) + nextBytes;
735
+ while (entry.fileSnapshots.size > WORKSPACE_DIFF_SNAPSHOT_MAX_FILES ||
736
+ (entry.snapshotBytes || 0) > WORKSPACE_DIFF_SNAPSHOT_MAX_TOTAL_BYTES) {
737
+ const oldestKey = entry.fileSnapshots.keys().next().value;
738
+ if (!oldestKey)
739
+ break;
740
+ deleteWorkspaceSnapshot(entry, oldestKey);
741
+ }
742
+ }
687
743
  function runWorkspaceCommand(command, args, cwd) {
688
744
  return new Promise((resolve, reject) => {
689
745
  const child = spawn(command, args, {
@@ -754,14 +810,14 @@ async function readWorkspaceTextSnapshot(filePath) {
754
810
  return null;
755
811
  }
756
812
  }
757
- async function initializeWorkspaceDirtySnapshots(rootPath, fileSnapshots) {
813
+ async function initializeWorkspaceDirtySnapshots(rootPath, entry) {
758
814
  try {
759
815
  const { stdout } = await runWorkspaceCommand('git', ['status', '--porcelain', '-z'], rootPath);
760
816
  const changedPaths = parseGitPorcelainZ(stdout).slice(0, WORKSPACE_DIFF_SNAPSHOT_MAX_FILES);
761
817
  await Promise.all(changedPaths.map(async (relativePath) => {
762
818
  const content = await readWorkspaceTextSnapshot(path.join(rootPath, relativePath));
763
819
  if (typeof content === 'string') {
764
- fileSnapshots.set(relativePath, content);
820
+ setWorkspaceSnapshot(entry, relativePath, content);
765
821
  }
766
822
  }));
767
823
  }
@@ -795,9 +851,10 @@ async function subscribeToWorkspace(ws, projectName) {
795
851
  pendingEvent: null,
796
852
  rootPath,
797
853
  fileSnapshots: new Map(),
854
+ snapshotBytes: 0,
798
855
  };
799
856
  workspaceWatchers.set(projectName, entry);
800
- await initializeWorkspaceDirtySnapshots(rootPath, entry.fileSnapshots);
857
+ await initializeWorkspaceDirtySnapshots(rootPath, entry);
801
858
  const broadcastFileUpdate = async (eventType, filePath) => {
802
859
  const relativePath = filePath ? normalizeWorkspaceRelativePath(rootPath, filePath) : null;
803
860
  const previousContent = relativePath ? entry.fileSnapshots.get(relativePath) : undefined;
@@ -805,13 +862,13 @@ async function subscribeToWorkspace(ws, projectName) {
805
862
  if (relativePath && !eventType.endsWith('Dir')) {
806
863
  if (eventType === 'unlink') {
807
864
  currentContent = '';
808
- entry.fileSnapshots.delete(relativePath);
865
+ deleteWorkspaceSnapshot(entry, relativePath);
809
866
  }
810
867
  else {
811
868
  const snapshot = await readWorkspaceTextSnapshot(filePath);
812
869
  if (typeof snapshot === 'string') {
813
870
  currentContent = snapshot;
814
- entry.fileSnapshots.set(relativePath, snapshot);
871
+ setWorkspaceSnapshot(entry, relativePath, snapshot);
815
872
  }
816
873
  }
817
874
  }
@@ -913,13 +970,13 @@ const app = express();
913
970
  const server = http.createServer(app);
914
971
  const ptySessionsMap = new Map();
915
972
  const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
916
- const COMPLETED_PTY_SESSION_TTL = 5 * 60 * 1000;
973
+ const COMPLETED_PTY_SESSION_TTL = Number.parseInt(process.env.PIXCODE_COMPLETED_PTY_TTL_MS || '', 10) || 60 * 1000;
917
974
  const SHELL_URL_PARSE_BUFFER_LIMIT = 32768;
918
975
  const SHELL_OUTPUT_FLUSH_MAX_CHARS = 128 * 1024;
919
976
  const SHELL_WS_BACKPRESSURE_LIMIT = 8 * 1024 * 1024;
920
- const PTY_SESSION_BUFFER_MAX_BYTES = Number.parseInt(process.env.PIXCODE_PTY_BUFFER_MAX_BYTES || '', 10) || (2 * 1024 * 1024);
977
+ const PTY_SESSION_BUFFER_MAX_BYTES = Number.parseInt(process.env.PIXCODE_PTY_BUFFER_MAX_BYTES || '', 10) || (512 * 1024);
921
978
  const SHELL_INPUT_CHUNK_CHARS = 4096;
922
- const SHELL_PENDING_INPUT_MAX_BYTES = 2 * 1024 * 1024;
979
+ const SHELL_PENDING_INPUT_MAX_BYTES = Number.parseInt(process.env.PIXCODE_SHELL_PENDING_INPUT_MAX_BYTES || '', 10) || (16 * 1024 * 1024);
923
980
  const SHELL_CLI_PROVIDERS = new Set(['claude', 'codex', 'cursor', 'gemini', 'qwen', 'opencode']);
924
981
  import { stripAnsiSequences, normalizeDetectedUrl, extractUrlsFromText, shouldAutoOpenUrlFromOutput } from './utils/url-detection.js';
925
982
  function terminatePtySession(sessionKey, session, reason) {
@@ -1161,6 +1218,65 @@ function writeTerminalInputChunks(ptyProcess, data) {
1161
1218
  }
1162
1219
  return true;
1163
1220
  }
1221
+ function readPtyTarget(value) {
1222
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
1223
+ }
1224
+ function findProviderPtySession({ provider, projectPath, user, tabId = null, sessionId = null, requireRunning = false, requirePty = false, }) {
1225
+ const requestedProjectPath = projectPath ? path.resolve(projectPath) : null;
1226
+ const requestUserId = user?.id ?? user?.userId ?? null;
1227
+ const canUseAnyShellSession = ['admin', 'owner'].includes(user?.role);
1228
+ const requestedTabId = readPtyTarget(tabId);
1229
+ const requestedSessionId = readPtyTarget(sessionId);
1230
+ const candidates = [];
1231
+ for (const session of ptySessionsMap.values()) {
1232
+ if (session?.provider !== provider || session?.isPlainShell)
1233
+ continue;
1234
+ if (requirePty && !session?.pty)
1235
+ continue;
1236
+ if (requireRunning && session?.lifecycleState !== 'running')
1237
+ continue;
1238
+ if (!canUseAnyShellSession && session?.userId !== requestUserId)
1239
+ continue;
1240
+ if (requestedProjectPath && path.resolve(session.projectPath || os.homedir()) !== requestedProjectPath)
1241
+ continue;
1242
+ if (requestedTabId && session.tabId !== requestedTabId)
1243
+ continue;
1244
+ if (requestedSessionId && session.sessionId !== requestedSessionId)
1245
+ continue;
1246
+ candidates.push(session);
1247
+ }
1248
+ if (candidates.length === 0) {
1249
+ return {
1250
+ status: 'missing',
1251
+ session: null,
1252
+ candidates,
1253
+ requestedProjectPath,
1254
+ };
1255
+ }
1256
+ if ((requestedTabId || requestedSessionId) && candidates.length > 1) {
1257
+ return {
1258
+ status: 'ambiguous',
1259
+ session: null,
1260
+ candidates,
1261
+ requestedProjectPath,
1262
+ };
1263
+ }
1264
+ const session = candidates.reduce((latest, candidate) => (!latest || (candidate.updatedAt || 0) > (latest.updatedAt || 0) ? candidate : latest), null);
1265
+ if (!requestedTabId && !requestedSessionId && candidates.length > 1) {
1266
+ return {
1267
+ status: 'legacy-latest',
1268
+ session,
1269
+ candidates,
1270
+ requestedProjectPath,
1271
+ };
1272
+ }
1273
+ return {
1274
+ status: 'matched',
1275
+ session,
1276
+ candidates,
1277
+ requestedProjectPath,
1278
+ };
1279
+ }
1164
1280
  function normalizeShellPermissionMode(value) {
1165
1281
  return typeof value === 'string' ? value.trim() : '';
1166
1282
  }
@@ -1320,33 +1436,36 @@ app.get('/api/shell/sessions/provider-output', authenticateToken, requireProject
1320
1436
  const projectPath = typeof req.query.projectPath === 'string' && req.query.projectPath.trim()
1321
1437
  ? req.query.projectPath.trim()
1322
1438
  : null;
1439
+ const tabId = readPtyTarget(req.query.tabId);
1440
+ const sessionId = readPtyTarget(req.query.sessionId);
1323
1441
  const maxChars = Math.min(20000, Math.max(1000, Number.parseInt(String(req.query.maxChars || '12000'), 10) || 12000));
1324
1442
  if (!SHELL_CLI_PROVIDERS.has(provider)) {
1325
1443
  return res.status(400).json({ error: 'Unsupported provider' });
1326
1444
  }
1327
- const requestedProjectPath = projectPath ? path.resolve(projectPath) : null;
1328
- const requestUserId = req.user?.id ?? req.user?.userId ?? null;
1329
- const canReadAnyShellSession = ['admin', 'owner'].includes(req.user?.role);
1330
- let matchedSession = null;
1331
- for (const session of ptySessionsMap.values()) {
1332
- if (session?.provider === provider &&
1333
- !session?.isPlainShell &&
1334
- (canReadAnyShellSession || session?.userId === requestUserId) &&
1335
- (!requestedProjectPath || path.resolve(session.projectPath || os.homedir()) === requestedProjectPath)) {
1336
- if (!matchedSession || (session.updatedAt || 0) > (matchedSession.updatedAt || 0)) {
1337
- matchedSession = session;
1338
- }
1445
+ const match = findProviderPtySession({ provider, projectPath, user: req.user, tabId, sessionId });
1446
+ if (!match.session) {
1447
+ if (match.status === 'ambiguous') {
1448
+ return res.status(409).json({
1449
+ active: false,
1450
+ provider,
1451
+ projectPath: match.requestedProjectPath,
1452
+ tabId,
1453
+ sessionId,
1454
+ output: '',
1455
+ message: 'Multiple provider terminal sessions match this target. Pick a specific tab.',
1456
+ });
1339
1457
  }
1340
- }
1341
- if (!matchedSession) {
1342
1458
  return res.json({
1343
1459
  active: false,
1344
1460
  provider,
1345
- projectPath: requestedProjectPath,
1461
+ projectPath: match.requestedProjectPath,
1462
+ tabId,
1463
+ sessionId,
1346
1464
  output: '',
1347
1465
  message: 'No active provider terminal session found for this project.',
1348
1466
  });
1349
1467
  }
1468
+ const matchedSession = match.session;
1350
1469
  const rawOutput = matchedSession.buffer.join('').slice(-maxChars);
1351
1470
  const output = stripAnsiSequences(rawOutput);
1352
1471
  const terminalState = resolveProviderTerminalState(matchedSession, provider, output);
@@ -1355,7 +1474,9 @@ app.get('/api/shell/sessions/provider-output', authenticateToken, requireProject
1355
1474
  provider,
1356
1475
  projectPath: path.resolve(matchedSession.projectPath || os.homedir()),
1357
1476
  sessionId: matchedSession.sessionId || null,
1477
+ tabId: matchedSession.tabId || null,
1358
1478
  updatedAt: matchedSession.updatedAt || null,
1479
+ matchStatus: match.status,
1359
1480
  ...terminalState,
1360
1481
  output,
1361
1482
  });
@@ -1365,36 +1486,45 @@ app.post('/api/shell/sessions/provider-input', authenticateToken, requireProject
1365
1486
  const projectPath = typeof req.body?.projectPath === 'string' && req.body.projectPath.trim()
1366
1487
  ? req.body.projectPath.trim()
1367
1488
  : null;
1489
+ const tabId = readPtyTarget(req.body?.tabId);
1490
+ const sessionId = readPtyTarget(req.body?.sessionId);
1368
1491
  const input = typeof req.body?.input === 'string' ? req.body.input : '';
1369
1492
  const submit = req.body?.submit !== false;
1370
1493
  if (!SHELL_CLI_PROVIDERS.has(provider)) {
1371
1494
  return res.status(400).json({ error: 'Unsupported provider' });
1372
1495
  }
1373
- const requestedProjectPath = projectPath ? path.resolve(projectPath) : null;
1374
- const requestUserId = req.user?.id ?? req.user?.userId ?? null;
1375
- const canWriteAnyShellSession = ['admin', 'owner'].includes(req.user?.role);
1376
- let matchedSession = null;
1377
- for (const session of ptySessionsMap.values()) {
1378
- if (session?.provider === provider &&
1379
- !session?.isPlainShell &&
1380
- session?.pty &&
1381
- session.lifecycleState === 'running' &&
1382
- (canWriteAnyShellSession || session?.userId === requestUserId) &&
1383
- (!requestedProjectPath || path.resolve(session.projectPath || os.homedir()) === requestedProjectPath)) {
1384
- if (!matchedSession || (session.updatedAt || 0) > (matchedSession.updatedAt || 0)) {
1385
- matchedSession = session;
1386
- }
1496
+ const match = findProviderPtySession({
1497
+ provider,
1498
+ projectPath,
1499
+ user: req.user,
1500
+ tabId,
1501
+ sessionId,
1502
+ requireRunning: true,
1503
+ requirePty: true,
1504
+ });
1505
+ if (!match.session?.pty) {
1506
+ if (match.status === 'ambiguous') {
1507
+ return res.status(409).json({
1508
+ ok: false,
1509
+ provider,
1510
+ projectPath: match.requestedProjectPath,
1511
+ tabId,
1512
+ sessionId,
1513
+ wrote: false,
1514
+ message: 'Multiple running provider terminal sessions match this target. Pick a specific tab.',
1515
+ });
1387
1516
  }
1388
- }
1389
- if (!matchedSession?.pty) {
1390
1517
  return res.status(404).json({
1391
1518
  ok: false,
1392
1519
  provider,
1393
- projectPath: requestedProjectPath,
1520
+ projectPath: match.requestedProjectPath,
1521
+ tabId,
1522
+ sessionId,
1394
1523
  wrote: false,
1395
1524
  message: 'No running provider terminal session found for this project.',
1396
1525
  });
1397
1526
  }
1527
+ const matchedSession = match.session;
1398
1528
  const data = submit ? normalizeTerminalStartupInput(input) : input;
1399
1529
  try {
1400
1530
  writeTerminalInputChunks(matchedSession.pty, data);
@@ -1404,9 +1534,11 @@ app.post('/api/shell/sessions/provider-input', authenticateToken, requireProject
1404
1534
  provider,
1405
1535
  projectPath: path.resolve(matchedSession.projectPath || os.homedir()),
1406
1536
  sessionId: matchedSession.sessionId || null,
1537
+ tabId: matchedSession.tabId || null,
1407
1538
  wrote: true,
1408
1539
  submitted: submit,
1409
1540
  bytes: Buffer.byteLength(data),
1541
+ matchStatus: match.status,
1410
1542
  });
1411
1543
  }
1412
1544
  catch (error) {
@@ -1418,6 +1550,99 @@ app.post('/api/shell/sessions/provider-input', authenticateToken, requireProject
1418
1550
  });
1419
1551
  }
1420
1552
  });
1553
+ const sanitizeTelegramActiveString = (value, maxLength = 240) => {
1554
+ if (typeof value !== 'string')
1555
+ return null;
1556
+ const trimmed = value.trim();
1557
+ return trimmed ? trimmed.slice(0, maxLength) : null;
1558
+ };
1559
+ function requireVerifiedTelegramLink(req, res) {
1560
+ const link = telegramLinksDb.getByUserId(req.user.id);
1561
+ if (!link?.chat_id || !link?.verified_at) {
1562
+ res.status(409).json({ error: 'Telegram is not paired for this user.' });
1563
+ return null;
1564
+ }
1565
+ return link;
1566
+ }
1567
+ // Bind the paired Telegram chat to an already running provider terminal tab.
1568
+ // This route lives next to the PTY registry so it can verify the target is live.
1569
+ app.post('/api/telegram/active-terminal', authenticateToken, requireProjectPathAccess('useShell'), (req, res) => {
1570
+ try {
1571
+ const link = requireVerifiedTelegramLink(req, res);
1572
+ if (!link)
1573
+ return;
1574
+ const provider = sanitizeTelegramActiveString(req.body?.provider, 40);
1575
+ const projectPathRaw = sanitizeTelegramActiveString(req.body?.projectPath, 2000);
1576
+ if (!provider || !SHELL_CLI_PROVIDERS.has(provider) || !projectPathRaw) {
1577
+ return res.status(400).json({ error: 'A supported provider and projectPath are required.' });
1578
+ }
1579
+ const projectPath = path.resolve(projectPathRaw);
1580
+ const projectName = sanitizeTelegramActiveString(req.body?.projectName) || path.basename(projectPath);
1581
+ const tabId = sanitizeTelegramActiveString(req.body?.tabId, 160);
1582
+ const sessionId = sanitizeTelegramActiveString(req.body?.sessionId, 240);
1583
+ const match = findProviderPtySession({
1584
+ provider,
1585
+ projectPath,
1586
+ user: req.user,
1587
+ tabId,
1588
+ sessionId,
1589
+ requireRunning: true,
1590
+ requirePty: true,
1591
+ });
1592
+ if (!match.session?.pty) {
1593
+ if (match.status === 'ambiguous') {
1594
+ return res.status(409).json({
1595
+ error: 'Multiple running provider terminal sessions match this target. Pick a specific tab.',
1596
+ candidates: match.candidates.map((session) => ({
1597
+ sessionId: session.sessionId || null,
1598
+ tabId: session.tabId || null,
1599
+ projectPath: path.resolve(session.projectPath || os.homedir()),
1600
+ updatedAt: session.updatedAt || null,
1601
+ })),
1602
+ });
1603
+ }
1604
+ return res.status(404).json({
1605
+ error: 'No running provider terminal session found for this project.',
1606
+ });
1607
+ }
1608
+ const matchedSession = match.session;
1609
+ const resolvedMatchedProjectPath = path.resolve(matchedSession.projectPath || projectPath);
1610
+ const activeTerminal = {
1611
+ provider,
1612
+ projectPath: resolvedMatchedProjectPath,
1613
+ projectName,
1614
+ projectLabel: sanitizeTelegramActiveString(req.body?.projectLabel) || projectName,
1615
+ sessionId: matchedSession.sessionId || sessionId || null,
1616
+ tabId: matchedSession.tabId || tabId || null,
1617
+ attachedAt: new Date().toISOString(),
1618
+ };
1619
+ const control = telegramLinksDb.updateControlState(req.user.id, {
1620
+ activeTerminal,
1621
+ selectedProvider: provider,
1622
+ selectedProjectName: projectName,
1623
+ selectedProjectPath: resolvedMatchedProjectPath,
1624
+ });
1625
+ res.json({ success: true, activeTerminal: control.activeTerminal, control, matchStatus: match.status });
1626
+ }
1627
+ catch (error) {
1628
+ console.error('telegram/active-terminal failed:', error);
1629
+ res.status(500).json({ error: 'Failed to attach Telegram to this terminal.' });
1630
+ }
1631
+ });
1632
+ // Return Telegram text input to the normal AI router.
1633
+ app.delete('/api/telegram/active-terminal', authenticateToken, (req, res) => {
1634
+ try {
1635
+ const link = requireVerifiedTelegramLink(req, res);
1636
+ if (!link)
1637
+ return;
1638
+ const control = telegramLinksDb.updateControlState(req.user.id, { activeTerminal: null });
1639
+ res.json({ success: true, control });
1640
+ }
1641
+ catch (error) {
1642
+ console.error('telegram/active-terminal delete failed:', error);
1643
+ res.status(500).json({ error: 'Failed to detach Telegram terminal.' });
1644
+ }
1645
+ });
1421
1646
  // Authentication routes (public)
1422
1647
  app.use('/api/auth', authRoutes);
1423
1648
  // Projects API Routes (protected)
@@ -3216,16 +3441,22 @@ function handleShellConnection(ws, request) {
3216
3441
  const chunks = splitTerminalInput(data);
3217
3442
  if (chunks.length === 0)
3218
3443
  return;
3444
+ let droppedBytes = 0;
3219
3445
  for (const chunk of chunks) {
3446
+ const chunkBytes = Buffer.byteLength(chunk, 'utf8');
3447
+ if (pendingInputBytes + chunkBytes > SHELL_PENDING_INPUT_MAX_BYTES) {
3448
+ droppedBytes += chunkBytes;
3449
+ continue;
3450
+ }
3220
3451
  pendingInputQueue.push(chunk);
3221
- pendingInputBytes += Buffer.byteLength(chunk, 'utf8');
3452
+ pendingInputBytes += chunkBytes;
3222
3453
  }
3223
- while (pendingInputQueue.length > 0 && pendingInputBytes > SHELL_PENDING_INPUT_MAX_BYTES) {
3224
- const dropped = pendingInputQueue.shift();
3225
- pendingInputBytes -= Buffer.byteLength(String(dropped || ''), 'utf8');
3454
+ if (droppedBytes > 0 && ws.readyState === WebSocket.OPEN) {
3455
+ ws.send(JSON.stringify({
3456
+ type: 'output',
3457
+ data: `\r\n\x1b[33m[Pixcode] ${droppedBytes} bytes of pasted terminal input were not sent because the input queue limit was reached. Split very large pastes into smaller chunks.\x1b[0m\r\n`,
3458
+ }));
3226
3459
  }
3227
- if (pendingInputBytes < 0)
3228
- pendingInputBytes = 0;
3229
3460
  scheduleInputFlush();
3230
3461
  }
3231
3462
  function flushOutputBuffer() {
@@ -3634,7 +3865,9 @@ function handleShellConnection(ws, request) {
3634
3865
  });
3635
3866
  // Handle process exit
3636
3867
  shellProcess.onExit((exitCode) => {
3637
- console.log('🔚 Shell process exited with code:', exitCode.exitCode, 'signal:', exitCode.signal);
3868
+ const cleanShellExit = exitCode.exitCode === 0;
3869
+ const normalizedExitSignal = cleanShellExit ? null : (exitCode.signal || null);
3870
+ console.log('🔚 Shell process exited with code:', exitCode.exitCode, 'signal:', normalizedExitSignal);
3638
3871
  if (outputFlushTimer) {
3639
3872
  clearTimeout(outputFlushTimer);
3640
3873
  outputFlushTimer = null;
@@ -3645,11 +3878,11 @@ function handleShellConnection(ws, request) {
3645
3878
  console.log('â†Šī¸ Ignoring stale PTY exit for replacement session:', ptySessionKey);
3646
3879
  return;
3647
3880
  }
3648
- const exitMessage = `\r\n\x1b[33mProcess exited with code ${exitCode.exitCode}${exitCode.signal ? ` (${exitCode.signal})` : ''}\x1b[0m\r\n`;
3881
+ const exitMessage = `\r\n\x1b[33mProcess exited with code ${exitCode.exitCode}${normalizedExitSignal ? ` (${normalizedExitSignal})` : ''}\x1b[0m\r\n`;
3649
3882
  if (session) {
3650
- session.lifecycleState = exitCode.exitCode === 0 && !exitCode.signal ? 'completed' : 'failed';
3883
+ session.lifecycleState = cleanShellExit ? 'completed' : 'failed';
3651
3884
  session.exitCode = typeof exitCode.exitCode === 'number' ? exitCode.exitCode : null;
3652
- session.exitSignal = exitCode.signal || null;
3885
+ session.exitSignal = normalizedExitSignal;
3653
3886
  session.completedAt = new Date().toISOString();
3654
3887
  session.updatedAt = Date.now();
3655
3888
  if (session.startupInputTimerId) {