@pixelbyte-software/pixcode 1.50.6 → 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,
@@ -13,6 +13,7 @@ const serverIndex = read('server/index.js');
13
13
  const shellTypes = read('src/components/shell/types/types.ts');
14
14
  const shellRuntime = read('src/components/shell/hooks/useShellRuntime.ts');
15
15
  const shellConnection = read('src/components/shell/hooks/useShellConnection.ts');
16
+ const shellTerminal = read('src/components/shell/hooks/useShellTerminal.ts');
16
17
  const shellView = read('src/components/shell/view/Shell.tsx');
17
18
  const standaloneShell = read('src/components/standalone-shell/view/StandaloneShell.tsx');
18
19
  const hermesRoutes = read('server/modules/orchestration/hermes/hermes.routes.ts');
@@ -33,6 +34,16 @@ assert.match(
33
34
  /pixcode:hermes-terminal[\s\S]+command[\s\S]+title/,
34
35
  'Hermes settings should dispatch command and title to the workbench terminal.',
35
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
+ );
36
47
  assert.match(
37
48
  settingsModal,
38
49
  /<HermesSettingsTab onClose=\{onClose\} \/>/,
@@ -73,6 +84,11 @@ assert.match(
73
84
  /pixcode_read_cli_terminal/,
74
85
  'Pixcode MCP should expose a terminal transcript reader so Hermes can report provider CLI output.',
75
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
+ );
76
92
  assert.match(
77
93
  pixcodeMcpServer,
78
94
  /Use this instead of Hermes shell\/proc\/skill execution/,
@@ -93,6 +109,11 @@ assert.match(
93
109
  /lastStrongBusy[\s\S]+lastPrompt[\s\S]+\?\s*'busy'\s*:\s*'idle'/,
94
110
  'Codex readback should ignore weak spinner remnants once the prompt has returned.',
95
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
+ );
96
117
  assert.match(
97
118
  serverIndex,
98
119
  /lastStrongBusy[\s\S]+lastPrompt[\s\S]+\?\s*'busy'\s*:\s*'idle'/,
@@ -108,6 +129,11 @@ assert.match(
108
129
  /requestedLaunchId[\s\S]+session\.hermesLaunchId === requestedLaunchId/,
109
130
  'Provider output API should filter by Hermes terminal launch id when supplied.',
110
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
+ );
111
137
  assert.match(
112
138
  serverIndex,
113
139
  /lifecycleState/,
@@ -215,8 +241,8 @@ assert.match(
215
241
  );
216
242
  assert.match(
217
243
  shellConnection,
218
- /normalizeStartupInput\(input: string, provider: LLMProvider\)/,
219
- 'Startup input normalization should be provider-aware.',
244
+ /startupInputDelivery/,
245
+ 'Startup input should be delivered through an explicit command-or-terminal mode.',
220
246
  );
221
247
  assert.match(
222
248
  shellConnection,
@@ -225,8 +251,48 @@ assert.match(
225
251
  );
226
252
  assert.match(
227
253
  shellConnection,
228
- /startupInput:\s*handlesStartupInputInCommand \? startupInputForCommand : null/,
229
- 'Shell websocket init should send Codex startup input to the backend command builder.',
254
+ /startupInputDelivery:\s*handlesStartupInputInCommand \? 'command' : 'terminal'/,
255
+ 'Shell websocket init should tell the backend whether startup input belongs in the command or visible terminal.',
256
+ );
257
+ assert.match(
258
+ shellTerminal,
259
+ /sanitizeTerminalInputData/,
260
+ 'Terminal input should filter xterm color-query reports before sending data to the PTY.',
261
+ );
262
+ assert.match(
263
+ shellTerminal,
264
+ /OSC_COLOR_REPORT_REGEX/,
265
+ 'Terminal input should drop OSC 10/11/12 color reports so resize/theme probes do not corrupt CLI prompts.',
266
+ );
267
+ assert.match(
268
+ serverIndex,
269
+ /writeTerminalStartupInput/,
270
+ 'Shell backend should submit Hermes startup input directly into reused visible PTYs.',
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
+ );
292
+ assert.match(
293
+ serverIndex,
294
+ /startupInputDelivery === 'terminal'[\s\S]+writeTerminalStartupInput/,
295
+ 'Existing visible provider sessions should receive terminal-delivered startup input before reconnect returns.',
230
296
  );
231
297
  assert.match(
232
298
  workbench,
@@ -253,6 +319,11 @@ assert.match(
253
319
  /startupInput\?: string \| null/,
254
320
  'Shell init messages should carry launch-time startup input for providers that accept an initial prompt.',
255
321
  );
322
+ assert.match(
323
+ shellTypes,
324
+ /startupInputDelivery\?: 'command' \| 'terminal'/,
325
+ 'Shell init messages should carry startup input delivery mode.',
326
+ );
256
327
  assert.match(
257
328
  serverIndex,
258
329
  /const startupInput = typeof data\.startupInput === 'string'/,
@@ -260,7 +331,7 @@ assert.match(
260
331
  );
261
332
  assert.match(
262
333
  serverIndex,
263
- /provider === 'codex'[\s\S]+startupInput[\s\S]+quoteShellArgForPlatform\(startupInput\)/,
334
+ /provider === 'codex'[\s\S]+commandStartupInput[\s\S]+quoteShellArgForPlatform\(commandStartupInput\)/,
264
335
  'Codex provider terminals should start with the requested prompt as a CLI argument so banners/update notices cannot swallow Enter.',
265
336
  );
266
337
  assert.match(
@@ -62,7 +62,8 @@ assert.match(workbench, /vscodeWorkbench\.cli\.closeTerminal/, 'CLI terminal clo
62
62
  assert.match(workbench, /WORKBENCH_CLI_STATE_STORAGE_KEY/, 'CLI terminal should remember per-project open state across workspace switches.');
63
63
  assert.match(workbench, /function WorkbenchBottomTerminal/, 'Terminal activity should render as a bottom plain-shell panel.');
64
64
  assert.match(workbench, /BOTTOM_TERMINAL_MIN_HEIGHT/, 'Bottom terminal should support height resizing.');
65
- assert.match(workbench, /isBottomTerminalMinimized/, 'Bottom terminal should support minimizing without closing.');
65
+ assert.match(workbench, /WorkbenchBottomTerminalViewMode/, 'Bottom terminal should support explicit half and full-screen modes.');
66
+ assert.doesNotMatch(workbench, /BOTTOM_TERMINAL_MINIMIZED_HEIGHT|Minimize terminal/, 'Bottom terminal should not expose the old minimized strip behavior.');
66
67
  assert.match(workbench, /bottomTerminalProject/, 'Bottom terminal should stay bound to the project it was opened for.');
67
68
  assert.match(workbench, /setBottomTerminalProject/, 'Opening a bottom terminal should capture its project instead of following workspace selection changes.');
68
69
  assert.match(workbench, /terminalProject = bottomTerminalProject \?\? selectedProject/, 'Workbench should render bottom terminals against their captured project binding.');
@@ -135,9 +136,9 @@ assert.match(hermesRoutes, /router\.post\('\/install'/, 'Hermes should install t
135
136
  assert.match(read('scripts/smoke/hermes-api-install.mjs'), /hermes API install smoke passed/, 'Hermes API install behavior should have a focused smoke test.');
136
137
  assert.match(workbench, /terminalStartupInput/, 'Hermes terminal launch prompts should be passed into the selected CLI.');
137
138
  assert.match(read('src/components/shell/hooks/useShellConnection.ts'), /startupInputRef/, 'Shell connections should support one-shot startup input for Hermes-triggered CLI work.');
138
- assert.match(read('src/components/shell/hooks/useShellConnection.ts'), /isCliReadyForStartupInput/, 'Hermes-triggered CLI input should wait until the provider TUI is ready.');
139
+ assert.match(read('src/components/shell/hooks/useShellTerminal.ts'), /sanitizeTerminalInputData/, 'Terminal should filter xterm color-query replies before forwarding input to provider CLIs.');
139
140
  assert.match(read('src/components/shell/hooks/useShellConnection.ts'), /forceNewSessionRef\.current[\s\S]+startupInputForCommand/, 'Codex startup input should only be sent as a CLI argument for explicit fresh sessions.');
140
- assert.doesNotMatch(read('src/components/shell/hooks/useShellConnection.ts'), /TERMINAL_INIT_DELAY_MS \* 3/, 'Hermes-triggered CLI input should not be sent on a blind fixed delay.');
141
+ assert.match(read('src/components/shell/hooks/useShellConnection.ts'), /startupInputDelivery:\s*handlesStartupInputInCommand \? 'command' : 'terminal'/, 'Reused visible sessions should submit startup input through the backend terminal path.');
141
142
  assert.match(shellTerminal, /handleTerminalPaste/, 'Terminal should support browser paste events.');
142
143
  assert.match(shellTerminal, /handleCopyPasteShortcut/, 'Terminal should normalize Ctrl/Cmd copy and paste shortcuts.');
143
144
  assert.match(shellTerminal, /event\.shiftKey/, 'Terminal should support Ctrl+Shift+C/V style shortcuts.');
@@ -284,9 +284,19 @@ assert.match(
284
284
  'Terminal activity should open a VS Code-style bottom terminal instead of the provider CLI picker.',
285
285
  );
286
286
 
287
- for (const token of ['BOTTOM_TERMINAL_MIN_HEIGHT', 'isBottomTerminalMinimized', 'shrinkCliPanel', 'expandCliPanel']) {
287
+ for (const token of ['BOTTOM_TERMINAL_MIN_HEIGHT', 'bottomTerminalViewMode', 'shrinkCliPanel', 'expandCliPanel']) {
288
288
  assert.match(workbench, new RegExp(token), `Workbench should include ${token}.`);
289
289
  }
290
+ assert.match(
291
+ workbench,
292
+ /WorkbenchBottomTerminalViewMode/,
293
+ 'Bottom terminal should expose half/full display modes instead of minimize/restore.',
294
+ );
295
+ assert.doesNotMatch(
296
+ workbench,
297
+ /BOTTOM_TERMINAL_MINIMIZED_HEIGHT|Minimize terminal|isBottomTerminalMinimized/,
298
+ 'Bottom terminal should not keep the old minimized strip UI.',
299
+ );
290
300
 
291
301
  assert.match(
292
302
  workbench,
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) {
@@ -435,6 +441,100 @@ function appendPtySessionBuffer(session, data) {
435
441
  }
436
442
  }
437
443
 
444
+ function normalizeTerminalStartupInput(input) {
445
+ return `\x15${String(input || '').replace(/(?:\r\n|\r|\n)+$/u, '')}\r`;
446
+ }
447
+
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);
496
+ }
497
+ return;
498
+ }
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);
536
+ }
537
+
438
538
  function normalizeShellPermissionMode(value) {
439
539
  return typeof value === 'string' ? value.trim() : '';
440
540
  }
@@ -2387,6 +2487,9 @@ function handleShellConnection(ws, request) {
2387
2487
  const startupInput = typeof data.startupInput === 'string' && data.startupInput.trim()
2388
2488
  ? data.startupInput.trim()
2389
2489
  : null;
2490
+ const startupInputDelivery = data.startupInputDelivery === 'terminal' ? 'terminal' : 'command';
2491
+ const commandStartupInput = startupInputDelivery === 'command' ? startupInput : null;
2492
+ const terminalStartupInput = startupInputDelivery === 'terminal' ? startupInput : null;
2390
2493
  const hermesLaunchId = Number.isFinite(Number(data.hermesLaunchId)) && Number(data.hermesLaunchId) > 0
2391
2494
  ? Number(data.hermesLaunchId)
2392
2495
  : null;
@@ -2474,6 +2577,11 @@ function handleShellConnection(ws, request) {
2474
2577
  }
2475
2578
 
2476
2579
  existingSession.ws = ws;
2580
+ existingSession.hermesLaunchId = hermesLaunchId || existingSession.hermesLaunchId;
2581
+ existingSession.updatedAt = Date.now();
2582
+ if (terminalStartupInput && !isPlainShell) {
2583
+ writeTerminalStartupInput(existingSession, terminalStartupInput, 'reused provider session', 350);
2584
+ }
2477
2585
 
2478
2586
  return;
2479
2587
  }
@@ -2551,8 +2659,8 @@ function handleShellConnection(ws, request) {
2551
2659
  } else {
2552
2660
  shellCommand = `${command} resume "${sessionId}" || ${command}`;
2553
2661
  }
2554
- } else if (startupInput) {
2555
- shellCommand = `${command} ${quoteShellArgForPlatform(startupInput)}`;
2662
+ } else if (commandStartupInput) {
2663
+ shellCommand = `${command} ${quoteShellArgForPlatform(commandStartupInput)}`;
2556
2664
  } else {
2557
2665
  shellCommand = command;
2558
2666
  }
@@ -2682,8 +2790,14 @@ function handleShellConnection(ws, request) {
2682
2790
  exitSignal: null,
2683
2791
  completedAt: null,
2684
2792
  keepAliveUntilExit: false,
2793
+ pendingStartupInputs: [],
2794
+ startupInputTimerId: null,
2685
2795
  updatedAt: Date.now(),
2686
2796
  });
2797
+ const createdSession = ptySessionsMap.get(ptySessionKey);
2798
+ if (terminalStartupInput && !isPlainShell) {
2799
+ writeTerminalStartupInput(createdSession, terminalStartupInput, 'new provider session', 4500);
2800
+ }
2687
2801
 
2688
2802
  // Handle data output
2689
2803
  shellProcess.onData((data) => {
@@ -2762,6 +2876,11 @@ function handleShellConnection(ws, request) {
2762
2876
  session.exitSignal = exitCode.signal || null;
2763
2877
  session.completedAt = new Date().toISOString();
2764
2878
  session.updatedAt = Date.now();
2879
+ if (session.startupInputTimerId) {
2880
+ clearTimeout(session.startupInputTimerId);
2881
+ session.startupInputTimerId = null;
2882
+ }
2883
+ session.pendingStartupInputs = [];
2765
2884
  session.pty = null;
2766
2885
  appendPtySessionBuffer(session, exitMessage);
2767
2886
  }