@plmbr/notebook-intelligence 5.0.1 → 5.1.0-a.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plmbr/notebook-intelligence",
3
- "version": "5.0.1",
3
+ "version": "5.1.0-a.1",
4
4
  "description": "AI coding assistant for JupyterLab",
5
5
  "keywords": [
6
6
  "AI",
@@ -49,6 +49,7 @@
49
49
  "install:extension": "jlpm build",
50
50
  "lint": "jlpm stylelint && jlpm prettier && jlpm eslint",
51
51
  "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check",
52
+ "postinstall": "husky || exit 0",
52
53
  "prettier": "jlpm prettier:base --write --list-different",
53
54
  "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"",
54
55
  "prettier:check": "jlpm prettier:base --check",
@@ -98,8 +99,10 @@
98
99
  "eslint": "^8.36.0",
99
100
  "eslint-config-prettier": "^8.8.0",
100
101
  "eslint-plugin-prettier": "^5.0.0",
102
+ "husky": "^9.1.7",
101
103
  "jest": "^29.7.0",
102
104
  "jest-environment-jsdom": "^29.7.0",
105
+ "lint-staged": "^15.2.10",
103
106
  "mkdirp": "^1.0.3",
104
107
  "monaco-editor-webpack-plugin": "^2.0.0",
105
108
  "npm-run-all": "^4.1.5",
@@ -238,6 +241,9 @@
238
241
  }
239
242
  ]
240
243
  },
244
+ "lint-staged": {
245
+ "*": "prettier --write --ignore-unknown"
246
+ },
241
247
  "stylelint": {
242
248
  "extends": [
243
249
  "stylelint-config-recommended",
package/src/api.ts CHANGED
@@ -388,6 +388,10 @@ export class NBIConfig {
388
388
  return this.capabilities.claude_settings;
389
389
  }
390
390
 
391
+ get spinnerVerbs(): { mode: string; verbs: string[] } | null {
392
+ return this.capabilities.spinner_verbs ?? null;
393
+ }
394
+
391
395
  get claudeModels(): IClaudeModelInfo[] {
392
396
  return (this.capabilities.claude_models ?? []).map(claudeModelFromWire);
393
397
  }
@@ -435,6 +439,10 @@ export class NBIConfig {
435
439
  return this.capabilities.chat_feedback_enabled === true;
436
440
  }
437
441
 
442
+ get chatFeedbackAlwaysVisible(): boolean {
443
+ return this.capabilities.chat_feedback_always_visible === true;
444
+ }
445
+
438
446
  // Admin-supplied tour-copy overrides, served from the capabilities
439
447
  // response after server-side validation. Returns the raw dict; the
440
448
  // tour module decides how to apply it. Defaults to a shared frozen
@@ -80,6 +80,8 @@ import { mcpServerSettingsToEnabledState } from './components/mcp-util';
80
80
  import claudeSvgStr from '../style/icons/claude.svg';
81
81
  import { AskUserQuestion } from './components/ask-user-question';
82
82
  import { ClaudeSessionPicker } from './components/claude-session-picker';
83
+ import { ToolCallGroup } from './components/tool-call-group';
84
+ import { upsertToolCallContent } from './tool-call-stream';
83
85
  import { TourOverlay } from './tour/tour-overlay';
84
86
  import { TOUR_ANCHOR } from './tour/tour-anchors';
85
87
  import { TOUR_START_EVENT, TOUR_STOP_EVENT } from './tour/tour-events';
@@ -522,6 +524,92 @@ function ChatResponseHTMLFrame(props: any) {
522
524
  // Memoize ChatResponse for performance
523
525
  function ChatResponse(props: any) {
524
526
  const [renderCount, setRenderCount] = useState(0);
527
+ const shuffledOrder = useRef<number[]>([]);
528
+ const shufflePos = useRef(0);
529
+
530
+ const _spinnerVerbs = NBIAPI.config.isInClaudeCodeMode
531
+ ? (NBIAPI.config.spinnerVerbs ?? null)
532
+ : null;
533
+ const hasCustomVerbs =
534
+ _spinnerVerbs?.mode === 'replace' &&
535
+ Array.isArray(_spinnerVerbs.verbs) &&
536
+ _spinnerVerbs.verbs.length > 0;
537
+
538
+ // Fisher-Yates shuffle. When `avoidFirst` is set, swap it out of
539
+ // position 0 so the new first verb is never the same as the last shown
540
+ // (prevents an identical repeat at the wrap point between passes).
541
+ const shuffleVerbs = (verbs: any[], avoidFirst?: number): number[] => {
542
+ const order = verbs.map((_, i) => i);
543
+ for (let i = order.length - 1; i > 0; i--) {
544
+ const j = Math.floor(Math.random() * (i + 1));
545
+ [order[i], order[j]] = [order[j], order[i]];
546
+ }
547
+ if (
548
+ avoidFirst !== undefined &&
549
+ order[0] === avoidFirst &&
550
+ order.length > 1
551
+ ) {
552
+ [order[0], order[1]] = [order[1], order[0]];
553
+ }
554
+ return order;
555
+ };
556
+
557
+ // Initialize the shuffle synchronously in useState so the correct verb
558
+ // is shown on the very first paint. A useEffect-based init fires after
559
+ // paint and causes a single-frame flash of verbs[0] before correcting.
560
+ const [verbIndex, setVerbIndex] = useState(() => {
561
+ const sv = NBIAPI.config.isInClaudeCodeMode
562
+ ? (NBIAPI.config.spinnerVerbs ?? null)
563
+ : null;
564
+ if (
565
+ !sv ||
566
+ sv.mode !== 'replace' ||
567
+ !Array.isArray(sv.verbs) ||
568
+ sv.verbs.length === 0
569
+ ) {
570
+ return 0;
571
+ }
572
+ const order = shuffleVerbs(sv.verbs);
573
+ shuffledOrder.current = order;
574
+ shufflePos.current = 0;
575
+ return order[0];
576
+ });
577
+
578
+ useEffect(() => {
579
+ if (!props.showGenerating || !hasCustomVerbs) {
580
+ return;
581
+ }
582
+
583
+ const verbs = _spinnerVerbs!.verbs;
584
+
585
+ // Shuffle already initialized by useState on mount. Only re-initialize
586
+ // if hasCustomVerbs just became true after a capabilities refresh
587
+ // (shuffledOrder would be empty because the lazy init found no verbs).
588
+ if (shuffledOrder.current.length === 0) {
589
+ shuffledOrder.current = shuffleVerbs(verbs);
590
+ shufflePos.current = 0;
591
+ setVerbIndex(shuffledOrder.current[0]);
592
+ }
593
+
594
+ let id: ReturnType<typeof setTimeout>;
595
+ const scheduleNext = () => {
596
+ const delay = 4000 + Math.random() * 3000;
597
+ id = setTimeout(() => {
598
+ shufflePos.current++;
599
+ if (shufflePos.current >= shuffledOrder.current.length) {
600
+ const lastShown =
601
+ shuffledOrder.current[shuffledOrder.current.length - 1];
602
+ shuffledOrder.current = shuffleVerbs(verbs, lastShown);
603
+ shufflePos.current = 0;
604
+ }
605
+ setVerbIndex(shuffledOrder.current[shufflePos.current]);
606
+ scheduleNext();
607
+ }, delay);
608
+ };
609
+ scheduleNext();
610
+ return () => clearTimeout(id);
611
+ }, [props.showGenerating, hasCustomVerbs]);
612
+
525
613
  const msg: IChatMessage = props.message;
526
614
  const timestamp = msg.date.toLocaleTimeString('en-US', { hour12: false });
527
615
 
@@ -630,6 +718,19 @@ function ChatResponse(props: any) {
630
718
  if (item.reasoningFinished) {
631
719
  lastItem.reasoningFinished = true;
632
720
  }
721
+ } else if (
722
+ item.type === ResponseStreamDataType.ToolCall &&
723
+ lastItemType === ResponseStreamDataType.ToolCall
724
+ ) {
725
+ // Bundle a run of consecutive tool calls into one group item so the
726
+ // renderer can collapse them; ToolCallGroup unwraps content.toolCalls.
727
+ const lastItem = groupedContents[groupedContents.length - 1];
728
+ lastItem.content.toolCalls.push(structuredClone(item.content));
729
+ } else if (item.type === ResponseStreamDataType.ToolCall) {
730
+ const grouped = structuredClone(item);
731
+ grouped.content = { toolCalls: [structuredClone(item.content)] };
732
+ groupedContents.push(grouped);
733
+ lastItemType = item.type;
633
734
  } else {
634
735
  groupedContents.push(structuredClone(item));
635
736
  lastItemType = item.type;
@@ -712,18 +813,26 @@ function ChatResponse(props: any) {
712
813
  }`}
713
814
  aria-hidden="true"
714
815
  />
715
- <div
716
- className="generating-label"
717
- aria-live="polite"
718
- aria-atomic="true"
719
- >
816
+ <div className="generating-label" aria-hidden="true">
720
817
  {props.isStalled
721
818
  ? 'Still working, server may be slow'
722
- : 'Generating'}
819
+ : hasCustomVerbs
820
+ ? _spinnerVerbs!.verbs[verbIndex]
821
+ : 'Generating'}
723
822
  {props.showGenerating && props.elapsedSeconds > 0
724
823
  ? ` (${formatElapsedSeconds(props.elapsedSeconds)})`
725
824
  : ''}
726
825
  </div>
826
+ {/* aria-live region contains only the verb — no elapsed suffix —
827
+ so screen readers announce only on verb changes, not on every
828
+ elapsed-seconds tick. */}
829
+ <div className="nbi-sr-only" aria-live="polite" aria-atomic="true">
830
+ {props.isStalled
831
+ ? 'Still working, server may be slow'
832
+ : hasCustomVerbs
833
+ ? _spinnerVerbs!.verbs[verbIndex]
834
+ : 'Generating'}
835
+ </div>
727
836
  </div>
728
837
  </div>
729
838
  <div className="chat-message-timestamp">{timestamp}</div>
@@ -857,6 +966,17 @@ function ChatResponse(props: any) {
857
966
  {item.content}
858
967
  </div>
859
968
  ) : null;
969
+ case ResponseStreamDataType.ToolCall:
970
+ // Unlike Progress, tool-call cards persist after the turn ends,
971
+ // so they render regardless of `showGenerating`. The grouping
972
+ // pass bundled this run's calls into content.toolCalls; the
973
+ // group renders a single card or a collapsible group.
974
+ return (
975
+ <ToolCallGroup
976
+ key={`key-${index}`}
977
+ toolCalls={item.content.toolCalls}
978
+ />
979
+ );
860
980
  case ResponseStreamDataType.Confirmation:
861
981
  return answeredForms.get(item.id) ===
862
982
  'confirmed' ? null : answeredForms.get(item.id) ===
@@ -973,9 +1093,13 @@ function ChatResponse(props: any) {
973
1093
  )}
974
1094
  </div>
975
1095
  {msg.from === 'copilot' &&
976
- !props.showGenerating &&
1096
+ (NBIAPI.config.chatFeedbackAlwaysVisible || !props.showGenerating) &&
977
1097
  NBIAPI.config.chatFeedbackEnabled && (
978
- <div className="chat-message-feedback">
1098
+ <div
1099
+ className={`chat-message-feedback${
1100
+ NBIAPI.config.chatFeedbackAlwaysVisible ? ' always-visible' : ''
1101
+ }`}
1102
+ >
979
1103
  <button
980
1104
  className={`chat-feedback-btn ${msg.feedback === 'positive' ? 'selected' : ''}`}
981
1105
  onClick={() => {
@@ -994,9 +1118,9 @@ function ChatResponse(props: any) {
994
1118
  });
995
1119
  }
996
1120
  }}
997
- aria-label="Rate as helpful"
1121
+ aria-label="Rate response as good"
998
1122
  aria-pressed={msg.feedback === 'positive'}
999
- title="Helpful"
1123
+ title="Good response"
1000
1124
  >
1001
1125
  {msg.feedback === 'positive' ? (
1002
1126
  <VscThumbsupFilled />
@@ -1022,9 +1146,9 @@ function ChatResponse(props: any) {
1022
1146
  });
1023
1147
  }
1024
1148
  }}
1025
- aria-label="Rate as unhelpful"
1149
+ aria-label="Rate response as bad"
1026
1150
  aria-pressed={msg.feedback === 'negative'}
1027
- title="Not helpful"
1151
+ title="Bad response"
1028
1152
  >
1029
1153
  {msg.feedback === 'negative' ? (
1030
1154
  <VscThumbsdownFilled />
@@ -2920,22 +3044,33 @@ function SidebarComponent(props: any) {
2920
3044
  }
2921
3045
  if (delta['nbiContent']) {
2922
3046
  const nbiContent = delta['nbiContent'];
2923
- contents.push({
2924
- id: UUID.uuid4(),
2925
- type: nbiContent.type,
2926
- content: nbiContent.content || '',
2927
- reasoningContent: nbiContent.reasoning_content || '',
2928
- reasoningTag: nbiContent.reasoning_content
2929
- ? '<think>'
2930
- : undefined,
2931
- reasoningFinished:
2932
- nbiContent.type === ResponseStreamDataType.Markdown &&
2933
- nbiContent.reasoning_content
2934
- ? true
2935
- : false,
2936
- contentDetail: nbiContent.detail,
2937
- created: new Date(response.created)
2938
- });
3047
+ if (nbiContent.type === ResponseStreamDataType.ToolCall) {
3048
+ // A tool call streams twice under one id (start, then finish);
3049
+ // merge by id so it stays one persistent card. See
3050
+ // upsertToolCallContent.
3051
+ upsertToolCallContent(
3052
+ contents,
3053
+ nbiContent.content,
3054
+ new Date(response.created)
3055
+ );
3056
+ } else {
3057
+ contents.push({
3058
+ id: UUID.uuid4(),
3059
+ type: nbiContent.type,
3060
+ content: nbiContent.content || '',
3061
+ reasoningContent: nbiContent.reasoning_content || '',
3062
+ reasoningTag: nbiContent.reasoning_content
3063
+ ? '<think>'
3064
+ : undefined,
3065
+ reasoningFinished:
3066
+ nbiContent.type === ResponseStreamDataType.Markdown &&
3067
+ nbiContent.reasoning_content
3068
+ ? true
3069
+ : false,
3070
+ contentDetail: nbiContent.detail,
3071
+ created: new Date(response.created)
3072
+ });
3073
+ }
2939
3074
  } else {
2940
3075
  responseMessage =
2941
3076
  response.data['choices']?.[0]?.['delta']?.['content'];
@@ -3356,22 +3491,33 @@ function SidebarComponent(props: any) {
3356
3491
 
3357
3492
  if (delta['nbiContent']) {
3358
3493
  const nbiContent = delta['nbiContent'];
3359
- contents.push({
3360
- id: UUID.uuid4(),
3361
- type: nbiContent.type,
3362
- content: nbiContent.content || '',
3363
- reasoningContent: nbiContent.reasoning_content || '',
3364
- reasoningTag: nbiContent.reasoning_content
3365
- ? '<think>'
3366
- : undefined,
3367
- reasoningFinished:
3368
- nbiContent.type === ResponseStreamDataType.Markdown &&
3369
- nbiContent.reasoning_content
3370
- ? true
3371
- : false,
3372
- contentDetail: nbiContent.detail,
3373
- created: new Date(response.created)
3374
- });
3494
+ if (nbiContent.type === ResponseStreamDataType.ToolCall) {
3495
+ // A tool call streams twice under one id (start, then finish);
3496
+ // merge by id so it stays one persistent card. See
3497
+ // upsertToolCallContent.
3498
+ upsertToolCallContent(
3499
+ contents,
3500
+ nbiContent.content,
3501
+ new Date(response.created)
3502
+ );
3503
+ } else {
3504
+ contents.push({
3505
+ id: UUID.uuid4(),
3506
+ type: nbiContent.type,
3507
+ content: nbiContent.content || '',
3508
+ reasoningContent: nbiContent.reasoning_content || '',
3509
+ reasoningTag: nbiContent.reasoning_content
3510
+ ? '<think>'
3511
+ : undefined,
3512
+ reasoningFinished:
3513
+ nbiContent.type === ResponseStreamDataType.Markdown &&
3514
+ nbiContent.reasoning_content
3515
+ ? true
3516
+ : false,
3517
+ contentDetail: nbiContent.detail,
3518
+ created: new Date(response.created)
3519
+ });
3520
+ }
3375
3521
  } else {
3376
3522
  const responseMessage =
3377
3523
  response.data['choices']?.[0]?.['delta']?.['content'];
@@ -0,0 +1,146 @@
1
+ import React, { useState } from 'react';
2
+ import {
3
+ VscChevronDown,
4
+ VscChevronRight,
5
+ VscClose,
6
+ VscEdit,
7
+ VscError,
8
+ VscEye,
9
+ VscPassFilled,
10
+ VscSync,
11
+ VscTerminal,
12
+ VscTools
13
+ } from '../icons';
14
+
15
+ export interface IToolCallDiffLine {
16
+ type: 'add' | 'remove' | 'context' | string;
17
+ content: string;
18
+ }
19
+
20
+ export interface IToolCallDiff {
21
+ path: string;
22
+ lines: IToolCallDiffLine[];
23
+ truncated?: boolean;
24
+ }
25
+
26
+ /**
27
+ * A single agent tool call surfaced as a persistent chat card. Mirrors the
28
+ * `ToolCallData` payload emitted by the server: it stays in the transcript
29
+ * after the turn ends and carries its final status, unlike the transient
30
+ * single progress line it replaces. File-edit tools also carry inline diffs.
31
+ */
32
+ export interface IToolCall {
33
+ id: string;
34
+ title: string;
35
+ // Coarse category, used only to pick the leading icon.
36
+ kind: 'read' | 'edit' | 'execute' | 'other' | string;
37
+ status: 'in_progress' | 'completed' | 'failed' | 'cancelled' | string;
38
+ diffs?: IToolCallDiff[];
39
+ }
40
+
41
+ const KIND_ICONS: Record<string, React.FC<any>> = {
42
+ read: VscEye,
43
+ edit: VscEdit,
44
+ execute: VscTerminal,
45
+ other: VscTools
46
+ };
47
+
48
+ const STATUS_LABELS: Record<string, string> = {
49
+ in_progress: 'in progress',
50
+ completed: 'completed',
51
+ failed: 'failed',
52
+ cancelled: 'cancelled'
53
+ };
54
+
55
+ const GUTTER: Record<string, string> = { add: '+', remove: '-' };
56
+
57
+ function ToolCallDiffView(props: { diffs: IToolCallDiff[] }): JSX.Element {
58
+ return (
59
+ <div className="nbi-tool-call-diffs">
60
+ {props.diffs.map((diff, i) => (
61
+ <div className="nbi-tool-call-diff" key={i}>
62
+ {diff.path ? (
63
+ <div className="nbi-tool-call-diff-path" title={diff.path}>
64
+ {diff.path}
65
+ </div>
66
+ ) : null}
67
+ <div className="nbi-tool-call-diff-body">
68
+ {diff.lines.map((line, j) => (
69
+ <div
70
+ className={`nbi-tool-call-diff-line nbi-diff-${line.type}`}
71
+ key={j}
72
+ >
73
+ <span className="nbi-diff-gutter" aria-hidden="true">
74
+ {GUTTER[line.type] ?? ' '}
75
+ </span>
76
+ {/* The +/- gutter is decorative; give screen readers the
77
+ add/remove distinction as text instead. */}
78
+ {line.type === 'add' || line.type === 'remove' ? (
79
+ <span className="nbi-sr-only">
80
+ {line.type === 'add' ? 'added ' : 'removed '}
81
+ </span>
82
+ ) : null}
83
+ <span className="nbi-diff-text">{line.content}</span>
84
+ </div>
85
+ ))}
86
+ </div>
87
+ {diff.truncated ? (
88
+ <div className="nbi-tool-call-diff-truncated">diff truncated</div>
89
+ ) : null}
90
+ </div>
91
+ ))}
92
+ </div>
93
+ );
94
+ }
95
+
96
+ export function ToolCallCard(props: { toolCall: IToolCall }): JSX.Element {
97
+ const { title, kind, status, diffs } = props.toolCall;
98
+ const [expanded, setExpanded] = useState(true);
99
+
100
+ const KindIcon = KIND_ICONS[kind] ?? VscTools;
101
+
102
+ let StatusIcon = VscSync;
103
+ if (status === 'completed') {
104
+ StatusIcon = VscPassFilled;
105
+ } else if (status === 'failed') {
106
+ StatusIcon = VscError;
107
+ } else if (status === 'cancelled') {
108
+ StatusIcon = VscClose;
109
+ }
110
+
111
+ const statusLabel = STATUS_LABELS[status] ?? status;
112
+ const statusModifier = status.replace(/_/g, '-');
113
+ const hasDiffs = Array.isArray(diffs) && diffs.length > 0;
114
+
115
+ return (
116
+ <div className="nbi-tool-call-wrapper">
117
+ <div className={`nbi-tool-call nbi-tool-call-${statusModifier}`}>
118
+ <KindIcon className="nbi-tool-call-kind-icon" aria-hidden="true" />
119
+ <span className="nbi-tool-call-title" title={title}>
120
+ {title}
121
+ </span>
122
+ {hasDiffs ? (
123
+ <button
124
+ type="button"
125
+ className="nbi-tool-call-diff-toggle"
126
+ onClick={() => setExpanded(e => !e)}
127
+ aria-expanded={expanded}
128
+ aria-label={expanded ? 'Hide diff' : 'Show diff'}
129
+ >
130
+ {expanded ? (
131
+ <VscChevronDown aria-hidden="true" />
132
+ ) : (
133
+ <VscChevronRight aria-hidden="true" />
134
+ )}
135
+ </button>
136
+ ) : null}
137
+ <StatusIcon className="nbi-tool-call-status-icon" aria-hidden="true" />
138
+ {/* Status reaches screen readers as text; the icons are decorative.
139
+ The visible title is already in the accessibility tree, so this
140
+ span carries only the status to avoid announcing the title twice. */}
141
+ <span className="nbi-sr-only">{statusLabel}</span>
142
+ </div>
143
+ {hasDiffs && expanded ? <ToolCallDiffView diffs={diffs!} /> : null}
144
+ </div>
145
+ );
146
+ }
@@ -0,0 +1,65 @@
1
+ import React, { useState } from 'react';
2
+ import { VscChevronDown, VscChevronRight } from '../icons';
3
+ import { IToolCall, ToolCallCard } from './tool-call-card';
4
+
5
+ // Groups with more calls than this start collapsed so a tool-heavy turn (or a
6
+ // reloaded transcript) doesn't flood the chat. A live turn mounts the group at
7
+ // length 1, so it starts expanded and stays visible as calls stream in.
8
+ const GROUP_COLLAPSE_THRESHOLD = 3;
9
+
10
+ /**
11
+ * Renders a run of consecutive tool calls. A single call is shown as a bare
12
+ * card; multiple calls are wrapped in one collapsible group with a summary
13
+ * header, so consecutive agent activity reads as one unit instead of a wall
14
+ * of rows.
15
+ */
16
+ export function ToolCallGroup(props: { toolCalls: IToolCall[] }): JSX.Element {
17
+ const { toolCalls } = props;
18
+ // Hooks must run unconditionally and in a stable order: the group's length
19
+ // grows as calls stream in, so call useState before any length branch.
20
+ // Start collapsed only for a large group whose calls are all settled --
21
+ // never hide a still-running or failed call (matters on transcript reload,
22
+ // where a group can mount already large).
23
+ const [expanded, setExpanded] = useState(
24
+ () =>
25
+ toolCalls.length <= GROUP_COLLAPSE_THRESHOLD ||
26
+ toolCalls.some(t => t.status === 'in_progress' || t.status === 'failed')
27
+ );
28
+
29
+ if (toolCalls.length <= 1) {
30
+ return toolCalls.length === 1 ? (
31
+ <ToolCallCard toolCall={toolCalls[0]} />
32
+ ) : (
33
+ <></>
34
+ );
35
+ }
36
+
37
+ const failed = toolCalls.filter(t => t.status === 'failed').length;
38
+ const summary =
39
+ `${toolCalls.length} tool calls` + (failed ? ` (${failed} failed)` : '');
40
+
41
+ return (
42
+ <div className="nbi-tool-call-group">
43
+ <button
44
+ type="button"
45
+ className="nbi-tool-call-group-header"
46
+ onClick={() => setExpanded(e => !e)}
47
+ aria-expanded={expanded}
48
+ >
49
+ {expanded ? (
50
+ <VscChevronDown aria-hidden="true" />
51
+ ) : (
52
+ <VscChevronRight aria-hidden="true" />
53
+ )}
54
+ <span className="nbi-tool-call-group-summary">{summary}</span>
55
+ </button>
56
+ {expanded ? (
57
+ <div className="nbi-tool-call-group-body">
58
+ {toolCalls.map(toolCall => (
59
+ <ToolCallCard key={toolCall.id} toolCall={toolCall} />
60
+ ))}
61
+ </div>
62
+ ) : null}
63
+ </div>
64
+ );
65
+ }
package/src/icons.ts CHANGED
@@ -41,6 +41,7 @@ export const VscClose = asIcon(Vsc.VscClose);
41
41
  export const VscCloudUpload = asIcon(Vsc.VscCloudUpload);
42
42
  export const VscCopy = asIcon(Vsc.VscCopy);
43
43
  export const VscEdit = asIcon(Vsc.VscEdit);
44
+ export const VscError = asIcon(Vsc.VscError);
44
45
  export const VscEye = asIcon(Vsc.VscEye);
45
46
  export const VscEyeClosed = asIcon(Vsc.VscEyeClosed);
46
47
  export const VscFile = asIcon(Vsc.VscFile);
@@ -55,6 +56,8 @@ export const VscSend = asIcon(Vsc.VscSend);
55
56
  export const VscSettingsGear = asIcon(Vsc.VscSettingsGear);
56
57
  export const VscSparkle = asIcon(Vsc.VscSparkle);
57
58
  export const VscStopCircle = asIcon(Vsc.VscStopCircle);
59
+ export const VscSync = asIcon(Vsc.VscSync);
60
+ export const VscTerminal = asIcon(Vsc.VscTerminal);
58
61
  export const VscThumbsdown = asIcon(Vsc.VscThumbsdown);
59
62
  export const VscThumbsdownFilled = asIcon(Vsc.VscThumbsdownFilled);
60
63
  export const VscThumbsup = asIcon(Vsc.VscThumbsup);
package/src/index.ts CHANGED
@@ -106,6 +106,7 @@ import {
106
106
  cellOutputHasError,
107
107
  chooseWorkspaceDirectory,
108
108
  compareSelections,
109
+ buildResumeCommand,
109
110
  extractLLMGeneratedCode,
110
111
  getSelectionInEditor,
111
112
  getTokenCount,
@@ -1403,10 +1404,9 @@ const plugin: JupyterFrontEndPlugin<INotebookIntelligence> = {
1403
1404
  return React.createElement(LauncherPicker, {
1404
1405
  onSessionSelected: (session: IClaudeSessionInfo) => {
1405
1406
  dialog.close();
1406
- const cmd = session.cwd
1407
- ? `cd ${session.cwd} && claude --resume ${session.session_id}`
1408
- : `claude --resume ${session.session_id}`;
1409
- launchCliInTerminal(cmd);
1407
+ launchCliInTerminal(
1408
+ buildResumeCommand(session.cwd ?? '', session.session_id)
1409
+ );
1410
1410
  }
1411
1411
  });
1412
1412
  }
@@ -1734,7 +1734,7 @@ const plugin: JupyterFrontEndPlugin<INotebookIntelligence> = {
1734
1734
  source: args.source as string
1735
1735
  });
1736
1736
 
1737
- return true;
1737
+ return { cellIndex: newCellIndex };
1738
1738
  }
1739
1739
  });
1740
1740
 
@@ -1755,7 +1755,7 @@ const plugin: JupyterFrontEndPlugin<INotebookIntelligence> = {
1755
1755
  source: args.source as string
1756
1756
  });
1757
1757
 
1758
- return true;
1758
+ return { cellIndex: newCellIndex };
1759
1759
  }
1760
1760
  });
1761
1761
 
package/src/tokens.ts CHANGED
@@ -48,6 +48,7 @@ export enum ResponseStreamDataType {
48
48
  Button = 'button',
49
49
  Anchor = 'anchor',
50
50
  Progress = 'progress',
51
+ ToolCall = 'tool-call',
51
52
  Confirmation = 'confirmation',
52
53
  AskUserQuestion = 'ask-user-question'
53
54
  }