@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/dist/assets/index-BEyC4q_s.css +32 -0
- package/dist/assets/{index-Bo8oVc2f.js → index-kP_gZ-wL.js} +178 -178
- 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 +258 -52
- 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/plugins.js +364 -0
- package/dist-server/server/routes/plugins.js.map +1 -1
- package/dist-server/server/services/telegram/control-center.js +169 -5
- package/dist-server/server/services/telegram/control-center.js.map +1 -1
- package/dist-server/server/services/telegram/translations.js +28 -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 +280 -57
- package/server/projects.js +128 -41
- package/server/routes/plugins.js +370 -0
- package/server/services/telegram/control-center.js +173 -5
- package/server/services/telegram/translations.js +28 -2
- package/dist/assets/index-FhOsv2Yy.css +0 -32
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 =
|
|
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
|
-
|
|
696
|
-
|
|
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 =
|
|
785
|
-
const WORKSPACE_DIFF_SNAPSHOT_MAX_FILES =
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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 =
|
|
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) || (
|
|
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 =
|
|
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
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
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
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
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 (!
|
|
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 +=
|
|
3743
|
+
pendingInputBytes += chunkBytes;
|
|
3522
3744
|
}
|
|
3523
3745
|
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
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
|
}
|