@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/README.md +6 -0
- package/lib/api.d.ts +5 -0
- package/lib/api.js +7 -0
- package/lib/chat-sidebar.js +158 -41
- package/lib/components/tool-call-card.d.ts +26 -0
- package/lib/components/tool-call-card.js +54 -0
- package/lib/components/tool-call-group.d.ts +11 -0
- package/lib/components/tool-call-group.js +33 -0
- package/lib/icons.d.ts +3 -0
- package/lib/icons.js +3 -0
- package/lib/index.js +5 -7
- package/lib/tokens.d.ts +1 -0
- package/lib/tokens.js +1 -0
- package/lib/tool-call-stream.d.ts +24 -0
- package/lib/tool-call-stream.js +28 -0
- package/lib/utils.js +3 -2
- package/package.json +7 -1
- package/src/api.ts +8 -0
- package/src/chat-sidebar.tsx +190 -44
- package/src/components/tool-call-card.tsx +146 -0
- package/src/components/tool-call-group.tsx +65 -0
- package/src/icons.ts +3 -0
- package/src/index.ts +6 -6
- package/src/tokens.ts +1 -0
- package/src/tool-call-stream.ts +44 -0
- package/src/utils.ts +3 -2
- package/style/base.css +173 -4
package/README.md
CHANGED
|
@@ -383,6 +383,12 @@ jupyter lab --NotebookIntelligence.enable_chat_feedback=true
|
|
|
383
383
|
|
|
384
384
|
The feedback fires an in-process `telemetry` event. Nothing leaves the process by default — see the [admin guide](docs/admin-guide.md#chat-feedback-event-hook) for how to wire it into your observability stack.
|
|
385
385
|
|
|
386
|
+
The thumbs buttons reveal on hover by default. To keep them always visible, enable:
|
|
387
|
+
|
|
388
|
+
```python
|
|
389
|
+
c.NotebookIntelligence.enable_chat_feedback_always_visible = True
|
|
390
|
+
```
|
|
391
|
+
|
|
386
392
|
<img src="media/chat-feedback.png" alt="Chat feedback" width=500 />
|
|
387
393
|
|
|
388
394
|
## Documentation
|
package/lib/api.d.ts
CHANGED
|
@@ -174,6 +174,10 @@ export declare class NBIConfig {
|
|
|
174
174
|
getMCPServerPrompt(serverId: string, promptName: string): any;
|
|
175
175
|
get mcpServerSettings(): any;
|
|
176
176
|
get claudeSettings(): any;
|
|
177
|
+
get spinnerVerbs(): {
|
|
178
|
+
mode: string;
|
|
179
|
+
verbs: string[];
|
|
180
|
+
} | null;
|
|
177
181
|
get claudeModels(): IClaudeModelInfo[];
|
|
178
182
|
get isInClaudeCodeMode(): boolean;
|
|
179
183
|
get isClaudeCliAvailable(): boolean;
|
|
@@ -183,6 +187,7 @@ export declare class NBIConfig {
|
|
|
183
187
|
get isCodexCliAvailable(): boolean;
|
|
184
188
|
isCodingAgentLauncherDisabledByPolicy(launcherId: string): boolean;
|
|
185
189
|
get chatFeedbackEnabled(): boolean;
|
|
190
|
+
get chatFeedbackAlwaysVisible(): boolean;
|
|
186
191
|
get tourOverrides(): Record<string, any>;
|
|
187
192
|
get allowGithubSkillImport(): boolean;
|
|
188
193
|
get additionalSkippedWorkspaceDirectories(): string[];
|
package/lib/api.js
CHANGED
|
@@ -142,6 +142,10 @@ export class NBIConfig {
|
|
|
142
142
|
get claudeSettings() {
|
|
143
143
|
return this.capabilities.claude_settings;
|
|
144
144
|
}
|
|
145
|
+
get spinnerVerbs() {
|
|
146
|
+
var _b;
|
|
147
|
+
return (_b = this.capabilities.spinner_verbs) !== null && _b !== void 0 ? _b : null;
|
|
148
|
+
}
|
|
145
149
|
get claudeModels() {
|
|
146
150
|
var _b;
|
|
147
151
|
return ((_b = this.capabilities.claude_models) !== null && _b !== void 0 ? _b : []).map(claudeModelFromWire);
|
|
@@ -181,6 +185,9 @@ export class NBIConfig {
|
|
|
181
185
|
get chatFeedbackEnabled() {
|
|
182
186
|
return this.capabilities.chat_feedback_enabled === true;
|
|
183
187
|
}
|
|
188
|
+
get chatFeedbackAlwaysVisible() {
|
|
189
|
+
return this.capabilities.chat_feedback_always_visible === true;
|
|
190
|
+
}
|
|
184
191
|
// Admin-supplied tour-copy overrides, served from the capabilities
|
|
185
192
|
// response after server-side validation. Returns the raw dict; the
|
|
186
193
|
// tour module decides how to apply it. Defaults to a shared frozen
|
package/lib/chat-sidebar.js
CHANGED
|
@@ -20,6 +20,8 @@ import { mcpServerSettingsToEnabledState } from './components/mcp-util';
|
|
|
20
20
|
import claudeSvgStr from '../style/icons/claude.svg';
|
|
21
21
|
import { AskUserQuestion } from './components/ask-user-question';
|
|
22
22
|
import { ClaudeSessionPicker } from './components/claude-session-picker';
|
|
23
|
+
import { ToolCallGroup } from './components/tool-call-group';
|
|
24
|
+
import { upsertToolCallContent } from './tool-call-stream';
|
|
23
25
|
import { TourOverlay } from './tour/tour-overlay';
|
|
24
26
|
import { TOUR_ANCHOR } from './tour/tour-anchors';
|
|
25
27
|
import { TOUR_START_EVENT, TOUR_STOP_EVENT } from './tour/tour-events';
|
|
@@ -282,8 +284,81 @@ function ChatResponseHTMLFrame(props) {
|
|
|
282
284
|
}
|
|
283
285
|
// Memoize ChatResponse for performance
|
|
284
286
|
function ChatResponse(props) {
|
|
285
|
-
var _a, _b, _c;
|
|
287
|
+
var _a, _b, _c, _d;
|
|
286
288
|
const [renderCount, setRenderCount] = useState(0);
|
|
289
|
+
const shuffledOrder = useRef([]);
|
|
290
|
+
const shufflePos = useRef(0);
|
|
291
|
+
const _spinnerVerbs = NBIAPI.config.isInClaudeCodeMode
|
|
292
|
+
? ((_a = NBIAPI.config.spinnerVerbs) !== null && _a !== void 0 ? _a : null)
|
|
293
|
+
: null;
|
|
294
|
+
const hasCustomVerbs = (_spinnerVerbs === null || _spinnerVerbs === void 0 ? void 0 : _spinnerVerbs.mode) === 'replace' &&
|
|
295
|
+
Array.isArray(_spinnerVerbs.verbs) &&
|
|
296
|
+
_spinnerVerbs.verbs.length > 0;
|
|
297
|
+
// Fisher-Yates shuffle. When `avoidFirst` is set, swap it out of
|
|
298
|
+
// position 0 so the new first verb is never the same as the last shown
|
|
299
|
+
// (prevents an identical repeat at the wrap point between passes).
|
|
300
|
+
const shuffleVerbs = (verbs, avoidFirst) => {
|
|
301
|
+
const order = verbs.map((_, i) => i);
|
|
302
|
+
for (let i = order.length - 1; i > 0; i--) {
|
|
303
|
+
const j = Math.floor(Math.random() * (i + 1));
|
|
304
|
+
[order[i], order[j]] = [order[j], order[i]];
|
|
305
|
+
}
|
|
306
|
+
if (avoidFirst !== undefined &&
|
|
307
|
+
order[0] === avoidFirst &&
|
|
308
|
+
order.length > 1) {
|
|
309
|
+
[order[0], order[1]] = [order[1], order[0]];
|
|
310
|
+
}
|
|
311
|
+
return order;
|
|
312
|
+
};
|
|
313
|
+
// Initialize the shuffle synchronously in useState so the correct verb
|
|
314
|
+
// is shown on the very first paint. A useEffect-based init fires after
|
|
315
|
+
// paint and causes a single-frame flash of verbs[0] before correcting.
|
|
316
|
+
const [verbIndex, setVerbIndex] = useState(() => {
|
|
317
|
+
var _a;
|
|
318
|
+
const sv = NBIAPI.config.isInClaudeCodeMode
|
|
319
|
+
? ((_a = NBIAPI.config.spinnerVerbs) !== null && _a !== void 0 ? _a : null)
|
|
320
|
+
: null;
|
|
321
|
+
if (!sv ||
|
|
322
|
+
sv.mode !== 'replace' ||
|
|
323
|
+
!Array.isArray(sv.verbs) ||
|
|
324
|
+
sv.verbs.length === 0) {
|
|
325
|
+
return 0;
|
|
326
|
+
}
|
|
327
|
+
const order = shuffleVerbs(sv.verbs);
|
|
328
|
+
shuffledOrder.current = order;
|
|
329
|
+
shufflePos.current = 0;
|
|
330
|
+
return order[0];
|
|
331
|
+
});
|
|
332
|
+
useEffect(() => {
|
|
333
|
+
if (!props.showGenerating || !hasCustomVerbs) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const verbs = _spinnerVerbs.verbs;
|
|
337
|
+
// Shuffle already initialized by useState on mount. Only re-initialize
|
|
338
|
+
// if hasCustomVerbs just became true after a capabilities refresh
|
|
339
|
+
// (shuffledOrder would be empty because the lazy init found no verbs).
|
|
340
|
+
if (shuffledOrder.current.length === 0) {
|
|
341
|
+
shuffledOrder.current = shuffleVerbs(verbs);
|
|
342
|
+
shufflePos.current = 0;
|
|
343
|
+
setVerbIndex(shuffledOrder.current[0]);
|
|
344
|
+
}
|
|
345
|
+
let id;
|
|
346
|
+
const scheduleNext = () => {
|
|
347
|
+
const delay = 4000 + Math.random() * 3000;
|
|
348
|
+
id = setTimeout(() => {
|
|
349
|
+
shufflePos.current++;
|
|
350
|
+
if (shufflePos.current >= shuffledOrder.current.length) {
|
|
351
|
+
const lastShown = shuffledOrder.current[shuffledOrder.current.length - 1];
|
|
352
|
+
shuffledOrder.current = shuffleVerbs(verbs, lastShown);
|
|
353
|
+
shufflePos.current = 0;
|
|
354
|
+
}
|
|
355
|
+
setVerbIndex(shuffledOrder.current[shufflePos.current]);
|
|
356
|
+
scheduleNext();
|
|
357
|
+
}, delay);
|
|
358
|
+
};
|
|
359
|
+
scheduleNext();
|
|
360
|
+
return () => clearTimeout(id);
|
|
361
|
+
}, [props.showGenerating, hasCustomVerbs]);
|
|
287
362
|
const msg = props.message;
|
|
288
363
|
const timestamp = msg.date.toLocaleTimeString('en-US', { hour12: false });
|
|
289
364
|
const openNotebook = (event) => {
|
|
@@ -377,6 +452,19 @@ function ChatResponse(props) {
|
|
|
377
452
|
lastItem.reasoningFinished = true;
|
|
378
453
|
}
|
|
379
454
|
}
|
|
455
|
+
else if (item.type === ResponseStreamDataType.ToolCall &&
|
|
456
|
+
lastItemType === ResponseStreamDataType.ToolCall) {
|
|
457
|
+
// Bundle a run of consecutive tool calls into one group item so the
|
|
458
|
+
// renderer can collapse them; ToolCallGroup unwraps content.toolCalls.
|
|
459
|
+
const lastItem = groupedContents[groupedContents.length - 1];
|
|
460
|
+
lastItem.content.toolCalls.push(structuredClone(item.content));
|
|
461
|
+
}
|
|
462
|
+
else if (item.type === ResponseStreamDataType.ToolCall) {
|
|
463
|
+
const grouped = structuredClone(item);
|
|
464
|
+
grouped.content = { toolCalls: [structuredClone(item.content)] };
|
|
465
|
+
groupedContents.push(grouped);
|
|
466
|
+
lastItemType = item.type;
|
|
467
|
+
}
|
|
380
468
|
else {
|
|
381
469
|
groupedContents.push(structuredClone(item));
|
|
382
470
|
lastItemType = item.type;
|
|
@@ -418,28 +506,35 @@ function ChatResponse(props) {
|
|
|
418
506
|
? 'Thought'
|
|
419
507
|
: `Thinking (${Math.floor(item.reasoningTime)} s)`;
|
|
420
508
|
};
|
|
421
|
-
const chatParticipantId = ((
|
|
509
|
+
const chatParticipantId = ((_b = msg.participant) === null || _b === void 0 ? void 0 : _b.id) || 'default';
|
|
422
510
|
return (React.createElement("div", { className: `chat-message chat-message-${msg.from}`, "data-render-count": renderCount },
|
|
423
511
|
React.createElement("div", { className: "chat-message-header" },
|
|
424
512
|
React.createElement("div", { className: "chat-message-from" },
|
|
425
|
-
((
|
|
513
|
+
((_c = msg.participant) === null || _c === void 0 ? void 0 : _c.iconPath) && (React.createElement("div", { className: `chat-message-from-icon chat-message-from-icon-${chatParticipantId} ${isDarkTheme() ? 'dark' : ''}` },
|
|
426
514
|
React.createElement("img", { src: msg.participant.iconPath, alt: "" }))),
|
|
427
515
|
React.createElement("div", { className: "chat-message-from-title" }, msg.from === 'user'
|
|
428
516
|
? 'User'
|
|
429
|
-
: ((
|
|
517
|
+
: ((_d = msg.participant) === null || _d === void 0 ? void 0 : _d.name) || 'AI Assistant'),
|
|
430
518
|
React.createElement("div", { className: "chat-message-from-progress", style: { display: `${props.showGenerating ? 'visible' : 'none'}` } },
|
|
431
519
|
React.createElement("span", {
|
|
432
520
|
// Key on the heartbeat tick so React re-mounts the dot on
|
|
433
521
|
// every beat; CSS-animation restart from an attribute-only
|
|
434
522
|
// change is not reliable across browsers.
|
|
435
523
|
key: props.heartbeatTick, className: `generating-pulse${props.isStalled ? ' is-stalled' : ''}`, "aria-hidden": "true" }),
|
|
436
|
-
React.createElement("div", { className: "generating-label", "aria-
|
|
524
|
+
React.createElement("div", { className: "generating-label", "aria-hidden": "true" },
|
|
437
525
|
props.isStalled
|
|
438
526
|
? 'Still working, server may be slow'
|
|
439
|
-
:
|
|
527
|
+
: hasCustomVerbs
|
|
528
|
+
? _spinnerVerbs.verbs[verbIndex]
|
|
529
|
+
: 'Generating',
|
|
440
530
|
props.showGenerating && props.elapsedSeconds > 0
|
|
441
531
|
? ` (${formatElapsedSeconds(props.elapsedSeconds)})`
|
|
442
|
-
: '')
|
|
532
|
+
: ''),
|
|
533
|
+
React.createElement("div", { className: "nbi-sr-only", "aria-live": "polite", "aria-atomic": "true" }, props.isStalled
|
|
534
|
+
? 'Still working, server may be slow'
|
|
535
|
+
: hasCustomVerbs
|
|
536
|
+
? _spinnerVerbs.verbs[verbIndex]
|
|
537
|
+
: 'Generating'))),
|
|
443
538
|
React.createElement("div", { className: "chat-message-timestamp" }, timestamp)),
|
|
444
539
|
React.createElement("div", { className: "chat-message-content" },
|
|
445
540
|
groupedContents.map((item, index) => {
|
|
@@ -487,6 +582,12 @@ function ChatResponse(props) {
|
|
|
487
582
|
// ✗ for error) rather than forcing a single rendering here.
|
|
488
583
|
return index === groupedContents.length - 1 &&
|
|
489
584
|
props.showGenerating ? (React.createElement("div", { className: "chat-response-progress", key: `key-${index}` }, item.content)) : null;
|
|
585
|
+
case ResponseStreamDataType.ToolCall:
|
|
586
|
+
// Unlike Progress, tool-call cards persist after the turn ends,
|
|
587
|
+
// so they render regardless of `showGenerating`. The grouping
|
|
588
|
+
// pass bundled this run's calls into content.toolCalls; the
|
|
589
|
+
// group renders a single card or a collapsible group.
|
|
590
|
+
return (React.createElement(ToolCallGroup, { key: `key-${index}`, toolCalls: item.content.toolCalls }));
|
|
490
591
|
case ResponseStreamDataType.Confirmation:
|
|
491
592
|
return answeredForms.get(item.id) ===
|
|
492
593
|
'confirmed' ? null : answeredForms.get(item.id) ===
|
|
@@ -540,8 +641,8 @@ function ChatResponse(props) {
|
|
|
540
641
|
}),
|
|
541
642
|
msg.notebookLink && (React.createElement("button", { type: "button", className: "copilot-generated-notebook-link", "data-ref": msg.notebookLink, "aria-label": `Open notebook ${msg.notebookLink}`, onClick: openNotebook }, "open notebook"))),
|
|
542
643
|
msg.from === 'copilot' &&
|
|
543
|
-
!props.showGenerating &&
|
|
544
|
-
NBIAPI.config.chatFeedbackEnabled && (React.createElement("div", { className:
|
|
644
|
+
(NBIAPI.config.chatFeedbackAlwaysVisible || !props.showGenerating) &&
|
|
645
|
+
NBIAPI.config.chatFeedbackEnabled && (React.createElement("div", { className: `chat-message-feedback${NBIAPI.config.chatFeedbackAlwaysVisible ? ' always-visible' : ''}` },
|
|
545
646
|
React.createElement("button", { className: `chat-feedback-btn ${msg.feedback === 'positive' ? 'selected' : ''}`, onClick: () => {
|
|
546
647
|
var _a;
|
|
547
648
|
props.onFeedback(msg.id, 'positive');
|
|
@@ -558,7 +659,7 @@ function ChatResponse(props) {
|
|
|
558
659
|
}
|
|
559
660
|
});
|
|
560
661
|
}
|
|
561
|
-
}, "aria-label": "Rate as
|
|
662
|
+
}, "aria-label": "Rate response as good", "aria-pressed": msg.feedback === 'positive', title: "Good response" }, msg.feedback === 'positive' ? (React.createElement(VscThumbsupFilled, null)) : (React.createElement(VscThumbsup, null))),
|
|
562
663
|
React.createElement("button", { className: `chat-feedback-btn ${msg.feedback === 'negative' ? 'selected' : ''}`, onClick: () => {
|
|
563
664
|
var _a;
|
|
564
665
|
props.onFeedback(msg.id, 'negative');
|
|
@@ -575,7 +676,7 @@ function ChatResponse(props) {
|
|
|
575
676
|
}
|
|
576
677
|
});
|
|
577
678
|
}
|
|
578
|
-
}, "aria-label": "Rate as
|
|
679
|
+
}, "aria-label": "Rate response as bad", "aria-pressed": msg.feedback === 'negative', title: "Bad response" }, msg.feedback === 'negative' ? (React.createElement(VscThumbsdownFilled, null)) : (React.createElement(VscThumbsdown, null)))))));
|
|
579
680
|
}
|
|
580
681
|
const MemoizedChatResponse = memo(ChatResponse);
|
|
581
682
|
async function submitCompletionRequest(request, responseEmitter) {
|
|
@@ -2123,21 +2224,29 @@ function SidebarComponent(props) {
|
|
|
2123
2224
|
}
|
|
2124
2225
|
if (delta['nbiContent']) {
|
|
2125
2226
|
const nbiContent = delta['nbiContent'];
|
|
2126
|
-
|
|
2127
|
-
id
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
nbiContent.
|
|
2136
|
-
|
|
2137
|
-
:
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2227
|
+
if (nbiContent.type === ResponseStreamDataType.ToolCall) {
|
|
2228
|
+
// A tool call streams twice under one id (start, then finish);
|
|
2229
|
+
// merge by id so it stays one persistent card. See
|
|
2230
|
+
// upsertToolCallContent.
|
|
2231
|
+
upsertToolCallContent(contents, nbiContent.content, new Date(response.created));
|
|
2232
|
+
}
|
|
2233
|
+
else {
|
|
2234
|
+
contents.push({
|
|
2235
|
+
id: UUID.uuid4(),
|
|
2236
|
+
type: nbiContent.type,
|
|
2237
|
+
content: nbiContent.content || '',
|
|
2238
|
+
reasoningContent: nbiContent.reasoning_content || '',
|
|
2239
|
+
reasoningTag: nbiContent.reasoning_content
|
|
2240
|
+
? '<think>'
|
|
2241
|
+
: undefined,
|
|
2242
|
+
reasoningFinished: nbiContent.type === ResponseStreamDataType.Markdown &&
|
|
2243
|
+
nbiContent.reasoning_content
|
|
2244
|
+
? true
|
|
2245
|
+
: false,
|
|
2246
|
+
contentDetail: nbiContent.detail,
|
|
2247
|
+
created: new Date(response.created)
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
2141
2250
|
}
|
|
2142
2251
|
else {
|
|
2143
2252
|
responseMessage =
|
|
@@ -2497,21 +2606,29 @@ function SidebarComponent(props) {
|
|
|
2497
2606
|
}
|
|
2498
2607
|
if (delta['nbiContent']) {
|
|
2499
2608
|
const nbiContent = delta['nbiContent'];
|
|
2500
|
-
|
|
2501
|
-
id
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
nbiContent.
|
|
2510
|
-
|
|
2511
|
-
:
|
|
2512
|
-
|
|
2513
|
-
|
|
2514
|
-
|
|
2609
|
+
if (nbiContent.type === ResponseStreamDataType.ToolCall) {
|
|
2610
|
+
// A tool call streams twice under one id (start, then finish);
|
|
2611
|
+
// merge by id so it stays one persistent card. See
|
|
2612
|
+
// upsertToolCallContent.
|
|
2613
|
+
upsertToolCallContent(contents, nbiContent.content, new Date(response.created));
|
|
2614
|
+
}
|
|
2615
|
+
else {
|
|
2616
|
+
contents.push({
|
|
2617
|
+
id: UUID.uuid4(),
|
|
2618
|
+
type: nbiContent.type,
|
|
2619
|
+
content: nbiContent.content || '',
|
|
2620
|
+
reasoningContent: nbiContent.reasoning_content || '',
|
|
2621
|
+
reasoningTag: nbiContent.reasoning_content
|
|
2622
|
+
? '<think>'
|
|
2623
|
+
: undefined,
|
|
2624
|
+
reasoningFinished: nbiContent.type === ResponseStreamDataType.Markdown &&
|
|
2625
|
+
nbiContent.reasoning_content
|
|
2626
|
+
? true
|
|
2627
|
+
: false,
|
|
2628
|
+
contentDetail: nbiContent.detail,
|
|
2629
|
+
created: new Date(response.created)
|
|
2630
|
+
});
|
|
2631
|
+
}
|
|
2515
2632
|
}
|
|
2516
2633
|
else {
|
|
2517
2634
|
const responseMessage = (_e = (_d = (_c = response.data['choices']) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d['delta']) === null || _e === void 0 ? void 0 : _e['content'];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
export interface IToolCallDiffLine {
|
|
3
|
+
type: 'add' | 'remove' | 'context' | string;
|
|
4
|
+
content: string;
|
|
5
|
+
}
|
|
6
|
+
export interface IToolCallDiff {
|
|
7
|
+
path: string;
|
|
8
|
+
lines: IToolCallDiffLine[];
|
|
9
|
+
truncated?: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A single agent tool call surfaced as a persistent chat card. Mirrors the
|
|
13
|
+
* `ToolCallData` payload emitted by the server: it stays in the transcript
|
|
14
|
+
* after the turn ends and carries its final status, unlike the transient
|
|
15
|
+
* single progress line it replaces. File-edit tools also carry inline diffs.
|
|
16
|
+
*/
|
|
17
|
+
export interface IToolCall {
|
|
18
|
+
id: string;
|
|
19
|
+
title: string;
|
|
20
|
+
kind: 'read' | 'edit' | 'execute' | 'other' | string;
|
|
21
|
+
status: 'in_progress' | 'completed' | 'failed' | 'cancelled' | string;
|
|
22
|
+
diffs?: IToolCallDiff[];
|
|
23
|
+
}
|
|
24
|
+
export declare function ToolCallCard(props: {
|
|
25
|
+
toolCall: IToolCall;
|
|
26
|
+
}): JSX.Element;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { VscChevronDown, VscChevronRight, VscClose, VscEdit, VscError, VscEye, VscPassFilled, VscSync, VscTerminal, VscTools } from '../icons';
|
|
3
|
+
const KIND_ICONS = {
|
|
4
|
+
read: VscEye,
|
|
5
|
+
edit: VscEdit,
|
|
6
|
+
execute: VscTerminal,
|
|
7
|
+
other: VscTools
|
|
8
|
+
};
|
|
9
|
+
const STATUS_LABELS = {
|
|
10
|
+
in_progress: 'in progress',
|
|
11
|
+
completed: 'completed',
|
|
12
|
+
failed: 'failed',
|
|
13
|
+
cancelled: 'cancelled'
|
|
14
|
+
};
|
|
15
|
+
const GUTTER = { add: '+', remove: '-' };
|
|
16
|
+
function ToolCallDiffView(props) {
|
|
17
|
+
return (React.createElement("div", { className: "nbi-tool-call-diffs" }, props.diffs.map((diff, i) => (React.createElement("div", { className: "nbi-tool-call-diff", key: i },
|
|
18
|
+
diff.path ? (React.createElement("div", { className: "nbi-tool-call-diff-path", title: diff.path }, diff.path)) : null,
|
|
19
|
+
React.createElement("div", { className: "nbi-tool-call-diff-body" }, diff.lines.map((line, j) => {
|
|
20
|
+
var _a;
|
|
21
|
+
return (React.createElement("div", { className: `nbi-tool-call-diff-line nbi-diff-${line.type}`, key: j },
|
|
22
|
+
React.createElement("span", { className: "nbi-diff-gutter", "aria-hidden": "true" }, (_a = GUTTER[line.type]) !== null && _a !== void 0 ? _a : ' '),
|
|
23
|
+
line.type === 'add' || line.type === 'remove' ? (React.createElement("span", { className: "nbi-sr-only" }, line.type === 'add' ? 'added ' : 'removed ')) : null,
|
|
24
|
+
React.createElement("span", { className: "nbi-diff-text" }, line.content)));
|
|
25
|
+
})),
|
|
26
|
+
diff.truncated ? (React.createElement("div", { className: "nbi-tool-call-diff-truncated" }, "diff truncated")) : null)))));
|
|
27
|
+
}
|
|
28
|
+
export function ToolCallCard(props) {
|
|
29
|
+
var _a, _b;
|
|
30
|
+
const { title, kind, status, diffs } = props.toolCall;
|
|
31
|
+
const [expanded, setExpanded] = useState(true);
|
|
32
|
+
const KindIcon = (_a = KIND_ICONS[kind]) !== null && _a !== void 0 ? _a : VscTools;
|
|
33
|
+
let StatusIcon = VscSync;
|
|
34
|
+
if (status === 'completed') {
|
|
35
|
+
StatusIcon = VscPassFilled;
|
|
36
|
+
}
|
|
37
|
+
else if (status === 'failed') {
|
|
38
|
+
StatusIcon = VscError;
|
|
39
|
+
}
|
|
40
|
+
else if (status === 'cancelled') {
|
|
41
|
+
StatusIcon = VscClose;
|
|
42
|
+
}
|
|
43
|
+
const statusLabel = (_b = STATUS_LABELS[status]) !== null && _b !== void 0 ? _b : status;
|
|
44
|
+
const statusModifier = status.replace(/_/g, '-');
|
|
45
|
+
const hasDiffs = Array.isArray(diffs) && diffs.length > 0;
|
|
46
|
+
return (React.createElement("div", { className: "nbi-tool-call-wrapper" },
|
|
47
|
+
React.createElement("div", { className: `nbi-tool-call nbi-tool-call-${statusModifier}` },
|
|
48
|
+
React.createElement(KindIcon, { className: "nbi-tool-call-kind-icon", "aria-hidden": "true" }),
|
|
49
|
+
React.createElement("span", { className: "nbi-tool-call-title", title: title }, title),
|
|
50
|
+
hasDiffs ? (React.createElement("button", { type: "button", className: "nbi-tool-call-diff-toggle", onClick: () => setExpanded(e => !e), "aria-expanded": expanded, "aria-label": expanded ? 'Hide diff' : 'Show diff' }, expanded ? (React.createElement(VscChevronDown, { "aria-hidden": "true" })) : (React.createElement(VscChevronRight, { "aria-hidden": "true" })))) : null,
|
|
51
|
+
React.createElement(StatusIcon, { className: "nbi-tool-call-status-icon", "aria-hidden": "true" }),
|
|
52
|
+
React.createElement("span", { className: "nbi-sr-only" }, statusLabel)),
|
|
53
|
+
hasDiffs && expanded ? React.createElement(ToolCallDiffView, { diffs: diffs }) : null));
|
|
54
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/// <reference types="react" />
|
|
2
|
+
import { IToolCall } from './tool-call-card';
|
|
3
|
+
/**
|
|
4
|
+
* Renders a run of consecutive tool calls. A single call is shown as a bare
|
|
5
|
+
* card; multiple calls are wrapped in one collapsible group with a summary
|
|
6
|
+
* header, so consecutive agent activity reads as one unit instead of a wall
|
|
7
|
+
* of rows.
|
|
8
|
+
*/
|
|
9
|
+
export declare function ToolCallGroup(props: {
|
|
10
|
+
toolCalls: IToolCall[];
|
|
11
|
+
}): JSX.Element;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { VscChevronDown, VscChevronRight } from '../icons';
|
|
3
|
+
import { ToolCallCard } from './tool-call-card';
|
|
4
|
+
// Groups with more calls than this start collapsed so a tool-heavy turn (or a
|
|
5
|
+
// reloaded transcript) doesn't flood the chat. A live turn mounts the group at
|
|
6
|
+
// length 1, so it starts expanded and stays visible as calls stream in.
|
|
7
|
+
const GROUP_COLLAPSE_THRESHOLD = 3;
|
|
8
|
+
/**
|
|
9
|
+
* Renders a run of consecutive tool calls. A single call is shown as a bare
|
|
10
|
+
* card; multiple calls are wrapped in one collapsible group with a summary
|
|
11
|
+
* header, so consecutive agent activity reads as one unit instead of a wall
|
|
12
|
+
* of rows.
|
|
13
|
+
*/
|
|
14
|
+
export function ToolCallGroup(props) {
|
|
15
|
+
const { toolCalls } = props;
|
|
16
|
+
// Hooks must run unconditionally and in a stable order: the group's length
|
|
17
|
+
// grows as calls stream in, so call useState before any length branch.
|
|
18
|
+
// Start collapsed only for a large group whose calls are all settled --
|
|
19
|
+
// never hide a still-running or failed call (matters on transcript reload,
|
|
20
|
+
// where a group can mount already large).
|
|
21
|
+
const [expanded, setExpanded] = useState(() => toolCalls.length <= GROUP_COLLAPSE_THRESHOLD ||
|
|
22
|
+
toolCalls.some(t => t.status === 'in_progress' || t.status === 'failed'));
|
|
23
|
+
if (toolCalls.length <= 1) {
|
|
24
|
+
return toolCalls.length === 1 ? (React.createElement(ToolCallCard, { toolCall: toolCalls[0] })) : (React.createElement(React.Fragment, null));
|
|
25
|
+
}
|
|
26
|
+
const failed = toolCalls.filter(t => t.status === 'failed').length;
|
|
27
|
+
const summary = `${toolCalls.length} tool calls` + (failed ? ` (${failed} failed)` : '');
|
|
28
|
+
return (React.createElement("div", { className: "nbi-tool-call-group" },
|
|
29
|
+
React.createElement("button", { type: "button", className: "nbi-tool-call-group-header", onClick: () => setExpanded(e => !e), "aria-expanded": expanded },
|
|
30
|
+
expanded ? (React.createElement(VscChevronDown, { "aria-hidden": "true" })) : (React.createElement(VscChevronRight, { "aria-hidden": "true" })),
|
|
31
|
+
React.createElement("span", { className: "nbi-tool-call-group-summary" }, summary)),
|
|
32
|
+
expanded ? (React.createElement("div", { className: "nbi-tool-call-group-body" }, toolCalls.map(toolCall => (React.createElement(ToolCallCard, { key: toolCall.id, toolCall: toolCall }))))) : null));
|
|
33
|
+
}
|
package/lib/icons.d.ts
CHANGED
|
@@ -17,6 +17,7 @@ export declare const VscClose: IconLike;
|
|
|
17
17
|
export declare const VscCloudUpload: IconLike;
|
|
18
18
|
export declare const VscCopy: IconLike;
|
|
19
19
|
export declare const VscEdit: IconLike;
|
|
20
|
+
export declare const VscError: IconLike;
|
|
20
21
|
export declare const VscEye: IconLike;
|
|
21
22
|
export declare const VscEyeClosed: IconLike;
|
|
22
23
|
export declare const VscFile: IconLike;
|
|
@@ -31,6 +32,8 @@ export declare const VscSend: IconLike;
|
|
|
31
32
|
export declare const VscSettingsGear: IconLike;
|
|
32
33
|
export declare const VscSparkle: IconLike;
|
|
33
34
|
export declare const VscStopCircle: IconLike;
|
|
35
|
+
export declare const VscSync: IconLike;
|
|
36
|
+
export declare const VscTerminal: IconLike;
|
|
34
37
|
export declare const VscThumbsdown: IconLike;
|
|
35
38
|
export declare const VscThumbsdownFilled: IconLike;
|
|
36
39
|
export declare const VscThumbsup: IconLike;
|
package/lib/icons.js
CHANGED
|
@@ -27,6 +27,7 @@ export const VscClose = asIcon(Vsc.VscClose);
|
|
|
27
27
|
export const VscCloudUpload = asIcon(Vsc.VscCloudUpload);
|
|
28
28
|
export const VscCopy = asIcon(Vsc.VscCopy);
|
|
29
29
|
export const VscEdit = asIcon(Vsc.VscEdit);
|
|
30
|
+
export const VscError = asIcon(Vsc.VscError);
|
|
30
31
|
export const VscEye = asIcon(Vsc.VscEye);
|
|
31
32
|
export const VscEyeClosed = asIcon(Vsc.VscEyeClosed);
|
|
32
33
|
export const VscFile = asIcon(Vsc.VscFile);
|
|
@@ -41,6 +42,8 @@ export const VscSend = asIcon(Vsc.VscSend);
|
|
|
41
42
|
export const VscSettingsGear = asIcon(Vsc.VscSettingsGear);
|
|
42
43
|
export const VscSparkle = asIcon(Vsc.VscSparkle);
|
|
43
44
|
export const VscStopCircle = asIcon(Vsc.VscStopCircle);
|
|
45
|
+
export const VscSync = asIcon(Vsc.VscSync);
|
|
46
|
+
export const VscTerminal = asIcon(Vsc.VscTerminal);
|
|
44
47
|
export const VscThumbsdown = asIcon(Vsc.VscThumbsdown);
|
|
45
48
|
export const VscThumbsdownFilled = asIcon(Vsc.VscThumbsdownFilled);
|
|
46
49
|
export const VscThumbsup = asIcon(Vsc.VscThumbsup);
|
package/lib/index.js
CHANGED
|
@@ -36,7 +36,7 @@ import sparklesWarningSvgstr from '../style/icons/sparkles-warning.svg';
|
|
|
36
36
|
import claudeSvgstr from '../style/icons/claude.svg';
|
|
37
37
|
import openaiSvgstr from '../style/icons/openai.svg';
|
|
38
38
|
import opencodeSvgstr from '../style/icons/opencode.svg';
|
|
39
|
-
import { applyCodeToSelectionInEditor, cellOutputAsText, cellOutputHasError, chooseWorkspaceDirectory, compareSelections, extractLLMGeneratedCode, getSelectionInEditor, getTokenCount, getWholeNotebookContent, isSelectionEmpty, markdownToComment, waitForDuration } from './utils';
|
|
39
|
+
import { applyCodeToSelectionInEditor, cellOutputAsText, cellOutputHasError, chooseWorkspaceDirectory, compareSelections, buildResumeCommand, extractLLMGeneratedCode, getSelectionInEditor, getTokenCount, getWholeNotebookContent, isSelectionEmpty, markdownToComment, waitForDuration } from './utils';
|
|
40
40
|
import { cellOutputAsContextBundle } from './cell-output-bundle';
|
|
41
41
|
import { UUID } from '@lumino/coreutils';
|
|
42
42
|
import * as path from 'path';
|
|
@@ -1108,11 +1108,9 @@ const plugin = {
|
|
|
1108
1108
|
render() {
|
|
1109
1109
|
return React.createElement(LauncherPicker, {
|
|
1110
1110
|
onSessionSelected: (session) => {
|
|
1111
|
+
var _a;
|
|
1111
1112
|
dialog.close();
|
|
1112
|
-
|
|
1113
|
-
? `cd ${session.cwd} && claude --resume ${session.session_id}`
|
|
1114
|
-
: `claude --resume ${session.session_id}`;
|
|
1115
|
-
launchCliInTerminal(cmd);
|
|
1113
|
+
launchCliInTerminal(buildResumeCommand((_a = session.cwd) !== null && _a !== void 0 ? _a : '', session.session_id));
|
|
1116
1114
|
}
|
|
1117
1115
|
});
|
|
1118
1116
|
}
|
|
@@ -1360,7 +1358,7 @@ const plugin = {
|
|
|
1360
1358
|
metadata: { trusted: true },
|
|
1361
1359
|
source: args.source
|
|
1362
1360
|
});
|
|
1363
|
-
return
|
|
1361
|
+
return { cellIndex: newCellIndex };
|
|
1364
1362
|
}
|
|
1365
1363
|
});
|
|
1366
1364
|
app.commands.addCommand(CommandIDs.addCodeCellToActiveNotebook, {
|
|
@@ -1378,7 +1376,7 @@ const plugin = {
|
|
|
1378
1376
|
metadata: { trusted: true },
|
|
1379
1377
|
source: args.source
|
|
1380
1378
|
});
|
|
1381
|
-
return
|
|
1379
|
+
return { cellIndex: newCellIndex };
|
|
1382
1380
|
}
|
|
1383
1381
|
});
|
|
1384
1382
|
app.commands.addCommand(CommandIDs.getCellTypeAndSource, {
|
package/lib/tokens.d.ts
CHANGED
package/lib/tokens.js
CHANGED
|
@@ -32,6 +32,7 @@ export var ResponseStreamDataType;
|
|
|
32
32
|
ResponseStreamDataType["Button"] = "button";
|
|
33
33
|
ResponseStreamDataType["Anchor"] = "anchor";
|
|
34
34
|
ResponseStreamDataType["Progress"] = "progress";
|
|
35
|
+
ResponseStreamDataType["ToolCall"] = "tool-call";
|
|
35
36
|
ResponseStreamDataType["Confirmation"] = "confirmation";
|
|
36
37
|
ResponseStreamDataType["AskUserQuestion"] = "ask-user-question";
|
|
37
38
|
})(ResponseStreamDataType || (ResponseStreamDataType = {}));
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ResponseStreamDataType } from './tokens';
|
|
2
|
+
/**
|
|
3
|
+
* The subset of a chat message's stream-content item this helper needs. A
|
|
4
|
+
* structural subset of `IChatMessageContent` (defined in chat-sidebar) so it
|
|
5
|
+
* can be unit-tested without importing the sidebar (and creating a cycle).
|
|
6
|
+
*/
|
|
7
|
+
export interface IToolCallStreamItem {
|
|
8
|
+
id: string;
|
|
9
|
+
type: ResponseStreamDataType;
|
|
10
|
+
content: any;
|
|
11
|
+
created: Date;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Merge a streamed tool-call payload into `contents` by its tool-call id.
|
|
15
|
+
*
|
|
16
|
+
* A tool call streams twice under one id (once when it starts, once when it
|
|
17
|
+
* finishes). The first emission pushes a new card; the second updates that
|
|
18
|
+
* card's content (its status) in place, so the call stays a single persistent
|
|
19
|
+
* row rather than appending a duplicate. Mutates `contents`.
|
|
20
|
+
*/
|
|
21
|
+
export declare function upsertToolCallContent(contents: IToolCallStreamItem[], content: {
|
|
22
|
+
id: string;
|
|
23
|
+
[key: string]: any;
|
|
24
|
+
}, created: Date): void;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { UUID } from '@lumino/coreutils';
|
|
2
|
+
import { ResponseStreamDataType } from './tokens';
|
|
3
|
+
/**
|
|
4
|
+
* Merge a streamed tool-call payload into `contents` by its tool-call id.
|
|
5
|
+
*
|
|
6
|
+
* A tool call streams twice under one id (once when it starts, once when it
|
|
7
|
+
* finishes). The first emission pushes a new card; the second updates that
|
|
8
|
+
* card's content (its status) in place, so the call stays a single persistent
|
|
9
|
+
* row rather than appending a duplicate. Mutates `contents`.
|
|
10
|
+
*/
|
|
11
|
+
export function upsertToolCallContent(contents, content, created) {
|
|
12
|
+
const existing = contents.find(c => {
|
|
13
|
+
var _a;
|
|
14
|
+
return c.type === ResponseStreamDataType.ToolCall &&
|
|
15
|
+
((_a = c.content) === null || _a === void 0 ? void 0 : _a.id) === (content === null || content === void 0 ? void 0 : content.id);
|
|
16
|
+
});
|
|
17
|
+
if (existing) {
|
|
18
|
+
existing.content = content;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
contents.push({
|
|
22
|
+
id: UUID.uuid4(),
|
|
23
|
+
type: ResponseStreamDataType.ToolCall,
|
|
24
|
+
content,
|
|
25
|
+
created
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
package/lib/utils.js
CHANGED
|
@@ -341,10 +341,11 @@ export function safeAnchorUri(uri) {
|
|
|
341
341
|
* user happens to be in the JupyterLab working directory.
|
|
342
342
|
*/
|
|
343
343
|
export function buildResumeCommand(cwd, sessionId) {
|
|
344
|
+
const quotedSessionId = shellSingleQuote(sessionId);
|
|
344
345
|
if (!cwd) {
|
|
345
|
-
return `claude --resume ${
|
|
346
|
+
return `claude --resume ${quotedSessionId}`;
|
|
346
347
|
}
|
|
347
|
-
return `cd ${shellSingleQuote(cwd)} && claude --resume ${
|
|
348
|
+
return `cd ${shellSingleQuote(cwd)} && claude --resume ${quotedSessionId}`;
|
|
348
349
|
}
|
|
349
350
|
/**
|
|
350
351
|
* Write `text` to the system clipboard. Falls back to a hidden textarea +
|