@pixelbyte-software/pixcode 1.48.2 → 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.
@@ -12,6 +12,10 @@ const chatComposer = read('src/components/chat/view/subcomponents/ChatComposer.t
12
12
  const workerSlots = read('src/components/chat/view/subcomponents/WorkerSlotsControl.tsx');
13
13
  const wizard = read('src/components/project-creation-wizard/ProjectCreationWizard.tsx');
14
14
  const sidebar = read('src/components/sidebar/view/Sidebar.tsx');
15
+ const shellTerminal = read('src/components/shell/hooks/useShellTerminal.ts');
16
+ const gitPanel = read('src/components/git-panel/view/GitPanel.tsx');
17
+ const gitPanelHeader = read('src/components/git-panel/view/GitPanelHeader.tsx');
18
+ const gitViewTabs = read('src/components/git-panel/view/GitViewTabs.tsx');
15
19
 
16
20
  assert.match(
17
21
  workbench,
@@ -59,6 +63,51 @@ assert.match(
59
63
  'Workbench should render Chrome-style workspace tabs directly under the menu bar.',
60
64
  );
61
65
 
66
+ assert.doesNotMatch(
67
+ workbench,
68
+ /\.\.\.currentTabs\.filter\(\(tab\) => tab\.id !== tabId\)/,
69
+ 'Selecting an existing workspace tab must preserve tab order instead of moving it to the end.',
70
+ );
71
+
72
+ assert.match(
73
+ workbench,
74
+ /function WorkspaceTabContextMenu/,
75
+ 'Workspace tabs should expose their actions through a right-click context menu.',
76
+ );
77
+
78
+ assert.match(
79
+ workbench,
80
+ /onContextMenu=\{\(event\) => openWorkspaceContextMenu\(event, tab\)\}/,
81
+ 'Workspace tab actions should open from right-click on the tab.',
82
+ );
83
+
84
+ const workspaceTabsSource = workbench.slice(
85
+ workbench.indexOf('function WorkbenchWorkspaceTabs'),
86
+ workbench.indexOf('function EditorTabContextMenu'),
87
+ );
88
+
89
+ assert.doesNotMatch(
90
+ workspaceTabsSource,
91
+ /MoreHorizontal/,
92
+ 'Workspace tabs should not render a three-dot action button.',
93
+ );
94
+
95
+ for (const token of ['closeOtherWorkspaces', 'closeAllWorkspaces']) {
96
+ assert.match(workbench, new RegExp(token), `Workspace tab context menu should support ${token}.`);
97
+ }
98
+
99
+ assert.match(
100
+ workspaceTabsSource,
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.',
109
+ );
110
+
62
111
  assert.match(
63
112
  workbench,
64
113
  /WORKBENCH_WORKSPACE_TABS_STORAGE_KEY/,
@@ -113,8 +162,8 @@ assert.match(
113
162
 
114
163
  assert.match(
115
164
  workbench,
116
- /onClose=\{\(\) => setIsTerminalOpen\(false\)\}/,
117
- '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.',
118
167
  );
119
168
 
120
169
  assert.match(
@@ -123,6 +172,48 @@ assert.match(
123
172
  'CLI history should be integrated as a polished project-scoped panel.',
124
173
  );
125
174
 
175
+ assert.match(
176
+ workbench,
177
+ /terminalSession/,
178
+ 'Right CLI panel should track whether the terminal is running a new session or a selected history session.',
179
+ );
180
+
181
+ assert.match(
182
+ workbench,
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.',
203
+ );
204
+
205
+ assert.match(
206
+ workbench,
207
+ /function WorkbenchCliPanelToolbar/,
208
+ 'Right CLI terminal should keep compact History and New Session actions visible while the terminal is open.',
209
+ );
210
+
211
+ assert.match(
212
+ workbench,
213
+ /onNewSession=\{openNewCliSessionPicker\}/,
214
+ 'Right CLI terminal toolbar should wire the plus button to the new-session picker flow.',
215
+ );
216
+
126
217
  assert.doesNotMatch(
127
218
  workbench,
128
219
  /return <Sidebar \{\.\.\.sidebarProps\} isMobile=\{false\} \/>/,
@@ -170,6 +261,28 @@ assert.match(
170
261
  'Projects activity should stay selected while the center chat tab is active.',
171
262
  );
172
263
 
264
+ assert.match(
265
+ workbench,
266
+ /setActivityPanel\('explorer'\)/,
267
+ 'Selecting a project from Projects should switch the left pane back to Explorer.',
268
+ );
269
+
270
+ assert.match(
271
+ workbench,
272
+ /<GitPanel selectedProject=\{selectedProject\} isMobile=\{false\} compact onFileOpen=\{handleFileOpen\}/,
273
+ 'Source Control should render in compact icon-first mode inside the VS Code workbench side panel.',
274
+ );
275
+
276
+ assert.match(gitPanel, /compact = false/, 'GitPanel should accept a compact prop.');
277
+ assert.match(gitPanelHeader, /compact/, 'GitPanelHeader should receive compact mode for narrow panes.');
278
+ assert.match(gitViewTabs, /compact/, 'GitViewTabs should render compact icon-only tabs.');
279
+
280
+ assert.doesNotMatch(
281
+ shellTerminal,
282
+ /new WebglAddon\(\)/,
283
+ 'Shell terminal should avoid the WebGL renderer that can leave stale glyph trails with OpenCode output.',
284
+ );
285
+
173
286
  assert.match(chatInterface, /compactComposer\?: boolean/, 'ChatInterface should expose compactComposer for narrow workbench panes.');
174
287
  assert.match(chatComposer, /compact\?: boolean/, 'ChatComposer should expose a compact prop.');
175
288
  assert.match(chatComposer, /flex-wrap/, 'ChatComposer footer should wrap controls in narrow panes.');
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