@pixelbyte-software/pixcode 1.50.7 → 1.50.8

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.
@@ -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));
@@ -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
@@ -153,6 +153,8 @@ let projectsWatchers = [];
153
153
  let projectsWatcherDebounceTimer = null;
154
154
  const connectedClients = new Set();
155
155
  let isGetProjectsRunning = false; // Flag to prevent reentrant calls
156
+ const STARTUP_INPUT_READY_TIMEOUT_MS = 10 * 60 * 1000;
157
+ const STARTUP_INPUT_POLL_MS = 750;
156
158
 
157
159
  // Broadcast progress to all connected WebSocket clients
158
160
  function broadcastProgress(progress) {
@@ -298,6 +300,10 @@ function terminatePtySession(sessionKey, session, reason) {
298
300
  if (session.timeoutId) {
299
301
  clearTimeout(session.timeoutId);
300
302
  }
303
+ if (session.startupInputTimerId) {
304
+ clearTimeout(session.startupInputTimerId);
305
+ session.startupInputTimerId = null;
306
+ }
301
307
 
302
308
  try {
303
309
  if (session.pty && session.pty.kill) {
@@ -436,23 +442,97 @@ function appendPtySessionBuffer(session, data) {
436
442
  }
437
443
 
438
444
  function normalizeTerminalStartupInput(input) {
439
- return `${String(input || '').replace(/(?:\r\n|\r|\n)+$/u, '')}\r`;
445
+ return `\x15${String(input || '').replace(/(?:\r\n|\r|\n)+$/u, '')}\r`;
440
446
  }
441
447
 
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})`);
448
+ function readSessionOutputForState(session, maxChars = 12000) {
449
+ return stripAnsiSequences((session?.buffer || []).join('').slice(-maxChars));
450
+ }
451
+
452
+ function shouldWaitForProviderIdle(provider) {
453
+ return provider === 'codex';
454
+ }
455
+
456
+ function isTerminalReadyForStartupInput(session) {
457
+ if (!session?.pty || session.lifecycleState !== 'running') {
458
+ return { ready: false, retry: false, terminalState: 'exited' };
459
+ }
460
+
461
+ const output = readSessionOutputForState(session);
462
+ const state = resolveProviderTerminalState(session, session.provider, output);
463
+ if (state.terminalState === 'busy') {
464
+ return { ready: false, retry: true, terminalState: state.terminalState };
465
+ }
466
+
467
+ if (state.terminalState === 'idle') {
468
+ return { ready: true, retry: false, terminalState: state.terminalState };
469
+ }
470
+
471
+ if (shouldWaitForProviderIdle(session.provider)) {
472
+ return { ready: false, retry: true, terminalState: state.terminalState };
473
+ }
474
+
475
+ return { ready: true, retry: false, terminalState: state.terminalState };
476
+ }
477
+
478
+ function processTerminalStartupInputQueue(session) {
479
+ if (!session?.pendingStartupInputs?.length) {
480
+ session.startupInputTimerId = null;
481
+ return;
482
+ }
483
+
484
+ const item = session.pendingStartupInputs[0];
485
+ const readiness = isTerminalReadyForStartupInput(session);
486
+ if (!readiness.ready) {
487
+ if (!readiness.retry || Date.now() - item.queuedAt > STARTUP_INPUT_READY_TIMEOUT_MS) {
488
+ session.pendingStartupInputs.shift();
489
+ session.startupInputTimerId = null;
490
+ const message = `\r\n\x1b[33m[Pixcode] Startup input was not sent because ${session.provider} is still ${readiness.terminalState || 'unavailable'}.\x1b[0m\r\n`;
491
+ try {
492
+ session.ws?.send?.(JSON.stringify({ type: 'output', data: message }));
493
+ } catch { /* websocket gone */ }
494
+ if (session.pendingStartupInputs.length > 0) {
495
+ session.startupInputTimerId = setTimeout(() => processTerminalStartupInputQueue(session), STARTUP_INPUT_POLL_MS);
451
496
  }
452
- } catch (error) {
453
- console.warn('Failed to submit startup input to visible PTY:', error?.message || error);
497
+ return;
454
498
  }
455
- }, delayMs);
499
+
500
+ session.startupInputTimerId = setTimeout(() => processTerminalStartupInputQueue(session), STARTUP_INPUT_POLL_MS);
501
+ return;
502
+ }
503
+
504
+ session.pendingStartupInputs.shift();
505
+ session.startupInputTimerId = null;
506
+ try {
507
+ session.pty.write(normalizeTerminalStartupInput(item.startupInput));
508
+ session.updatedAt = Date.now();
509
+ console.log(`⌨️ Submitted startup input to visible PTY (${item.reason})`);
510
+ } catch (error) {
511
+ console.warn('Failed to submit startup input to visible PTY:', error?.message || error);
512
+ }
513
+
514
+ if (session.pendingStartupInputs.length > 0) {
515
+ session.startupInputTimerId = setTimeout(() => processTerminalStartupInputQueue(session), STARTUP_INPUT_POLL_MS);
516
+ }
517
+ }
518
+
519
+ function queueTerminalStartupInput(session, startupInput, reason, delayMs = 500) {
520
+ if (!session?.pty || !startupInput) return;
521
+ if (!Array.isArray(session.pendingStartupInputs)) {
522
+ session.pendingStartupInputs = [];
523
+ }
524
+ session.pendingStartupInputs.push({
525
+ startupInput,
526
+ reason,
527
+ queuedAt: Date.now(),
528
+ });
529
+
530
+ if (session.startupInputTimerId) return;
531
+ session.startupInputTimerId = setTimeout(() => processTerminalStartupInputQueue(session), delayMs);
532
+ }
533
+
534
+ function writeTerminalStartupInput(session, startupInput, reason, delayMs = 500) {
535
+ queueTerminalStartupInput(session, startupInput, reason, delayMs);
456
536
  }
457
537
 
458
538
  function normalizeShellPermissionMode(value) {
@@ -2497,6 +2577,8 @@ function handleShellConnection(ws, request) {
2497
2577
  }
2498
2578
 
2499
2579
  existingSession.ws = ws;
2580
+ existingSession.hermesLaunchId = hermesLaunchId || existingSession.hermesLaunchId;
2581
+ existingSession.updatedAt = Date.now();
2500
2582
  if (terminalStartupInput && !isPlainShell) {
2501
2583
  writeTerminalStartupInput(existingSession, terminalStartupInput, 'reused provider session', 350);
2502
2584
  }
@@ -2708,6 +2790,8 @@ function handleShellConnection(ws, request) {
2708
2790
  exitSignal: null,
2709
2791
  completedAt: null,
2710
2792
  keepAliveUntilExit: false,
2793
+ pendingStartupInputs: [],
2794
+ startupInputTimerId: null,
2711
2795
  updatedAt: Date.now(),
2712
2796
  });
2713
2797
  const createdSession = ptySessionsMap.get(ptySessionKey);
@@ -2792,6 +2876,11 @@ function handleShellConnection(ws, request) {
2792
2876
  session.exitSignal = exitCode.signal || null;
2793
2877
  session.completedAt = new Date().toISOString();
2794
2878
  session.updatedAt = Date.now();
2879
+ if (session.startupInputTimerId) {
2880
+ clearTimeout(session.startupInputTimerId);
2881
+ session.startupInputTimerId = null;
2882
+ }
2883
+ session.pendingStartupInputs = [];
2795
2884
  session.pty = null;
2796
2885
  appendPtySessionBuffer(session, exitMessage);
2797
2886
  }