jupyterlab_claude_code_extension 1.2.24 → 1.2.26

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.
@@ -240,15 +240,56 @@ describe('launch spinner dismiss contract', () => {
240
240
  );
241
241
  });
242
242
 
243
- it('delete opens a confirmation dialog and only deletes on accept', () => {
243
+ it('delete removes selected sessions immediately, no confirmation dialog', () => {
244
244
  const popup = (widgetSrc.match(
245
245
  /private _showBranchPopup[\s\S]*?\n \}/
246
246
  ) ?? [''])[0];
247
- expect(popup).toMatch(/showDialog\(\{\s*title: 'Delete Sessions'/);
248
- expect(popup).toMatch(/Dialog\.warnButton\(\{ label: 'Delete' \}\)/);
249
- expect(popup).toMatch(
250
- /if \(!confirm\.button\.accept\) \{\s*return;[\s\S]*?_deleteBranches/
247
+ // The per-row delete must NOT stack a second Lumino dialog on the popup
248
+ // (it renders detached) - confirmation is reserved for cleanup / remove.
249
+ expect(popup).not.toMatch(/title: 'Delete Sessions'/);
250
+ const delHandler = (popup.match(
251
+ /deleteBtn\.addEventListener\('click'[\s\S]*?\n \}\);/
252
+ ) ?? [''])[0];
253
+ expect(delHandler).toMatch(/this\._deleteBranches\(\[\.\.\.selected\]\)/);
254
+ expect(delHandler).not.toMatch(/showDialog/);
255
+ });
256
+
257
+ it('manage sessions popup is a table with an accented pinned current row', () => {
258
+ const popup = (widgetSrc.match(
259
+ /private _showBranchPopup[\s\S]*?\n \}/
260
+ ) ?? [''])[0];
261
+ expect(popup).toMatch(/title: 'Manage Sessions'/);
262
+ expect(popup).toMatch(/branchHeader/);
263
+ expect(popup).toMatch(/branchHeaderCount/);
264
+ expect(popup).toMatch(/branchSelectCell/);
265
+ const css: string = fs.readFileSync(
266
+ path.join(__dirname, '..', '..', 'style', 'base.css'),
267
+ 'utf-8'
251
268
  );
269
+ const cur = (css.match(
270
+ /\.jp-ClaudeSessionsPanel-branchRow\.jp-mod-current \{[\s\S]*?\}/
271
+ ) ?? [''])[0];
272
+ expect(cur).toMatch(/position: sticky/);
273
+ expect(cur).toMatch(/border-left: 3px solid var\(--jp-brand-color1\)/);
274
+ const del = (css.match(
275
+ /\.jp-ClaudeSessionsPanel-branchDelete \{[\s\S]*?\}/
276
+ ) ?? [''])[0];
277
+ expect(del).toMatch(/--jp-error-color1/);
278
+ });
279
+
280
+ it('popup delete is accessible and gives feedback without a prompt', () => {
281
+ const popup = (widgetSrc.match(
282
+ /private _showBranchPopup[\s\S]*?\n \}/
283
+ ) ?? [''])[0];
284
+ // per-checkbox accessible name + polite live region for the result
285
+ expect(popup).toMatch(/`Select \$\{this\._branchDisplayName/);
286
+ expect(popup).toMatch(/aria-live', 'polite'/);
287
+ // the delete handler announces "N moved to trash" and keeps focus
288
+ const delHandler = (popup.match(
289
+ /deleteBtn\.addEventListener\('click'[\s\S]*?\n \}\);/
290
+ ) ?? [''])[0];
291
+ expect(delHandler).toMatch(/moved to trash/);
292
+ expect(delHandler).toMatch(/selectAll\.focus\(\)/);
252
293
  });
253
294
 
254
295
  it('delete posts to delete-branches and resyncs the panel', () => {
@@ -465,6 +506,141 @@ describe('launch spinner dismiss contract', () => {
465
506
  });
466
507
  });
467
508
 
509
+ /**
510
+ * Contract for conversation-aware terminal reuse and the Open Branched
511
+ * Conversation feature. The reuse path must refuse to focus a terminal
512
+ * running a DIFFERENT known conversation (the switch-then-click bug), and
513
+ * opening a specific branch must launch its own terminal (strict) so
514
+ * several branches stay open independently.
515
+ */
516
+ describe('conversation-aware reuse + open-branch contract', () => {
517
+ const findTerm = (widgetSrc.match(
518
+ /private async _findTerminalForCwd[\s\S]*?\n \}/
519
+ ) ?? [''])[0];
520
+ const doResume = (widgetSrc.match(
521
+ /private async _doResumeInTerminal[\s\S]*?\n \}/
522
+ ) ?? [''])[0];
523
+ const resume = (widgetSrc.match(
524
+ /private async _resumeInTerminal[\s\S]*?\n \}/
525
+ ) ?? [''])[0];
526
+ const openBranch = (widgetSrc.match(
527
+ /private async _openBranch[\s\S]*?\n \}/
528
+ ) ?? [''])[0];
529
+ const watch = (widgetSrc.match(/private _watchForBranch[\s\S]*?\n \}/) ?? [
530
+ ''
531
+ ])[0];
532
+
533
+ it('terminal-cwd response carries the running conversation id', () => {
534
+ const iface = (widgetSrc.match(
535
+ /interface ITerminalCwdResponse \{[\s\S]*?\}/
536
+ ) ?? [''])[0];
537
+ expect(iface).toMatch(/session_id\?: string \| null/);
538
+ });
539
+
540
+ it('_findTerminalForCwd takes the wanted id and a strict flag', () => {
541
+ expect(findTerm).toMatch(
542
+ /_findTerminalForCwd\(\s*projectPath: string,\s*wantedSessionId: string \| undefined,\s*strict: boolean/
543
+ );
544
+ });
545
+
546
+ it('reuse rejects a terminal running a known different conversation', () => {
547
+ // Same conversation, or unknown (fresh) only in lenient mode, may reuse.
548
+ expect(findTerm).toMatch(/const runningId = data\.session_id \?\? null/);
549
+ expect(findTerm).toMatch(
550
+ /sameConversation = runningId === wantedSessionId/
551
+ );
552
+ expect(findTerm).toMatch(/unknownConversation = runningId === null/);
553
+ expect(findTerm).toMatch(
554
+ /if \(!\(sameConversation \|\| \(!strict && unknownConversation\)\)\) \{\s*continue;/
555
+ );
556
+ });
557
+
558
+ it('microcache reuse is gated on the conversation id', () => {
559
+ expect(doResume).toMatch(/cached\.sessionId === session\.session_id/);
560
+ expect(doResume).toMatch(/!strict && unknownConversation/);
561
+ });
562
+
563
+ it('microcache is tagged with the OBSERVED running id, not the wanted one', () => {
564
+ // Storing the wanted id would let a later strict reuse trust a
565
+ // fabricated tag and focus the wrong conversation (review finding 1).
566
+ expect(doResume).toMatch(
567
+ /widget: found\.widget,\s*sessionId: found\.runningId \?\? undefined/
568
+ );
569
+ expect(findTerm).toMatch(/return \{ widget, runningId \}/);
570
+ });
571
+
572
+ it('in-flight launches are keyed per conversation, not per project', () => {
573
+ expect(resume).toMatch(
574
+ /const key = `\$\{session\.project_path\}\\n\$\{session\.session_id \?\? ''\}`/
575
+ );
576
+ expect(resume).toMatch(/this\._pendingByPath\.(get|set)\(key/);
577
+ });
578
+
579
+ it('_openBranch is strict for a different branch, lenient for the current', () => {
580
+ // Strict on a non-current branch lands the user in THAT conversation;
581
+ // lenient on the current avoids a `claude --resume` "already in use"
582
+ // collision with a fresh terminal already running it (review finding 2).
583
+ expect(openBranch).toMatch(
584
+ /const strict = sessionId !== active\.session_id/
585
+ );
586
+ expect(openBranch).toMatch(
587
+ /this\._resumeInTerminal\(\s*\{ \.\.\.active, session_id: sessionId \},\s*false,\s*strict\s*\)/
588
+ );
589
+ });
590
+
591
+ it('open-branch command and submenu are wired up', () => {
592
+ expect(widgetSrc).toMatch(/'claude-code-sessions:open-branch'/);
593
+ expect(widgetSrc).toMatch(
594
+ /this\._openBranchSubmenu\.title\.label = 'Open Branched Conversation'/
595
+ );
596
+ // Top 5 branches populate the open submenu, each via the open command.
597
+ const populate = (widgetSrc.match(
598
+ /this\._openBranchSubmenu\.clearItems[\s\S]*?switch-branch-more'\s*\}\);/
599
+ ) ?? [''])[0];
600
+ expect(populate).toMatch(/data\.branches\.slice\(0, 5\)/);
601
+ expect(populate).toMatch(/command: 'claude-code-sessions:open-branch'/);
602
+ // The context menu shows the open submenu when the row has branches.
603
+ const rebuild = (widgetSrc.match(
604
+ /private _rebuildContextMenu[\s\S]*?\n \}/
605
+ ) ?? [''])[0];
606
+ expect(rebuild).toMatch(/submenu: this\._openBranchSubmenu/);
607
+ });
608
+
609
+ it('popup rows carry an Open button that launches the branch', () => {
610
+ const popup = (widgetSrc.match(
611
+ /private _showBranchPopup[\s\S]*?\n \}\n/
612
+ ) ?? [''])[0];
613
+ expect(popup).toMatch(/jp-ClaudeSessionsPanel-branchOpen/);
614
+ expect(popup).toMatch(/void this\._openBranch\(sessionId\)/);
615
+ // Open appears on both the current row and each branch row.
616
+ expect(popup).toMatch(/openButton\(current\)/);
617
+ expect(popup).toMatch(/openButton\(b\.session_id\)/);
618
+ });
619
+
620
+ it('a new branch is watched for and surfaces fast, not on the slow poll', () => {
621
+ expect(watch).toMatch(/data\.current === forkId/);
622
+ expect(watch).toMatch(/b\.session_id === forkId/);
623
+ expect(watch).toMatch(/await this\._fetch\(\)/);
624
+ expect(watch).toMatch(/BRANCH_WATCH_MAX_ATTEMPTS/);
625
+ // Fork launch arms the watcher.
626
+ const fork = (widgetSrc.match(
627
+ /private async _branchSession[\s\S]*?\n \}/
628
+ ) ?? [''])[0];
629
+ expect(fork).toMatch(
630
+ /this\._watchForBranch\(session\.encoded_path, forkId\)/
631
+ );
632
+ expect(widgetSrc).toMatch(/BRANCH_WATCH_INTERVAL_MS = 2_000/);
633
+ });
634
+
635
+ it('the Open button is styled in base.css', () => {
636
+ const css: string = fs.readFileSync(
637
+ path.join(__dirname, '..', '..', 'style', 'base.css'),
638
+ 'utf-8'
639
+ );
640
+ expect(css).toMatch(/\.jp-ClaudeSessionsPanel-branchOpen \{/);
641
+ });
642
+ });
643
+
468
644
  it('_doResumeInTerminal dismisses spinner via dispose(), not resolve()', () => {
469
645
  expect(widgetSrc).toMatch(/spinner\.dispose\(\)/);
470
646
  expect(widgetSrc).not.toMatch(/spinner\.resolve\(\)/);