@pixelbyte-software/pixcode 1.53.10 → 1.53.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-BEyC4q_s.css +32 -0
- package/dist/assets/{index-ChssgA1r.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 +290 -57
- 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/git.js +16 -0
- package/dist-server/server/routes/git.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/bot.js +43 -5
- package/dist-server/server/services/telegram/bot.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 +312 -62
- package/server/projects.js +128 -41
- package/server/routes/git.js +17 -0
- package/server/routes/plugins.js +370 -0
- package/server/services/telegram/bot.js +45 -5
- 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
|
@@ -45,23 +45,48 @@ const DAEMON_COMMAND_CONTEXT = {
|
|
|
45
45
|
function resolveMonacoAssetsPath() {
|
|
46
46
|
const appParent = path.dirname(APP_ROOT);
|
|
47
47
|
const appGrandparent = path.dirname(appParent);
|
|
48
|
+
const nodePathRoots = String(process.env.NODE_PATH || '')
|
|
49
|
+
.split(path.delimiter)
|
|
50
|
+
.map((entry) => entry.trim())
|
|
51
|
+
.filter(Boolean);
|
|
48
52
|
const candidates = [
|
|
49
53
|
path.join(APP_ROOT, 'node_modules', 'monaco-editor', 'min', 'vs'),
|
|
50
54
|
path.join(appParent, 'node_modules', 'monaco-editor', 'min', 'vs'),
|
|
51
55
|
path.join(appParent, 'monaco-editor', 'min', 'vs'),
|
|
52
56
|
path.join(appGrandparent, 'node_modules', 'monaco-editor', 'min', 'vs'),
|
|
53
57
|
path.join(appGrandparent, 'monaco-editor', 'min', 'vs'),
|
|
58
|
+
...nodePathRoots.flatMap((nodePathRoot) => [
|
|
59
|
+
path.join(nodePathRoot, 'monaco-editor', 'min', 'vs'),
|
|
60
|
+
path.join(nodePathRoot, 'node_modules', 'monaco-editor', 'min', 'vs'),
|
|
61
|
+
]),
|
|
62
|
+
];
|
|
63
|
+
const resolutionPaths = [
|
|
64
|
+
APP_ROOT,
|
|
65
|
+
appParent,
|
|
66
|
+
appGrandparent,
|
|
67
|
+
__dirname,
|
|
68
|
+
process.cwd(),
|
|
69
|
+
...nodePathRoots,
|
|
54
70
|
];
|
|
55
71
|
|
|
56
72
|
try {
|
|
57
73
|
const monacoPackagePath = require.resolve('monaco-editor/package.json', {
|
|
58
|
-
paths:
|
|
74
|
+
paths: resolutionPaths,
|
|
59
75
|
});
|
|
60
76
|
candidates.push(path.join(path.dirname(monacoPackagePath), 'min', 'vs'));
|
|
61
77
|
} catch {
|
|
62
78
|
// The editor will show its normal load failure if the dependency is unavailable.
|
|
63
79
|
}
|
|
64
80
|
|
|
81
|
+
try {
|
|
82
|
+
const monacoLoaderPath = require.resolve('monaco-editor/min/vs/loader.js', {
|
|
83
|
+
paths: resolutionPaths,
|
|
84
|
+
});
|
|
85
|
+
candidates.push(path.dirname(monacoLoaderPath));
|
|
86
|
+
} catch {
|
|
87
|
+
// Some package managers only expose the package root; candidates above cover that case.
|
|
88
|
+
}
|
|
89
|
+
|
|
65
90
|
return candidates.find((candidate) => fs.existsSync(path.join(candidate, 'loader.js'))) || null;
|
|
66
91
|
}
|
|
67
92
|
|
|
@@ -132,7 +157,7 @@ import {
|
|
|
132
157
|
} from './services/provider-credentials.js';
|
|
133
158
|
import { primeCliBinPath } from './services/install-jobs.js';
|
|
134
159
|
import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js';
|
|
135
|
-
import { initializeDatabase, sessionNamesDb, applyCustomSessionNames, appConfigDb } from './database/db.js';
|
|
160
|
+
import { initializeDatabase, sessionNamesDb, applyCustomSessionNames, appConfigDb, telegramLinksDb } from './database/db.js';
|
|
136
161
|
import { setNotificationWebSocketServer } from './services/notification-orchestrator.js';
|
|
137
162
|
import { configureWebPush } from './services/vapid-keys.js';
|
|
138
163
|
import { validateApiKey, authenticateToken, authenticateWebSocket, requireAdmin, requireApiScope } from './middleware/auth.js';
|
|
@@ -612,7 +637,7 @@ const WATCHER_IGNORED_PATTERNS = [
|
|
|
612
637
|
// every open tab, which shows up as mouse/UI stutter. 1500ms collapses
|
|
613
638
|
// a full chat reply into ~1 scan while still feeling responsive when
|
|
614
639
|
// the user flips to the projects list.
|
|
615
|
-
const WATCHER_DEBOUNCE_MS =
|
|
640
|
+
const WATCHER_DEBOUNCE_MS = Number.parseInt(process.env.PIXCODE_PROJECT_WATCH_DEBOUNCE_MS || '', 10) || 10000;
|
|
616
641
|
let projectsWatchers = [];
|
|
617
642
|
let projectsWatcherDebounceTimer = null;
|
|
618
643
|
const connectedClients = new Set();
|
|
@@ -667,8 +692,9 @@ async function setupProjectsWatcher() {
|
|
|
667
692
|
try {
|
|
668
693
|
isGetProjectsRunning = true;
|
|
669
694
|
|
|
670
|
-
|
|
671
|
-
|
|
695
|
+
if (eventType === 'addDir' || eventType === 'unlinkDir') {
|
|
696
|
+
clearProjectDirectoryCache();
|
|
697
|
+
}
|
|
672
698
|
|
|
673
699
|
// Get updated projects list
|
|
674
700
|
const updatedProjects = await getProjects(broadcastProgress);
|
|
@@ -756,10 +782,44 @@ async function setupProjectsWatcher() {
|
|
|
756
782
|
// `project_files_updated` events to subscribed clients only, letting the
|
|
757
783
|
// explorer refresh automatically without HTTP polling.
|
|
758
784
|
const WORKSPACE_WATCHER_DEBOUNCE_MS = 800;
|
|
759
|
-
const WORKSPACE_DIFF_SNAPSHOT_MAX_BYTES =
|
|
760
|
-
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;
|
|
761
788
|
const workspaceWatchers = new Map(); // projectName -> { watcher, subscribers, debounceTimer, pendingEvent, rootPath, fileSnapshots }
|
|
762
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
|
+
|
|
763
823
|
function runWorkspaceCommand(command, args, cwd) {
|
|
764
824
|
return new Promise((resolve, reject) => {
|
|
765
825
|
const child = spawn(command, args, {
|
|
@@ -843,7 +903,7 @@ async function readWorkspaceTextSnapshot(filePath) {
|
|
|
843
903
|
}
|
|
844
904
|
}
|
|
845
905
|
|
|
846
|
-
async function initializeWorkspaceDirtySnapshots(rootPath,
|
|
906
|
+
async function initializeWorkspaceDirtySnapshots(rootPath, entry) {
|
|
847
907
|
try {
|
|
848
908
|
const { stdout } = await runWorkspaceCommand('git', ['status', '--porcelain', '-z'], rootPath);
|
|
849
909
|
const changedPaths = parseGitPorcelainZ(stdout).slice(0, WORKSPACE_DIFF_SNAPSHOT_MAX_FILES);
|
|
@@ -851,7 +911,7 @@ async function initializeWorkspaceDirtySnapshots(rootPath, fileSnapshots) {
|
|
|
851
911
|
await Promise.all(changedPaths.map(async (relativePath) => {
|
|
852
912
|
const content = await readWorkspaceTextSnapshot(path.join(rootPath, relativePath));
|
|
853
913
|
if (typeof content === 'string') {
|
|
854
|
-
|
|
914
|
+
setWorkspaceSnapshot(entry, relativePath, content);
|
|
855
915
|
}
|
|
856
916
|
}));
|
|
857
917
|
} catch {
|
|
@@ -885,9 +945,10 @@ async function subscribeToWorkspace(ws, projectName) {
|
|
|
885
945
|
pendingEvent: null,
|
|
886
946
|
rootPath,
|
|
887
947
|
fileSnapshots: new Map(),
|
|
948
|
+
snapshotBytes: 0,
|
|
888
949
|
};
|
|
889
950
|
workspaceWatchers.set(projectName, entry);
|
|
890
|
-
await initializeWorkspaceDirtySnapshots(rootPath, entry
|
|
951
|
+
await initializeWorkspaceDirtySnapshots(rootPath, entry);
|
|
891
952
|
|
|
892
953
|
const broadcastFileUpdate = async (eventType, filePath) => {
|
|
893
954
|
const relativePath = filePath ? normalizeWorkspaceRelativePath(rootPath, filePath) : null;
|
|
@@ -897,12 +958,12 @@ async function subscribeToWorkspace(ws, projectName) {
|
|
|
897
958
|
if (relativePath && !eventType.endsWith('Dir')) {
|
|
898
959
|
if (eventType === 'unlink') {
|
|
899
960
|
currentContent = '';
|
|
900
|
-
entry
|
|
961
|
+
deleteWorkspaceSnapshot(entry, relativePath);
|
|
901
962
|
} else {
|
|
902
963
|
const snapshot = await readWorkspaceTextSnapshot(filePath);
|
|
903
964
|
if (typeof snapshot === 'string') {
|
|
904
965
|
currentContent = snapshot;
|
|
905
|
-
entry
|
|
966
|
+
setWorkspaceSnapshot(entry, relativePath, snapshot);
|
|
906
967
|
}
|
|
907
968
|
}
|
|
908
969
|
}
|
|
@@ -1016,13 +1077,13 @@ const server = http.createServer(app);
|
|
|
1016
1077
|
|
|
1017
1078
|
const ptySessionsMap = new Map();
|
|
1018
1079
|
const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
|
|
1019
|
-
const COMPLETED_PTY_SESSION_TTL =
|
|
1080
|
+
const COMPLETED_PTY_SESSION_TTL = Number.parseInt(process.env.PIXCODE_COMPLETED_PTY_TTL_MS || '', 10) || 60 * 1000;
|
|
1020
1081
|
const SHELL_URL_PARSE_BUFFER_LIMIT = 32768;
|
|
1021
1082
|
const SHELL_OUTPUT_FLUSH_MAX_CHARS = 128 * 1024;
|
|
1022
1083
|
const SHELL_WS_BACKPRESSURE_LIMIT = 8 * 1024 * 1024;
|
|
1023
|
-
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);
|
|
1024
1085
|
const SHELL_INPUT_CHUNK_CHARS = 4096;
|
|
1025
|
-
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);
|
|
1026
1087
|
const SHELL_CLI_PROVIDERS = new Set(['claude', 'codex', 'cursor', 'gemini', 'qwen', 'opencode']);
|
|
1027
1088
|
import { stripAnsiSequences, normalizeDetectedUrl, extractUrlsFromText, shouldAutoOpenUrlFromOutput } from './utils/url-detection.js';
|
|
1028
1089
|
|
|
@@ -1300,6 +1361,76 @@ function writeTerminalInputChunks(ptyProcess, data) {
|
|
|
1300
1361
|
return true;
|
|
1301
1362
|
}
|
|
1302
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
|
+
|
|
1303
1434
|
function normalizeShellPermissionMode(value) {
|
|
1304
1435
|
return typeof value === 'string' ? value.trim() : '';
|
|
1305
1436
|
}
|
|
@@ -1490,6 +1621,8 @@ app.get('/api/shell/sessions/provider-output', authenticateToken, requireProject
|
|
|
1490
1621
|
const projectPath = typeof req.query.projectPath === 'string' && req.query.projectPath.trim()
|
|
1491
1622
|
? req.query.projectPath.trim()
|
|
1492
1623
|
: null;
|
|
1624
|
+
const tabId = readPtyTarget(req.query.tabId);
|
|
1625
|
+
const sessionId = readPtyTarget(req.query.sessionId);
|
|
1493
1626
|
const maxChars = Math.min(
|
|
1494
1627
|
20000,
|
|
1495
1628
|
Math.max(1000, Number.parseInt(String(req.query.maxChars || '12000'), 10) || 12000)
|
|
@@ -1499,33 +1632,32 @@ app.get('/api/shell/sessions/provider-output', authenticateToken, requireProject
|
|
|
1499
1632
|
return res.status(400).json({ error: 'Unsupported provider' });
|
|
1500
1633
|
}
|
|
1501
1634
|
|
|
1502
|
-
const
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
}
|
|
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
|
+
});
|
|
1516
1648
|
}
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
if (!matchedSession) {
|
|
1520
1649
|
return res.json({
|
|
1521
1650
|
active: false,
|
|
1522
1651
|
provider,
|
|
1523
|
-
projectPath: requestedProjectPath,
|
|
1652
|
+
projectPath: match.requestedProjectPath,
|
|
1653
|
+
tabId,
|
|
1654
|
+
sessionId,
|
|
1524
1655
|
output: '',
|
|
1525
1656
|
message: 'No active provider terminal session found for this project.',
|
|
1526
1657
|
});
|
|
1527
1658
|
}
|
|
1528
1659
|
|
|
1660
|
+
const matchedSession = match.session;
|
|
1529
1661
|
const rawOutput = matchedSession.buffer.join('').slice(-maxChars);
|
|
1530
1662
|
const output = stripAnsiSequences(rawOutput);
|
|
1531
1663
|
const terminalState = resolveProviderTerminalState(matchedSession, provider, output);
|
|
@@ -1534,7 +1666,9 @@ app.get('/api/shell/sessions/provider-output', authenticateToken, requireProject
|
|
|
1534
1666
|
provider,
|
|
1535
1667
|
projectPath: path.resolve(matchedSession.projectPath || os.homedir()),
|
|
1536
1668
|
sessionId: matchedSession.sessionId || null,
|
|
1669
|
+
tabId: matchedSession.tabId || null,
|
|
1537
1670
|
updatedAt: matchedSession.updatedAt || null,
|
|
1671
|
+
matchStatus: match.status,
|
|
1538
1672
|
...terminalState,
|
|
1539
1673
|
output,
|
|
1540
1674
|
});
|
|
@@ -1545,6 +1679,8 @@ app.post('/api/shell/sessions/provider-input', authenticateToken, requireProject
|
|
|
1545
1679
|
const projectPath = typeof req.body?.projectPath === 'string' && req.body.projectPath.trim()
|
|
1546
1680
|
? req.body.projectPath.trim()
|
|
1547
1681
|
: null;
|
|
1682
|
+
const tabId = readPtyTarget(req.body?.tabId);
|
|
1683
|
+
const sessionId = readPtyTarget(req.body?.sessionId);
|
|
1548
1684
|
const input = typeof req.body?.input === 'string' ? req.body.input : '';
|
|
1549
1685
|
const submit = req.body?.submit !== false;
|
|
1550
1686
|
|
|
@@ -1552,35 +1688,40 @@ app.post('/api/shell/sessions/provider-input', authenticateToken, requireProject
|
|
|
1552
1688
|
return res.status(400).json({ error: 'Unsupported provider' });
|
|
1553
1689
|
}
|
|
1554
1690
|
|
|
1555
|
-
const
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
session.lifecycleState === 'running' &&
|
|
1565
|
-
(canWriteAnyShellSession || session?.userId === requestUserId) &&
|
|
1566
|
-
(!requestedProjectPath || path.resolve(session.projectPath || os.homedir()) === requestedProjectPath)
|
|
1567
|
-
) {
|
|
1568
|
-
if (!matchedSession || (session.updatedAt || 0) > (matchedSession.updatedAt || 0)) {
|
|
1569
|
-
matchedSession = session;
|
|
1570
|
-
}
|
|
1571
|
-
}
|
|
1572
|
-
}
|
|
1691
|
+
const match = findProviderPtySession({
|
|
1692
|
+
provider,
|
|
1693
|
+
projectPath,
|
|
1694
|
+
user: req.user,
|
|
1695
|
+
tabId,
|
|
1696
|
+
sessionId,
|
|
1697
|
+
requireRunning: true,
|
|
1698
|
+
requirePty: true,
|
|
1699
|
+
});
|
|
1573
1700
|
|
|
1574
|
-
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
|
+
}
|
|
1575
1713
|
return res.status(404).json({
|
|
1576
1714
|
ok: false,
|
|
1577
1715
|
provider,
|
|
1578
|
-
projectPath: requestedProjectPath,
|
|
1716
|
+
projectPath: match.requestedProjectPath,
|
|
1717
|
+
tabId,
|
|
1718
|
+
sessionId,
|
|
1579
1719
|
wrote: false,
|
|
1580
1720
|
message: 'No running provider terminal session found for this project.',
|
|
1581
1721
|
});
|
|
1582
1722
|
}
|
|
1583
1723
|
|
|
1724
|
+
const matchedSession = match.session;
|
|
1584
1725
|
const data = submit ? normalizeTerminalStartupInput(input) : input;
|
|
1585
1726
|
try {
|
|
1586
1727
|
writeTerminalInputChunks(matchedSession.pty, data);
|
|
@@ -1590,9 +1731,11 @@ app.post('/api/shell/sessions/provider-input', authenticateToken, requireProject
|
|
|
1590
1731
|
provider,
|
|
1591
1732
|
projectPath: path.resolve(matchedSession.projectPath || os.homedir()),
|
|
1592
1733
|
sessionId: matchedSession.sessionId || null,
|
|
1734
|
+
tabId: matchedSession.tabId || null,
|
|
1593
1735
|
wrote: true,
|
|
1594
1736
|
submitted: submit,
|
|
1595
1737
|
bytes: Buffer.byteLength(data),
|
|
1738
|
+
matchStatus: match.status,
|
|
1596
1739
|
});
|
|
1597
1740
|
} catch (error) {
|
|
1598
1741
|
res.status(500).json({
|
|
@@ -1604,6 +1747,104 @@ app.post('/api/shell/sessions/provider-input', authenticateToken, requireProject
|
|
|
1604
1747
|
}
|
|
1605
1748
|
});
|
|
1606
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
|
+
|
|
1607
1848
|
// Authentication routes (public)
|
|
1608
1849
|
app.use('/api/auth', authRoutes);
|
|
1609
1850
|
|
|
@@ -3491,16 +3732,23 @@ function handleShellConnection(ws, request) {
|
|
|
3491
3732
|
const chunks = splitTerminalInput(data);
|
|
3492
3733
|
if (chunks.length === 0) return;
|
|
3493
3734
|
|
|
3735
|
+
let droppedBytes = 0;
|
|
3494
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
|
+
}
|
|
3495
3742
|
pendingInputQueue.push(chunk);
|
|
3496
|
-
pendingInputBytes +=
|
|
3743
|
+
pendingInputBytes += chunkBytes;
|
|
3497
3744
|
}
|
|
3498
3745
|
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
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
|
+
}));
|
|
3502
3751
|
}
|
|
3503
|
-
if (pendingInputBytes < 0) pendingInputBytes = 0;
|
|
3504
3752
|
|
|
3505
3753
|
scheduleInputFlush();
|
|
3506
3754
|
}
|
|
@@ -3937,7 +4185,9 @@ function handleShellConnection(ws, request) {
|
|
|
3937
4185
|
|
|
3938
4186
|
// Handle process exit
|
|
3939
4187
|
shellProcess.onExit((exitCode) => {
|
|
3940
|
-
|
|
4188
|
+
const cleanShellExit = exitCode.exitCode === 0;
|
|
4189
|
+
const normalizedExitSignal = cleanShellExit ? null : (exitCode.signal || null);
|
|
4190
|
+
console.log('🔚 Shell process exited with code:', exitCode.exitCode, 'signal:', normalizedExitSignal);
|
|
3941
4191
|
if (outputFlushTimer) {
|
|
3942
4192
|
clearTimeout(outputFlushTimer);
|
|
3943
4193
|
outputFlushTimer = null;
|
|
@@ -3949,11 +4199,11 @@ function handleShellConnection(ws, request) {
|
|
|
3949
4199
|
return;
|
|
3950
4200
|
}
|
|
3951
4201
|
|
|
3952
|
-
const exitMessage = `\r\n\x1b[33mProcess exited with code ${exitCode.exitCode}${
|
|
4202
|
+
const exitMessage = `\r\n\x1b[33mProcess exited with code ${exitCode.exitCode}${normalizedExitSignal ? ` (${normalizedExitSignal})` : ''}\x1b[0m\r\n`;
|
|
3953
4203
|
if (session) {
|
|
3954
|
-
session.lifecycleState =
|
|
4204
|
+
session.lifecycleState = cleanShellExit ? 'completed' : 'failed';
|
|
3955
4205
|
session.exitCode = typeof exitCode.exitCode === 'number' ? exitCode.exitCode : null;
|
|
3956
|
-
session.exitSignal =
|
|
4206
|
+
session.exitSignal = normalizedExitSignal;
|
|
3957
4207
|
session.completedAt = new Date().toISOString();
|
|
3958
4208
|
session.updatedAt = Date.now();
|
|
3959
4209
|
if (session.startupInputTimerId) {
|