@pixelbyte-software/pixcode 1.53.13 → 1.53.15
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-CZReJ0_S.js → index-BgMvphaU.js} +1 -1
- package/dist/index.html +1 -1
- package/dist-server/server/index.js +67 -3
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/routes/git.js +60 -3
- package/dist-server/server/routes/git.js.map +1 -1
- package/dist-server/server/services/telegram/control-center.js +205 -6
- package/dist-server/server/services/telegram/control-center.js.map +1 -1
- package/dist-server/server/services/telegram/translations.js +14 -0
- package/dist-server/server/services/telegram/translations.js.map +1 -1
- package/package.json +1 -1
- package/server/index.js +74 -3
- package/server/routes/git.js +70 -3
- package/server/services/telegram/control-center.js +220 -6
- package/server/services/telegram/translations.js +14 -0
package/server/index.js
CHANGED
|
@@ -1237,6 +1237,7 @@ function appendPtySessionBuffer(session, data) {
|
|
|
1237
1237
|
valueBytes = Buffer.byteLength(value, 'utf8');
|
|
1238
1238
|
}
|
|
1239
1239
|
|
|
1240
|
+
session.totalOutputBytes = (session.totalOutputBytes || 0) + valueBytes;
|
|
1240
1241
|
session.buffer.push(value);
|
|
1241
1242
|
session.bufferBytes = (session.bufferBytes || 0) + valueBytes;
|
|
1242
1243
|
|
|
@@ -1247,10 +1248,39 @@ function appendPtySessionBuffer(session, data) {
|
|
|
1247
1248
|
if (session.bufferBytes < 0) session.bufferBytes = 0;
|
|
1248
1249
|
}
|
|
1249
1250
|
|
|
1251
|
+
function readPtySessionBufferedOutput(session, { maxChars = 12000, sinceCursor = null } = {}) {
|
|
1252
|
+
const rawBuffer = (session?.buffer || []).join('');
|
|
1253
|
+
const bufferBytes = session?.bufferBytes || Buffer.byteLength(rawBuffer, 'utf8');
|
|
1254
|
+
const outputCursor = session?.totalOutputBytes || bufferBytes;
|
|
1255
|
+
const bufferStartCursor = Math.max(0, outputCursor - bufferBytes);
|
|
1256
|
+
let rawOutput = rawBuffer;
|
|
1257
|
+
|
|
1258
|
+
if (Number.isFinite(sinceCursor)) {
|
|
1259
|
+
const skipBytes = Math.max(0, sinceCursor - bufferStartCursor);
|
|
1260
|
+
if (skipBytes > 0) {
|
|
1261
|
+
rawOutput = Buffer.from(rawBuffer, 'utf8').slice(skipBytes).toString('utf8');
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
if (rawOutput.length > maxChars) {
|
|
1266
|
+
rawOutput = rawOutput.slice(-maxChars);
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
return {
|
|
1270
|
+
rawOutput,
|
|
1271
|
+
outputCursor,
|
|
1272
|
+
bufferStartCursor,
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1250
1276
|
function normalizeTerminalStartupInput(input) {
|
|
1251
1277
|
return `\x15${String(input || '').replace(/(?:\r\n|\r|\n)+$/u, '')}\r`;
|
|
1252
1278
|
}
|
|
1253
1279
|
|
|
1280
|
+
function normalizeTerminalBufferedInput(input) {
|
|
1281
|
+
return `\x15${String(input || '').replace(/(?:\r\n|\r|\n)+$/u, '')}`;
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1254
1284
|
function readSessionOutputForState(session, maxChars = 12000) {
|
|
1255
1285
|
return stripAnsiSequences((session?.buffer || []).join('').slice(-maxChars));
|
|
1256
1286
|
}
|
|
@@ -1627,6 +1657,8 @@ app.get('/api/shell/sessions/provider-output', authenticateToken, requireProject
|
|
|
1627
1657
|
20000,
|
|
1628
1658
|
Math.max(1000, Number.parseInt(String(req.query.maxChars || '12000'), 10) || 12000)
|
|
1629
1659
|
);
|
|
1660
|
+
const sinceCursorRaw = Number.parseInt(String(req.query.sinceCursor ?? ''), 10);
|
|
1661
|
+
const sinceCursor = Number.isFinite(sinceCursorRaw) ? Math.max(0, sinceCursorRaw) : null;
|
|
1630
1662
|
|
|
1631
1663
|
if (!SHELL_CLI_PROVIDERS.has(provider)) {
|
|
1632
1664
|
return res.status(400).json({ error: 'Unsupported provider' });
|
|
@@ -1643,6 +1675,8 @@ app.get('/api/shell/sessions/provider-output', authenticateToken, requireProject
|
|
|
1643
1675
|
tabId,
|
|
1644
1676
|
sessionId,
|
|
1645
1677
|
output: '',
|
|
1678
|
+
outputCursor: 0,
|
|
1679
|
+
bufferStartCursor: 0,
|
|
1646
1680
|
message: 'Multiple provider terminal sessions match this target. Pick a specific tab.',
|
|
1647
1681
|
});
|
|
1648
1682
|
}
|
|
@@ -1653,14 +1687,21 @@ app.get('/api/shell/sessions/provider-output', authenticateToken, requireProject
|
|
|
1653
1687
|
tabId,
|
|
1654
1688
|
sessionId,
|
|
1655
1689
|
output: '',
|
|
1690
|
+
outputCursor: 0,
|
|
1691
|
+
bufferStartCursor: 0,
|
|
1656
1692
|
message: 'No active provider terminal session found for this project.',
|
|
1657
1693
|
});
|
|
1658
1694
|
}
|
|
1659
1695
|
|
|
1660
1696
|
const matchedSession = match.session;
|
|
1661
|
-
const
|
|
1697
|
+
const {
|
|
1698
|
+
rawOutput,
|
|
1699
|
+
outputCursor,
|
|
1700
|
+
bufferStartCursor,
|
|
1701
|
+
} = readPtySessionBufferedOutput(matchedSession, { maxChars, sinceCursor });
|
|
1662
1702
|
const output = stripAnsiSequences(rawOutput);
|
|
1663
|
-
const
|
|
1703
|
+
const fullOutput = readSessionOutputForState(matchedSession);
|
|
1704
|
+
const terminalState = resolveProviderTerminalState(matchedSession, provider, fullOutput);
|
|
1664
1705
|
res.json({
|
|
1665
1706
|
active: true,
|
|
1666
1707
|
provider,
|
|
@@ -1671,6 +1712,9 @@ app.get('/api/shell/sessions/provider-output', authenticateToken, requireProject
|
|
|
1671
1712
|
matchStatus: match.status,
|
|
1672
1713
|
...terminalState,
|
|
1673
1714
|
output,
|
|
1715
|
+
outputCursor,
|
|
1716
|
+
bufferStartCursor,
|
|
1717
|
+
sinceCursor,
|
|
1674
1718
|
});
|
|
1675
1719
|
});
|
|
1676
1720
|
|
|
@@ -1683,6 +1727,7 @@ app.post('/api/shell/sessions/provider-input', authenticateToken, requireProject
|
|
|
1683
1727
|
const sessionId = readPtyTarget(req.body?.sessionId);
|
|
1684
1728
|
const input = typeof req.body?.input === 'string' ? req.body.input : '';
|
|
1685
1729
|
const submit = req.body?.submit !== false;
|
|
1730
|
+
const submitMode = req.body?.submitMode === 'deferred-enter' ? 'deferred-enter' : 'inline';
|
|
1686
1731
|
|
|
1687
1732
|
if (!SHELL_CLI_PROVIDERS.has(provider)) {
|
|
1688
1733
|
return res.status(400).json({ error: 'Unsupported provider' });
|
|
@@ -1722,9 +1767,31 @@ app.post('/api/shell/sessions/provider-input', authenticateToken, requireProject
|
|
|
1722
1767
|
}
|
|
1723
1768
|
|
|
1724
1769
|
const matchedSession = match.session;
|
|
1725
|
-
const data = submit
|
|
1770
|
+
const data = submit
|
|
1771
|
+
? (submitMode === 'deferred-enter' ? normalizeTerminalBufferedInput(input) : normalizeTerminalStartupInput(input))
|
|
1772
|
+
: input;
|
|
1773
|
+
const outputCursorBefore = matchedSession.totalOutputBytes || matchedSession.bufferBytes || 0;
|
|
1726
1774
|
try {
|
|
1727
1775
|
writeTerminalInputChunks(matchedSession.pty, data);
|
|
1776
|
+
if (submit && submitMode === 'deferred-enter') {
|
|
1777
|
+
setTimeout(() => {
|
|
1778
|
+
const currentSession = findProviderPtySession({
|
|
1779
|
+
provider,
|
|
1780
|
+
projectPath,
|
|
1781
|
+
user: req.user,
|
|
1782
|
+
tabId,
|
|
1783
|
+
sessionId,
|
|
1784
|
+
requireRunning: true,
|
|
1785
|
+
requirePty: true,
|
|
1786
|
+
}).session;
|
|
1787
|
+
try {
|
|
1788
|
+
writeTerminalInputChunks(currentSession?.pty, '\r');
|
|
1789
|
+
if (currentSession) currentSession.updatedAt = Date.now();
|
|
1790
|
+
} catch (error) {
|
|
1791
|
+
console.warn('Deferred terminal submit failed:', error?.message || error);
|
|
1792
|
+
}
|
|
1793
|
+
}, 120);
|
|
1794
|
+
}
|
|
1728
1795
|
matchedSession.updatedAt = Date.now();
|
|
1729
1796
|
res.json({
|
|
1730
1797
|
ok: true,
|
|
@@ -1734,8 +1801,11 @@ app.post('/api/shell/sessions/provider-input', authenticateToken, requireProject
|
|
|
1734
1801
|
tabId: matchedSession.tabId || null,
|
|
1735
1802
|
wrote: true,
|
|
1736
1803
|
submitted: submit,
|
|
1804
|
+
submitMode,
|
|
1737
1805
|
bytes: Buffer.byteLength(data),
|
|
1738
1806
|
matchStatus: match.status,
|
|
1807
|
+
outputCursorBefore,
|
|
1808
|
+
outputCursorAfter: matchedSession.totalOutputBytes || matchedSession.bufferBytes || 0,
|
|
1739
1809
|
});
|
|
1740
1810
|
} catch (error) {
|
|
1741
1811
|
res.status(500).json({
|
|
@@ -4152,6 +4222,7 @@ function handleShellConnection(ws, request) {
|
|
|
4152
4222
|
ws: ws,
|
|
4153
4223
|
buffer: [],
|
|
4154
4224
|
bufferBytes: 0,
|
|
4225
|
+
totalOutputBytes: 0,
|
|
4155
4226
|
droppedOutputBytes: 0,
|
|
4156
4227
|
timeoutId: null,
|
|
4157
4228
|
userId: ownerUserId,
|
package/server/routes/git.js
CHANGED
|
@@ -46,9 +46,11 @@ router.use((req, res, next) => {
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
function isNotGitRepositoryMessage(message = '') {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
||
|
|
49
|
+
const normalized = String(message || '').toLowerCase();
|
|
50
|
+
return normalized.includes('not a git repository')
|
|
51
|
+
|| normalized.includes('not inside a git work tree')
|
|
52
|
+
|| normalized.includes('does not contain a .git folder')
|
|
53
|
+
|| normalized.includes('project directory is not a git repository');
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
function isMissingFileError(error) {
|
|
@@ -178,6 +180,35 @@ async function buildFilesystemStatus(projectPath) {
|
|
|
178
180
|
};
|
|
179
181
|
}
|
|
180
182
|
|
|
183
|
+
async function readFilesystemFileWithDiff(projectPath, filePath) {
|
|
184
|
+
const projectRoot = path.resolve(projectPath);
|
|
185
|
+
const resolvedFilePath = path.resolve(projectRoot, String(filePath || ''));
|
|
186
|
+
const relativePath = path.relative(projectRoot, resolvedFilePath);
|
|
187
|
+
|
|
188
|
+
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
|
189
|
+
const error = new Error('Invalid file path: path traversal detected');
|
|
190
|
+
error.statusCode = 400;
|
|
191
|
+
throw error;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const stats = await fs.stat(resolvedFilePath);
|
|
195
|
+
if (stats.isDirectory()) {
|
|
196
|
+
const error = new Error('Cannot show diff for directories');
|
|
197
|
+
error.statusCode = 400;
|
|
198
|
+
throw error;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const currentContent = await fs.readFile(resolvedFilePath, 'utf-8');
|
|
202
|
+
return {
|
|
203
|
+
currentContent,
|
|
204
|
+
oldContent: currentContent,
|
|
205
|
+
isDeleted: false,
|
|
206
|
+
isUntracked: false,
|
|
207
|
+
isGitRepository: false,
|
|
208
|
+
trackingMode: 'filesystem',
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
181
212
|
function spawnAsync(command, args, options = {}) {
|
|
182
213
|
return new Promise((resolve, reject) => {
|
|
183
214
|
const child = spawn(command, args, {
|
|
@@ -634,6 +665,14 @@ router.get('/diff', async (req, res) => {
|
|
|
634
665
|
|
|
635
666
|
res.json({ diff });
|
|
636
667
|
} catch (error) {
|
|
668
|
+
if (isNotGitRepositoryMessage(getGitErrorDetails(error))) {
|
|
669
|
+
return res.json({
|
|
670
|
+
diff: '',
|
|
671
|
+
isGitRepository: false,
|
|
672
|
+
trackingMode: 'filesystem',
|
|
673
|
+
});
|
|
674
|
+
}
|
|
675
|
+
|
|
637
676
|
console.error('Git diff error:', error);
|
|
638
677
|
res.json({ error: error.message });
|
|
639
678
|
}
|
|
@@ -763,6 +802,34 @@ router.get('/file-with-diff', async (req, res) => {
|
|
|
763
802
|
isUntracked: false,
|
|
764
803
|
});
|
|
765
804
|
}
|
|
805
|
+
|
|
806
|
+
if (isNotGitRepositoryMessage(getGitErrorDetails(error))) {
|
|
807
|
+
try {
|
|
808
|
+
return res.json(await readFilesystemFileWithDiff(
|
|
809
|
+
await getActualProjectPath(project),
|
|
810
|
+
file,
|
|
811
|
+
));
|
|
812
|
+
} catch (fallbackError) {
|
|
813
|
+
if (isMissingFileError(fallbackError)) {
|
|
814
|
+
return res.status(404).json({
|
|
815
|
+
error: 'File not found',
|
|
816
|
+
currentContent: '',
|
|
817
|
+
oldContent: '',
|
|
818
|
+
isDeleted: true,
|
|
819
|
+
isUntracked: false,
|
|
820
|
+
isGitRepository: false,
|
|
821
|
+
trackingMode: 'filesystem',
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
return res.status(fallbackError.statusCode || 500).json({
|
|
826
|
+
error: fallbackError.message || 'Failed to read file',
|
|
827
|
+
isGitRepository: false,
|
|
828
|
+
trackingMode: 'filesystem',
|
|
829
|
+
});
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
766
833
|
console.error('Git file-with-diff error:', error);
|
|
767
834
|
res.json({ error: error.message });
|
|
768
835
|
}
|
|
@@ -32,9 +32,14 @@ const MAX_SSE_BUFFER_CHARS = 256_000;
|
|
|
32
32
|
const ACTIVITY_EDIT_THROTTLE_MS = 1200;
|
|
33
33
|
const ACTIVITY_HEARTBEAT_MS = 8000;
|
|
34
34
|
const INTENT_ROUTER_TIMEOUT_MS = 45_000;
|
|
35
|
+
const TERMINAL_BRIDGE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
36
|
+
const TERMINAL_BRIDGE_POLL_MS = 1500;
|
|
37
|
+
const TERMINAL_BRIDGE_EDIT_THROTTLE_MS = 3500;
|
|
38
|
+
const TERMINAL_BRIDGE_SETTLE_MS = 6000;
|
|
35
39
|
const callbackActions = new Map();
|
|
36
40
|
const runMonitors = new Map();
|
|
37
41
|
const activeLongTasks = new Map();
|
|
42
|
+
const terminalBridgeMonitors = new Map();
|
|
38
43
|
|
|
39
44
|
const MODEL_FALLBACKS = Object.fromEntries(
|
|
40
45
|
PROVIDERS.map((provider) => [provider, getStaticProviderModels(provider)]),
|
|
@@ -587,7 +592,7 @@ function terminalProjectLabel(terminal) {
|
|
|
587
592
|
return terminal?.projectLabel || terminal?.projectName || terminal?.projectPath || '-';
|
|
588
593
|
}
|
|
589
594
|
|
|
590
|
-
function terminalOutputUrl(terminal, maxChars = 3200) {
|
|
595
|
+
function terminalOutputUrl(terminal, maxChars = 3200, sinceCursor = null) {
|
|
591
596
|
const params = new URLSearchParams({
|
|
592
597
|
provider: terminal.provider,
|
|
593
598
|
projectPath: terminal.projectPath,
|
|
@@ -595,6 +600,7 @@ function terminalOutputUrl(terminal, maxChars = 3200) {
|
|
|
595
600
|
});
|
|
596
601
|
if (terminal.tabId) params.set('tabId', terminal.tabId);
|
|
597
602
|
if (terminal.sessionId) params.set('sessionId', terminal.sessionId);
|
|
603
|
+
if (Number.isFinite(sinceCursor)) params.set('sinceCursor', String(sinceCursor));
|
|
598
604
|
return `/api/shell/sessions/provider-output?${params.toString()}`;
|
|
599
605
|
}
|
|
600
606
|
|
|
@@ -621,6 +627,193 @@ function renderTerminalSnapshot(lang, terminal, data, { prefix = '', includeOutp
|
|
|
621
627
|
return truncate(lines.join('\n'), 3400);
|
|
622
628
|
}
|
|
623
629
|
|
|
630
|
+
function terminalBridgeMonitorKey(chatId, terminal) {
|
|
631
|
+
return [
|
|
632
|
+
chatId,
|
|
633
|
+
terminal.provider,
|
|
634
|
+
terminal.projectPath,
|
|
635
|
+
terminal.tabId || '-',
|
|
636
|
+
terminal.sessionId || '-',
|
|
637
|
+
].join(':');
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function normalizeBridgeLine(value) {
|
|
641
|
+
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function isNoisyTerminalBridgeLine(line, prompt) {
|
|
645
|
+
const normalized = normalizeBridgeLine(line);
|
|
646
|
+
if (!normalized || normalized.length <= 1) return true;
|
|
647
|
+
if (/^[╭╮╰╯│─═┌┐└┘├┤┬┴┼\s]+$/u.test(normalized)) return true;
|
|
648
|
+
if (/^[●✶✻✽✢✳⠂⠐⏵·\s0;:()\-|/\\]+$/u.test(normalized)) return true;
|
|
649
|
+
|
|
650
|
+
const lower = normalized.toLowerCase();
|
|
651
|
+
const promptNeedle = normalizeBridgeLine(prompt).toLowerCase();
|
|
652
|
+
if (promptNeedle && lower.includes(promptNeedle.slice(0, 120))) return true;
|
|
653
|
+
if (/^[›❯>]\s*/u.test(normalized)) return true;
|
|
654
|
+
if (lower.includes('welcome back')) return true;
|
|
655
|
+
if (lower.includes('api usage billing')) return true;
|
|
656
|
+
if (lower.includes('bypass permissions')) return true;
|
|
657
|
+
if (lower.includes('try "')) return true;
|
|
658
|
+
if (lower.includes('esc to interrupt') || lower.includes('press esc')) return true;
|
|
659
|
+
if (lower.includes('/effort')) return true;
|
|
660
|
+
if (lower.includes('determining')) return true;
|
|
661
|
+
if (/^(?:claude code|codex)\b/i.test(normalized) && normalized.length < 80) return true;
|
|
662
|
+
if (/^\(?\d+s\s*·\s*[↓↑]?\d*\s*tokens?\)?$/iu.test(normalized)) return true;
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
function cleanTerminalBridgeOutput(output, prompt) {
|
|
667
|
+
const text = String(output || '')
|
|
668
|
+
.replace(/\r/g, '\n')
|
|
669
|
+
.replace(/[\x00-\x08\x0B-\x1F\x7F]/g, '')
|
|
670
|
+
.replace(/\u00a0/g, ' ');
|
|
671
|
+
const lines = text
|
|
672
|
+
.split('\n')
|
|
673
|
+
.map((line) => line.replace(/[ \t]+/g, ' ').trim())
|
|
674
|
+
.filter((line) => !isNoisyTerminalBridgeLine(line, prompt));
|
|
675
|
+
const deduped = [];
|
|
676
|
+
for (const line of lines) {
|
|
677
|
+
if (deduped.at(-1) === line) continue;
|
|
678
|
+
deduped.push(line);
|
|
679
|
+
}
|
|
680
|
+
return deduped.join('\n').trim();
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function renderTerminalBridgeProgress(lang, terminal, {
|
|
684
|
+
output = '',
|
|
685
|
+
statusKey = 'control.terminalWaiting',
|
|
686
|
+
startedAt = Date.now(),
|
|
687
|
+
final = false,
|
|
688
|
+
terminalState = null,
|
|
689
|
+
} = {}) {
|
|
690
|
+
const lines = [
|
|
691
|
+
`${final ? '✅' : '⏳'} ${t(lang, statusKey)}`,
|
|
692
|
+
'',
|
|
693
|
+
`🤖 ${t(lang, 'control.activity.provider')}: ${terminal.provider}`,
|
|
694
|
+
`📁 ${t(lang, 'control.activity.project')}: ${compact(terminalProjectLabel(terminal), 90)}`,
|
|
695
|
+
];
|
|
696
|
+
if (terminal.sessionId) {
|
|
697
|
+
lines.push(`🧵 ${t(lang, 'control.activity.session')}: ${terminal.sessionId}`);
|
|
698
|
+
}
|
|
699
|
+
if (terminalState) {
|
|
700
|
+
lines.push(`📌 ${t(lang, 'control.activity.status')}: ${terminalState}`);
|
|
701
|
+
}
|
|
702
|
+
if (output) {
|
|
703
|
+
lines.push('', `💬 ${t(lang, 'control.activity.output')}:`);
|
|
704
|
+
lines.push(truncate(output, final ? 2800 : 2200));
|
|
705
|
+
} else {
|
|
706
|
+
lines.push('', t(lang, 'control.terminalWaitingHint'));
|
|
707
|
+
}
|
|
708
|
+
if (!final) {
|
|
709
|
+
lines.push('', `⏱ ${t(lang, 'control.activity.elapsed')}: ${formatElapsed(startedAt)}`);
|
|
710
|
+
}
|
|
711
|
+
return truncate(lines.join('\n'), 3400);
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
async function monitorTerminalBridgeResponse({
|
|
715
|
+
bot,
|
|
716
|
+
chatId,
|
|
717
|
+
link,
|
|
718
|
+
terminal,
|
|
719
|
+
prompt,
|
|
720
|
+
sinceCursor,
|
|
721
|
+
editMessageId,
|
|
722
|
+
monitorKey,
|
|
723
|
+
monitorToken,
|
|
724
|
+
}) {
|
|
725
|
+
const lang = languageFor(link);
|
|
726
|
+
const startedAt = Date.now();
|
|
727
|
+
let lastCleanOutput = '';
|
|
728
|
+
let lastOutputChangeAt = startedAt;
|
|
729
|
+
let lastEditAt = 0;
|
|
730
|
+
|
|
731
|
+
const isCurrent = () => terminalBridgeMonitors.get(monitorKey) === monitorToken;
|
|
732
|
+
|
|
733
|
+
try {
|
|
734
|
+
while (isCurrent() && Date.now() - startedAt < TERMINAL_BRIDGE_TIMEOUT_MS) {
|
|
735
|
+
await new Promise((resolve) => setTimeout(resolve, TERMINAL_BRIDGE_POLL_MS));
|
|
736
|
+
if (!isCurrent()) return;
|
|
737
|
+
|
|
738
|
+
const data = await localApi(
|
|
739
|
+
link.user_id,
|
|
740
|
+
terminalOutputUrl(terminal, 12000, sinceCursor),
|
|
741
|
+
{ timeoutMs: 12_000 },
|
|
742
|
+
);
|
|
743
|
+
if (data?.active === false) {
|
|
744
|
+
await send(bot, chatId, renderTerminalBridgeProgress(lang, terminal, {
|
|
745
|
+
output: lastCleanOutput,
|
|
746
|
+
statusKey: 'control.terminalNotRunning',
|
|
747
|
+
startedAt,
|
|
748
|
+
final: true,
|
|
749
|
+
terminalState: 'not running',
|
|
750
|
+
}), {
|
|
751
|
+
editMessageId,
|
|
752
|
+
parse_mode: undefined,
|
|
753
|
+
reply_markup: { inline_keyboard: terminalControlKeyboard(lang) },
|
|
754
|
+
});
|
|
755
|
+
return;
|
|
756
|
+
}
|
|
757
|
+
const cleanOutput = cleanTerminalBridgeOutput(data?.output, prompt);
|
|
758
|
+
const terminalState = data?.terminalState || data?.lifecycleState || 'unknown';
|
|
759
|
+
const now = Date.now();
|
|
760
|
+
|
|
761
|
+
if (cleanOutput && cleanOutput !== lastCleanOutput) {
|
|
762
|
+
lastCleanOutput = cleanOutput;
|
|
763
|
+
lastOutputChangeAt = now;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
const finishedByState = ['idle', 'completed', 'failed', 'exited'].includes(terminalState);
|
|
767
|
+
const finishedByQuietOutput = Boolean(lastCleanOutput) && now - lastOutputChangeAt >= TERMINAL_BRIDGE_SETTLE_MS;
|
|
768
|
+
const shouldFinish = Boolean(lastCleanOutput) && (finishedByState || finishedByQuietOutput);
|
|
769
|
+
|
|
770
|
+
if (shouldFinish) {
|
|
771
|
+
await send(bot, chatId, renderTerminalBridgeProgress(lang, terminal, {
|
|
772
|
+
output: lastCleanOutput,
|
|
773
|
+
statusKey: 'control.terminalResponseReady',
|
|
774
|
+
startedAt,
|
|
775
|
+
final: true,
|
|
776
|
+
terminalState,
|
|
777
|
+
}), {
|
|
778
|
+
editMessageId,
|
|
779
|
+
parse_mode: undefined,
|
|
780
|
+
reply_markup: { inline_keyboard: terminalControlKeyboard(lang) },
|
|
781
|
+
});
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
if (now - lastEditAt >= TERMINAL_BRIDGE_EDIT_THROTTLE_MS) {
|
|
786
|
+
lastEditAt = now;
|
|
787
|
+
await send(bot, chatId, renderTerminalBridgeProgress(lang, terminal, {
|
|
788
|
+
output: lastCleanOutput,
|
|
789
|
+
statusKey: lastCleanOutput ? 'control.terminalResponding' : 'control.terminalWaiting',
|
|
790
|
+
startedAt,
|
|
791
|
+
terminalState,
|
|
792
|
+
}), {
|
|
793
|
+
editMessageId,
|
|
794
|
+
parse_mode: undefined,
|
|
795
|
+
reply_markup: { inline_keyboard: terminalControlKeyboard(lang) },
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
if (!isCurrent()) return;
|
|
801
|
+
await send(bot, chatId, renderTerminalBridgeProgress(lang, terminal, {
|
|
802
|
+
output: lastCleanOutput,
|
|
803
|
+
statusKey: lastCleanOutput ? 'control.terminalStillRunning' : 'control.terminalNoReadableOutput',
|
|
804
|
+
startedAt,
|
|
805
|
+
final: Boolean(lastCleanOutput),
|
|
806
|
+
terminalState: 'running',
|
|
807
|
+
}), {
|
|
808
|
+
editMessageId,
|
|
809
|
+
parse_mode: undefined,
|
|
810
|
+
reply_markup: { inline_keyboard: terminalControlKeyboard(lang) },
|
|
811
|
+
});
|
|
812
|
+
} finally {
|
|
813
|
+
if (isCurrent()) terminalBridgeMonitors.delete(monitorKey);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
624
817
|
export async function sendActiveTerminalAttachedNotice({ bot, chatId, link, terminal }) {
|
|
625
818
|
const lang = languageFor(link);
|
|
626
819
|
const lines = [
|
|
@@ -695,7 +888,7 @@ async function sendToActiveTerminal({ bot, chatId, link, text }) {
|
|
|
695
888
|
const editMessageId = sent?.message_id || sent?.message?.message_id || null;
|
|
696
889
|
|
|
697
890
|
try {
|
|
698
|
-
await localApi(link.user_id, '/api/shell/sessions/provider-input', {
|
|
891
|
+
const inputResult = await localApi(link.user_id, '/api/shell/sessions/provider-input', {
|
|
699
892
|
method: 'POST',
|
|
700
893
|
timeoutMs: 15_000,
|
|
701
894
|
body: {
|
|
@@ -705,17 +898,38 @@ async function sendToActiveTerminal({ bot, chatId, link, text }) {
|
|
|
705
898
|
sessionId: terminal.sessionId,
|
|
706
899
|
input: text,
|
|
707
900
|
submit: true,
|
|
901
|
+
submitMode: 'deferred-enter',
|
|
708
902
|
},
|
|
709
903
|
});
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
904
|
+
const sinceCursor = Number.isFinite(inputResult?.outputCursorBefore)
|
|
905
|
+
? inputResult.outputCursorBefore
|
|
906
|
+
: null;
|
|
907
|
+
const monitorKey = terminalBridgeMonitorKey(chatId, terminal);
|
|
908
|
+
const monitorToken = crypto.randomUUID();
|
|
909
|
+
terminalBridgeMonitors.set(monitorKey, monitorToken);
|
|
910
|
+
|
|
911
|
+
await send(bot, chatId, renderTerminalBridgeProgress(lang, terminal, {
|
|
912
|
+
statusKey: 'control.terminalWaiting',
|
|
913
|
+
startedAt: Date.now(),
|
|
914
|
+
terminalState: 'running',
|
|
714
915
|
}), {
|
|
715
916
|
editMessageId,
|
|
716
917
|
parse_mode: undefined,
|
|
717
918
|
reply_markup: { inline_keyboard: terminalControlKeyboard(lang) },
|
|
718
919
|
});
|
|
920
|
+
monitorTerminalBridgeResponse({
|
|
921
|
+
bot,
|
|
922
|
+
chatId,
|
|
923
|
+
link,
|
|
924
|
+
terminal,
|
|
925
|
+
prompt: text,
|
|
926
|
+
sinceCursor,
|
|
927
|
+
editMessageId,
|
|
928
|
+
monitorKey,
|
|
929
|
+
monitorToken,
|
|
930
|
+
}).catch((error) => {
|
|
931
|
+
console.warn('[telegram-control] terminal bridge monitor failed:', error?.message || error);
|
|
932
|
+
});
|
|
719
933
|
} catch (error) {
|
|
720
934
|
await send(bot, chatId, t(lang, 'control.terminalSendFailed', {
|
|
721
935
|
error: error?.message || String(error),
|
|
@@ -86,6 +86,13 @@ const EN = {
|
|
|
86
86
|
'control.terminalOutputHidden': 'Live CLI screen output is not dumped into Telegram because full-screen terminal UIs are noisy. Keep sending messages here, or open Pixcode for the live terminal.',
|
|
87
87
|
'control.terminalSending': '📲 Sending to {{provider}} terminal on {{project}}...',
|
|
88
88
|
'control.terminalSent': '📲 Sent to the active terminal.',
|
|
89
|
+
'control.terminalSentHint': 'I will not dump the live terminal screen here because full-screen CLI UIs are noisy. Keep sending messages here, or open Pixcode for the live terminal.',
|
|
90
|
+
'control.terminalWaiting': 'Message sent to the active terminal. Waiting for the CLI response...',
|
|
91
|
+
'control.terminalWaitingHint': 'The same web CLI session is still attached. I will edit this message when readable output arrives.',
|
|
92
|
+
'control.terminalResponding': 'CLI response is streaming.',
|
|
93
|
+
'control.terminalResponseReady': 'CLI response is ready.',
|
|
94
|
+
'control.terminalStillRunning': 'The CLI is still running. I am leaving this Telegram bridge attached.',
|
|
95
|
+
'control.terminalNoReadableOutput': 'The message was sent, but I could not extract a clean Telegram reply yet.',
|
|
89
96
|
'control.terminalSendFailed': 'Could not send to the active terminal: {{error}}',
|
|
90
97
|
'control.terminalStatusFailed': 'Could not read the active terminal: {{error}}',
|
|
91
98
|
'control.terminalDetached': 'Telegram is detached from the web CLI session. Plain messages will use the normal AI router again.',
|
|
@@ -246,6 +253,13 @@ const TR = {
|
|
|
246
253
|
'control.terminalOutputHidden': 'Canlı CLI ekran çıktısı Telegrama dökülmez; tam ekran terminal arayüzleri burada gürültülü görünür. Buradan mesaj göndermeye devam et veya canlı terminal için Pixcodeu aç.',
|
|
247
254
|
'control.terminalSending': '📲 {{project}} üzerinde {{provider}} terminaline gönderiliyor...',
|
|
248
255
|
'control.terminalSent': '📲 Aktif terminale gönderildi.',
|
|
256
|
+
'control.terminalSentHint': 'Canlı terminal ekranını buraya dökmeyeceğim; tam ekran CLI arayüzleri Telegramda bozuk görünebilir. Buradan mesaj göndermeye devam et veya canlı terminal için Pixcodeu aç.',
|
|
257
|
+
'control.terminalWaiting': 'Mesaj aktif terminale gönderildi. CLI yanıtı bekleniyor...',
|
|
258
|
+
'control.terminalWaitingHint': 'Aynı web CLI oturumu bağlı kalıyor. Okunabilir çıktı gelince bu mesajı düzenleyeceğim.',
|
|
259
|
+
'control.terminalResponding': 'CLI yanıtı geliyor.',
|
|
260
|
+
'control.terminalResponseReady': 'CLI yanıtı hazır.',
|
|
261
|
+
'control.terminalStillRunning': 'CLI hâlâ çalışıyor. Telegram köprüsünü bağlı bırakıyorum.',
|
|
262
|
+
'control.terminalNoReadableOutput': 'Mesaj gönderildi ama henüz temiz bir Telegram yanıtı çıkaramadım.',
|
|
249
263
|
'control.terminalSendFailed': 'Aktif terminale gönderilemedi: {{error}}',
|
|
250
264
|
'control.terminalStatusFailed': 'Aktif terminal okunamadı: {{error}}',
|
|
251
265
|
'control.terminalDetached': 'Telegram web CLI oturumundan ayrıldı. Düz mesajlar tekrar normal AI router akışına gidecek.',
|