@pixelbyte-software/pixcode 1.53.11 → 1.53.13
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/dist/assets/index-BEyC4q_s.css +32 -0
- package/dist/assets/{index-Bo8oVc2f.js → index-CZReJ0_S.js} +184 -184
- package/dist/index.html +2 -2
- package/dist-server/server/database/db.js +26 -0
- package/dist-server/server/database/db.js.map +1 -1
- package/dist-server/server/index.js +269 -53
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/projects.js +126 -43
- package/dist-server/server/projects.js.map +1 -1
- package/dist-server/server/routes/auth.js +41 -1
- package/dist-server/server/routes/auth.js.map +1 -1
- package/dist-server/server/routes/network.js +17 -1
- package/dist-server/server/routes/network.js.map +1 -1
- package/dist-server/server/routes/plugins.js +364 -0
- package/dist-server/server/routes/plugins.js.map +1 -1
- package/dist-server/server/services/external-access.js +45 -21
- package/dist-server/server/services/external-access.js.map +1 -1
- package/dist-server/server/services/qr-login.js +115 -0
- package/dist-server/server/services/qr-login.js.map +1 -0
- package/dist-server/server/services/telegram/bot.js +18 -1
- package/dist-server/server/services/telegram/bot.js.map +1 -1
- package/dist-server/server/services/telegram/control-center.js +187 -5
- package/dist-server/server/services/telegram/control-center.js.map +1 -1
- package/dist-server/server/services/telegram/translations.js +34 -2
- package/dist-server/server/services/telegram/translations.js.map +1 -1
- package/package.json +1 -1
- package/server/database/db.js +26 -0
- package/server/index.js +291 -58
- package/server/projects.js +128 -41
- package/server/routes/auth.js +47 -1
- package/server/routes/network.js +18 -1
- package/server/routes/plugins.js +370 -0
- package/server/services/external-access.js +49 -21
- package/server/services/qr-login.js +126 -0
- package/server/services/telegram/bot.js +15 -0
- package/server/services/telegram/control-center.js +192 -5
- package/server/services/telegram/translations.js +34 -2
- package/dist/assets/index-FhOsv2Yy.css +0 -32
|
@@ -120,12 +120,12 @@ import { adapterRegistry, ClaudeCodeA2AAdapter, CodexA2AAdapter, CursorA2AAdapte
|
|
|
120
120
|
import networkRoutes from './routes/network.js';
|
|
121
121
|
import telegramRoutes from './routes/telegram.js';
|
|
122
122
|
import { restoreRequestedTunnel } from './services/external-access.js';
|
|
123
|
-
import { restoreBotFromConfig } from './services/telegram/bot.js';
|
|
123
|
+
import { notifyTelegramTerminalAttached, restoreBotFromConfig } from './services/telegram/bot.js';
|
|
124
124
|
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 =
|
|
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
|
-
|
|
628
|
-
|
|
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 =
|
|
710
|
-
const WORKSPACE_DIFF_SNAPSHOT_MAX_FILES =
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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) || (
|
|
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 =
|
|
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
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
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
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
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,109 @@ 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'), async (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
|
+
notifyTelegramTerminalAttached({
|
|
1626
|
+
userId: req.user.id,
|
|
1627
|
+
terminal: control.activeTerminal,
|
|
1628
|
+
}).then((telegramNotice) => {
|
|
1629
|
+
if (telegramNotice?.ok === false) {
|
|
1630
|
+
console.warn('[telegram] terminal attach notice not delivered:', telegramNotice.reason);
|
|
1631
|
+
}
|
|
1632
|
+
}).catch((error) => {
|
|
1633
|
+
console.warn('[telegram] terminal attach notice failed:', error?.message || error);
|
|
1634
|
+
});
|
|
1635
|
+
res.json({ success: true, activeTerminal: control.activeTerminal, control, matchStatus: match.status, telegramNotice: { queued: true } });
|
|
1636
|
+
}
|
|
1637
|
+
catch (error) {
|
|
1638
|
+
console.error('telegram/active-terminal failed:', error);
|
|
1639
|
+
res.status(500).json({ error: 'Failed to attach Telegram to this terminal.' });
|
|
1640
|
+
}
|
|
1641
|
+
});
|
|
1642
|
+
// Return Telegram text input to the normal AI router.
|
|
1643
|
+
app.delete('/api/telegram/active-terminal', authenticateToken, (req, res) => {
|
|
1644
|
+
try {
|
|
1645
|
+
const link = requireVerifiedTelegramLink(req, res);
|
|
1646
|
+
if (!link)
|
|
1647
|
+
return;
|
|
1648
|
+
const control = telegramLinksDb.updateControlState(req.user.id, { activeTerminal: null });
|
|
1649
|
+
res.json({ success: true, control });
|
|
1650
|
+
}
|
|
1651
|
+
catch (error) {
|
|
1652
|
+
console.error('telegram/active-terminal delete failed:', error);
|
|
1653
|
+
res.status(500).json({ error: 'Failed to detach Telegram terminal.' });
|
|
1654
|
+
}
|
|
1655
|
+
});
|
|
1446
1656
|
// Authentication routes (public)
|
|
1447
1657
|
app.use('/api/auth', authRoutes);
|
|
1448
1658
|
// Projects API Routes (protected)
|
|
@@ -3241,16 +3451,22 @@ function handleShellConnection(ws, request) {
|
|
|
3241
3451
|
const chunks = splitTerminalInput(data);
|
|
3242
3452
|
if (chunks.length === 0)
|
|
3243
3453
|
return;
|
|
3454
|
+
let droppedBytes = 0;
|
|
3244
3455
|
for (const chunk of chunks) {
|
|
3456
|
+
const chunkBytes = Buffer.byteLength(chunk, 'utf8');
|
|
3457
|
+
if (pendingInputBytes + chunkBytes > SHELL_PENDING_INPUT_MAX_BYTES) {
|
|
3458
|
+
droppedBytes += chunkBytes;
|
|
3459
|
+
continue;
|
|
3460
|
+
}
|
|
3245
3461
|
pendingInputQueue.push(chunk);
|
|
3246
|
-
pendingInputBytes +=
|
|
3462
|
+
pendingInputBytes += chunkBytes;
|
|
3247
3463
|
}
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3464
|
+
if (droppedBytes > 0 && ws.readyState === WebSocket.OPEN) {
|
|
3465
|
+
ws.send(JSON.stringify({
|
|
3466
|
+
type: 'output',
|
|
3467
|
+
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`,
|
|
3468
|
+
}));
|
|
3251
3469
|
}
|
|
3252
|
-
if (pendingInputBytes < 0)
|
|
3253
|
-
pendingInputBytes = 0;
|
|
3254
3470
|
scheduleInputFlush();
|
|
3255
3471
|
}
|
|
3256
3472
|
function flushOutputBuffer() {
|