@pixelbyte-software/pixcode 1.50.3 → 1.50.5

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/index.html CHANGED
@@ -35,7 +35,7 @@
35
35
 
36
36
  <!-- Prevent zoom on iOS -->
37
37
  <meta name="format-detection" content="telephone=no" />
38
- <script type="module" crossorigin src="/assets/index-DpdiWohD.js"></script>
38
+ <script type="module" crossorigin src="/assets/index-BSxc8Vid.js"></script>
39
39
  <link rel="modulepreload" crossorigin href="/assets/vendor-react-DB6V5Fl1.js">
40
40
  <link rel="modulepreload" crossorigin href="/assets/vendor-codemirror-CIYNS698.js">
41
41
  <link rel="modulepreload" crossorigin href="/assets/vendor-xterm-C7tpxJl7.js">
@@ -69,6 +69,7 @@ import providerRoutes from './modules/providers/provider.routes.js';
69
69
  import { createHermesTaskRouter, adapterRegistry, ClaudeCodeA2AAdapter, CodexA2AAdapter, CursorA2AAdapter, GeminiA2AAdapter, OpenCodeA2AAdapter, QwenA2AAdapter, createPreviewProxyRouter, createOrchestrationTaskRouter, createHermesRouter, createWorkflowRouter, } from './modules/orchestration/index.js';
70
70
  import networkRoutes from './routes/network.js';
71
71
  import telegramRoutes from './routes/telegram.js';
72
+ import { restoreRequestedTunnel } from './services/external-access.js';
72
73
  import { restoreBotFromConfig } from './services/telegram/bot.js';
73
74
  import { ensurePortOpen } from './utils/port-access.js';
74
75
  import { applyAllStoredCredentialsToEnv, } from './services/provider-credentials.js';
@@ -232,6 +233,7 @@ const app = express();
232
233
  const server = http.createServer(app);
233
234
  const ptySessionsMap = new Map();
234
235
  const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
236
+ const COMPLETED_PTY_SESSION_TTL = 5 * 60 * 1000;
235
237
  const SHELL_URL_PARSE_BUFFER_LIMIT = 32768;
236
238
  const SHELL_CLI_PROVIDERS = new Set(['claude', 'codex', 'cursor', 'gemini', 'qwen', 'opencode']);
237
239
  import { stripAnsiSequences, normalizeDetectedUrl, extractUrlsFromText, shouldAutoOpenUrlFromOutput } from './utils/url-detection.js';
@@ -264,6 +266,97 @@ function killProviderPtySessions(projectPath, provider) {
264
266
  }
265
267
  return killed;
266
268
  }
269
+ function getLastRegexMatchIndex(text, pattern) {
270
+ let lastIndex = -1;
271
+ for (const match of text.matchAll(pattern)) {
272
+ lastIndex = match.index ?? lastIndex;
273
+ }
274
+ return lastIndex;
275
+ }
276
+ function detectProviderTerminalState(provider, output) {
277
+ const cleanOutput = String(output || '');
278
+ if (!cleanOutput.trim()) {
279
+ return {
280
+ terminalState: 'unknown',
281
+ isBusy: false,
282
+ terminalStateReason: 'empty_output',
283
+ };
284
+ }
285
+ if (/Process exited with code/iu.test(cleanOutput)) {
286
+ return {
287
+ terminalState: 'exited',
288
+ isBusy: false,
289
+ terminalStateReason: 'process_exit',
290
+ };
291
+ }
292
+ const lastBusy = Math.max(getLastRegexMatchIndex(cleanOutput, /(?:^|\n)\s*[•*]\s*(?:Working|Running|Thinking)\b/giu), getLastRegexMatchIndex(cleanOutput, /\bWorking\s*\([^)]*esc to interrupt[^)]*\)/giu), getLastRegexMatchIndex(cleanOutput, /\bmsg=interrupt\b/giu));
293
+ if (provider === 'codex') {
294
+ const lastPrompt = Math.max(getLastRegexMatchIndex(cleanOutput, /(?:^|\n)\s*›(?:\s|$)/gu), getLastRegexMatchIndex(cleanOutput, /(?:^|\n)\s*❯(?:\s|$)/gu));
295
+ if (lastBusy >= 0) {
296
+ const isBusy = lastPrompt <= lastBusy;
297
+ return {
298
+ terminalState: isBusy ? 'busy' : 'idle',
299
+ isBusy,
300
+ terminalStateReason: isBusy ? 'codex_busy_marker_after_prompt' : 'codex_prompt_after_busy_marker',
301
+ };
302
+ }
303
+ if (lastPrompt >= 0 && /(?:Initialized|Baseline check passed|I did not modify files|Use \/skills)/iu.test(cleanOutput)) {
304
+ return {
305
+ terminalState: 'idle',
306
+ isBusy: false,
307
+ terminalStateReason: 'codex_idle_prompt',
308
+ };
309
+ }
310
+ }
311
+ if (lastBusy >= 0) {
312
+ return {
313
+ terminalState: 'busy',
314
+ isBusy: true,
315
+ terminalStateReason: 'generic_busy_marker',
316
+ };
317
+ }
318
+ return {
319
+ terminalState: 'unknown',
320
+ isBusy: false,
321
+ terminalStateReason: 'no_known_marker',
322
+ };
323
+ }
324
+ function resolveProviderTerminalState(session, provider, output) {
325
+ if (session?.lifecycleState === 'completed' || session?.lifecycleState === 'failed' || session?.lifecycleState === 'exited') {
326
+ const exitCode = typeof session.exitCode === 'number' ? session.exitCode : null;
327
+ const terminalFailed = exitCode !== null ? exitCode !== 0 : Boolean(session.exitSignal);
328
+ return {
329
+ terminalState: terminalFailed ? 'failed' : 'completed',
330
+ lifecycleState: session.lifecycleState,
331
+ isBusy: false,
332
+ terminalFailed,
333
+ exitCode,
334
+ exitSignal: session.exitSignal || null,
335
+ completedAt: session.completedAt || null,
336
+ terminalStateReason: terminalFailed ? 'pty_failed' : 'pty_completed',
337
+ };
338
+ }
339
+ const detected = detectProviderTerminalState(provider, output);
340
+ return {
341
+ ...detected,
342
+ lifecycleState: session?.lifecycleState || 'running',
343
+ terminalFailed: false,
344
+ exitCode: null,
345
+ exitSignal: null,
346
+ completedAt: null,
347
+ };
348
+ }
349
+ function appendPtySessionBuffer(session, data) {
350
+ if (!session)
351
+ return;
352
+ if (session.buffer.length < 5000) {
353
+ session.buffer.push(data);
354
+ }
355
+ else {
356
+ session.buffer.shift();
357
+ session.buffer.push(data);
358
+ }
359
+ }
267
360
  function normalizeShellPermissionMode(value) {
268
361
  return typeof value === 'string' ? value.trim() : '';
269
362
  }
@@ -439,6 +532,8 @@ app.get('/api/shell/sessions/provider-output', authenticateToken, (req, res) =>
439
532
  const projectPath = typeof req.query.projectPath === 'string' && req.query.projectPath.trim()
440
533
  ? req.query.projectPath.trim()
441
534
  : null;
535
+ const launchId = Number.parseInt(String(req.query.launchId || ''), 10);
536
+ const requestedLaunchId = Number.isFinite(launchId) && launchId > 0 ? launchId : null;
442
537
  const maxChars = Math.min(20000, Math.max(1000, Number.parseInt(String(req.query.maxChars || '12000'), 10) || 12000));
443
538
  if (!SHELL_CLI_PROVIDERS.has(provider)) {
444
539
  return res.status(400).json({ error: 'Unsupported provider' });
@@ -448,7 +543,8 @@ app.get('/api/shell/sessions/provider-output', authenticateToken, (req, res) =>
448
543
  for (const session of ptySessionsMap.values()) {
449
544
  if (session?.provider === provider &&
450
545
  !session?.isPlainShell &&
451
- (!requestedProjectPath || path.resolve(session.projectPath || os.homedir()) === requestedProjectPath)) {
546
+ (!requestedProjectPath || path.resolve(session.projectPath || os.homedir()) === requestedProjectPath) &&
547
+ (!requestedLaunchId || session.hermesLaunchId === requestedLaunchId)) {
452
548
  if (!matchedSession || (session.updatedAt || 0) > (matchedSession.updatedAt || 0)) {
453
549
  matchedSession = session;
454
550
  }
@@ -459,18 +555,23 @@ app.get('/api/shell/sessions/provider-output', authenticateToken, (req, res) =>
459
555
  active: false,
460
556
  provider,
461
557
  projectPath: requestedProjectPath,
558
+ launchId: requestedLaunchId,
462
559
  output: '',
463
560
  message: 'No active provider terminal session found for this project.',
464
561
  });
465
562
  }
466
563
  const rawOutput = matchedSession.buffer.join('').slice(-maxChars);
564
+ const output = stripAnsiSequences(rawOutput);
565
+ const terminalState = resolveProviderTerminalState(matchedSession, provider, output);
467
566
  res.json({
468
567
  active: true,
469
568
  provider,
470
569
  projectPath: path.resolve(matchedSession.projectPath || os.homedir()),
471
570
  sessionId: matchedSession.sessionId || null,
571
+ launchId: matchedSession.hermesLaunchId || null,
472
572
  updatedAt: matchedSession.updatedAt || null,
473
- output: stripAnsiSequences(rawOutput),
573
+ ...terminalState,
574
+ output,
474
575
  });
475
576
  });
476
577
  // Authentication routes (public)
@@ -2080,6 +2181,9 @@ function handleShellConnection(ws, request) {
2080
2181
  const startupInput = typeof data.startupInput === 'string' && data.startupInput.trim()
2081
2182
  ? data.startupInput.trim()
2082
2183
  : null;
2184
+ const hermesLaunchId = Number.isFinite(Number(data.hermesLaunchId)) && Number(data.hermesLaunchId) > 0
2185
+ ? Number(data.hermesLaunchId)
2186
+ : null;
2083
2187
  const isPlainShell = data.isPlainShell || (!!initialCommand && !hasSession) || provider === 'plain-shell';
2084
2188
  const isHermesCliLaunch = isPlainShell && isHermesCliCommand(initialCommand);
2085
2189
  const forceNewSession = Boolean(data.forceNewSession);
@@ -2136,24 +2240,29 @@ function handleShellConnection(ws, request) {
2136
2240
  }
2137
2241
  const existingSession = (isLoginCommand || forceNewSession) ? null : ptySessionsMap.get(ptySessionKey);
2138
2242
  if (existingSession) {
2139
- console.log('♻️ Reconnecting to existing PTY session:', ptySessionKey);
2140
- shellProcess = existingSession.pty;
2141
- clearTimeout(existingSession.timeoutId);
2142
- ws.send(JSON.stringify({
2143
- type: 'output',
2144
- data: `\x1b[36m[Reconnected to existing session]\x1b[0m\r\n`
2145
- }));
2146
- if (existingSession.buffer && existingSession.buffer.length > 0) {
2147
- console.log(`📜 Sending ${existingSession.buffer.length} buffered messages`);
2148
- existingSession.buffer.forEach(bufferedData => {
2149
- ws.send(JSON.stringify({
2150
- type: 'output',
2151
- data: bufferedData
2152
- }));
2153
- });
2243
+ if (!existingSession.pty || existingSession.lifecycleState === 'completed' || existingSession.lifecycleState === 'failed') {
2244
+ ptySessionsMap.delete(ptySessionKey);
2245
+ }
2246
+ else {
2247
+ console.log('♻️ Reconnecting to existing PTY session:', ptySessionKey);
2248
+ shellProcess = existingSession.pty;
2249
+ clearTimeout(existingSession.timeoutId);
2250
+ ws.send(JSON.stringify({
2251
+ type: 'output',
2252
+ data: `\x1b[36m[Reconnected to existing session]\x1b[0m\r\n`
2253
+ }));
2254
+ if (existingSession.buffer && existingSession.buffer.length > 0) {
2255
+ console.log(`📜 Sending ${existingSession.buffer.length} buffered messages`);
2256
+ existingSession.buffer.forEach(bufferedData => {
2257
+ ws.send(JSON.stringify({
2258
+ type: 'output',
2259
+ data: bufferedData
2260
+ }));
2261
+ });
2262
+ }
2263
+ existingSession.ws = ws;
2264
+ return;
2154
2265
  }
2155
- existingSession.ws = ws;
2156
- return;
2157
2266
  }
2158
2267
  console.log('[INFO] Starting shell in:', projectPath);
2159
2268
  console.log('📋 Session info:', hasSession ? `Resume session ${sessionId}` : (isPlainShell ? 'Plain shell mode' : 'New session'));
@@ -2356,8 +2465,13 @@ function handleShellConnection(ws, request) {
2356
2465
  timeoutId: null,
2357
2466
  projectPath,
2358
2467
  sessionId,
2468
+ hermesLaunchId,
2359
2469
  provider,
2360
2470
  isPlainShell,
2471
+ lifecycleState: 'running',
2472
+ exitCode: null,
2473
+ exitSignal: null,
2474
+ completedAt: null,
2361
2475
  keepAliveUntilExit: false,
2362
2476
  updatedAt: Date.now(),
2363
2477
  });
@@ -2367,13 +2481,7 @@ function handleShellConnection(ws, request) {
2367
2481
  if (!session)
2368
2482
  return;
2369
2483
  session.updatedAt = Date.now();
2370
- if (session.buffer.length < 5000) {
2371
- session.buffer.push(data);
2372
- }
2373
- else {
2374
- session.buffer.shift();
2375
- session.buffer.push(data);
2376
- }
2484
+ appendPtySessionBuffer(session, data);
2377
2485
  if (session.ws && session.ws.readyState === WebSocket.OPEN) {
2378
2486
  let outputData = data;
2379
2487
  const cleanChunk = stripAnsiSequences(data);
@@ -2414,16 +2522,37 @@ function handleShellConnection(ws, request) {
2414
2522
  shellProcess.onExit((exitCode) => {
2415
2523
  console.log('🔚 Shell process exited with code:', exitCode.exitCode, 'signal:', exitCode.signal);
2416
2524
  const session = ptySessionsMap.get(ptySessionKey);
2525
+ const exitMessage = `\r\n\x1b[33mProcess exited with code ${exitCode.exitCode}${exitCode.signal ? ` (${exitCode.signal})` : ''}\x1b[0m\r\n`;
2526
+ if (session) {
2527
+ session.lifecycleState = exitCode.exitCode === 0 && !exitCode.signal ? 'completed' : 'failed';
2528
+ session.exitCode = typeof exitCode.exitCode === 'number' ? exitCode.exitCode : null;
2529
+ session.exitSignal = exitCode.signal || null;
2530
+ session.completedAt = new Date().toISOString();
2531
+ session.updatedAt = Date.now();
2532
+ session.pty = null;
2533
+ appendPtySessionBuffer(session, exitMessage);
2534
+ }
2417
2535
  if (session && session.ws && session.ws.readyState === WebSocket.OPEN) {
2418
2536
  session.ws.send(JSON.stringify({
2419
2537
  type: 'output',
2420
- data: `\r\n\x1b[33mProcess exited with code ${exitCode.exitCode}${exitCode.signal ? ` (${exitCode.signal})` : ''}\x1b[0m\r\n`
2538
+ data: exitMessage
2421
2539
  }));
2422
2540
  }
2423
2541
  if (session && session.timeoutId) {
2424
2542
  clearTimeout(session.timeoutId);
2425
2543
  }
2426
- ptySessionsMap.delete(ptySessionKey);
2544
+ if (session) {
2545
+ session.ws = null;
2546
+ session.timeoutId = setTimeout(() => {
2547
+ const current = ptySessionsMap.get(ptySessionKey);
2548
+ if (current && current.lifecycleState !== 'running') {
2549
+ ptySessionsMap.delete(ptySessionKey);
2550
+ }
2551
+ }, COMPLETED_PTY_SESSION_TTL);
2552
+ }
2553
+ else {
2554
+ ptySessionsMap.delete(ptySessionKey);
2555
+ }
2427
2556
  shellProcess = null;
2428
2557
  });
2429
2558
  }
@@ -3128,6 +3257,9 @@ async function startServer() {
3128
3257
  catch (err) {
3129
3258
  console.log(`${c.dim('[INFO]')} Port-access helper failed: ${err?.message || err}`);
3130
3259
  }
3260
+ restoreRequestedTunnel({ port: Number(SERVER_PORT) }).catch((err) => {
3261
+ console.warn('[external-access] tunnel restore failed:', err?.message || err);
3262
+ });
3131
3263
  console.log(`${c.tip('[TIP]')} Run "pixcode status" for full configuration details`);
3132
3264
  console.log('');
3133
3265
  // Start watching the projects folder for changes