@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
|
@@ -39,22 +39,47 @@ const DAEMON_COMMAND_CONTEXT = {
|
|
|
39
39
|
function resolveMonacoAssetsPath() {
|
|
40
40
|
const appParent = path.dirname(APP_ROOT);
|
|
41
41
|
const appGrandparent = path.dirname(appParent);
|
|
42
|
+
const nodePathRoots = String(process.env.NODE_PATH || '')
|
|
43
|
+
.split(path.delimiter)
|
|
44
|
+
.map((entry) => entry.trim())
|
|
45
|
+
.filter(Boolean);
|
|
42
46
|
const candidates = [
|
|
43
47
|
path.join(APP_ROOT, 'node_modules', 'monaco-editor', 'min', 'vs'),
|
|
44
48
|
path.join(appParent, 'node_modules', 'monaco-editor', 'min', 'vs'),
|
|
45
49
|
path.join(appParent, 'monaco-editor', 'min', 'vs'),
|
|
46
50
|
path.join(appGrandparent, 'node_modules', 'monaco-editor', 'min', 'vs'),
|
|
47
51
|
path.join(appGrandparent, 'monaco-editor', 'min', 'vs'),
|
|
52
|
+
...nodePathRoots.flatMap((nodePathRoot) => [
|
|
53
|
+
path.join(nodePathRoot, 'monaco-editor', 'min', 'vs'),
|
|
54
|
+
path.join(nodePathRoot, 'node_modules', 'monaco-editor', 'min', 'vs'),
|
|
55
|
+
]),
|
|
56
|
+
];
|
|
57
|
+
const resolutionPaths = [
|
|
58
|
+
APP_ROOT,
|
|
59
|
+
appParent,
|
|
60
|
+
appGrandparent,
|
|
61
|
+
__dirname,
|
|
62
|
+
process.cwd(),
|
|
63
|
+
...nodePathRoots,
|
|
48
64
|
];
|
|
49
65
|
try {
|
|
50
66
|
const monacoPackagePath = require.resolve('monaco-editor/package.json', {
|
|
51
|
-
paths:
|
|
67
|
+
paths: resolutionPaths,
|
|
52
68
|
});
|
|
53
69
|
candidates.push(path.join(path.dirname(monacoPackagePath), 'min', 'vs'));
|
|
54
70
|
}
|
|
55
71
|
catch {
|
|
56
72
|
// The editor will show its normal load failure if the dependency is unavailable.
|
|
57
73
|
}
|
|
74
|
+
try {
|
|
75
|
+
const monacoLoaderPath = require.resolve('monaco-editor/min/vs/loader.js', {
|
|
76
|
+
paths: resolutionPaths,
|
|
77
|
+
});
|
|
78
|
+
candidates.push(path.dirname(monacoLoaderPath));
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Some package managers only expose the package root; candidates above cover that case.
|
|
82
|
+
}
|
|
58
83
|
return candidates.find((candidate) => fs.existsSync(path.join(candidate, 'loader.js'))) || null;
|
|
59
84
|
}
|
|
60
85
|
import { c } from './utils/colors.js';
|
|
@@ -100,7 +125,7 @@ import { ensurePortOpen } from './utils/port-access.js';
|
|
|
100
125
|
import { applyAllStoredCredentialsToEnv, } from './services/provider-credentials.js';
|
|
101
126
|
import { primeCliBinPath } from './services/install-jobs.js';
|
|
102
127
|
import { startEnabledPluginServers, stopAllPlugins, getPluginPort } from './utils/plugin-process-manager.js';
|
|
103
|
-
import { initializeDatabase, sessionNamesDb, applyCustomSessionNames, appConfigDb } from './database/db.js';
|
|
128
|
+
import { initializeDatabase, sessionNamesDb, applyCustomSessionNames, appConfigDb, telegramLinksDb } from './database/db.js';
|
|
104
129
|
import { setNotificationWebSocketServer } from './services/notification-orchestrator.js';
|
|
105
130
|
import { configureWebPush } from './services/vapid-keys.js';
|
|
106
131
|
import { validateApiKey, authenticateToken, authenticateWebSocket, requireAdmin, requireApiScope } from './middleware/auth.js';
|
|
@@ -553,7 +578,7 @@ const WATCHER_IGNORED_PATTERNS = [
|
|
|
553
578
|
// every open tab, which shows up as mouse/UI stutter. 1500ms collapses
|
|
554
579
|
// a full chat reply into ~1 scan while still feeling responsive when
|
|
555
580
|
// the user flips to the projects list.
|
|
556
|
-
const WATCHER_DEBOUNCE_MS =
|
|
581
|
+
const WATCHER_DEBOUNCE_MS = Number.parseInt(process.env.PIXCODE_PROJECT_WATCH_DEBOUNCE_MS || '', 10) || 10000;
|
|
557
582
|
let projectsWatchers = [];
|
|
558
583
|
let projectsWatcherDebounceTimer = null;
|
|
559
584
|
const connectedClients = new Set();
|
|
@@ -599,8 +624,9 @@ async function setupProjectsWatcher() {
|
|
|
599
624
|
}
|
|
600
625
|
try {
|
|
601
626
|
isGetProjectsRunning = true;
|
|
602
|
-
|
|
603
|
-
|
|
627
|
+
if (eventType === 'addDir' || eventType === 'unlinkDir') {
|
|
628
|
+
clearProjectDirectoryCache();
|
|
629
|
+
}
|
|
604
630
|
// Get updated projects list
|
|
605
631
|
const updatedProjects = await getProjects(broadcastProgress);
|
|
606
632
|
const updatePayload = {
|
|
@@ -681,9 +707,39 @@ async function setupProjectsWatcher() {
|
|
|
681
707
|
// `project_files_updated` events to subscribed clients only, letting the
|
|
682
708
|
// explorer refresh automatically without HTTP polling.
|
|
683
709
|
const WORKSPACE_WATCHER_DEBOUNCE_MS = 800;
|
|
684
|
-
const WORKSPACE_DIFF_SNAPSHOT_MAX_BYTES =
|
|
685
|
-
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;
|
|
686
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
|
+
}
|
|
687
743
|
function runWorkspaceCommand(command, args, cwd) {
|
|
688
744
|
return new Promise((resolve, reject) => {
|
|
689
745
|
const child = spawn(command, args, {
|
|
@@ -754,14 +810,14 @@ async function readWorkspaceTextSnapshot(filePath) {
|
|
|
754
810
|
return null;
|
|
755
811
|
}
|
|
756
812
|
}
|
|
757
|
-
async function initializeWorkspaceDirtySnapshots(rootPath,
|
|
813
|
+
async function initializeWorkspaceDirtySnapshots(rootPath, entry) {
|
|
758
814
|
try {
|
|
759
815
|
const { stdout } = await runWorkspaceCommand('git', ['status', '--porcelain', '-z'], rootPath);
|
|
760
816
|
const changedPaths = parseGitPorcelainZ(stdout).slice(0, WORKSPACE_DIFF_SNAPSHOT_MAX_FILES);
|
|
761
817
|
await Promise.all(changedPaths.map(async (relativePath) => {
|
|
762
818
|
const content = await readWorkspaceTextSnapshot(path.join(rootPath, relativePath));
|
|
763
819
|
if (typeof content === 'string') {
|
|
764
|
-
|
|
820
|
+
setWorkspaceSnapshot(entry, relativePath, content);
|
|
765
821
|
}
|
|
766
822
|
}));
|
|
767
823
|
}
|
|
@@ -795,9 +851,10 @@ async function subscribeToWorkspace(ws, projectName) {
|
|
|
795
851
|
pendingEvent: null,
|
|
796
852
|
rootPath,
|
|
797
853
|
fileSnapshots: new Map(),
|
|
854
|
+
snapshotBytes: 0,
|
|
798
855
|
};
|
|
799
856
|
workspaceWatchers.set(projectName, entry);
|
|
800
|
-
await initializeWorkspaceDirtySnapshots(rootPath, entry
|
|
857
|
+
await initializeWorkspaceDirtySnapshots(rootPath, entry);
|
|
801
858
|
const broadcastFileUpdate = async (eventType, filePath) => {
|
|
802
859
|
const relativePath = filePath ? normalizeWorkspaceRelativePath(rootPath, filePath) : null;
|
|
803
860
|
const previousContent = relativePath ? entry.fileSnapshots.get(relativePath) : undefined;
|
|
@@ -805,13 +862,13 @@ async function subscribeToWorkspace(ws, projectName) {
|
|
|
805
862
|
if (relativePath && !eventType.endsWith('Dir')) {
|
|
806
863
|
if (eventType === 'unlink') {
|
|
807
864
|
currentContent = '';
|
|
808
|
-
entry
|
|
865
|
+
deleteWorkspaceSnapshot(entry, relativePath);
|
|
809
866
|
}
|
|
810
867
|
else {
|
|
811
868
|
const snapshot = await readWorkspaceTextSnapshot(filePath);
|
|
812
869
|
if (typeof snapshot === 'string') {
|
|
813
870
|
currentContent = snapshot;
|
|
814
|
-
entry
|
|
871
|
+
setWorkspaceSnapshot(entry, relativePath, snapshot);
|
|
815
872
|
}
|
|
816
873
|
}
|
|
817
874
|
}
|
|
@@ -913,13 +970,13 @@ const app = express();
|
|
|
913
970
|
const server = http.createServer(app);
|
|
914
971
|
const ptySessionsMap = new Map();
|
|
915
972
|
const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
|
|
916
|
-
const COMPLETED_PTY_SESSION_TTL =
|
|
973
|
+
const COMPLETED_PTY_SESSION_TTL = Number.parseInt(process.env.PIXCODE_COMPLETED_PTY_TTL_MS || '', 10) || 60 * 1000;
|
|
917
974
|
const SHELL_URL_PARSE_BUFFER_LIMIT = 32768;
|
|
918
975
|
const SHELL_OUTPUT_FLUSH_MAX_CHARS = 128 * 1024;
|
|
919
976
|
const SHELL_WS_BACKPRESSURE_LIMIT = 8 * 1024 * 1024;
|
|
920
|
-
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);
|
|
921
978
|
const SHELL_INPUT_CHUNK_CHARS = 4096;
|
|
922
|
-
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);
|
|
923
980
|
const SHELL_CLI_PROVIDERS = new Set(['claude', 'codex', 'cursor', 'gemini', 'qwen', 'opencode']);
|
|
924
981
|
import { stripAnsiSequences, normalizeDetectedUrl, extractUrlsFromText, shouldAutoOpenUrlFromOutput } from './utils/url-detection.js';
|
|
925
982
|
function terminatePtySession(sessionKey, session, reason) {
|
|
@@ -1161,6 +1218,65 @@ function writeTerminalInputChunks(ptyProcess, data) {
|
|
|
1161
1218
|
}
|
|
1162
1219
|
return true;
|
|
1163
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
|
+
}
|
|
1164
1280
|
function normalizeShellPermissionMode(value) {
|
|
1165
1281
|
return typeof value === 'string' ? value.trim() : '';
|
|
1166
1282
|
}
|
|
@@ -1320,33 +1436,36 @@ app.get('/api/shell/sessions/provider-output', authenticateToken, requireProject
|
|
|
1320
1436
|
const projectPath = typeof req.query.projectPath === 'string' && req.query.projectPath.trim()
|
|
1321
1437
|
? req.query.projectPath.trim()
|
|
1322
1438
|
: null;
|
|
1439
|
+
const tabId = readPtyTarget(req.query.tabId);
|
|
1440
|
+
const sessionId = readPtyTarget(req.query.sessionId);
|
|
1323
1441
|
const maxChars = Math.min(20000, Math.max(1000, Number.parseInt(String(req.query.maxChars || '12000'), 10) || 12000));
|
|
1324
1442
|
if (!SHELL_CLI_PROVIDERS.has(provider)) {
|
|
1325
1443
|
return res.status(400).json({ error: 'Unsupported provider' });
|
|
1326
1444
|
}
|
|
1327
|
-
const
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
}
|
|
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
|
+
});
|
|
1339
1457
|
}
|
|
1340
|
-
}
|
|
1341
|
-
if (!matchedSession) {
|
|
1342
1458
|
return res.json({
|
|
1343
1459
|
active: false,
|
|
1344
1460
|
provider,
|
|
1345
|
-
projectPath: requestedProjectPath,
|
|
1461
|
+
projectPath: match.requestedProjectPath,
|
|
1462
|
+
tabId,
|
|
1463
|
+
sessionId,
|
|
1346
1464
|
output: '',
|
|
1347
1465
|
message: 'No active provider terminal session found for this project.',
|
|
1348
1466
|
});
|
|
1349
1467
|
}
|
|
1468
|
+
const matchedSession = match.session;
|
|
1350
1469
|
const rawOutput = matchedSession.buffer.join('').slice(-maxChars);
|
|
1351
1470
|
const output = stripAnsiSequences(rawOutput);
|
|
1352
1471
|
const terminalState = resolveProviderTerminalState(matchedSession, provider, output);
|
|
@@ -1355,7 +1474,9 @@ app.get('/api/shell/sessions/provider-output', authenticateToken, requireProject
|
|
|
1355
1474
|
provider,
|
|
1356
1475
|
projectPath: path.resolve(matchedSession.projectPath || os.homedir()),
|
|
1357
1476
|
sessionId: matchedSession.sessionId || null,
|
|
1477
|
+
tabId: matchedSession.tabId || null,
|
|
1358
1478
|
updatedAt: matchedSession.updatedAt || null,
|
|
1479
|
+
matchStatus: match.status,
|
|
1359
1480
|
...terminalState,
|
|
1360
1481
|
output,
|
|
1361
1482
|
});
|
|
@@ -1365,36 +1486,45 @@ app.post('/api/shell/sessions/provider-input', authenticateToken, requireProject
|
|
|
1365
1486
|
const projectPath = typeof req.body?.projectPath === 'string' && req.body.projectPath.trim()
|
|
1366
1487
|
? req.body.projectPath.trim()
|
|
1367
1488
|
: null;
|
|
1489
|
+
const tabId = readPtyTarget(req.body?.tabId);
|
|
1490
|
+
const sessionId = readPtyTarget(req.body?.sessionId);
|
|
1368
1491
|
const input = typeof req.body?.input === 'string' ? req.body.input : '';
|
|
1369
1492
|
const submit = req.body?.submit !== false;
|
|
1370
1493
|
if (!SHELL_CLI_PROVIDERS.has(provider)) {
|
|
1371
1494
|
return res.status(400).json({ error: 'Unsupported provider' });
|
|
1372
1495
|
}
|
|
1373
|
-
const
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
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
|
+
});
|
|
1387
1516
|
}
|
|
1388
|
-
}
|
|
1389
|
-
if (!matchedSession?.pty) {
|
|
1390
1517
|
return res.status(404).json({
|
|
1391
1518
|
ok: false,
|
|
1392
1519
|
provider,
|
|
1393
|
-
projectPath: requestedProjectPath,
|
|
1520
|
+
projectPath: match.requestedProjectPath,
|
|
1521
|
+
tabId,
|
|
1522
|
+
sessionId,
|
|
1394
1523
|
wrote: false,
|
|
1395
1524
|
message: 'No running provider terminal session found for this project.',
|
|
1396
1525
|
});
|
|
1397
1526
|
}
|
|
1527
|
+
const matchedSession = match.session;
|
|
1398
1528
|
const data = submit ? normalizeTerminalStartupInput(input) : input;
|
|
1399
1529
|
try {
|
|
1400
1530
|
writeTerminalInputChunks(matchedSession.pty, data);
|
|
@@ -1404,9 +1534,11 @@ app.post('/api/shell/sessions/provider-input', authenticateToken, requireProject
|
|
|
1404
1534
|
provider,
|
|
1405
1535
|
projectPath: path.resolve(matchedSession.projectPath || os.homedir()),
|
|
1406
1536
|
sessionId: matchedSession.sessionId || null,
|
|
1537
|
+
tabId: matchedSession.tabId || null,
|
|
1407
1538
|
wrote: true,
|
|
1408
1539
|
submitted: submit,
|
|
1409
1540
|
bytes: Buffer.byteLength(data),
|
|
1541
|
+
matchStatus: match.status,
|
|
1410
1542
|
});
|
|
1411
1543
|
}
|
|
1412
1544
|
catch (error) {
|
|
@@ -1418,6 +1550,99 @@ app.post('/api/shell/sessions/provider-input', authenticateToken, requireProject
|
|
|
1418
1550
|
});
|
|
1419
1551
|
}
|
|
1420
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
|
+
});
|
|
1421
1646
|
// Authentication routes (public)
|
|
1422
1647
|
app.use('/api/auth', authRoutes);
|
|
1423
1648
|
// Projects API Routes (protected)
|
|
@@ -3216,16 +3441,22 @@ function handleShellConnection(ws, request) {
|
|
|
3216
3441
|
const chunks = splitTerminalInput(data);
|
|
3217
3442
|
if (chunks.length === 0)
|
|
3218
3443
|
return;
|
|
3444
|
+
let droppedBytes = 0;
|
|
3219
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
|
+
}
|
|
3220
3451
|
pendingInputQueue.push(chunk);
|
|
3221
|
-
pendingInputBytes +=
|
|
3452
|
+
pendingInputBytes += chunkBytes;
|
|
3222
3453
|
}
|
|
3223
|
-
|
|
3224
|
-
|
|
3225
|
-
|
|
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
|
+
}));
|
|
3226
3459
|
}
|
|
3227
|
-
if (pendingInputBytes < 0)
|
|
3228
|
-
pendingInputBytes = 0;
|
|
3229
3460
|
scheduleInputFlush();
|
|
3230
3461
|
}
|
|
3231
3462
|
function flushOutputBuffer() {
|
|
@@ -3634,7 +3865,9 @@ function handleShellConnection(ws, request) {
|
|
|
3634
3865
|
});
|
|
3635
3866
|
// Handle process exit
|
|
3636
3867
|
shellProcess.onExit((exitCode) => {
|
|
3637
|
-
|
|
3868
|
+
const cleanShellExit = exitCode.exitCode === 0;
|
|
3869
|
+
const normalizedExitSignal = cleanShellExit ? null : (exitCode.signal || null);
|
|
3870
|
+
console.log('đ Shell process exited with code:', exitCode.exitCode, 'signal:', normalizedExitSignal);
|
|
3638
3871
|
if (outputFlushTimer) {
|
|
3639
3872
|
clearTimeout(outputFlushTimer);
|
|
3640
3873
|
outputFlushTimer = null;
|
|
@@ -3645,11 +3878,11 @@ function handleShellConnection(ws, request) {
|
|
|
3645
3878
|
console.log('âŠī¸ Ignoring stale PTY exit for replacement session:', ptySessionKey);
|
|
3646
3879
|
return;
|
|
3647
3880
|
}
|
|
3648
|
-
const exitMessage = `\r\n\x1b[33mProcess exited with code ${exitCode.exitCode}${
|
|
3881
|
+
const exitMessage = `\r\n\x1b[33mProcess exited with code ${exitCode.exitCode}${normalizedExitSignal ? ` (${normalizedExitSignal})` : ''}\x1b[0m\r\n`;
|
|
3649
3882
|
if (session) {
|
|
3650
|
-
session.lifecycleState =
|
|
3883
|
+
session.lifecycleState = cleanShellExit ? 'completed' : 'failed';
|
|
3651
3884
|
session.exitCode = typeof exitCode.exitCode === 'number' ? exitCode.exitCode : null;
|
|
3652
|
-
session.exitSignal =
|
|
3885
|
+
session.exitSignal = normalizedExitSignal;
|
|
3653
3886
|
session.completedAt = new Date().toISOString();
|
|
3654
3887
|
session.updatedAt = Date.now();
|
|
3655
3888
|
if (session.startupInputTimerId) {
|