@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.
@@ -125,7 +125,7 @@ import { ensurePortOpen } from './utils/port-access.js';
125
125
  import { applyAllStoredCredentialsToEnv, } from './services/provider-credentials.js';
126
126
  import { primeCliBinPath } from './services/install-jobs.js';
127
127
  import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js';
128
- import { initializeDatabase, sessionNamesDb, applyCustomSessionNames, appConfigDb } from './database/db.js';
128
+ import { initializeDatabase, sessionNamesDb, applyCustomSessionNames, appConfigDb, telegramLinksDb } from './database/db.js';
129
129
  import { setNotificationWebSocketServer } from './services/notification-orchestrator.js';
130
130
  import { configureWebPush } from './services/vapid-keys.js';
131
131
  import { validateApiKey, authenticateToken, authenticateWebSocket, requireAdmin, requireApiScope } from './middleware/auth.js';
@@ -578,7 +578,7 @@ const WATCHER_IGNORED_PATTERNS = [
578
578
  // every open tab, which shows up as mouse/UI stutter. 1500ms collapses
579
579
  // a full chat reply into ~1 scan while still feeling responsive when
580
580
  // the user flips to the projects list.
581
- const WATCHER_DEBOUNCE_MS = 1500;
581
+ const WATCHER_DEBOUNCE_MS = Number.parseInt(process.env.PIXCODE_PROJECT_WATCH_DEBOUNCE_MS || '', 10) || 10000;
582
582
  let projectsWatchers = [];
583
583
  let projectsWatcherDebounceTimer = null;
584
584
  const connectedClients = new Set();
@@ -624,8 +624,9 @@ async function setupProjectsWatcher() {
624
624
  }
625
625
  try {
626
626
  isGetProjectsRunning = true;
627
- // Clear project directory cache when files change
628
- clearProjectDirectoryCache();
627
+ if (eventType === 'addDir' || eventType === 'unlinkDir') {
628
+ clearProjectDirectoryCache();
629
+ }
629
630
  // Get updated projects list
630
631
  const updatedProjects = await getProjects(broadcastProgress);
631
632
  const updatePayload = {
@@ -706,9 +707,39 @@ async function setupProjectsWatcher() {
706
707
  // `project_files_updated` events to subscribed clients only, letting the
707
708
  // explorer refresh automatically without HTTP polling.
708
709
  const WORKSPACE_WATCHER_DEBOUNCE_MS = 800;
709
- const WORKSPACE_DIFF_SNAPSHOT_MAX_BYTES = 512 * 1024;
710
- 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;
711
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
+ }
712
743
  function runWorkspaceCommand(command, args, cwd) {
713
744
  return new Promise((resolve, reject) => {
714
745
  const child = spawn(command, args, {
@@ -779,14 +810,14 @@ async function readWorkspaceTextSnapshot(filePath) {
779
810
  return null;
780
811
  }
781
812
  }
782
- async function initializeWorkspaceDirtySnapshots(rootPath, fileSnapshots) {
813
+ async function initializeWorkspaceDirtySnapshots(rootPath, entry) {
783
814
  try {
784
815
  const { stdout } = await runWorkspaceCommand('git', ['status', '--porcelain', '-z'], rootPath);
785
816
  const changedPaths = parseGitPorcelainZ(stdout).slice(0, WORKSPACE_DIFF_SNAPSHOT_MAX_FILES);
786
817
  await Promise.all(changedPaths.map(async (relativePath) => {
787
818
  const content = await readWorkspaceTextSnapshot(path.join(rootPath, relativePath));
788
819
  if (typeof content === 'string') {
789
- fileSnapshots.set(relativePath, content);
820
+ setWorkspaceSnapshot(entry, relativePath, content);
790
821
  }
791
822
  }));
792
823
  }
@@ -820,9 +851,10 @@ async function subscribeToWorkspace(ws, projectName) {
820
851
  pendingEvent: null,
821
852
  rootPath,
822
853
  fileSnapshots: new Map(),
854
+ snapshotBytes: 0,
823
855
  };
824
856
  workspaceWatchers.set(projectName, entry);
825
- await initializeWorkspaceDirtySnapshots(rootPath, entry.fileSnapshots);
857
+ await initializeWorkspaceDirtySnapshots(rootPath, entry);
826
858
  const broadcastFileUpdate = async (eventType, filePath) => {
827
859
  const relativePath = filePath ? normalizeWorkspaceRelativePath(rootPath, filePath) : null;
828
860
  const previousContent = relativePath ? entry.fileSnapshots.get(relativePath) : undefined;
@@ -830,13 +862,13 @@ async function subscribeToWorkspace(ws, projectName) {
830
862
  if (relativePath && !eventType.endsWith('Dir')) {
831
863
  if (eventType === 'unlink') {
832
864
  currentContent = '';
833
- entry.fileSnapshots.delete(relativePath);
865
+ deleteWorkspaceSnapshot(entry, relativePath);
834
866
  }
835
867
  else {
836
868
  const snapshot = await readWorkspaceTextSnapshot(filePath);
837
869
  if (typeof snapshot === 'string') {
838
870
  currentContent = snapshot;
839
- entry.fileSnapshots.set(relativePath, snapshot);
871
+ setWorkspaceSnapshot(entry, relativePath, snapshot);
840
872
  }
841
873
  }
842
874
  }
@@ -938,13 +970,13 @@ const app = express();
938
970
  const server = http.createServer(app);
939
971
  const ptySessionsMap = new Map();
940
972
  const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
941
- 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;
942
974
  const SHELL_URL_PARSE_BUFFER_LIMIT = 32768;
943
975
  const SHELL_OUTPUT_FLUSH_MAX_CHARS = 128 * 1024;
944
976
  const SHELL_WS_BACKPRESSURE_LIMIT = 8 * 1024 * 1024;
945
- 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);
946
978
  const SHELL_INPUT_CHUNK_CHARS = 4096;
947
- 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);
948
980
  const SHELL_CLI_PROVIDERS = new Set(['claude', 'codex', 'cursor', 'gemini', 'qwen', 'opencode']);
949
981
  import { stripAnsiSequences, normalizeDetectedUrl, extractUrlsFromText, shouldAutoOpenUrlFromOutput } from './utils/url-detection.js';
950
982
  function terminatePtySession(sessionKey, session, reason) {
@@ -1186,6 +1218,65 @@ function writeTerminalInputChunks(ptyProcess, data) {
1186
1218
  }
1187
1219
  return true;
1188
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
+ }
1189
1280
  function normalizeShellPermissionMode(value) {
1190
1281
  return typeof value === 'string' ? value.trim() : '';
1191
1282
  }
@@ -1345,33 +1436,36 @@ app.get('/api/shell/sessions/provider-output', authenticateToken, requireProject
1345
1436
  const projectPath = typeof req.query.projectPath === 'string' && req.query.projectPath.trim()
1346
1437
  ? req.query.projectPath.trim()
1347
1438
  : null;
1439
+ const tabId = readPtyTarget(req.query.tabId);
1440
+ const sessionId = readPtyTarget(req.query.sessionId);
1348
1441
  const maxChars = Math.min(20000, Math.max(1000, Number.parseInt(String(req.query.maxChars || '12000'), 10) || 12000));
1349
1442
  if (!SHELL_CLI_PROVIDERS.has(provider)) {
1350
1443
  return res.status(400).json({ error: 'Unsupported provider' });
1351
1444
  }
1352
- const requestedProjectPath = projectPath ? path.resolve(projectPath) : null;
1353
- const requestUserId = req.user?.id ?? req.user?.userId ?? null;
1354
- const canReadAnyShellSession = ['admin', 'owner'].includes(req.user?.role);
1355
- let matchedSession = null;
1356
- for (const session of ptySessionsMap.values()) {
1357
- if (session?.provider === provider &&
1358
- !session?.isPlainShell &&
1359
- (canReadAnyShellSession || session?.userId === requestUserId) &&
1360
- (!requestedProjectPath || path.resolve(session.projectPath || os.homedir()) === requestedProjectPath)) {
1361
- if (!matchedSession || (session.updatedAt || 0) > (matchedSession.updatedAt || 0)) {
1362
- matchedSession = session;
1363
- }
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
+ });
1364
1457
  }
1365
- }
1366
- if (!matchedSession) {
1367
1458
  return res.json({
1368
1459
  active: false,
1369
1460
  provider,
1370
- projectPath: requestedProjectPath,
1461
+ projectPath: match.requestedProjectPath,
1462
+ tabId,
1463
+ sessionId,
1371
1464
  output: '',
1372
1465
  message: 'No active provider terminal session found for this project.',
1373
1466
  });
1374
1467
  }
1468
+ const matchedSession = match.session;
1375
1469
  const rawOutput = matchedSession.buffer.join('').slice(-maxChars);
1376
1470
  const output = stripAnsiSequences(rawOutput);
1377
1471
  const terminalState = resolveProviderTerminalState(matchedSession, provider, output);
@@ -1380,7 +1474,9 @@ app.get('/api/shell/sessions/provider-output', authenticateToken, requireProject
1380
1474
  provider,
1381
1475
  projectPath: path.resolve(matchedSession.projectPath || os.homedir()),
1382
1476
  sessionId: matchedSession.sessionId || null,
1477
+ tabId: matchedSession.tabId || null,
1383
1478
  updatedAt: matchedSession.updatedAt || null,
1479
+ matchStatus: match.status,
1384
1480
  ...terminalState,
1385
1481
  output,
1386
1482
  });
@@ -1390,36 +1486,45 @@ app.post('/api/shell/sessions/provider-input', authenticateToken, requireProject
1390
1486
  const projectPath = typeof req.body?.projectPath === 'string' && req.body.projectPath.trim()
1391
1487
  ? req.body.projectPath.trim()
1392
1488
  : null;
1489
+ const tabId = readPtyTarget(req.body?.tabId);
1490
+ const sessionId = readPtyTarget(req.body?.sessionId);
1393
1491
  const input = typeof req.body?.input === 'string' ? req.body.input : '';
1394
1492
  const submit = req.body?.submit !== false;
1395
1493
  if (!SHELL_CLI_PROVIDERS.has(provider)) {
1396
1494
  return res.status(400).json({ error: 'Unsupported provider' });
1397
1495
  }
1398
- const requestedProjectPath = projectPath ? path.resolve(projectPath) : null;
1399
- const requestUserId = req.user?.id ?? req.user?.userId ?? null;
1400
- const canWriteAnyShellSession = ['admin', 'owner'].includes(req.user?.role);
1401
- let matchedSession = null;
1402
- for (const session of ptySessionsMap.values()) {
1403
- if (session?.provider === provider &&
1404
- !session?.isPlainShell &&
1405
- session?.pty &&
1406
- session.lifecycleState === 'running' &&
1407
- (canWriteAnyShellSession || session?.userId === requestUserId) &&
1408
- (!requestedProjectPath || path.resolve(session.projectPath || os.homedir()) === requestedProjectPath)) {
1409
- if (!matchedSession || (session.updatedAt || 0) > (matchedSession.updatedAt || 0)) {
1410
- matchedSession = session;
1411
- }
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
+ });
1412
1516
  }
1413
- }
1414
- if (!matchedSession?.pty) {
1415
1517
  return res.status(404).json({
1416
1518
  ok: false,
1417
1519
  provider,
1418
- projectPath: requestedProjectPath,
1520
+ projectPath: match.requestedProjectPath,
1521
+ tabId,
1522
+ sessionId,
1419
1523
  wrote: false,
1420
1524
  message: 'No running provider terminal session found for this project.',
1421
1525
  });
1422
1526
  }
1527
+ const matchedSession = match.session;
1423
1528
  const data = submit ? normalizeTerminalStartupInput(input) : input;
1424
1529
  try {
1425
1530
  writeTerminalInputChunks(matchedSession.pty, data);
@@ -1429,9 +1534,11 @@ app.post('/api/shell/sessions/provider-input', authenticateToken, requireProject
1429
1534
  provider,
1430
1535
  projectPath: path.resolve(matchedSession.projectPath || os.homedir()),
1431
1536
  sessionId: matchedSession.sessionId || null,
1537
+ tabId: matchedSession.tabId || null,
1432
1538
  wrote: true,
1433
1539
  submitted: submit,
1434
1540
  bytes: Buffer.byteLength(data),
1541
+ matchStatus: match.status,
1435
1542
  });
1436
1543
  }
1437
1544
  catch (error) {
@@ -1443,6 +1550,99 @@ app.post('/api/shell/sessions/provider-input', authenticateToken, requireProject
1443
1550
  });
1444
1551
  }
1445
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
+ });
1446
1646
  // Authentication routes (public)
1447
1647
  app.use('/api/auth', authRoutes);
1448
1648
  // Projects API Routes (protected)
@@ -3241,16 +3441,22 @@ function handleShellConnection(ws, request) {
3241
3441
  const chunks = splitTerminalInput(data);
3242
3442
  if (chunks.length === 0)
3243
3443
  return;
3444
+ let droppedBytes = 0;
3244
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
+ }
3245
3451
  pendingInputQueue.push(chunk);
3246
- pendingInputBytes += Buffer.byteLength(chunk, 'utf8');
3452
+ pendingInputBytes += chunkBytes;
3247
3453
  }
3248
- while (pendingInputQueue.length > 0 && pendingInputBytes > SHELL_PENDING_INPUT_MAX_BYTES) {
3249
- const dropped = pendingInputQueue.shift();
3250
- 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
+ }));
3251
3459
  }
3252
- if (pendingInputBytes < 0)
3253
- pendingInputBytes = 0;
3254
3460
  scheduleInputFlush();
3255
3461
  }
3256
3462
  function flushOutputBuffer() {