@pixelbyte-software/pixcode 1.50.7 → 1.50.9
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-CR4j4iu_.js → index-CXTCtUku.js} +149 -149
- package/dist/assets/localMonaco-DFIbtDNp.js +1 -0
- package/dist/index.html +1 -1
- package/dist-server/server/index.js +121 -14
- package/dist-server/server/index.js.map +1 -1
- package/package.json +2 -2
- package/scripts/hermes/pixcode-mcp-server.mjs +35 -14
- package/scripts/smoke/code-editor-vscode-engine.mjs +31 -0
- package/scripts/smoke/hermes-mcp-pixcode-roundtrip.mjs +22 -1
- package/scripts/smoke/hermes-settings-commands.mjs +45 -0
- package/server/index.js +134 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pixelbyte-software/pixcode",
|
|
3
|
-
"version": "1.50.
|
|
3
|
+
"version": "1.50.9",
|
|
4
4
|
"description": "Self-hosted AI coding agent control room for Claude Code, Cursor CLI, OpenAI Codex, Gemini CLI, Qwen Code, and OpenCode with chat, files, shell, Git, orchestration, API keys, Telegram, MCP, plugins, themes, and desktop/server deployment.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist-server/server/index.js",
|
|
@@ -124,6 +124,7 @@
|
|
|
124
124
|
"gray-matter": "^4.0.3",
|
|
125
125
|
"jsonwebtoken": "^9.0.2",
|
|
126
126
|
"mime-types": "^3.0.1",
|
|
127
|
+
"monaco-editor": "^0.55.1",
|
|
127
128
|
"multer": "^2.0.1",
|
|
128
129
|
"node-fetch": "^2.7.0",
|
|
129
130
|
"node-pty": "^1.2.0-beta.12",
|
|
@@ -186,7 +187,6 @@
|
|
|
186
187
|
"katex": "^0.16.25",
|
|
187
188
|
"lint-staged": "^16.3.2",
|
|
188
189
|
"lucide-react": "^0.515.0",
|
|
189
|
-
"monaco-editor": "^0.55.1",
|
|
190
190
|
"node-gyp": "^12.0.0",
|
|
191
191
|
"postcss": "^8.4.32",
|
|
192
192
|
"qrcode": "^1.5.4",
|
|
@@ -9,6 +9,7 @@ const appRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..',
|
|
|
9
9
|
const mcpServerPath = path.join(appRoot, 'scripts', 'hermes', 'pixcode-mcp-server.mjs');
|
|
10
10
|
const READBACK_IDLE_STABLE_MS = 2500;
|
|
11
11
|
const DEFAULT_STARTUP_WAIT_MS = 100000;
|
|
12
|
+
const CODEX_PROMPT_INPUT_PENDING_REASON = 'codex_prompt_input_pending';
|
|
12
13
|
|
|
13
14
|
const tools = [
|
|
14
15
|
{
|
|
@@ -140,7 +141,7 @@ const tools = [
|
|
|
140
141
|
},
|
|
141
142
|
startIfNeeded: {
|
|
142
143
|
type: 'boolean',
|
|
143
|
-
description: 'When
|
|
144
|
+
description: 'When false, only probe an already-running gateway. Defaults to true so Pixcode keeps Hermes REST ready.',
|
|
144
145
|
},
|
|
145
146
|
},
|
|
146
147
|
additionalProperties: false,
|
|
@@ -218,13 +219,29 @@ function getLastMatchIndex(text, pattern) {
|
|
|
218
219
|
return lastIndex;
|
|
219
220
|
}
|
|
220
221
|
|
|
221
|
-
function
|
|
222
|
+
function normalizePromptInput(value) {
|
|
223
|
+
return String(value || '').replace(/(?:\r\n|\r|\n)+$/u, '').trim();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function hasCodexPromptInputPending(output, expectedInput) {
|
|
227
|
+
const expected = normalizePromptInput(expectedInput);
|
|
228
|
+
if (!expected) return false;
|
|
229
|
+
const match = String(output || '').match(/(?:^|\n)[^\S\r\n]*[›❯][ \t]+([^\r\n]+)[\r\n]*$/u);
|
|
230
|
+
if (!match) return false;
|
|
231
|
+
return normalizePromptInput(match[1]) === expected;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function inferTerminalState(provider, terminalOutput, expectedInput = null) {
|
|
222
235
|
if (!terminalOutput) return 'unknown';
|
|
236
|
+
const output = String(terminalOutput.output || '');
|
|
237
|
+
if (provider === 'codex' && hasCodexPromptInputPending(output, expectedInput)) {
|
|
238
|
+
terminalOutput.terminalStateReason = terminalOutput.terminalStateReason || CODEX_PROMPT_INPUT_PENDING_REASON;
|
|
239
|
+
return 'busy';
|
|
240
|
+
}
|
|
223
241
|
if (typeof terminalOutput.terminalState === 'string') return terminalOutput.terminalState;
|
|
224
242
|
if (typeof terminalOutput.isBusy === 'boolean') return terminalOutput.isBusy ? 'busy' : 'idle';
|
|
225
243
|
if (terminalOutput.active === false) return terminalOutput.output ? 'idle' : 'unknown';
|
|
226
244
|
|
|
227
|
-
const output = String(terminalOutput.output || '');
|
|
228
245
|
if (!output.trim()) return 'unknown';
|
|
229
246
|
if (/Process exited with code/iu.test(output)) return 'idle';
|
|
230
247
|
|
|
@@ -240,6 +257,10 @@ function inferTerminalState(provider, terminalOutput) {
|
|
|
240
257
|
getLastMatchIndex(output, /(?:^|\n)\s*›(?:\s|$)/gu),
|
|
241
258
|
getLastMatchIndex(output, /(?:^|\n)\s*❯(?:\s|$)/gu),
|
|
242
259
|
);
|
|
260
|
+
if (hasCodexPromptInputPending(output, expectedInput)) {
|
|
261
|
+
terminalOutput.terminalStateReason = terminalOutput.terminalStateReason || CODEX_PROMPT_INPUT_PENDING_REASON;
|
|
262
|
+
return 'busy';
|
|
263
|
+
}
|
|
243
264
|
if (lastPrompt >= 0) return lastStrongBusy > lastPrompt ? 'busy' : 'idle';
|
|
244
265
|
if (lastBusy >= 0) return 'busy';
|
|
245
266
|
return 'unknown';
|
|
@@ -249,13 +270,13 @@ function inferTerminalState(provider, terminalOutput) {
|
|
|
249
270
|
return 'unknown';
|
|
250
271
|
}
|
|
251
272
|
|
|
252
|
-
function isTerminalReadbackFinal(provider, terminalOutput) {
|
|
253
|
-
const terminalState = inferTerminalState(provider, terminalOutput);
|
|
273
|
+
function isTerminalReadbackFinal(provider, terminalOutput, expectedInput = null) {
|
|
274
|
+
const terminalState = inferTerminalState(provider, terminalOutput, expectedInput);
|
|
254
275
|
return terminalState === 'idle' || terminalState === 'completed' || terminalState === 'exited' || terminalState === 'failed';
|
|
255
276
|
}
|
|
256
277
|
|
|
257
|
-
function isTerminalReadbackHardFinal(provider, terminalOutput) {
|
|
258
|
-
const terminalState = inferTerminalState(provider, terminalOutput);
|
|
278
|
+
function isTerminalReadbackHardFinal(provider, terminalOutput, expectedInput = null) {
|
|
279
|
+
const terminalState = inferTerminalState(provider, terminalOutput, expectedInput);
|
|
259
280
|
return terminalState === 'completed' || terminalState === 'exited' || terminalState === 'failed' || Boolean(terminalOutput?.terminalFailed);
|
|
260
281
|
}
|
|
261
282
|
|
|
@@ -269,7 +290,7 @@ function getReadbackFingerprint(terminalOutput) {
|
|
|
269
290
|
].join('\n---pixcode-readback---\n');
|
|
270
291
|
}
|
|
271
292
|
|
|
272
|
-
async function waitForProviderTerminalOutput(provider, projectPath, waitMs, launchId = null) {
|
|
293
|
+
async function waitForProviderTerminalOutput(provider, projectPath, waitMs, launchId = null, expectedInput = null) {
|
|
273
294
|
const startedAt = Date.now();
|
|
274
295
|
let latestOutput = null;
|
|
275
296
|
let stableFingerprint = null;
|
|
@@ -285,8 +306,8 @@ async function waitForProviderTerminalOutput(provider, projectPath, waitMs, laun
|
|
|
285
306
|
error: error instanceof Error ? error.message : String(error),
|
|
286
307
|
}));
|
|
287
308
|
|
|
288
|
-
if (latestOutput?.output && isTerminalReadbackFinal(provider, latestOutput)) {
|
|
289
|
-
if (isTerminalReadbackHardFinal(provider, latestOutput)) {
|
|
309
|
+
if (latestOutput?.output && isTerminalReadbackFinal(provider, latestOutput, expectedInput)) {
|
|
310
|
+
if (isTerminalReadbackHardFinal(provider, latestOutput, expectedInput)) {
|
|
290
311
|
stableFinal = true;
|
|
291
312
|
break;
|
|
292
313
|
}
|
|
@@ -307,7 +328,7 @@ async function waitForProviderTerminalOutput(provider, projectPath, waitMs, laun
|
|
|
307
328
|
} while (Date.now() - startedAt < waitMs);
|
|
308
329
|
|
|
309
330
|
if (latestOutput && !latestOutput.terminalState) {
|
|
310
|
-
latestOutput.terminalState = inferTerminalState(provider, latestOutput);
|
|
331
|
+
latestOutput.terminalState = inferTerminalState(provider, latestOutput, expectedInput);
|
|
311
332
|
}
|
|
312
333
|
if (latestOutput && typeof latestOutput.isBusy !== 'boolean') {
|
|
313
334
|
latestOutput.isBusy = latestOutput.terminalState === 'busy';
|
|
@@ -436,10 +457,10 @@ async function callTool(name, args = {}) {
|
|
|
436
457
|
const requestedWaitMs = Number(args.waitForCompletionMs ?? args.waitForOutputMs ?? defaultWaitMs);
|
|
437
458
|
const waitForOutputMs = Math.min(600000, Math.max(0, requestedWaitMs));
|
|
438
459
|
if (waitForOutputMs > 0) {
|
|
439
|
-
terminalOutput = await waitForProviderTerminalOutput(provider, projectPath, waitForOutputMs, launchId);
|
|
460
|
+
terminalOutput = await waitForProviderTerminalOutput(provider, projectPath, waitForOutputMs, launchId, startupInput);
|
|
440
461
|
}
|
|
441
462
|
const terminalOutputFinal = terminalOutput
|
|
442
|
-
? Boolean(terminalOutput.terminalOutputFinal ?? isTerminalReadbackFinal(provider, terminalOutput))
|
|
463
|
+
? Boolean(terminalOutput.terminalOutputFinal ?? isTerminalReadbackFinal(provider, terminalOutput, startupInput))
|
|
443
464
|
: false;
|
|
444
465
|
return textResult(JSON.stringify({
|
|
445
466
|
launched: true,
|
|
@@ -495,7 +516,7 @@ async function callTool(name, args = {}) {
|
|
|
495
516
|
body: JSON.stringify({
|
|
496
517
|
projectPath: args.projectPath || null,
|
|
497
518
|
input: args.input || null,
|
|
498
|
-
startIfNeeded: args.startIfNeeded
|
|
519
|
+
startIfNeeded: args.startIfNeeded !== false,
|
|
499
520
|
}),
|
|
500
521
|
});
|
|
501
522
|
return textResult(JSON.stringify(body, null, 2));
|
|
@@ -6,6 +6,7 @@ const read = (path) => fs.readFileSync(path, 'utf8');
|
|
|
6
6
|
const packageJson = JSON.parse(read('package.json'));
|
|
7
7
|
const surface = read('src/components/code-editor/view/subcomponents/CodeEditorSurface.tsx');
|
|
8
8
|
const editor = read('src/components/code-editor/view/CodeEditor.tsx');
|
|
9
|
+
const localMonaco = read('src/components/code-editor/utils/localMonaco.ts');
|
|
9
10
|
|
|
10
11
|
const allDeps = {
|
|
11
12
|
...(packageJson.dependencies ?? {}),
|
|
@@ -21,6 +22,36 @@ assert.match(
|
|
|
21
22
|
'Normal file editing should lazy-load Monaco instead of keeping a CodeMirror-only surface.',
|
|
22
23
|
);
|
|
23
24
|
|
|
25
|
+
assert.match(
|
|
26
|
+
surface,
|
|
27
|
+
/ensureLocalMonaco\(\)/,
|
|
28
|
+
'Monaco should be configured before mount so Linux/self-hosted installs do not depend on the CDN loader.',
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
assert.match(
|
|
32
|
+
localMonaco,
|
|
33
|
+
/LOCAL_MONACO_BASE_PATH\s*=\s*['"]\/vendor\/monaco-editor\/min\/vs['"]/,
|
|
34
|
+
"Code editor should load Monaco from Pixcode's same-origin vendor route.",
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
assert.match(
|
|
38
|
+
localMonaco,
|
|
39
|
+
/loader\.config\(\{\s*paths:\s*\{\s*vs:\s*LOCAL_MONACO_BASE_PATH/s,
|
|
40
|
+
'Code editor should point @monaco-editor/react at the local Monaco loader path.',
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
assert.doesNotMatch(
|
|
44
|
+
localMonaco,
|
|
45
|
+
/https:\/\/cdn\.jsdelivr\.net|unpkg\.com|from ['"]monaco-editor/,
|
|
46
|
+
'Local Monaco setup should not use a remote CDN or import the full monaco-editor barrel.',
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
assert.match(
|
|
50
|
+
read('server/index.js'),
|
|
51
|
+
/\/vendor\/monaco-editor\/min\/vs[\s\S]*express\.static/,
|
|
52
|
+
'Server should serve Monaco loader and workers from the same-origin vendor route.',
|
|
53
|
+
);
|
|
54
|
+
|
|
24
55
|
assert.match(
|
|
25
56
|
surface,
|
|
26
57
|
/onMount=\{handleMonacoMount\}/,
|
|
@@ -127,7 +127,28 @@ const server = createServer(async (req, res) => {
|
|
|
127
127
|
provider: 'codex',
|
|
128
128
|
projectPath: '/root/pixcode',
|
|
129
129
|
terminalState: 'idle',
|
|
130
|
-
output: 'OpenAI Codex\n› /init\n
|
|
130
|
+
output: 'OpenAI Codex\n› /init\n',
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
active: true,
|
|
134
|
+
provider: 'codex',
|
|
135
|
+
projectPath: '/root/pixcode',
|
|
136
|
+
terminalState: 'idle',
|
|
137
|
+
output: 'OpenAI Codex\n› /init\n',
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
active: true,
|
|
141
|
+
provider: 'codex',
|
|
142
|
+
projectPath: '/root/pixcode',
|
|
143
|
+
terminalState: 'idle',
|
|
144
|
+
output: 'OpenAI Codex\n› /init\n',
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
active: true,
|
|
148
|
+
provider: 'codex',
|
|
149
|
+
projectPath: '/root/pixcode',
|
|
150
|
+
terminalState: 'idle',
|
|
151
|
+
output: 'OpenAI Codex\n› /init\n',
|
|
131
152
|
},
|
|
132
153
|
{
|
|
133
154
|
active: true,
|
|
@@ -34,6 +34,16 @@ assert.match(
|
|
|
34
34
|
/pixcode:hermes-terminal[\s\S]+command[\s\S]+title/,
|
|
35
35
|
'Hermes settings should dispatch command and title to the workbench terminal.',
|
|
36
36
|
);
|
|
37
|
+
assert.match(
|
|
38
|
+
settingsTab,
|
|
39
|
+
/ensureGatewayReady/,
|
|
40
|
+
'Hermes settings should automatically start/probe the REST API gateway when Hermes is installed.',
|
|
41
|
+
);
|
|
42
|
+
assert.match(
|
|
43
|
+
settingsTab,
|
|
44
|
+
/startIfNeeded:\s*true/,
|
|
45
|
+
'Hermes settings should start the REST API gateway through Pixcode when checking gateway readiness.',
|
|
46
|
+
);
|
|
37
47
|
assert.match(
|
|
38
48
|
settingsModal,
|
|
39
49
|
/<HermesSettingsTab onClose=\{onClose\} \/>/,
|
|
@@ -74,6 +84,11 @@ assert.match(
|
|
|
74
84
|
/pixcode_read_cli_terminal/,
|
|
75
85
|
'Pixcode MCP should expose a terminal transcript reader so Hermes can report provider CLI output.',
|
|
76
86
|
);
|
|
87
|
+
assert.match(
|
|
88
|
+
pixcodeMcpServer,
|
|
89
|
+
/startIfNeeded:\s*args\.startIfNeeded !== false/,
|
|
90
|
+
'Pixcode MCP gateway probes should start the managed REST gateway by default unless Hermes explicitly opts out.',
|
|
91
|
+
);
|
|
77
92
|
assert.match(
|
|
78
93
|
pixcodeMcpServer,
|
|
79
94
|
/Use this instead of Hermes shell\/proc\/skill execution/,
|
|
@@ -94,6 +109,11 @@ assert.match(
|
|
|
94
109
|
/lastStrongBusy[\s\S]+lastPrompt[\s\S]+\?\s*'busy'\s*:\s*'idle'/,
|
|
95
110
|
'Codex readback should ignore weak spinner remnants once the prompt has returned.',
|
|
96
111
|
);
|
|
112
|
+
assert.match(
|
|
113
|
+
pixcodeMcpServer,
|
|
114
|
+
/codex_prompt_input_pending/,
|
|
115
|
+
'Codex readback should not treat a prompt line with typed-but-unsubmitted input as final output.',
|
|
116
|
+
);
|
|
97
117
|
assert.match(
|
|
98
118
|
serverIndex,
|
|
99
119
|
/lastStrongBusy[\s\S]+lastPrompt[\s\S]+\?\s*'busy'\s*:\s*'idle'/,
|
|
@@ -109,6 +129,11 @@ assert.match(
|
|
|
109
129
|
/requestedLaunchId[\s\S]+session\.hermesLaunchId === requestedLaunchId/,
|
|
110
130
|
'Provider output API should filter by Hermes terminal launch id when supplied.',
|
|
111
131
|
);
|
|
132
|
+
assert.match(
|
|
133
|
+
serverIndex,
|
|
134
|
+
/existingSession\.hermesLaunchId = hermesLaunchId \|\| existingSession\.hermesLaunchId/,
|
|
135
|
+
'Reused visible provider PTYs should be rebound to the latest Hermes launch id so MCP readback follows the current request.',
|
|
136
|
+
);
|
|
112
137
|
assert.match(
|
|
113
138
|
serverIndex,
|
|
114
139
|
/lifecycleState/,
|
|
@@ -244,6 +269,26 @@ assert.match(
|
|
|
244
269
|
/writeTerminalStartupInput/,
|
|
245
270
|
'Shell backend should submit Hermes startup input directly into reused visible PTYs.',
|
|
246
271
|
);
|
|
272
|
+
assert.match(
|
|
273
|
+
serverIndex,
|
|
274
|
+
/queueTerminalStartupInput/,
|
|
275
|
+
'Shell backend should queue Hermes startup input until the visible provider terminal is ready instead of writing blindly after a fixed delay.',
|
|
276
|
+
);
|
|
277
|
+
assert.match(
|
|
278
|
+
serverIndex,
|
|
279
|
+
/STARTUP_INPUT_READY_TIMEOUT_MS/,
|
|
280
|
+
'Queued provider startup input should have a bounded readiness timeout.',
|
|
281
|
+
);
|
|
282
|
+
assert.match(
|
|
283
|
+
serverIndex,
|
|
284
|
+
/resolveProviderTerminalState[\s\S]+terminalState === 'busy'/,
|
|
285
|
+
'Queued startup input should inspect the provider terminal state and avoid sending while the CLI is busy.',
|
|
286
|
+
);
|
|
287
|
+
assert.match(
|
|
288
|
+
serverIndex,
|
|
289
|
+
/\\x15/,
|
|
290
|
+
'Provider startup input should clear any half-typed prompt line before typing the requested work.',
|
|
291
|
+
);
|
|
247
292
|
assert.match(
|
|
248
293
|
serverIndex,
|
|
249
294
|
/startupInputDelivery === 'terminal'[\s\S]+writeTerminalStartupInput/,
|
package/server/index.js
CHANGED
|
@@ -7,6 +7,7 @@ import path from 'path';
|
|
|
7
7
|
import os from 'os';
|
|
8
8
|
import http from 'http';
|
|
9
9
|
import net from 'node:net';
|
|
10
|
+
import { createRequire } from 'node:module';
|
|
10
11
|
import { spawn } from 'child_process';
|
|
11
12
|
|
|
12
13
|
import express from 'express';
|
|
@@ -23,6 +24,8 @@ const __dirname = getModuleDir(import.meta.url);
|
|
|
23
24
|
// The server source runs from /server, while the compiled output runs from /dist-server/server.
|
|
24
25
|
// Resolving the app root once keeps every repo-level lookup below aligned across both layouts.
|
|
25
26
|
const APP_ROOT = findAppRoot(__dirname);
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
28
|
+
const MONACO_ASSETS_ROUTE = '/vendor/monaco-editor/min/vs';
|
|
26
29
|
const installMode = fs.existsSync(path.join(APP_ROOT, '.git')) ? 'git' : 'npm';
|
|
27
30
|
const SERVER_VERSION = (() => {
|
|
28
31
|
try {
|
|
@@ -38,6 +41,23 @@ const DAEMON_COMMAND_CONTEXT = {
|
|
|
38
41
|
nodeExecPath: process.execPath,
|
|
39
42
|
};
|
|
40
43
|
|
|
44
|
+
function resolveMonacoAssetsPath() {
|
|
45
|
+
const candidates = [
|
|
46
|
+
path.join(APP_ROOT, 'node_modules', 'monaco-editor', 'min', 'vs'),
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const monacoPackagePath = require.resolve('monaco-editor/package.json', {
|
|
51
|
+
paths: [APP_ROOT, __dirname],
|
|
52
|
+
});
|
|
53
|
+
candidates.push(path.join(path.dirname(monacoPackagePath), 'min', 'vs'));
|
|
54
|
+
} catch {
|
|
55
|
+
// The editor will show its normal load failure if the dependency is unavailable.
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return candidates.find((candidate) => fs.existsSync(path.join(candidate, 'loader.js'))) || null;
|
|
59
|
+
}
|
|
60
|
+
|
|
41
61
|
import { c } from './utils/colors.js';
|
|
42
62
|
|
|
43
63
|
console.log('SERVER_PORT from env:', process.env.SERVER_PORT);
|
|
@@ -153,6 +173,8 @@ let projectsWatchers = [];
|
|
|
153
173
|
let projectsWatcherDebounceTimer = null;
|
|
154
174
|
const connectedClients = new Set();
|
|
155
175
|
let isGetProjectsRunning = false; // Flag to prevent reentrant calls
|
|
176
|
+
const STARTUP_INPUT_READY_TIMEOUT_MS = 10 * 60 * 1000;
|
|
177
|
+
const STARTUP_INPUT_POLL_MS = 750;
|
|
156
178
|
|
|
157
179
|
// Broadcast progress to all connected WebSocket clients
|
|
158
180
|
function broadcastProgress(progress) {
|
|
@@ -298,6 +320,10 @@ function terminatePtySession(sessionKey, session, reason) {
|
|
|
298
320
|
if (session.timeoutId) {
|
|
299
321
|
clearTimeout(session.timeoutId);
|
|
300
322
|
}
|
|
323
|
+
if (session.startupInputTimerId) {
|
|
324
|
+
clearTimeout(session.startupInputTimerId);
|
|
325
|
+
session.startupInputTimerId = null;
|
|
326
|
+
}
|
|
301
327
|
|
|
302
328
|
try {
|
|
303
329
|
if (session.pty && session.pty.kill) {
|
|
@@ -436,23 +462,97 @@ function appendPtySessionBuffer(session, data) {
|
|
|
436
462
|
}
|
|
437
463
|
|
|
438
464
|
function normalizeTerminalStartupInput(input) {
|
|
439
|
-
return
|
|
465
|
+
return `\x15${String(input || '').replace(/(?:\r\n|\r|\n)+$/u, '')}\r`;
|
|
440
466
|
}
|
|
441
467
|
|
|
442
|
-
function
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
468
|
+
function readSessionOutputForState(session, maxChars = 12000) {
|
|
469
|
+
return stripAnsiSequences((session?.buffer || []).join('').slice(-maxChars));
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function shouldWaitForProviderIdle(provider) {
|
|
473
|
+
return provider === 'codex';
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function isTerminalReadyForStartupInput(session) {
|
|
477
|
+
if (!session?.pty || session.lifecycleState !== 'running') {
|
|
478
|
+
return { ready: false, retry: false, terminalState: 'exited' };
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const output = readSessionOutputForState(session);
|
|
482
|
+
const state = resolveProviderTerminalState(session, session.provider, output);
|
|
483
|
+
if (state.terminalState === 'busy') {
|
|
484
|
+
return { ready: false, retry: true, terminalState: state.terminalState };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (state.terminalState === 'idle') {
|
|
488
|
+
return { ready: true, retry: false, terminalState: state.terminalState };
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (shouldWaitForProviderIdle(session.provider)) {
|
|
492
|
+
return { ready: false, retry: true, terminalState: state.terminalState };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return { ready: true, retry: false, terminalState: state.terminalState };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function processTerminalStartupInputQueue(session) {
|
|
499
|
+
if (!session?.pendingStartupInputs?.length) {
|
|
500
|
+
session.startupInputTimerId = null;
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const item = session.pendingStartupInputs[0];
|
|
505
|
+
const readiness = isTerminalReadyForStartupInput(session);
|
|
506
|
+
if (!readiness.ready) {
|
|
507
|
+
if (!readiness.retry || Date.now() - item.queuedAt > STARTUP_INPUT_READY_TIMEOUT_MS) {
|
|
508
|
+
session.pendingStartupInputs.shift();
|
|
509
|
+
session.startupInputTimerId = null;
|
|
510
|
+
const message = `\r\n\x1b[33m[Pixcode] Startup input was not sent because ${session.provider} is still ${readiness.terminalState || 'unavailable'}.\x1b[0m\r\n`;
|
|
511
|
+
try {
|
|
512
|
+
session.ws?.send?.(JSON.stringify({ type: 'output', data: message }));
|
|
513
|
+
} catch { /* websocket gone */ }
|
|
514
|
+
if (session.pendingStartupInputs.length > 0) {
|
|
515
|
+
session.startupInputTimerId = setTimeout(() => processTerminalStartupInputQueue(session), STARTUP_INPUT_POLL_MS);
|
|
451
516
|
}
|
|
452
|
-
|
|
453
|
-
console.warn('Failed to submit startup input to visible PTY:', error?.message || error);
|
|
517
|
+
return;
|
|
454
518
|
}
|
|
455
|
-
|
|
519
|
+
|
|
520
|
+
session.startupInputTimerId = setTimeout(() => processTerminalStartupInputQueue(session), STARTUP_INPUT_POLL_MS);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
session.pendingStartupInputs.shift();
|
|
525
|
+
session.startupInputTimerId = null;
|
|
526
|
+
try {
|
|
527
|
+
session.pty.write(normalizeTerminalStartupInput(item.startupInput));
|
|
528
|
+
session.updatedAt = Date.now();
|
|
529
|
+
console.log(`⌨️ Submitted startup input to visible PTY (${item.reason})`);
|
|
530
|
+
} catch (error) {
|
|
531
|
+
console.warn('Failed to submit startup input to visible PTY:', error?.message || error);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (session.pendingStartupInputs.length > 0) {
|
|
535
|
+
session.startupInputTimerId = setTimeout(() => processTerminalStartupInputQueue(session), STARTUP_INPUT_POLL_MS);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
function queueTerminalStartupInput(session, startupInput, reason, delayMs = 500) {
|
|
540
|
+
if (!session?.pty || !startupInput) return;
|
|
541
|
+
if (!Array.isArray(session.pendingStartupInputs)) {
|
|
542
|
+
session.pendingStartupInputs = [];
|
|
543
|
+
}
|
|
544
|
+
session.pendingStartupInputs.push({
|
|
545
|
+
startupInput,
|
|
546
|
+
reason,
|
|
547
|
+
queuedAt: Date.now(),
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
if (session.startupInputTimerId) return;
|
|
551
|
+
session.startupInputTimerId = setTimeout(() => processTerminalStartupInputQueue(session), delayMs);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function writeTerminalStartupInput(session, startupInput, reason, delayMs = 500) {
|
|
555
|
+
queueTerminalStartupInput(session, startupInput, reason, delayMs);
|
|
456
556
|
}
|
|
457
557
|
|
|
458
558
|
function normalizeShellPermissionMode(value) {
|
|
@@ -808,6 +908,18 @@ app.use('/api/agent', agentRoutes);
|
|
|
808
908
|
// Static app files served after API routes. Keep dist before public so
|
|
809
909
|
// / and /index.html always resolve to the Pixcode app, not the GitHub Pages
|
|
810
910
|
// landing page that also lives in public/index.html.
|
|
911
|
+
const monacoAssetsPath = resolveMonacoAssetsPath();
|
|
912
|
+
if (monacoAssetsPath) {
|
|
913
|
+
app.use(MONACO_ASSETS_ROUTE, express.static(monacoAssetsPath, {
|
|
914
|
+
index: false,
|
|
915
|
+
setHeaders: (res) => {
|
|
916
|
+
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
|
917
|
+
},
|
|
918
|
+
}));
|
|
919
|
+
} else {
|
|
920
|
+
console.warn('[monaco] Local Monaco assets not found; code editor loader may fail.');
|
|
921
|
+
}
|
|
922
|
+
|
|
811
923
|
app.use(express.static(path.join(APP_ROOT, 'dist'), {
|
|
812
924
|
setHeaders: (res, filePath) => {
|
|
813
925
|
if (filePath.endsWith('.html')) {
|
|
@@ -2497,6 +2609,8 @@ function handleShellConnection(ws, request) {
|
|
|
2497
2609
|
}
|
|
2498
2610
|
|
|
2499
2611
|
existingSession.ws = ws;
|
|
2612
|
+
existingSession.hermesLaunchId = hermesLaunchId || existingSession.hermesLaunchId;
|
|
2613
|
+
existingSession.updatedAt = Date.now();
|
|
2500
2614
|
if (terminalStartupInput && !isPlainShell) {
|
|
2501
2615
|
writeTerminalStartupInput(existingSession, terminalStartupInput, 'reused provider session', 350);
|
|
2502
2616
|
}
|
|
@@ -2708,6 +2822,8 @@ function handleShellConnection(ws, request) {
|
|
|
2708
2822
|
exitSignal: null,
|
|
2709
2823
|
completedAt: null,
|
|
2710
2824
|
keepAliveUntilExit: false,
|
|
2825
|
+
pendingStartupInputs: [],
|
|
2826
|
+
startupInputTimerId: null,
|
|
2711
2827
|
updatedAt: Date.now(),
|
|
2712
2828
|
});
|
|
2713
2829
|
const createdSession = ptySessionsMap.get(ptySessionKey);
|
|
@@ -2792,6 +2908,11 @@ function handleShellConnection(ws, request) {
|
|
|
2792
2908
|
session.exitSignal = exitCode.signal || null;
|
|
2793
2909
|
session.completedAt = new Date().toISOString();
|
|
2794
2910
|
session.updatedAt = Date.now();
|
|
2911
|
+
if (session.startupInputTimerId) {
|
|
2912
|
+
clearTimeout(session.startupInputTimerId);
|
|
2913
|
+
session.startupInputTimerId = null;
|
|
2914
|
+
}
|
|
2915
|
+
session.pendingStartupInputs = [];
|
|
2795
2916
|
session.pty = null;
|
|
2796
2917
|
appendPtySessionBuffer(session, exitMessage);
|
|
2797
2918
|
}
|