@pixelbyte-software/pixcode 1.48.3 → 1.48.4

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.
@@ -40,9 +40,13 @@ assert.match(workbench, /openEditorTabs|activeEditorPath/, 'Workbench editor sho
40
40
  assert.doesNotMatch(workbench, /<ChatInterface/, 'Right workbench panel should not render the chat composer.');
41
41
  assert.match(workbench, /WorkbenchCliPanel/, 'Right workbench panel should render the CLI terminal panel.');
42
42
  assert.match(workbench, /setIsTerminalOpen\(true\)/, 'CLI picker should give way to a full-height terminal after the user starts a provider.');
43
- assert.match(workbench, /onClose=\{\(\) => setIsTerminalOpen\(false\)\}/, 'Closing the workbench terminal should return to the CLI picker.');
43
+ assert.match(workbench, /onClose=\{closeTerminal\}/, 'Closing the workbench terminal should return to the CLI picker.');
44
44
  assert.match(workbench, /WorkbenchCliPanelToolbar/, 'CLI terminal should keep history and new-session actions visible.');
45
- assert.match(workbench, /startNewCliSession/, 'CLI terminal should support starting a fresh provider session from the right pane.');
45
+ assert.match(workbench, /WORKBENCH_CLI_STATE_STORAGE_KEY/, 'CLI terminal should remember per-project open state across workspace switches.');
46
+ assert.match(workbench, /openNewCliSessionPicker/, 'CLI terminal plus should return to provider selection before starting a fresh session.');
47
+ assert.match(workbench, /terminateCurrentCliSession\(selectedProvider\)/, 'CLI terminal plus should terminate the existing provider PTY before showing selection.');
48
+ assert.match(workbench, /forceNewSession=\{terminalLaunch\.forceNewSession\}/, 'Fresh CLI sessions should bypass the cached default PTY.');
49
+ assert.match(serverIndex, /\/api\/shell\/sessions\/terminate/, 'Backend should expose an authenticated endpoint to terminate cached provider PTYs immediately.');
46
50
  assert.doesNotMatch(shellTerminal, /new WebglAddon\(\)/, 'Workbench terminal should use the stable xterm renderer.');
47
51
  assert.match(workbench, /setActivityPanel\('explorer'\)/, 'Selecting a project should return the side panel to Explorer.');
48
52
  assert.match(gitPanelHeader, /compact/, 'Workbench Source Control should have compact icon-only controls.');
@@ -64,5 +68,7 @@ assert.doesNotMatch(orchestration, /A2A|a2a/, 'Orchestration page should not pre
64
68
  assert.match(serverIndex, /app\.use\('\/hermes', createHermesTaskRouter\(\)\)/, 'Internal task router should be mounted behind Hermes.');
65
69
  assert.doesNotMatch(serverIndex, /app\.use\('\/a2a'/, 'Server should not expose the old A2A route.');
66
70
  assert.match(hermesRoutes, /createHermesRouter/, 'Hermes should have a dedicated orchestration API router.');
71
+ assert.match(serverIndex, /forceNewSession/, 'Shell backend should support explicit fresh-session launches from the workbench.');
72
+ assert.match(serverIndex, /killProviderPtySessions/, 'Shell backend should terminate old provider PTYs when a fresh CLI session is requested.');
67
73
 
68
74
  console.log('pixcode workbench 1.48 smoke passed');
@@ -98,8 +98,14 @@ for (const token of ['closeOtherWorkspaces', 'closeAllWorkspaces']) {
98
98
 
99
99
  assert.match(
100
100
  workspaceTabsSource,
101
- /self-center/,
102
- 'Workspace add button should be vertically centered in the tab strip.',
101
+ /items-center justify-center/,
102
+ 'Workspace add button should center its plus icon instead of rendering an off-center bare icon.',
103
+ );
104
+
105
+ assert.match(
106
+ workspaceTabsSource,
107
+ /border-r border-border/,
108
+ 'Workspace add button should read as part of the tab strip instead of a floating bare button.',
103
109
  );
104
110
 
105
111
  assert.match(
@@ -156,8 +162,8 @@ assert.match(
156
162
 
157
163
  assert.match(
158
164
  workbench,
159
- /onClose=\{\(\) => setIsTerminalOpen\(false\)\}/,
160
- 'Right CLI terminal close button should return to the picker instead of leaving a dead reconnect overlay.',
165
+ /onClose=\{closeTerminal\}/,
166
+ 'Right CLI terminal close button should return to the picker through the persisted close flow.',
161
167
  );
162
168
 
163
169
  assert.match(
@@ -174,8 +180,26 @@ assert.match(
174
180
 
175
181
  assert.match(
176
182
  workbench,
177
- /startNewCliSession/,
178
- 'Right CLI panel should expose a one-click new CLI session action.',
183
+ /WORKBENCH_CLI_STATE_STORAGE_KEY/,
184
+ 'Right CLI panel should persist per-project terminal state when switching workspaces.',
185
+ );
186
+
187
+ assert.match(
188
+ workbench,
189
+ /openNewCliSessionPicker/,
190
+ 'Right CLI panel toolbar plus should stop the current terminal view and return to CLI selection.',
191
+ );
192
+
193
+ assert.match(
194
+ workbench,
195
+ /terminateCurrentCliSession\(selectedProvider\)/,
196
+ 'Right CLI panel toolbar plus should explicitly terminate the current provider PTY before showing the picker.',
197
+ );
198
+
199
+ assert.match(
200
+ workbench,
201
+ /forceNewSession=\{terminalLaunch\.forceNewSession\}/,
202
+ 'Right CLI panel should mark explicitly started new sessions so the backend does not reconnect the old PTY.',
179
203
  );
180
204
 
181
205
  assert.match(
@@ -186,8 +210,8 @@ assert.match(
186
210
 
187
211
  assert.match(
188
212
  workbench,
189
- /onNewSession=\{startNewCliSession\}/,
190
- 'Right CLI terminal toolbar should wire the plus button to a new CLI session.',
213
+ /onNewSession=\{openNewCliSessionPicker\}/,
214
+ 'Right CLI terminal toolbar should wire the plus button to the new-session picker flow.',
191
215
  );
192
216
 
193
217
  assert.doesNotMatch(
package/server/index.js CHANGED
@@ -285,8 +285,44 @@ const server = http.createServer(app);
285
285
  const ptySessionsMap = new Map();
286
286
  const PTY_SESSION_TIMEOUT = 30 * 60 * 1000;
287
287
  const SHELL_URL_PARSE_BUFFER_LIMIT = 32768;
288
+ const SHELL_CLI_PROVIDERS = new Set(['claude', 'codex', 'cursor', 'gemini', 'qwen', 'opencode']);
288
289
  import { stripAnsiSequences, normalizeDetectedUrl, extractUrlsFromText, shouldAutoOpenUrlFromOutput } from './utils/url-detection.js';
289
290
 
291
+ function terminatePtySession(sessionKey, session, reason) {
292
+ if (!session) return false;
293
+
294
+ console.log(`🧹 Terminating PTY session (${reason}):`, sessionKey);
295
+ if (session.timeoutId) {
296
+ clearTimeout(session.timeoutId);
297
+ }
298
+
299
+ try {
300
+ if (session.pty && session.pty.kill) {
301
+ session.pty.kill();
302
+ }
303
+ } catch (error) {
304
+ console.warn('Failed to kill PTY session:', error.message);
305
+ }
306
+
307
+ ptySessionsMap.delete(sessionKey);
308
+ return true;
309
+ }
310
+
311
+ function killProviderPtySessions(projectPath, provider) {
312
+ let killed = 0;
313
+ for (const [sessionKey, session] of ptySessionsMap.entries()) {
314
+ if (
315
+ session?.projectPath === projectPath &&
316
+ session?.provider === provider &&
317
+ !session?.isPlainShell
318
+ ) {
319
+ killed += terminatePtySession(sessionKey, session, 'fresh provider session') ? 1 : 0;
320
+ }
321
+ }
322
+
323
+ return killed;
324
+ }
325
+
290
326
  // Single WebSocket server that handles both paths
291
327
  const wss = new WebSocketServer({
292
328
  server,
@@ -358,6 +394,18 @@ app.get('/health', (req, res) => {
358
394
  // Optional API key validation (if configured)
359
395
  app.use('/api', validateApiKey);
360
396
 
397
+ app.post('/api/shell/sessions/terminate', authenticateToken, (req, res) => {
398
+ const provider = req.body?.provider || 'claude';
399
+ const projectPath = req.body?.projectPath || os.homedir();
400
+
401
+ if (!SHELL_CLI_PROVIDERS.has(provider)) {
402
+ return res.status(400).json({ error: 'Unsupported provider' });
403
+ }
404
+
405
+ const killedSessions = killProviderPtySessions(projectPath, provider);
406
+ res.json({ success: true, killedSessions });
407
+ });
408
+
361
409
  // Authentication routes (public)
362
410
  app.use('/api/auth', authRoutes);
363
411
 
@@ -2044,6 +2092,7 @@ function handleShellConnection(ws) {
2044
2092
  const provider = data.provider || 'claude';
2045
2093
  const initialCommand = data.initialCommand;
2046
2094
  const isPlainShell = data.isPlainShell || (!!initialCommand && !hasSession) || provider === 'plain-shell';
2095
+ const forceNewSession = Boolean(data.forceNewSession && !isPlainShell);
2047
2096
  urlDetectionBuffer = '';
2048
2097
  announcedAuthUrls.clear();
2049
2098
 
@@ -2077,14 +2126,16 @@ function handleShellConnection(ws) {
2077
2126
  if (isLoginCommand) {
2078
2127
  const oldSession = ptySessionsMap.get(ptySessionKey);
2079
2128
  if (oldSession) {
2080
- console.log('🧹 Cleaning up existing login session:', ptySessionKey);
2081
- if (oldSession.timeoutId) clearTimeout(oldSession.timeoutId);
2082
- if (oldSession.pty && oldSession.pty.kill) oldSession.pty.kill();
2083
- ptySessionsMap.delete(ptySessionKey);
2129
+ terminatePtySession(ptySessionKey, oldSession, 'fresh login');
2130
+ }
2131
+ } else if (forceNewSession) {
2132
+ const killedSessions = killProviderPtySessions(projectPath, provider);
2133
+ if (killedSessions > 0) {
2134
+ console.log(`🧹 Fresh ${provider} session requested; terminated ${killedSessions} cached PTY session(s).`);
2084
2135
  }
2085
2136
  }
2086
2137
 
2087
- const existingSession = isLoginCommand ? null : ptySessionsMap.get(ptySessionKey);
2138
+ const existingSession = (isLoginCommand || forceNewSession) ? null : ptySessionsMap.get(ptySessionKey);
2088
2139
  if (existingSession) {
2089
2140
  console.log('♻️ Reconnecting to existing PTY session:', ptySessionKey);
2090
2141
  shellProcess = existingSession.pty;
@@ -2292,7 +2343,9 @@ function handleShellConnection(ws) {
2292
2343
  buffer: [],
2293
2344
  timeoutId: null,
2294
2345
  projectPath,
2295
- sessionId
2346
+ sessionId,
2347
+ provider,
2348
+ isPlainShell
2296
2349
  });
2297
2350
 
2298
2351
  // Handle data output