jupyterlab_claude_code_extension 1.2.5 → 1.2.9

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.
@@ -138,10 +138,15 @@ describe('launch spinner dismiss contract', () => {
138
138
  /private async _switchBranch[\s\S]*?\n \}/
139
139
  ) ?? [''])[0];
140
140
 
141
- it('shows the conversation count only when the project has branches', () => {
142
- expect(widgetSrc).toMatch(
143
- /session\.extra_sessions > 0\s*\?\s*`\$\{this\._lookupName\(session\)\} \(\$\{session\.extra_sessions \+ 1\}\)`/
141
+ it('shows a branch icon + count badge only when the project has branches', () => {
142
+ const renderRow = (widgetSrc.match(
143
+ /private _renderRow[\s\S]*?\n \}/
144
+ ) ?? [''])[0];
145
+ expect(renderRow).toMatch(
146
+ /session\.extra_sessions > 0[\s\S]*?jp-ClaudeSessionsPanel-branchBadge/
144
147
  );
148
+ expect(renderRow).toMatch(/branchIcon\.element/);
149
+ expect(renderRow).toMatch(/String\(session\.extra_sessions \+ 1\)/);
145
150
  });
146
151
 
147
152
  it('rebuilds submenu items from a fresh branches fetch on open', () => {
@@ -150,10 +155,21 @@ describe('launch spinner dismiss contract', () => {
150
155
  expect(openMenu).toMatch(/_rebuildContextMenu\(hasBranches\)/);
151
156
  });
152
157
 
153
- it('caps the submenu at 5 and adds More... beyond that', () => {
158
+ it('caps the inline submenu at 5 most recent', () => {
154
159
  expect(openMenu).toMatch(/\.slice\(0, 5\)/);
160
+ });
161
+
162
+ it('always adds the Manage Sessions entry, no >5 gate', () => {
163
+ // The popup is the management hub - it must be reachable even for
164
+ // projects with 2-5 conversations, so the entry is unconditional.
165
+ expect(openMenu).toMatch(/switch-branch-more/);
166
+ expect(openMenu).not.toMatch(/branches\.length > 5/);
167
+ expect(widgetSrc).toMatch(/`Manage Sessions\.\.\. \(\$\{/);
168
+ });
169
+
170
+ it('titles the submenu Switch and Manage Sessions with the count', () => {
155
171
  expect(openMenu).toMatch(
156
- /branches\.length > 5[\s\S]*?switch-branch-more/
172
+ /title\.label = `Switch and Manage Sessions \(\$\{data\.branches\.length\}\)`/
157
173
  );
158
174
  });
159
175
 
@@ -183,6 +199,70 @@ describe('launch spinner dismiss contract', () => {
183
199
  );
184
200
  });
185
201
 
202
+ it('popup leads with the current row, badged and without checkbox', () => {
203
+ const popup = (widgetSrc.match(
204
+ /private _showBranchPopup[\s\S]*?\n \}/
205
+ ) ?? [''])[0];
206
+ expect(popup).toMatch(/jp-mod-current/);
207
+ expect(popup).toMatch(/branchCurrentBadge/);
208
+ // The current row is appended before any checkbox is created.
209
+ const currentIdx = popup.indexOf('jp-mod-current');
210
+ const checkboxIdx = popup.indexOf("check.type = 'checkbox'");
211
+ expect(currentIdx).toBeGreaterThan(-1);
212
+ expect(checkboxIdx).toBeGreaterThan(currentIdx);
213
+ });
214
+
215
+ it('checkbox is its own click zone and selection gates row switch', () => {
216
+ const popup = (widgetSrc.match(
217
+ /private _showBranchPopup[\s\S]*?\n \}/
218
+ ) ?? [''])[0];
219
+ expect(popup).toMatch(/stopPropagation\(\)/);
220
+ // Selection mode: row click toggles while anything is selected.
221
+ expect(popup).toMatch(
222
+ /if \(selected\.size > 0\) \{[\s\S]*?return;[\s\S]*?dialog\.dispose\(\)/
223
+ );
224
+ });
225
+
226
+ it('select-all toggles the visible (filtered) rows only', () => {
227
+ const popup = (widgetSrc.match(
228
+ /private _showBranchPopup[\s\S]*?\n \}/
229
+ ) ?? [''])[0];
230
+ expect(popup).toMatch(
231
+ /selectAll\.addEventListener\('change'[\s\S]*?visibleMatches\(\)/
232
+ );
233
+ });
234
+
235
+ it('delete is two-step and any selection change disarms it', () => {
236
+ const popup = (widgetSrc.match(
237
+ /private _showBranchPopup[\s\S]*?\n \}/
238
+ ) ?? [''])[0];
239
+ expect(popup).toMatch(/`Confirm delete \(\$\{selected\.size\}\)`/);
240
+ // updateControls (run on every selection change) resets the armed
241
+ // state and the button label.
242
+ expect(popup).toMatch(
243
+ /const updateControls = \(\) => \{\s*confirmArmed = false/
244
+ );
245
+ });
246
+
247
+ it('delete posts to delete-branches and resyncs the panel', () => {
248
+ const del = (widgetSrc.match(
249
+ /private async _deleteBranches[\s\S]*?\n \}/
250
+ ) ?? [''])[0];
251
+ expect(del).toMatch(/sessions\/delete-branches/);
252
+ expect(del).toMatch(/finally[\s\S]*?await this\._fetch\(\)/);
253
+ });
254
+
255
+ it('rows carry age emphasis classes at the 60s and 7d thresholds', () => {
256
+ expect(widgetSrc).toMatch(/age < 60_000[\s\S]*?jp-mod-recentlyActive/);
257
+ expect(widgetSrc).toMatch(/age > 7 \* 86_400_000[\s\S]*?jp-mod-stale/);
258
+ const css: string = fs.readFileSync(
259
+ path.join(__dirname, '..', '..', 'style', 'base.css'),
260
+ 'utf-8'
261
+ );
262
+ expect(css).toMatch(/jp-mod-recentlyActive[\s\S]*?--jp-brand-color1/);
263
+ expect(css).toMatch(/jp-mod-stale \{\s*opacity/);
264
+ });
265
+
186
266
  it('opens the menu without the submenu when the fetch fails', () => {
187
267
  expect(openMenu).toMatch(/catch[\s\S]*?hasBranches = false/);
188
268
  });
@@ -218,6 +298,87 @@ describe('launch spinner dismiss contract', () => {
218
298
  /result\.current !== result\.requested[\s\S]*?Notification\.warning/
219
299
  );
220
300
  });
301
+
302
+ it('renders the favourite star before the time column', () => {
303
+ const renderRow = (widgetSrc.match(
304
+ /private _renderRow[\s\S]*?\n \}/
305
+ ) ?? [''])[0];
306
+ const starAt = renderRow.indexOf('jp-ClaudeSessionsPanel-favStar');
307
+ const timeAt = renderRow.indexOf('jp-ClaudeSessionsPanel-rowTime');
308
+ expect(starAt).toBeGreaterThan(-1);
309
+ expect(timeAt).toBeGreaterThan(-1);
310
+ expect(starAt).toBeLessThan(timeAt);
311
+ });
312
+
313
+ it('time labels form fixed-width right-aligned columns', () => {
314
+ const css: string = fs.readFileSync(
315
+ path.join(__dirname, '..', '..', 'style', 'base.css'),
316
+ 'utf-8'
317
+ );
318
+ const rowTime = (css.match(
319
+ /\.jp-ClaudeSessionsPanel-rowTime \{[\s\S]*?\}/
320
+ ) ?? [''])[0];
321
+ expect(rowTime).toMatch(/width: 52px/);
322
+ expect(rowTime).toMatch(/text-align: right/);
323
+ const branchTime = (css.match(
324
+ /\.jp-ClaudeSessionsPanel-branchTime \{[\s\S]*?\}/
325
+ ) ?? [''])[0];
326
+ expect(branchTime).toMatch(/width: 52px/);
327
+ expect(branchTime).toMatch(/text-align: right/);
328
+ });
329
+
330
+ it('now label shares the recently-active emphasis colour', () => {
331
+ const css: string = fs.readFileSync(
332
+ path.join(__dirname, '..', '..', 'style', 'base.css'),
333
+ 'utf-8'
334
+ );
335
+ expect(css).toMatch(
336
+ /jp-mod-recentlyActive[\s\S]{0,200}?jp-ClaudeSessionsPanel-rowTime[\s\S]{0,80}?--jp-brand-color1/
337
+ );
338
+ });
339
+
340
+ it('branch session commands exist in normal and skip-permissions modes', () => {
341
+ expect(widgetSrc).toMatch(/claude-code-sessions:branch-session'/);
342
+ expect(widgetSrc).toMatch(
343
+ /claude-code-sessions:branch-session-dangerous/
344
+ );
345
+ expect(widgetSrc).toMatch(
346
+ /Branch Session \(Skip Permissions\)\.\.\.[\s\S]{0,80}?icon: shieldIcon/
347
+ );
348
+ });
349
+
350
+ it('branch session asks for a name and launches a known fork id', () => {
351
+ const branch = (widgetSrc.match(
352
+ /private async _branchSession[\s\S]*?\n \}/
353
+ ) ?? [''])[0];
354
+ expect(branch).toMatch(/InputDialog\.getText/);
355
+ expect(branch).toMatch(/UUID\.uuid4\(\)/);
356
+ expect(branch).toMatch(/fork_session_id: forkId/);
357
+ expect(branch).toMatch(/session_id: session\.session_id/);
358
+ expect(branch).toMatch(/_stampForkTitle/);
359
+ });
360
+
361
+ it('fork title stamping retries on 404 until the JSONL appears', () => {
362
+ const stamp = (widgetSrc.match(
363
+ /private async _stampForkTitle[\s\S]*?\n \}/
364
+ ) ?? [''])[0];
365
+ expect(stamp).toMatch(/sessions\/set-title/);
366
+ expect(stamp).toMatch(/status === 404/);
367
+ expect(stamp).toMatch(/await this\._fetch\(\)/);
368
+ expect(stamp).toMatch(/Notification\.warning/);
369
+ });
370
+
371
+ it('live dot is softened with reduced opacity', () => {
372
+ const css: string = fs.readFileSync(
373
+ path.join(__dirname, '..', '..', 'style', 'base.css'),
374
+ 'utf-8'
375
+ );
376
+ const dot = (css.match(/\.jp-ClaudeSessionsPanel-dot \{[\s\S]*?\}/) ?? [
377
+ ''
378
+ ])[0];
379
+ expect(dot).toMatch(/--jp-success-color1/);
380
+ expect(dot).toMatch(/opacity: 0\.75/);
381
+ });
221
382
  });
222
383
 
223
384
  it('_doResumeInTerminal dismisses spinner via dispose(), not resolve()', () => {
package/src/icons.ts CHANGED
@@ -71,6 +71,17 @@ export const addIcon = new LabIcon({
71
71
  svgstr: addSvgStr
72
72
  });
73
73
 
74
+ // Git-branch glyph (Octicons git-branch-16, MIT) - marks rows that carry
75
+ // parallel conversations and the Branch Session menu entries.
76
+ const branchSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
77
+ <path class="jp-icon3" fill="#616161" d="M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.493 2.493 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628a2.25 2.25 0 0 1-1.5-2.122Zm-6 0a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Zm8.25-.75a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z"/>
78
+ </svg>`;
79
+
80
+ export const branchIcon = new LabIcon({
81
+ name: 'jupyterlab_claude_code_extension:branch',
82
+ svgstr: branchSvgStr
83
+ });
84
+
74
85
  // Funnel copied verbatim from @jupyterlab/ui-components'
75
86
  // `search/filter.svg` - the same image the file browser's filter
76
87
  // toggle uses. The `class="jp-icon3"` lets JupyterLab's theme drive
package/src/types.ts CHANGED
@@ -73,6 +73,15 @@ export interface ISwitchResponse {
73
73
  current: string | null;
74
74
  }
75
75
 
76
+ export interface IDeleteBranchesRequest {
77
+ encoded_path: string;
78
+ session_ids: string[];
79
+ }
80
+
81
+ export interface IDeleteBranchesResponse {
82
+ removed_count: number;
83
+ }
84
+
76
85
  export interface ILaunchTerminalRequest {
77
86
  project_path: string;
78
87
  /** Omit to start a brand-new claude session instead of resuming one. */