jupyter-chat-components 0.3.0 → 0.4.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 CHANGED
@@ -26,13 +26,7 @@ The registry is available directly on the `IComponentsRendererFactory` token as
26
26
 
27
27
  Other JupyterLab extensions can consume the `IComponentsRendererFactory` token and use `registry.add()` to register their own components, which will then be available for rendering via the MIME bundle.
28
28
 
29
- ## Available components
30
-
31
- ### `tool-call`
32
-
33
- Renders an AI tool call, displaying the tool name, input arguments, and output in a structured and readable format. Useful for visualizing function calls made by AI assistants during a conversation.
34
-
35
- More components are planned for future releases.
29
+ For live end-to-end metadata examples, see the deployed demo notebook at [brichet.github.io/jupyter-chat-components/lab/index.html?path=components_demo.ipynb](https://brichet.github.io/jupyter-chat-components/lab/index.html?path=components_demo.ipynb). The source notebook lives at [demo/contents/components_demo.ipynb](./demo/contents/components_demo.ipynb).
36
30
 
37
31
  ## Requirements
38
32
 
@@ -0,0 +1,30 @@
1
+ import * as React from 'react';
2
+ import { IComponentProps, IToolCallsEntry, IToolCallsMetadata, OpenToolCallPath, ToolCallPermissionDecision } from '../token';
3
+ /**
4
+ * Props for rendering grouped tool calls.
5
+ */
6
+ export interface IGroupedToolCallsProps extends IComponentProps, IToolCallsMetadata {
7
+ toolCallPermissionDecision?: ToolCallPermissionDecision;
8
+ openToolCallPath?: OpenToolCallPath;
9
+ }
10
+ /**
11
+ * Convert an absolute filesystem path to a server-relative path when possible.
12
+ */
13
+ export declare function toServerRelativePath(absolutePath: string): string;
14
+ /**
15
+ * Format tool payload (input/output) for display.
16
+ */
17
+ export declare function formatToolCallIO(payload: unknown): string;
18
+ /**
19
+ * Compute the line title shown for a tool call.
20
+ */
21
+ export declare function getToolCallDisplayTitle(toolCall: IToolCallsEntry): string;
22
+ /**
23
+ * Compute the pre-permission detail text for a tool call, or null if the
24
+ * title alone is enough.
25
+ */
26
+ export declare function buildPermissionDetail(toolCall: IToolCallsEntry): string | null;
27
+ /**
28
+ * React component for rendering grouped tool calls.
29
+ */
30
+ export declare const GroupedToolCalls: React.FC<IGroupedToolCallsProps>;
@@ -0,0 +1,432 @@
1
+ import { PageConfig, PathExt } from '@jupyterlab/coreutils';
2
+ import { nullTranslator } from '@jupyterlab/translation';
3
+ import * as React from 'react';
4
+ import { structuredPatch } from 'diff';
5
+ /** Maximum number of rendered diff lines before truncation. */
6
+ const MAX_DIFF_LINES = 20;
7
+ /** Maximum number of lines shown in an expanded detail. */
8
+ const MAX_DETAIL_LINES = 15;
9
+ /** Tool kinds where expanded view shows file paths from locations. */
10
+ const FILE_KINDS = new Set(['read', 'edit', 'delete', 'move']);
11
+ const TOOL_KIND_LABELS = {
12
+ read: 'Reading',
13
+ edit: 'Editing',
14
+ delete: 'Deleting',
15
+ move: 'Moving',
16
+ search: 'Searching',
17
+ execute: 'Running command',
18
+ think: 'Thinking',
19
+ fetch: 'Fetching',
20
+ switch_mode: 'Switching mode'
21
+ };
22
+ function getConfiguredServerRoot() {
23
+ const rootUri = PageConfig.getOption('rootUri');
24
+ if (rootUri) {
25
+ try {
26
+ return new URL(rootUri, 'http://localhost').pathname;
27
+ }
28
+ catch (error) {
29
+ console.warn('Could not parse rootUri while rendering tool calls.', error);
30
+ }
31
+ }
32
+ const serverRoot = PageConfig.getOption('serverRoot');
33
+ return serverRoot || null;
34
+ }
35
+ /**
36
+ * Convert an absolute filesystem path to a server-relative path when possible.
37
+ */
38
+ export function toServerRelativePath(absolutePath) {
39
+ const serverRoot = getConfiguredServerRoot();
40
+ if (!serverRoot) {
41
+ return absolutePath;
42
+ }
43
+ const relativePath = PathExt.relative(serverRoot, absolutePath);
44
+ if (relativePath.startsWith('..')) {
45
+ return absolutePath;
46
+ }
47
+ return relativePath;
48
+ }
49
+ /**
50
+ * Format tool payload (input/output) for display.
51
+ */
52
+ export function formatToolCallIO(payload) {
53
+ if (typeof payload === 'string') {
54
+ return payload;
55
+ }
56
+ if (Array.isArray(payload) &&
57
+ payload.every(item => typeof item === 'object' &&
58
+ item !== null &&
59
+ typeof item.text === 'string')) {
60
+ return payload.map(item => item.text).join('\n');
61
+ }
62
+ return JSON.stringify(payload, null, 2);
63
+ }
64
+ function getLocationSummary(toolCall) {
65
+ var _a;
66
+ const firstLocation = (_a = toolCall.locations) === null || _a === void 0 ? void 0 : _a[0];
67
+ if (!firstLocation) {
68
+ return null;
69
+ }
70
+ return PathExt.basename(firstLocation) || firstLocation;
71
+ }
72
+ /**
73
+ * Compute the line title shown for a tool call.
74
+ */
75
+ export function getToolCallDisplayTitle(toolCall) {
76
+ var _a, _b, _c;
77
+ const title = (_a = toolCall.title) === null || _a === void 0 ? void 0 : _a.trim();
78
+ if (title) {
79
+ return title;
80
+ }
81
+ const verb = (_c = TOOL_KIND_LABELS[(_b = toolCall.kind) !== null && _b !== void 0 ? _b : '']) !== null && _c !== void 0 ? _c : 'Working';
82
+ const location = getLocationSummary(toolCall);
83
+ if (location) {
84
+ return `${verb} ${location}`;
85
+ }
86
+ return `${verb}...`;
87
+ }
88
+ /**
89
+ * Compute the pre-permission detail text for a tool call, or null if the
90
+ * title alone is enough.
91
+ */
92
+ export function buildPermissionDetail(toolCall) {
93
+ const { kind, title, locations, rawInput } = toolCall;
94
+ if (kind === 'execute') {
95
+ const rawObject = typeof rawInput === 'object' &&
96
+ rawInput !== null &&
97
+ !Array.isArray(rawInput)
98
+ ? rawInput
99
+ : null;
100
+ const command = rawObject && typeof rawObject.command === 'string'
101
+ ? rawObject.command
102
+ : (title === null || title === void 0 ? void 0 : title.replace(/^Running:\s*/i, '').replace(/\.\.\.$/, '').trim()) || null;
103
+ if (!command || command === title) {
104
+ return null;
105
+ }
106
+ return '$ ' + command;
107
+ }
108
+ if ((kind === 'delete' || kind === 'move' || kind === 'read') &&
109
+ (locations === null || locations === void 0 ? void 0 : locations.length)) {
110
+ return kind === 'move' && locations.length >= 2
111
+ ? `${toServerRelativePath(locations[0])} \u2192 ${toServerRelativePath(locations[1])}`
112
+ : locations.map(toServerRelativePath).join('\n');
113
+ }
114
+ if (rawInput !== null &&
115
+ rawInput !== undefined &&
116
+ typeof rawInput === 'object' &&
117
+ !Array.isArray(rawInput)) {
118
+ const rawObject = rawInput;
119
+ const purpose = typeof rawObject.__tool_use_purpose === 'string'
120
+ ? rawObject.__tool_use_purpose
121
+ : null;
122
+ const paramEntries = Object.entries(rawObject).filter(([key]) => !key.startsWith('__'));
123
+ const filteredParams = paramEntries.reduce((result, [key, value]) => {
124
+ result[key] = value;
125
+ return result;
126
+ }, {});
127
+ const params = paramEntries.length > 0 ? formatToolCallIO(filteredParams) : null;
128
+ if (purpose && params) {
129
+ return `${purpose}\n${params}`;
130
+ }
131
+ if (purpose) {
132
+ return purpose;
133
+ }
134
+ if (params) {
135
+ return params;
136
+ }
137
+ return null;
138
+ }
139
+ if (rawInput !== null && rawInput !== undefined) {
140
+ return formatToolCallIO(rawInput);
141
+ }
142
+ return null;
143
+ }
144
+ /**
145
+ * Returns true when a completed/failed tool call has expandable detail content.
146
+ */
147
+ function hasDetailContent(toolCall) {
148
+ const { kind, locations, rawInput, rawOutput } = toolCall;
149
+ const hasInput = rawInput !== null && rawInput !== undefined;
150
+ const hasOutput = rawOutput !== null && rawOutput !== undefined;
151
+ if (kind && FILE_KINDS.has(kind) && (locations === null || locations === void 0 ? void 0 : locations.length)) {
152
+ return true;
153
+ }
154
+ return hasInput || hasOutput;
155
+ }
156
+ function toDiffLineInfo(type, text, key) {
157
+ switch (type) {
158
+ case 'added':
159
+ return {
160
+ cssClass: 'jp-mod-added',
161
+ prefix: '+',
162
+ text,
163
+ key
164
+ };
165
+ case 'removed':
166
+ return {
167
+ cssClass: 'jp-mod-removed',
168
+ prefix: '-',
169
+ text,
170
+ key
171
+ };
172
+ case 'context':
173
+ return {
174
+ cssClass: 'jp-mod-context',
175
+ prefix: ' ',
176
+ text,
177
+ key
178
+ };
179
+ }
180
+ }
181
+ function buildDiffLinesFromHunk(hunk, hunkIndex) {
182
+ return hunk.lines
183
+ .filter(line => !line.startsWith('\\'))
184
+ .map((line, lineIndex) => {
185
+ var _a;
186
+ const prefix = (_a = line[0]) !== null && _a !== void 0 ? _a : ' ';
187
+ const text = line.slice(1);
188
+ const key = `${hunkIndex}-${hunk.oldStart}-${hunk.newStart}-${lineIndex}`;
189
+ if (prefix === '+') {
190
+ return toDiffLineInfo('added', text, key);
191
+ }
192
+ if (prefix === '-') {
193
+ return toDiffLineInfo('removed', text, key);
194
+ }
195
+ return toDiffLineInfo('context', text, key);
196
+ });
197
+ }
198
+ function buildDiffLines(diff) {
199
+ var _a;
200
+ const patch = structuredPatch(diff.path, diff.path, (_a = diff.oldText) !== null && _a !== void 0 ? _a : '', diff.newText, undefined, undefined, { context: Infinity });
201
+ return patch.hunks.reduce((lines, hunk, index) => {
202
+ lines.push(...buildDiffLinesFromHunk(hunk, index));
203
+ return lines;
204
+ }, []);
205
+ }
206
+ /**
207
+ * A labeled section with a capped height and show-all/show-less toggle.
208
+ */
209
+ function ToolCallSection({ label, children, trans }) {
210
+ const [expanded, setExpanded] = React.useState(false);
211
+ const [isOverflowing, setIsOverflowing] = React.useState(false);
212
+ const preRef = React.useRef(null);
213
+ React.useEffect(() => {
214
+ var _a;
215
+ const details = (_a = preRef.current) === null || _a === void 0 ? void 0 : _a.closest('details');
216
+ if (!details) {
217
+ return;
218
+ }
219
+ const handleToggle = () => {
220
+ if (!details.open) {
221
+ setExpanded(false);
222
+ }
223
+ };
224
+ details.addEventListener('toggle', handleToggle);
225
+ return () => details.removeEventListener('toggle', handleToggle);
226
+ }, []);
227
+ React.useLayoutEffect(() => {
228
+ const el = preRef.current;
229
+ if (!el) {
230
+ return;
231
+ }
232
+ const measure = () => {
233
+ if (!expanded) {
234
+ setIsOverflowing(el.scrollHeight > el.clientHeight);
235
+ }
236
+ };
237
+ const observer = new ResizeObserver(measure);
238
+ observer.observe(el);
239
+ measure();
240
+ return () => observer.disconnect();
241
+ }, [expanded]);
242
+ return (React.createElement("div", { className: "jp-ai-tool-call-detail-section" },
243
+ React.createElement("div", { className: "jp-ai-tool-call-detail-label" }, label),
244
+ React.createElement("pre", { ref: preRef, className: "jp-ai-tool-call-detail-code", style: {
245
+ maxHeight: expanded
246
+ ? undefined
247
+ : `calc(${MAX_DETAIL_LINES} * var(--jp-content-line-height) * var(--jp-ui-font-size1))`
248
+ } },
249
+ React.createElement("code", null, children)),
250
+ !expanded && isOverflowing && (React.createElement("button", { className: "jp-ai-tool-call-diff-toggle", onClick: () => setExpanded(true), type: "button" }, trans.__('Show all'))),
251
+ expanded && (React.createElement("button", { className: "jp-ai-tool-call-diff-toggle", onClick: () => setExpanded(false), type: "button" }, trans.__('Show less')))));
252
+ }
253
+ /**
254
+ * Expandable detail view for a completed or failed tool call.
255
+ */
256
+ function ToolCallDetail({ toolCall, trans }) {
257
+ const { kind, locations, rawInput, rawOutput } = toolCall;
258
+ const hasInput = rawInput !== null && rawInput !== undefined;
259
+ const hasOutput = rawOutput !== null && rawOutput !== undefined;
260
+ if (kind && FILE_KINDS.has(kind) && (locations === null || locations === void 0 ? void 0 : locations.length)) {
261
+ return (React.createElement("div", { className: "jp-ai-tool-call-item-detail" }, locations.map((loc, i) => (React.createElement("div", { key: i, className: "jp-ai-tool-call-item-detail-path" }, toServerRelativePath(loc))))));
262
+ }
263
+ return (React.createElement("div", { className: "jp-ai-tool-call-item-detail" },
264
+ hasInput && (React.createElement(ToolCallSection, { label: trans.__('Input'), trans: trans }, formatToolCallIO(rawInput))),
265
+ hasOutput && (React.createElement(ToolCallSection, { label: trans.__('Output'), trans: trans }, formatToolCallIO(rawOutput)))));
266
+ }
267
+ function ToolCallDiffBlock({ diff, trans, openToolCallPath, pendingPermission }) {
268
+ const [expanded, setExpanded] = React.useState(false);
269
+ const allLines = React.useMemo(() => buildDiffLines(diff), [diff]);
270
+ const canTruncate = allLines.length > MAX_DIFF_LINES;
271
+ const visibleLines = canTruncate && !expanded ? allLines.slice(0, MAX_DIFF_LINES) : allLines;
272
+ const hiddenCount = allLines.length - MAX_DIFF_LINES;
273
+ const displayPath = toServerRelativePath(diff.path);
274
+ const canOpenPath = !!openToolCallPath && !(pendingPermission && !diff.oldText);
275
+ return (React.createElement("div", { className: "jp-ai-tool-call-diff-block" },
276
+ React.createElement("div", { className: `jp-ai-tool-call-diff-header${canOpenPath ? ' jp-ai-tool-call-diff-header-clickable' : ''}`, onClick: canOpenPath ? () => openToolCallPath(displayPath) : undefined, title: displayPath }, displayPath),
277
+ React.createElement("div", { className: "jp-ai-tool-call-diff-content" },
278
+ visibleLines.length ? (visibleLines.map(line => (React.createElement("div", { key: line.key, className: `jp-ai-tool-call-diff-line ${line.cssClass}` },
279
+ React.createElement("span", { className: "jp-ai-tool-call-diff-line-prefix" }, line.prefix),
280
+ React.createElement("span", { className: "jp-ai-tool-call-diff-line-text" }, line.text))))) : (React.createElement("div", { className: "jp-ai-tool-call-diff-empty" }, trans.__('No changes'))),
281
+ canTruncate && !expanded && (React.createElement("button", { className: "jp-ai-tool-call-diff-toggle", onClick: () => setExpanded(true), type: "button" }, trans.__('... %1 more lines', hiddenCount))),
282
+ canTruncate && expanded && (React.createElement("button", { className: "jp-ai-tool-call-diff-toggle", onClick: () => setExpanded(false), type: "button" }, trans.__('Show less'))))));
283
+ }
284
+ function ToolCallDiffView({ diffs, trans, openToolCallPath, pendingPermission }) {
285
+ return (React.createElement("div", { className: "jp-ai-tool-call-diff-container" }, diffs.map((diff, index) => (React.createElement(ToolCallDiffBlock, { key: `${diff.path}-${index}`, diff: diff, trans: trans, openToolCallPath: openToolCallPath, pendingPermission: pendingPermission })))));
286
+ }
287
+ function PermissionLabel({ toolCall }) {
288
+ var _a, _b;
289
+ if (toolCall.permissionStatus !== 'resolved' || !toolCall.selectedOptionId) {
290
+ return null;
291
+ }
292
+ const selectedName = (_b = (_a = toolCall.permissionOptions) === null || _a === void 0 ? void 0 : _a.find(option => option.optionId === toolCall.selectedOptionId)) === null || _b === void 0 ? void 0 : _b.name;
293
+ if (!selectedName) {
294
+ return null;
295
+ }
296
+ return (React.createElement("span", { className: "jp-ai-tool-call-permission-label" },
297
+ " - ",
298
+ selectedName));
299
+ }
300
+ function PermissionButtons({ toolCall, trans, toolCallPermissionDecision }) {
301
+ var _a;
302
+ const [submitting, setSubmitting] = React.useState(false);
303
+ if (!((_a = toolCall.permissionOptions) === null || _a === void 0 ? void 0 : _a.length) ||
304
+ toolCall.permissionStatus !== 'pending') {
305
+ return null;
306
+ }
307
+ const canSubmit = !!toolCallPermissionDecision &&
308
+ !!toolCall.sessionId &&
309
+ !!toolCall.toolCallId;
310
+ const handleClick = async (optionId) => {
311
+ if (!canSubmit) {
312
+ return;
313
+ }
314
+ setSubmitting(true);
315
+ try {
316
+ await toolCallPermissionDecision(toolCall.sessionId, toolCall.toolCallId, optionId);
317
+ }
318
+ catch (error) {
319
+ console.error('Failed to submit tool call permission decision:', error);
320
+ setSubmitting(false);
321
+ }
322
+ };
323
+ return (React.createElement("div", { className: "jp-ai-tool-call-permission-buttons" },
324
+ React.createElement("span", { className: "jp-ai-tool-call-permission-tree" }, '\u2514\u2500'),
325
+ React.createElement("span", null, trans.__('Allow?')),
326
+ toolCall.permissionOptions.map(option => {
327
+ const optionClass = option.kind
328
+ ? ` jp-ai-tool-call-permission-btn-${option.kind.replace(/_/g, '-')}`
329
+ : '';
330
+ return (React.createElement("button", { key: option.optionId, className: `jp-ai-tool-call-permission-btn${optionClass}`, onClick: () => handleClick(option.optionId), disabled: submitting || !canSubmit, title: option.kind, type: "button" }, option.name));
331
+ })));
332
+ }
333
+ function ToolCallRow({ toolCall, trans, openToolCallPath, toolCallPermissionDecision }) {
334
+ var _a, _b, _c, _d;
335
+ const displayTitle = getToolCallDisplayTitle(toolCall);
336
+ const selectedOption = (_a = toolCall.permissionOptions) === null || _a === void 0 ? void 0 : _a.find(option => option.optionId === toolCall.selectedOptionId);
337
+ const status = (_b = toolCall.status) !== null && _b !== void 0 ? _b : 'in_progress';
338
+ const isRejected = toolCall.permissionStatus === 'resolved' &&
339
+ (!!((_c = selectedOption === null || selectedOption === void 0 ? void 0 : selectedOption.kind) === null || _c === void 0 ? void 0 : _c.includes('reject')) || status === 'rejected');
340
+ const hasPendingPermission = toolCall.permissionStatus === 'pending';
341
+ const isInProgress = !isRejected &&
342
+ (status === 'in_progress' || status === 'pending' || hasPendingPermission);
343
+ const isCompleted = status === 'completed';
344
+ const isFailed = status === 'failed' || isRejected;
345
+ const icon = isInProgress
346
+ ? '\u2022'
347
+ : isCompleted
348
+ ? '\u2713'
349
+ : isFailed
350
+ ? '\u2717'
351
+ : '\u2022';
352
+ const effectiveStatus = (isRejected ? 'failed' : status).replace(/_/g, '-');
353
+ const hasDiffs = !!((_d = toolCall.diffs) === null || _d === void 0 ? void 0 : _d.length);
354
+ const hasExpandableContent = hasDiffs ||
355
+ (!hasDiffs && (isCompleted || isFailed) && hasDetailContent(toolCall));
356
+ const cssClass = `jp-ai-tool-call-item jp-ai-tool-call-item-${effectiveStatus}${!hasExpandableContent && !hasPendingPermission ? ' jp-ai-tool-call-item-no-detail' : ''}`;
357
+ if (hasDiffs && hasPendingPermission) {
358
+ return (React.createElement("div", { className: cssClass },
359
+ React.createElement("details", { open: true },
360
+ React.createElement("summary", null,
361
+ React.createElement("span", { className: "jp-ai-tool-call-item-icon" }, icon),
362
+ ' ',
363
+ React.createElement("div", { className: "jp-ai-tool-call-item-title" },
364
+ displayTitle,
365
+ toolCall.summary && (React.createElement("span", { className: "jp-ai-tool-call-item-summary" },
366
+ ' ',
367
+ toolCall.summary)))),
368
+ React.createElement(ToolCallDiffView, { diffs: toolCall.diffs, trans: trans, openToolCallPath: openToolCallPath, pendingPermission: true })),
369
+ React.createElement(PermissionButtons, { toolCall: toolCall, trans: trans, toolCallPermissionDecision: toolCallPermissionDecision })));
370
+ }
371
+ if (!hasDiffs && hasPendingPermission) {
372
+ const permissionDetail = buildPermissionDetail(toolCall);
373
+ if (permissionDetail !== null) {
374
+ return (React.createElement("div", { className: cssClass },
375
+ React.createElement("details", { open: true },
376
+ React.createElement("summary", null,
377
+ React.createElement("span", { className: "jp-ai-tool-call-item-icon" }, icon),
378
+ ' ',
379
+ React.createElement("div", { className: "jp-ai-tool-call-item-title" },
380
+ displayTitle,
381
+ toolCall.summary && (React.createElement("span", { className: "jp-ai-tool-call-item-summary" },
382
+ ' ',
383
+ toolCall.summary)))),
384
+ React.createElement("div", { className: "jp-ai-tool-call-item-detail" }, permissionDetail)),
385
+ React.createElement(PermissionButtons, { toolCall: toolCall, trans: trans, toolCallPermissionDecision: toolCallPermissionDecision })));
386
+ }
387
+ }
388
+ if ((isCompleted || isFailed) && hasExpandableContent) {
389
+ return (React.createElement("details", { className: cssClass },
390
+ React.createElement("summary", null,
391
+ React.createElement("span", { className: "jp-ai-tool-call-item-icon" }, icon),
392
+ ' ',
393
+ React.createElement("div", { className: "jp-ai-tool-call-item-title" },
394
+ displayTitle,
395
+ toolCall.summary && (React.createElement("span", { className: "jp-ai-tool-call-item-summary" },
396
+ ' ',
397
+ toolCall.summary))),
398
+ React.createElement(PermissionLabel, { toolCall: toolCall })),
399
+ hasDiffs ? (React.createElement(ToolCallDiffView, { diffs: toolCall.diffs, trans: trans, openToolCallPath: openToolCallPath })) : (React.createElement(ToolCallDetail, { toolCall: toolCall, trans: trans }))));
400
+ }
401
+ if (isInProgress) {
402
+ return (React.createElement("div", { className: cssClass },
403
+ React.createElement("span", { className: "jp-ai-tool-call-item-icon" }, icon),
404
+ ' ',
405
+ React.createElement("div", { className: "jp-ai-tool-call-item-title" },
406
+ displayTitle,
407
+ toolCall.summary && (React.createElement("span", { className: "jp-ai-tool-call-item-summary" },
408
+ ' ',
409
+ toolCall.summary))),
410
+ React.createElement(PermissionButtons, { toolCall: toolCall, trans: trans, toolCallPermissionDecision: toolCallPermissionDecision })));
411
+ }
412
+ return (React.createElement("div", { className: cssClass },
413
+ React.createElement("span", { className: "jp-ai-tool-call-item-icon" }, icon),
414
+ React.createElement("div", { className: "jp-ai-tool-call-item-title" },
415
+ displayTitle,
416
+ toolCall.summary && (React.createElement("span", { className: "jp-ai-tool-call-item-summary" },
417
+ ' ',
418
+ toolCall.summary))),
419
+ React.createElement(PermissionLabel, { toolCall: toolCall })));
420
+ }
421
+ /**
422
+ * React component for rendering grouped tool calls.
423
+ */
424
+ export const GroupedToolCalls = props => {
425
+ var _a;
426
+ const trans = (_a = props.trans) !== null && _a !== void 0 ? _a : nullTranslator.load('jupyterlab');
427
+ const { toolCalls } = props;
428
+ if (!toolCalls.length) {
429
+ return null;
430
+ }
431
+ return (React.createElement("div", { className: "jp-ai-tool-calls" }, toolCalls.map(toolCall => (React.createElement(ToolCallRow, { key: toolCall.toolCallId, toolCall: toolCall, trans: trans, openToolCallPath: props.openToolCallPath, toolCallPermissionDecision: props.toolCallPermissionDecision })))));
432
+ };
@@ -1,3 +1,4 @@
1
1
  export * from './inline-diff';
2
2
  export * from './message-queue';
3
3
  export * from './tool-call';
4
+ export * from './grouped-tool-calls';
@@ -1,3 +1,4 @@
1
1
  export * from './inline-diff';
2
2
  export * from './message-queue';
3
3
  export * from './tool-call';
4
+ export * from './grouped-tool-calls';
package/lib/factory.d.ts CHANGED
@@ -2,7 +2,7 @@ import { IRenderMime } from '@jupyterlab/rendermime-interfaces';
2
2
  import { ReactWidget } from '@jupyterlab/ui-components';
3
3
  import * as React from 'react';
4
4
  import { ComponentRegistry } from './registry';
5
- import { IComponentRegistry, IComponentsRendererFactory, RemoveQueuedMessage, ToolCallApproval } from './token';
5
+ import { IComponentRegistry, IComponentsRendererFactory, OpenToolCallPath, RemoveQueuedMessage, ToolCallApproval, ToolCallPermissionDecision } from './token';
6
6
  type ReactRenderElement = Array<React.ReactElement<any>> | React.ReactElement<any>;
7
7
  /**
8
8
  * The options for the chat components renderer.
@@ -16,6 +16,14 @@ interface IComponentsRendererOptions extends IRenderMime.IRendererOptions {
16
16
  * The callback to remove a queued message.
17
17
  */
18
18
  removeQueuedMessage?: RemoveQueuedMessage;
19
+ /**
20
+ * The callback to submit a permission decision for grouped tool calls.
21
+ */
22
+ toolCallPermissionDecision?: ToolCallPermissionDecision;
23
+ /**
24
+ * The callback to open a path referenced by grouped tool calls.
25
+ */
26
+ openToolCallPath?: OpenToolCallPath;
19
27
  /**
20
28
  * The component registry.
21
29
  */
@@ -38,6 +46,8 @@ export declare class ComponentsRenderer extends ReactWidget implements IRenderMi
38
46
  private _mimeType;
39
47
  private _toolCallApproval?;
40
48
  private _removeQueuedMessage?;
49
+ private _toolCallPermissionDecision?;
50
+ private _openToolCallPath?;
41
51
  private _registry;
42
52
  private _data;
43
53
  private _metadata;
@@ -52,6 +62,8 @@ export declare class RendererFactory implements IComponentsRendererFactory {
52
62
  readonly registry: ComponentRegistry;
53
63
  toolCallApproval: ToolCallApproval;
54
64
  removeQueuedMessage: RemoveQueuedMessage;
65
+ toolCallPermissionDecision: ToolCallPermissionDecision;
66
+ openToolCallPath: OpenToolCallPath;
55
67
  constructor();
56
68
  createRenderer: (options: IRenderMime.IRendererOptions) => ComponentsRenderer;
57
69
  }
package/lib/factory.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { nullTranslator } from '@jupyterlab/translation';
2
2
  import { ReactWidget } from '@jupyterlab/ui-components';
3
3
  import * as React from 'react';
4
- import { InlineDiff, MessageQueue, ToolCall } from './components';
4
+ import { GroupedToolCalls, InlineDiff, MessageQueue, ToolCall } from './components';
5
5
  import { ComponentRegistry } from './registry';
6
6
  /**
7
7
  * The default mime type for the extension.
@@ -27,6 +27,8 @@ export class ComponentsRenderer extends ReactWidget {
27
27
  this._mimeType = options.mimeType;
28
28
  this._toolCallApproval = options.toolCallApproval;
29
29
  this._removeQueuedMessage = options.removeQueuedMessage;
30
+ this._toolCallPermissionDecision = options.toolCallPermissionDecision;
31
+ this._openToolCallPath = options.openToolCallPath;
30
32
  this._registry = options.registry;
31
33
  this.addClass(CLASS_NAME);
32
34
  }
@@ -57,6 +59,11 @@ export class ComponentsRenderer extends ReactWidget {
57
59
  if (this._data === 'message-queue') {
58
60
  componentsProps.removeQueuedMessage = this._removeQueuedMessage;
59
61
  }
62
+ if (this._data === 'grouped-tool-calls') {
63
+ componentsProps.toolCallPermissionDecision =
64
+ this._toolCallPermissionDecision;
65
+ componentsProps.openToolCallPath = this._openToolCallPath;
66
+ }
60
67
  return React.createElement(Component, { ...componentsProps, trans: this._trans });
61
68
  }
62
69
  }
@@ -70,16 +77,21 @@ export class RendererFactory {
70
77
  this.defaultRank = 100;
71
78
  this.toolCallApproval = null;
72
79
  this.removeQueuedMessage = null;
80
+ this.toolCallPermissionDecision = null;
81
+ this.openToolCallPath = null;
73
82
  this.createRenderer = (options) => {
74
83
  return new ComponentsRenderer({
75
84
  ...options,
76
85
  toolCallApproval: this.toolCallApproval,
77
86
  removeQueuedMessage: this.removeQueuedMessage,
87
+ toolCallPermissionDecision: this.toolCallPermissionDecision,
88
+ openToolCallPath: this.openToolCallPath,
78
89
  registry: this.registry
79
90
  });
80
91
  };
81
92
  this.registry = new ComponentRegistry();
82
93
  this.registry.add('tool-call', ToolCall);
94
+ this.registry.add('grouped-tool-calls', GroupedToolCalls);
83
95
  this.registry.add('inline-diff', InlineDiff);
84
96
  this.registry.add('message-queue', MessageQueue);
85
97
  }