@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixelbyte-software/pixcode",
3
- "version": "1.50.7",
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 true, Pixcode starts the managed Hermes gateway before probing.',
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 inferTerminalState(provider, terminalOutput) {
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 === true,
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\n• Ran npm test\n\n› Implement {feature}\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 `${String(input || '').replace(/(?:\r\n|\r|\n)+$/u, '')}\r`;
465
+ return `\x15${String(input || '').replace(/(?:\r\n|\r|\n)+$/u, '')}\r`;
440
466
  }
441
467
 
442
- function writeTerminalStartupInput(session, startupInput, reason, delayMs = 500) {
443
- if (!session?.pty || !startupInput) return;
444
- const submittedInput = normalizeTerminalStartupInput(startupInput);
445
- setTimeout(() => {
446
- try {
447
- if (session.pty && session.lifecycleState === 'running') {
448
- session.pty.write(submittedInput);
449
- session.updatedAt = Date.now();
450
- console.log(`⌨️ Submitted startup input to visible PTY (${reason})`);
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
- } catch (error) {
453
- console.warn('Failed to submit startup input to visible PTY:', error?.message || error);
517
+ return;
454
518
  }
455
- }, delayMs);
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
  }