@pixelbyte-software/pixcode 1.53.11 → 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.
package/server/index.js CHANGED
@@ -157,7 +157,7 @@ import {
157
157
  } from './services/provider-credentials.js';
158
158
  import { primeCliBinPath } from './services/install-jobs.js';
159
159
  import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js';
160
- import { initializeDatabase, sessionNamesDb, applyCustomSessionNames, appConfigDb } from './database/db.js';
160
+ import { initializeDatabase, sessionNamesDb, applyCustomSessionNames, appConfigDb, telegramLinksDb } from './database/db.js';
161
161
  import { setNotificationWebSocketServer } from './services/notification-orchestrator.js';
162
162
  import { configureWebPush } from './services/vapid-keys.js';
163
163
  import { validateApiKey, authenticateToken, authenticateWebSocket, requireAdmin, requireApiScope } from './middleware/auth.js';
@@ -637,7 +637,7 @@ const WATCHER_IGNORED_PATTERNS = [
637
637
  // every open tab, which shows up as mouse/UI stutter. 1500ms collapses
638
638
  // a full chat reply into ~1 scan while still feeling responsive when
639
639
  // the user flips to the projects list.
640
- const WATCHER_DEBOUNCE_MS = 1500;
640
+ const WATCHER_DEBOUNCE_MS = Number.parseInt(process.env.PIXCODE_PROJECT_WATCH_DEBOUNCE_MS || '', 10) || 10000;
641
641
  let projectsWatchers = [];
642
642
  let projectsWatcherDebounceTimer = null;
643
643
  const connectedClients = new Set();
@@ -692,8 +692,9 @@ async function setupProjectsWatcher() {
692
692
  try {
693
693
  isGetProjectsRunning = true;
694
694
 
695
- // Clear project directory cache when files change
696
- clearProjectDirectoryCache();
695
+ if (eventType === 'addDir' || eventType === 'unlinkDir') {
696
+ clearProjectDirectoryCache();
697
+ }
697
698
 
698
699
  // Get updated projects list
699
700
  const updatedProjects = await getProjects(broadcastProgress);
@@ -781,10 +782,44 @@ async function setupProjectsWatcher() {
781
782
  // `project_files_updated` events to subscribed clients only, letting the
782
783
  // explorer refresh automatically without HTTP polling.
783
784
  const WORKSPACE_WATCHER_DEBOUNCE_MS = 800;
784
- const WORKSPACE_DIFF_SNAPSHOT_MAX_BYTES = 512 * 1024;
785
- const WORKSPACE_DIFF_SNAPSHOT_MAX_FILES = 800;
785
+ const WORKSPACE_DIFF_SNAPSHOT_MAX_BYTES = 128 * 1024;
786
+ const WORKSPACE_DIFF_SNAPSHOT_MAX_FILES = 80;
787
+ const WORKSPACE_DIFF_SNAPSHOT_MAX_TOTAL_BYTES = 2 * 1024 * 1024;
786
788
  const workspaceWatchers = new Map(); // projectName -> { watcher, subscribers, debounceTimer, pendingEvent, rootPath, fileSnapshots }
787
789
 
790
+ function getWorkspaceSnapshotBytes(content) {
791
+ return Buffer.byteLength(String(content || ''), 'utf8');
792
+ }
793
+
794
+ function deleteWorkspaceSnapshot(entry, relativePath) {
795
+ if (!entry?.fileSnapshots?.has(relativePath)) return;
796
+ const previous = entry.fileSnapshots.get(relativePath);
797
+ entry.snapshotBytes = Math.max(0, (entry.snapshotBytes || 0) - getWorkspaceSnapshotBytes(previous));
798
+ entry.fileSnapshots.delete(relativePath);
799
+ }
800
+
801
+ function setWorkspaceSnapshot(entry, relativePath, content) {
802
+ if (!entry || !relativePath || typeof content !== 'string') return;
803
+ const nextBytes = getWorkspaceSnapshotBytes(content);
804
+ if (nextBytes > WORKSPACE_DIFF_SNAPSHOT_MAX_BYTES) {
805
+ deleteWorkspaceSnapshot(entry, relativePath);
806
+ return;
807
+ }
808
+
809
+ deleteWorkspaceSnapshot(entry, relativePath);
810
+ entry.fileSnapshots.set(relativePath, content);
811
+ entry.snapshotBytes = (entry.snapshotBytes || 0) + nextBytes;
812
+
813
+ while (
814
+ entry.fileSnapshots.size > WORKSPACE_DIFF_SNAPSHOT_MAX_FILES ||
815
+ (entry.snapshotBytes || 0) > WORKSPACE_DIFF_SNAPSHOT_MAX_TOTAL_BYTES
816
+ ) {
817
+ const oldestKey = entry.fileSnapshots.keys().next().value;
818
+ if (!oldestKey) break;
819
+ deleteWorkspaceSnapshot(entry, oldestKey);
820
+ }
821
+ }
822
+
788
823
  function runWorkspaceCommand(command, args, cwd) {
789
824
  return new Promise((resolve, reject) => {
790
825
  const child = spawn(command, args, {
@@ -868,7 +903,7 @@ async function readWorkspaceTextSnapshot(filePath) {
868
903
  }
869
904
  }
870
905
 
871
- async function initializeWorkspaceDirtySnapshots(rootPath, fileSnapshots) {
906
+ async function initializeWorkspaceDirtySnapshots(rootPath, entry) {
872
907
  try {
873
908
  const { stdout } = await runWorkspaceCommand('git', ['status', '--porcelain', '-z'], rootPath);
874
909
  const changedPaths = parseGitPorcelainZ(stdout).slice(0, WORKSPACE_DIFF_SNAPSHOT_MAX_FILES);
@@ -876,7 +911,7 @@ async function initializeWorkspaceDirtySnapshots(rootPath, fileSnapshots) {
876
911
  await Promise.all(changedPaths.map(async (relativePath) => {
877
912
  const content = await readWorkspaceTextSnapshot(path.join(rootPath, relativePath));
878
913
  if (typeof content === 'string') {
879
- fileSnapshots.set(relativePath, content);
914
+ setWorkspaceSnapshot(entry, relativePath, content);
880
915
  }
881
916
  }));
882
917
  } catch {
@@ -910,9 +945,10 @@ async function subscribeToWorkspace(ws, projectName) {
910
945
  pendingEvent: null,
911
946
  rootPath,
912
947
  fileSnapshots: new Map(),
948
+ snapshotBytes: 0,
913
949
  };
914
950
  workspaceWatchers.set(projectName, entry);
915
- await initializeWorkspaceDirtySnapshots(rootPath, entry.fileSnapshots);
951
+ await initializeWorkspaceDirtySnapshots(rootPath, entry);
916
952
 
917
953
  const broadcastFileUpdate = async (eventType, filePath) => {
918
954
  const relativePath = filePath ? normalizeWorkspaceRelativePath(rootPath, filePath) : null;
@@ -922,12 +958,12 @@ async function subscribeToWorkspace(ws, projectName) {
922
958
  if (relativePath && !eventType.endsWith('Dir')) {
923
959
  if (eventType === 'unlink') {
924
960
  currentContent = '';
925
- entry.fileSnapshots.delete(relativePath);
961
+ deleteWorkspaceSnapshot(entry, relativePath);
926
962
  } else {
927
963
  const snapshot = await readWorkspaceTextSnapshot(filePath);
928
964
  if (typeof snapshot === 'string') {
929
965
  currentContent = snapshot;
930
- entry.fileSnapshots.set(relativePath, snapshot);
966
+ setWorkspaceSnapshot(entry, relativePath, snapshot);
931
967
  }
932
968
  }
933
969
  }
@@ -1041,13 +1077,13 @@ const server = http.createServer(app);
1041
1077
 
1042
1078
  const ptySessionsMap = new Map();
1043
1079
  const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
1044
- const COMPLETED_PTY_SESSION_TTL = 5 * 60 * 1000;
1080
+ const COMPLETED_PTY_SESSION_TTL = Number.parseInt(process.env.PIXCODE_COMPLETED_PTY_TTL_MS || '', 10) || 60 * 1000;
1045
1081
  const SHELL_URL_PARSE_BUFFER_LIMIT = 32768;
1046
1082
  const SHELL_OUTPUT_FLUSH_MAX_CHARS = 128 * 1024;
1047
1083
  const SHELL_WS_BACKPRESSURE_LIMIT = 8 * 1024 * 1024;
1048
- const PTY_SESSION_BUFFER_MAX_BYTES = Number.parseInt(process.env.PIXCODE_PTY_BUFFER_MAX_BYTES || '', 10) || (2 * 1024 * 1024);
1084
+ const PTY_SESSION_BUFFER_MAX_BYTES = Number.parseInt(process.env.PIXCODE_PTY_BUFFER_MAX_BYTES || '', 10) || (512 * 1024);
1049
1085
  const SHELL_INPUT_CHUNK_CHARS = 4096;
1050
- const SHELL_PENDING_INPUT_MAX_BYTES = 2 * 1024 * 1024;
1086
+ const SHELL_PENDING_INPUT_MAX_BYTES = Number.parseInt(process.env.PIXCODE_SHELL_PENDING_INPUT_MAX_BYTES || '', 10) || (16 * 1024 * 1024);
1051
1087
  const SHELL_CLI_PROVIDERS = new Set(['claude', 'codex', 'cursor', 'gemini', 'qwen', 'opencode']);
1052
1088
  import { stripAnsiSequences, normalizeDetectedUrl, extractUrlsFromText, shouldAutoOpenUrlFromOutput } from './utils/url-detection.js';
1053
1089
 
@@ -1325,6 +1361,76 @@ function writeTerminalInputChunks(ptyProcess, data) {
1325
1361
  return true;
1326
1362
  }
1327
1363
 
1364
+ function readPtyTarget(value) {
1365
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
1366
+ }
1367
+
1368
+ function findProviderPtySession({
1369
+ provider,
1370
+ projectPath,
1371
+ user,
1372
+ tabId = null,
1373
+ sessionId = null,
1374
+ requireRunning = false,
1375
+ requirePty = false,
1376
+ }) {
1377
+ const requestedProjectPath = projectPath ? path.resolve(projectPath) : null;
1378
+ const requestUserId = user?.id ?? user?.userId ?? null;
1379
+ const canUseAnyShellSession = ['admin', 'owner'].includes(user?.role);
1380
+ const requestedTabId = readPtyTarget(tabId);
1381
+ const requestedSessionId = readPtyTarget(sessionId);
1382
+ const candidates = [];
1383
+
1384
+ for (const session of ptySessionsMap.values()) {
1385
+ if (session?.provider !== provider || session?.isPlainShell) continue;
1386
+ if (requirePty && !session?.pty) continue;
1387
+ if (requireRunning && session?.lifecycleState !== 'running') continue;
1388
+ if (!canUseAnyShellSession && session?.userId !== requestUserId) continue;
1389
+ if (requestedProjectPath && path.resolve(session.projectPath || os.homedir()) !== requestedProjectPath) continue;
1390
+ if (requestedTabId && session.tabId !== requestedTabId) continue;
1391
+ if (requestedSessionId && session.sessionId !== requestedSessionId) continue;
1392
+ candidates.push(session);
1393
+ }
1394
+
1395
+ if (candidates.length === 0) {
1396
+ return {
1397
+ status: 'missing',
1398
+ session: null,
1399
+ candidates,
1400
+ requestedProjectPath,
1401
+ };
1402
+ }
1403
+
1404
+ if ((requestedTabId || requestedSessionId) && candidates.length > 1) {
1405
+ return {
1406
+ status: 'ambiguous',
1407
+ session: null,
1408
+ candidates,
1409
+ requestedProjectPath,
1410
+ };
1411
+ }
1412
+
1413
+ const session = candidates.reduce((latest, candidate) => (
1414
+ !latest || (candidate.updatedAt || 0) > (latest.updatedAt || 0) ? candidate : latest
1415
+ ), null);
1416
+
1417
+ if (!requestedTabId && !requestedSessionId && candidates.length > 1) {
1418
+ return {
1419
+ status: 'legacy-latest',
1420
+ session,
1421
+ candidates,
1422
+ requestedProjectPath,
1423
+ };
1424
+ }
1425
+
1426
+ return {
1427
+ status: 'matched',
1428
+ session,
1429
+ candidates,
1430
+ requestedProjectPath,
1431
+ };
1432
+ }
1433
+
1328
1434
  function normalizeShellPermissionMode(value) {
1329
1435
  return typeof value === 'string' ? value.trim() : '';
1330
1436
  }
@@ -1515,6 +1621,8 @@ app.get('/api/shell/sessions/provider-output', authenticateToken, requireProject
1515
1621
  const projectPath = typeof req.query.projectPath === 'string' && req.query.projectPath.trim()
1516
1622
  ? req.query.projectPath.trim()
1517
1623
  : null;
1624
+ const tabId = readPtyTarget(req.query.tabId);
1625
+ const sessionId = readPtyTarget(req.query.sessionId);
1518
1626
  const maxChars = Math.min(
1519
1627
  20000,
1520
1628
  Math.max(1000, Number.parseInt(String(req.query.maxChars || '12000'), 10) || 12000)
@@ -1524,33 +1632,32 @@ app.get('/api/shell/sessions/provider-output', authenticateToken, requireProject
1524
1632
  return res.status(400).json({ error: 'Unsupported provider' });
1525
1633
  }
1526
1634
 
1527
- const requestedProjectPath = projectPath ? path.resolve(projectPath) : null;
1528
- const requestUserId = req.user?.id ?? req.user?.userId ?? null;
1529
- const canReadAnyShellSession = ['admin', 'owner'].includes(req.user?.role);
1530
- let matchedSession = null;
1531
- for (const session of ptySessionsMap.values()) {
1532
- if (
1533
- session?.provider === provider &&
1534
- !session?.isPlainShell &&
1535
- (canReadAnyShellSession || session?.userId === requestUserId) &&
1536
- (!requestedProjectPath || path.resolve(session.projectPath || os.homedir()) === requestedProjectPath)
1537
- ) {
1538
- if (!matchedSession || (session.updatedAt || 0) > (matchedSession.updatedAt || 0)) {
1539
- matchedSession = session;
1540
- }
1635
+ const match = findProviderPtySession({ provider, projectPath, user: req.user, tabId, sessionId });
1636
+
1637
+ if (!match.session) {
1638
+ if (match.status === 'ambiguous') {
1639
+ return res.status(409).json({
1640
+ active: false,
1641
+ provider,
1642
+ projectPath: match.requestedProjectPath,
1643
+ tabId,
1644
+ sessionId,
1645
+ output: '',
1646
+ message: 'Multiple provider terminal sessions match this target. Pick a specific tab.',
1647
+ });
1541
1648
  }
1542
- }
1543
-
1544
- if (!matchedSession) {
1545
1649
  return res.json({
1546
1650
  active: false,
1547
1651
  provider,
1548
- projectPath: requestedProjectPath,
1652
+ projectPath: match.requestedProjectPath,
1653
+ tabId,
1654
+ sessionId,
1549
1655
  output: '',
1550
1656
  message: 'No active provider terminal session found for this project.',
1551
1657
  });
1552
1658
  }
1553
1659
 
1660
+ const matchedSession = match.session;
1554
1661
  const rawOutput = matchedSession.buffer.join('').slice(-maxChars);
1555
1662
  const output = stripAnsiSequences(rawOutput);
1556
1663
  const terminalState = resolveProviderTerminalState(matchedSession, provider, output);
@@ -1559,7 +1666,9 @@ app.get('/api/shell/sessions/provider-output', authenticateToken, requireProject
1559
1666
  provider,
1560
1667
  projectPath: path.resolve(matchedSession.projectPath || os.homedir()),
1561
1668
  sessionId: matchedSession.sessionId || null,
1669
+ tabId: matchedSession.tabId || null,
1562
1670
  updatedAt: matchedSession.updatedAt || null,
1671
+ matchStatus: match.status,
1563
1672
  ...terminalState,
1564
1673
  output,
1565
1674
  });
@@ -1570,6 +1679,8 @@ app.post('/api/shell/sessions/provider-input', authenticateToken, requireProject
1570
1679
  const projectPath = typeof req.body?.projectPath === 'string' && req.body.projectPath.trim()
1571
1680
  ? req.body.projectPath.trim()
1572
1681
  : null;
1682
+ const tabId = readPtyTarget(req.body?.tabId);
1683
+ const sessionId = readPtyTarget(req.body?.sessionId);
1573
1684
  const input = typeof req.body?.input === 'string' ? req.body.input : '';
1574
1685
  const submit = req.body?.submit !== false;
1575
1686
 
@@ -1577,35 +1688,40 @@ app.post('/api/shell/sessions/provider-input', authenticateToken, requireProject
1577
1688
  return res.status(400).json({ error: 'Unsupported provider' });
1578
1689
  }
1579
1690
 
1580
- const requestedProjectPath = projectPath ? path.resolve(projectPath) : null;
1581
- const requestUserId = req.user?.id ?? req.user?.userId ?? null;
1582
- const canWriteAnyShellSession = ['admin', 'owner'].includes(req.user?.role);
1583
- let matchedSession = null;
1584
- for (const session of ptySessionsMap.values()) {
1585
- if (
1586
- session?.provider === provider &&
1587
- !session?.isPlainShell &&
1588
- session?.pty &&
1589
- session.lifecycleState === 'running' &&
1590
- (canWriteAnyShellSession || session?.userId === requestUserId) &&
1591
- (!requestedProjectPath || path.resolve(session.projectPath || os.homedir()) === requestedProjectPath)
1592
- ) {
1593
- if (!matchedSession || (session.updatedAt || 0) > (matchedSession.updatedAt || 0)) {
1594
- matchedSession = session;
1595
- }
1596
- }
1597
- }
1691
+ const match = findProviderPtySession({
1692
+ provider,
1693
+ projectPath,
1694
+ user: req.user,
1695
+ tabId,
1696
+ sessionId,
1697
+ requireRunning: true,
1698
+ requirePty: true,
1699
+ });
1598
1700
 
1599
- if (!matchedSession?.pty) {
1701
+ if (!match.session?.pty) {
1702
+ if (match.status === 'ambiguous') {
1703
+ return res.status(409).json({
1704
+ ok: false,
1705
+ provider,
1706
+ projectPath: match.requestedProjectPath,
1707
+ tabId,
1708
+ sessionId,
1709
+ wrote: false,
1710
+ message: 'Multiple running provider terminal sessions match this target. Pick a specific tab.',
1711
+ });
1712
+ }
1600
1713
  return res.status(404).json({
1601
1714
  ok: false,
1602
1715
  provider,
1603
- projectPath: requestedProjectPath,
1716
+ projectPath: match.requestedProjectPath,
1717
+ tabId,
1718
+ sessionId,
1604
1719
  wrote: false,
1605
1720
  message: 'No running provider terminal session found for this project.',
1606
1721
  });
1607
1722
  }
1608
1723
 
1724
+ const matchedSession = match.session;
1609
1725
  const data = submit ? normalizeTerminalStartupInput(input) : input;
1610
1726
  try {
1611
1727
  writeTerminalInputChunks(matchedSession.pty, data);
@@ -1615,9 +1731,11 @@ app.post('/api/shell/sessions/provider-input', authenticateToken, requireProject
1615
1731
  provider,
1616
1732
  projectPath: path.resolve(matchedSession.projectPath || os.homedir()),
1617
1733
  sessionId: matchedSession.sessionId || null,
1734
+ tabId: matchedSession.tabId || null,
1618
1735
  wrote: true,
1619
1736
  submitted: submit,
1620
1737
  bytes: Buffer.byteLength(data),
1738
+ matchStatus: match.status,
1621
1739
  });
1622
1740
  } catch (error) {
1623
1741
  res.status(500).json({
@@ -1629,6 +1747,104 @@ app.post('/api/shell/sessions/provider-input', authenticateToken, requireProject
1629
1747
  }
1630
1748
  });
1631
1749
 
1750
+ const sanitizeTelegramActiveString = (value, maxLength = 240) => {
1751
+ if (typeof value !== 'string') return null;
1752
+ const trimmed = value.trim();
1753
+ return trimmed ? trimmed.slice(0, maxLength) : null;
1754
+ };
1755
+
1756
+ function requireVerifiedTelegramLink(req, res) {
1757
+ const link = telegramLinksDb.getByUserId(req.user.id);
1758
+ if (!link?.chat_id || !link?.verified_at) {
1759
+ res.status(409).json({ error: 'Telegram is not paired for this user.' });
1760
+ return null;
1761
+ }
1762
+ return link;
1763
+ }
1764
+
1765
+ // Bind the paired Telegram chat to an already running provider terminal tab.
1766
+ // This route lives next to the PTY registry so it can verify the target is live.
1767
+ app.post('/api/telegram/active-terminal', authenticateToken, requireProjectPathAccess('useShell'), (req, res) => {
1768
+ try {
1769
+ const link = requireVerifiedTelegramLink(req, res);
1770
+ if (!link) return;
1771
+
1772
+ const provider = sanitizeTelegramActiveString(req.body?.provider, 40);
1773
+ const projectPathRaw = sanitizeTelegramActiveString(req.body?.projectPath, 2000);
1774
+ if (!provider || !SHELL_CLI_PROVIDERS.has(provider) || !projectPathRaw) {
1775
+ return res.status(400).json({ error: 'A supported provider and projectPath are required.' });
1776
+ }
1777
+
1778
+ const projectPath = path.resolve(projectPathRaw);
1779
+ const projectName = sanitizeTelegramActiveString(req.body?.projectName) || path.basename(projectPath);
1780
+ const tabId = sanitizeTelegramActiveString(req.body?.tabId, 160);
1781
+ const sessionId = sanitizeTelegramActiveString(req.body?.sessionId, 240);
1782
+
1783
+ const match = findProviderPtySession({
1784
+ provider,
1785
+ projectPath,
1786
+ user: req.user,
1787
+ tabId,
1788
+ sessionId,
1789
+ requireRunning: true,
1790
+ requirePty: true,
1791
+ });
1792
+
1793
+ if (!match.session?.pty) {
1794
+ if (match.status === 'ambiguous') {
1795
+ return res.status(409).json({
1796
+ error: 'Multiple running provider terminal sessions match this target. Pick a specific tab.',
1797
+ candidates: match.candidates.map((session) => ({
1798
+ sessionId: session.sessionId || null,
1799
+ tabId: session.tabId || null,
1800
+ projectPath: path.resolve(session.projectPath || os.homedir()),
1801
+ updatedAt: session.updatedAt || null,
1802
+ })),
1803
+ });
1804
+ }
1805
+ return res.status(404).json({
1806
+ error: 'No running provider terminal session found for this project.',
1807
+ });
1808
+ }
1809
+
1810
+ const matchedSession = match.session;
1811
+ const resolvedMatchedProjectPath = path.resolve(matchedSession.projectPath || projectPath);
1812
+ const activeTerminal = {
1813
+ provider,
1814
+ projectPath: resolvedMatchedProjectPath,
1815
+ projectName,
1816
+ projectLabel: sanitizeTelegramActiveString(req.body?.projectLabel) || projectName,
1817
+ sessionId: matchedSession.sessionId || sessionId || null,
1818
+ tabId: matchedSession.tabId || tabId || null,
1819
+ attachedAt: new Date().toISOString(),
1820
+ };
1821
+ const control = telegramLinksDb.updateControlState(req.user.id, {
1822
+ activeTerminal,
1823
+ selectedProvider: provider,
1824
+ selectedProjectName: projectName,
1825
+ selectedProjectPath: resolvedMatchedProjectPath,
1826
+ });
1827
+
1828
+ res.json({ success: true, activeTerminal: control.activeTerminal, control, matchStatus: match.status });
1829
+ } catch (error) {
1830
+ console.error('telegram/active-terminal failed:', error);
1831
+ res.status(500).json({ error: 'Failed to attach Telegram to this terminal.' });
1832
+ }
1833
+ });
1834
+
1835
+ // Return Telegram text input to the normal AI router.
1836
+ app.delete('/api/telegram/active-terminal', authenticateToken, (req, res) => {
1837
+ try {
1838
+ const link = requireVerifiedTelegramLink(req, res);
1839
+ if (!link) return;
1840
+ const control = telegramLinksDb.updateControlState(req.user.id, { activeTerminal: null });
1841
+ res.json({ success: true, control });
1842
+ } catch (error) {
1843
+ console.error('telegram/active-terminal delete failed:', error);
1844
+ res.status(500).json({ error: 'Failed to detach Telegram terminal.' });
1845
+ }
1846
+ });
1847
+
1632
1848
  // Authentication routes (public)
1633
1849
  app.use('/api/auth', authRoutes);
1634
1850
 
@@ -3516,16 +3732,23 @@ function handleShellConnection(ws, request) {
3516
3732
  const chunks = splitTerminalInput(data);
3517
3733
  if (chunks.length === 0) return;
3518
3734
 
3735
+ let droppedBytes = 0;
3519
3736
  for (const chunk of chunks) {
3737
+ const chunkBytes = Buffer.byteLength(chunk, 'utf8');
3738
+ if (pendingInputBytes + chunkBytes > SHELL_PENDING_INPUT_MAX_BYTES) {
3739
+ droppedBytes += chunkBytes;
3740
+ continue;
3741
+ }
3520
3742
  pendingInputQueue.push(chunk);
3521
- pendingInputBytes += Buffer.byteLength(chunk, 'utf8');
3743
+ pendingInputBytes += chunkBytes;
3522
3744
  }
3523
3745
 
3524
- while (pendingInputQueue.length > 0 && pendingInputBytes > SHELL_PENDING_INPUT_MAX_BYTES) {
3525
- const dropped = pendingInputQueue.shift();
3526
- pendingInputBytes -= Buffer.byteLength(String(dropped || ''), 'utf8');
3746
+ if (droppedBytes > 0 && ws.readyState === WebSocket.OPEN) {
3747
+ ws.send(JSON.stringify({
3748
+ type: 'output',
3749
+ 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`,
3750
+ }));
3527
3751
  }
3528
- if (pendingInputBytes < 0) pendingInputBytes = 0;
3529
3752
 
3530
3753
  scheduleInputFlush();
3531
3754
  }