@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.
- package/dist/assets/{index-DVEXTVKy.js → index-gecaamTl.js} +156 -156
- package/dist/index.html +1 -1
- package/dist-server/server/index.js +109 -2
- package/dist-server/server/index.js.map +1 -1
- package/package.json +1 -1
- package/scripts/hermes/pixcode-mcp-server.mjs +35 -14
- package/scripts/smoke/hermes-mcp-pixcode-roundtrip.mjs +22 -1
- package/scripts/smoke/hermes-settings-commands.mjs +76 -5
- package/scripts/smoke/pixcode-workbench-1-48.mjs +4 -3
- package/scripts/smoke/vscode-workbench-polish.mjs +11 -1
- package/server/index.js +121 -2
|
@@ -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));
|
|
@@ -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,
|
|
@@ -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
|
-
/
|
|
219
|
-
'Startup input
|
|
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
|
-
/
|
|
229
|
-
'Shell websocket init should
|
|
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]+
|
|
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, /
|
|
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/
|
|
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.
|
|
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', '
|
|
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 (
|
|
2555
|
-
shellCommand = `${command} ${quoteShellArgForPlatform(
|
|
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
|
}
|