jupyter-chat-components 0.2.0 → 0.4.0
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 +26 -1
- package/lib/components/grouped-tool-calls.d.ts +38 -0
- package/lib/components/grouped-tool-calls.js +381 -0
- package/lib/components/index.d.ts +2 -0
- package/lib/components/index.js +2 -0
- package/lib/components/inline-diff.d.ts +4 -2
- package/lib/components/inline-diff.js +51 -5
- package/lib/components/message-queue.d.ts +13 -0
- package/lib/components/message-queue.js +13 -0
- package/lib/factory.d.ts +19 -1
- package/lib/factory.js +20 -1
- package/lib/token.d.ts +190 -2
- package/package.json +1 -1
- package/src/components/grouped-tool-calls.tsx +702 -0
- package/src/components/index.ts +2 -0
- package/src/components/inline-diff.tsx +72 -7
- package/src/components/message-queue.tsx +50 -0
- package/src/factory.tsx +49 -2
- package/src/token.ts +215 -2
- package/style/base.css +275 -0
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
import { PageConfig, PathExt } from '@jupyterlab/coreutils';
|
|
2
|
+
import { nullTranslator, TranslationBundle } from '@jupyterlab/translation';
|
|
3
|
+
|
|
4
|
+
import * as React from 'react';
|
|
5
|
+
import { structuredPatch } from 'diff';
|
|
6
|
+
import type { StructuredPatchHunk } from 'diff';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
IComponentProps,
|
|
10
|
+
IToolCallDiff,
|
|
11
|
+
IToolCallsEntry,
|
|
12
|
+
IToolCallsMetadata,
|
|
13
|
+
OpenToolCallPath,
|
|
14
|
+
ToolCallPermissionDecision
|
|
15
|
+
} from '../token';
|
|
16
|
+
|
|
17
|
+
/** Maximum number of rendered diff lines before truncation. */
|
|
18
|
+
const MAX_DIFF_LINES = 20;
|
|
19
|
+
|
|
20
|
+
/** Tool kinds where expanded view shows file paths from locations. */
|
|
21
|
+
const FILE_KINDS = new Set(['read', 'edit', 'delete', 'move']);
|
|
22
|
+
|
|
23
|
+
/** Tool kinds where expanded view shows raw output. */
|
|
24
|
+
const OUTPUT_KINDS = new Set(['search', 'execute', 'think', 'fetch']);
|
|
25
|
+
|
|
26
|
+
const TOOL_KIND_LABELS: Record<string, string> = {
|
|
27
|
+
read: 'Reading',
|
|
28
|
+
edit: 'Editing',
|
|
29
|
+
delete: 'Deleting',
|
|
30
|
+
move: 'Moving',
|
|
31
|
+
search: 'Searching',
|
|
32
|
+
execute: 'Running command',
|
|
33
|
+
think: 'Thinking',
|
|
34
|
+
fetch: 'Fetching',
|
|
35
|
+
switch_mode: 'Switching mode'
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
interface IDiffLineInfo {
|
|
39
|
+
cssClass: string;
|
|
40
|
+
prefix: string;
|
|
41
|
+
text: string;
|
|
42
|
+
key: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Props for rendering grouped tool calls.
|
|
47
|
+
*/
|
|
48
|
+
export interface IGroupedToolCallsProps
|
|
49
|
+
extends IComponentProps, IToolCallsMetadata {
|
|
50
|
+
toolCallPermissionDecision?: ToolCallPermissionDecision;
|
|
51
|
+
openToolCallPath?: OpenToolCallPath;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getConfiguredServerRoot(): string | null {
|
|
55
|
+
const rootUri = PageConfig.getOption('rootUri');
|
|
56
|
+
|
|
57
|
+
if (rootUri) {
|
|
58
|
+
try {
|
|
59
|
+
return new URL(rootUri, 'http://localhost').pathname;
|
|
60
|
+
} catch (error) {
|
|
61
|
+
console.warn(
|
|
62
|
+
'Could not parse rootUri while rendering tool calls.',
|
|
63
|
+
error
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const serverRoot = PageConfig.getOption('serverRoot');
|
|
69
|
+
return serverRoot || null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Convert an absolute filesystem path to a server-relative path when possible.
|
|
74
|
+
*/
|
|
75
|
+
export function toServerRelativePath(absolutePath: string): string {
|
|
76
|
+
const serverRoot = getConfiguredServerRoot();
|
|
77
|
+
|
|
78
|
+
if (!serverRoot) {
|
|
79
|
+
return absolutePath;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const relativePath = PathExt.relative(serverRoot, absolutePath);
|
|
83
|
+
if (relativePath.startsWith('..')) {
|
|
84
|
+
return absolutePath;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return relativePath;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Format tool output for display.
|
|
92
|
+
*/
|
|
93
|
+
export function formatToolCallOutput(rawOutput: unknown): string {
|
|
94
|
+
if (typeof rawOutput === 'string') {
|
|
95
|
+
return rawOutput;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (
|
|
99
|
+
Array.isArray(rawOutput) &&
|
|
100
|
+
rawOutput.every(
|
|
101
|
+
item =>
|
|
102
|
+
typeof item === 'object' &&
|
|
103
|
+
item !== null &&
|
|
104
|
+
typeof (item as { text?: unknown }).text === 'string'
|
|
105
|
+
)
|
|
106
|
+
) {
|
|
107
|
+
return rawOutput.map(item => (item as { text: string }).text).join('\n');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return JSON.stringify(rawOutput, null, 2);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Format tool input for display.
|
|
115
|
+
*/
|
|
116
|
+
export function formatToolCallInput(input: unknown): string {
|
|
117
|
+
if (typeof input === 'string') {
|
|
118
|
+
return input;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
122
|
+
return JSON.stringify(input, null, 2);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const entries = Object.entries(input as Record<string, unknown>);
|
|
126
|
+
const isFlat = entries.every(
|
|
127
|
+
([, value]) =>
|
|
128
|
+
typeof value === 'string' ||
|
|
129
|
+
typeof value === 'number' ||
|
|
130
|
+
typeof value === 'boolean'
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
if (isFlat) {
|
|
134
|
+
return entries.map(([key, value]) => `${key}: ${value}`).join('\n');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return JSON.stringify(input, null, 2);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function getLocationSummary(toolCall: IToolCallsEntry): string | null {
|
|
141
|
+
const firstLocation = toolCall.locations?.[0];
|
|
142
|
+
|
|
143
|
+
if (!firstLocation) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return PathExt.basename(firstLocation) || firstLocation;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Compute the line title shown for a tool call.
|
|
152
|
+
*/
|
|
153
|
+
export function getToolCallDisplayTitle(toolCall: IToolCallsEntry): string {
|
|
154
|
+
const title = toolCall.title?.trim();
|
|
155
|
+
|
|
156
|
+
if (title) {
|
|
157
|
+
return title;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const verb = TOOL_KIND_LABELS[toolCall.kind ?? ''] ?? 'Working';
|
|
161
|
+
const location = getLocationSummary(toolCall);
|
|
162
|
+
|
|
163
|
+
if (location) {
|
|
164
|
+
return `${verb} ${location}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return `${verb}...`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Compute the pre-permission detail text for a tool call, or null if the
|
|
172
|
+
* title alone is enough.
|
|
173
|
+
*/
|
|
174
|
+
export function buildPermissionDetail(
|
|
175
|
+
toolCall: IToolCallsEntry
|
|
176
|
+
): string | null {
|
|
177
|
+
const { kind, title, locations, rawInput } = toolCall;
|
|
178
|
+
|
|
179
|
+
if (kind === 'execute') {
|
|
180
|
+
const rawObject =
|
|
181
|
+
typeof rawInput === 'object' &&
|
|
182
|
+
rawInput !== null &&
|
|
183
|
+
!Array.isArray(rawInput)
|
|
184
|
+
? (rawInput as Record<string, unknown>)
|
|
185
|
+
: null;
|
|
186
|
+
const command =
|
|
187
|
+
rawObject && typeof rawObject.command === 'string'
|
|
188
|
+
? rawObject.command
|
|
189
|
+
: title
|
|
190
|
+
?.replace(/^Running:\s*/i, '')
|
|
191
|
+
.replace(/\.\.\.$/, '')
|
|
192
|
+
.trim() || null;
|
|
193
|
+
|
|
194
|
+
if (!command || command === title) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return '$ ' + command;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (
|
|
202
|
+
(kind === 'delete' || kind === 'move' || kind === 'read') &&
|
|
203
|
+
locations?.length
|
|
204
|
+
) {
|
|
205
|
+
return kind === 'move' && locations.length >= 2
|
|
206
|
+
? `${toServerRelativePath(locations[0])} \u2192 ${toServerRelativePath(
|
|
207
|
+
locations[1]
|
|
208
|
+
)}`
|
|
209
|
+
: locations.map(toServerRelativePath).join('\n');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (
|
|
213
|
+
rawInput !== null &&
|
|
214
|
+
rawInput !== undefined &&
|
|
215
|
+
typeof rawInput === 'object' &&
|
|
216
|
+
!Array.isArray(rawInput)
|
|
217
|
+
) {
|
|
218
|
+
const rawObject = rawInput as Record<string, unknown>;
|
|
219
|
+
const purpose =
|
|
220
|
+
typeof rawObject.__tool_use_purpose === 'string'
|
|
221
|
+
? rawObject.__tool_use_purpose
|
|
222
|
+
: null;
|
|
223
|
+
const paramEntries = Object.entries(rawObject).filter(
|
|
224
|
+
([key]) => !key.startsWith('__')
|
|
225
|
+
);
|
|
226
|
+
const filteredParams = paramEntries.reduce<Record<string, unknown>>(
|
|
227
|
+
(result, [key, value]) => {
|
|
228
|
+
result[key] = value;
|
|
229
|
+
return result;
|
|
230
|
+
},
|
|
231
|
+
{}
|
|
232
|
+
);
|
|
233
|
+
const params =
|
|
234
|
+
paramEntries.length > 0 ? formatToolCallInput(filteredParams) : null;
|
|
235
|
+
|
|
236
|
+
if (purpose && params) {
|
|
237
|
+
return `${purpose}\n${params}`;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (purpose) {
|
|
241
|
+
return purpose;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (params) {
|
|
245
|
+
return params;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (rawInput !== null && rawInput !== undefined) {
|
|
252
|
+
return formatToolCallInput(rawInput);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Build the expandable detail lines shown for completed and failed tool calls.
|
|
260
|
+
*/
|
|
261
|
+
export function buildToolCallDetailsLines(toolCall: IToolCallsEntry): string[] {
|
|
262
|
+
const lines: string[] = [];
|
|
263
|
+
const { kind, locations, rawOutput } = toolCall;
|
|
264
|
+
|
|
265
|
+
if (kind && FILE_KINDS.has(kind) && locations?.length) {
|
|
266
|
+
lines.push(...locations.map(toServerRelativePath));
|
|
267
|
+
} else if (
|
|
268
|
+
kind &&
|
|
269
|
+
OUTPUT_KINDS.has(kind) &&
|
|
270
|
+
rawOutput !== null &&
|
|
271
|
+
rawOutput !== undefined
|
|
272
|
+
) {
|
|
273
|
+
lines.push(formatToolCallOutput(rawOutput));
|
|
274
|
+
} else if (typeof rawOutput === 'string') {
|
|
275
|
+
lines.push(rawOutput);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return lines;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function toDiffLineInfo(
|
|
282
|
+
type: 'added' | 'removed' | 'context',
|
|
283
|
+
text: string,
|
|
284
|
+
key: string
|
|
285
|
+
): IDiffLineInfo {
|
|
286
|
+
switch (type) {
|
|
287
|
+
case 'added':
|
|
288
|
+
return {
|
|
289
|
+
cssClass: 'jp-mod-added',
|
|
290
|
+
prefix: '+',
|
|
291
|
+
text,
|
|
292
|
+
key
|
|
293
|
+
};
|
|
294
|
+
case 'removed':
|
|
295
|
+
return {
|
|
296
|
+
cssClass: 'jp-mod-removed',
|
|
297
|
+
prefix: '-',
|
|
298
|
+
text,
|
|
299
|
+
key
|
|
300
|
+
};
|
|
301
|
+
case 'context':
|
|
302
|
+
return {
|
|
303
|
+
cssClass: 'jp-mod-context',
|
|
304
|
+
prefix: ' ',
|
|
305
|
+
text,
|
|
306
|
+
key
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function buildDiffLinesFromHunk(
|
|
312
|
+
hunk: StructuredPatchHunk,
|
|
313
|
+
hunkIndex: number
|
|
314
|
+
): IDiffLineInfo[] {
|
|
315
|
+
return hunk.lines
|
|
316
|
+
.filter(line => !line.startsWith('\\'))
|
|
317
|
+
.map((line, lineIndex) => {
|
|
318
|
+
const prefix = line[0] ?? ' ';
|
|
319
|
+
const text = line.slice(1);
|
|
320
|
+
const key = `${hunkIndex}-${hunk.oldStart}-${hunk.newStart}-${lineIndex}`;
|
|
321
|
+
|
|
322
|
+
if (prefix === '+') {
|
|
323
|
+
return toDiffLineInfo('added', text, key);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (prefix === '-') {
|
|
327
|
+
return toDiffLineInfo('removed', text, key);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return toDiffLineInfo('context', text, key);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function buildDiffLines(diff: IToolCallDiff): IDiffLineInfo[] {
|
|
335
|
+
const patch = structuredPatch(
|
|
336
|
+
diff.path,
|
|
337
|
+
diff.path,
|
|
338
|
+
diff.oldText ?? '',
|
|
339
|
+
diff.newText,
|
|
340
|
+
undefined,
|
|
341
|
+
undefined,
|
|
342
|
+
{ context: Infinity }
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
return patch.hunks.reduce<IDiffLineInfo[]>((lines, hunk, index) => {
|
|
346
|
+
lines.push(...buildDiffLinesFromHunk(hunk, index));
|
|
347
|
+
return lines;
|
|
348
|
+
}, []);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function ToolCallDiffBlock({
|
|
352
|
+
diff,
|
|
353
|
+
trans,
|
|
354
|
+
openToolCallPath,
|
|
355
|
+
pendingPermission
|
|
356
|
+
}: {
|
|
357
|
+
diff: IToolCallDiff;
|
|
358
|
+
trans: TranslationBundle;
|
|
359
|
+
openToolCallPath?: OpenToolCallPath;
|
|
360
|
+
pendingPermission?: boolean;
|
|
361
|
+
}): JSX.Element {
|
|
362
|
+
const [expanded, setExpanded] = React.useState(false);
|
|
363
|
+
const allLines = React.useMemo(() => buildDiffLines(diff), [diff]);
|
|
364
|
+
const canTruncate = allLines.length > MAX_DIFF_LINES;
|
|
365
|
+
const visibleLines =
|
|
366
|
+
canTruncate && !expanded ? allLines.slice(0, MAX_DIFF_LINES) : allLines;
|
|
367
|
+
const hiddenCount = allLines.length - MAX_DIFF_LINES;
|
|
368
|
+
const displayPath = toServerRelativePath(diff.path);
|
|
369
|
+
const canOpenPath =
|
|
370
|
+
!!openToolCallPath && !(pendingPermission && !diff.oldText);
|
|
371
|
+
|
|
372
|
+
return (
|
|
373
|
+
<div className="jp-ai-tool-call-diff-block">
|
|
374
|
+
<div
|
|
375
|
+
className={`jp-ai-tool-call-diff-header${
|
|
376
|
+
canOpenPath ? ' jp-ai-tool-call-diff-header-clickable' : ''
|
|
377
|
+
}`}
|
|
378
|
+
onClick={canOpenPath ? () => openToolCallPath!(displayPath) : undefined}
|
|
379
|
+
title={displayPath}
|
|
380
|
+
>
|
|
381
|
+
{displayPath}
|
|
382
|
+
</div>
|
|
383
|
+
<div className="jp-ai-tool-call-diff-content">
|
|
384
|
+
{visibleLines.length ? (
|
|
385
|
+
visibleLines.map(line => (
|
|
386
|
+
<div
|
|
387
|
+
key={line.key}
|
|
388
|
+
className={`jp-ai-tool-call-diff-line ${line.cssClass}`}
|
|
389
|
+
>
|
|
390
|
+
<span className="jp-ai-tool-call-diff-line-prefix">
|
|
391
|
+
{line.prefix}
|
|
392
|
+
</span>
|
|
393
|
+
<span className="jp-ai-tool-call-diff-line-text">
|
|
394
|
+
{line.text}
|
|
395
|
+
</span>
|
|
396
|
+
</div>
|
|
397
|
+
))
|
|
398
|
+
) : (
|
|
399
|
+
<div className="jp-ai-tool-call-diff-empty">
|
|
400
|
+
{trans.__('No changes')}
|
|
401
|
+
</div>
|
|
402
|
+
)}
|
|
403
|
+
{canTruncate && !expanded && (
|
|
404
|
+
<button
|
|
405
|
+
className="jp-ai-tool-call-diff-toggle"
|
|
406
|
+
onClick={() => setExpanded(true)}
|
|
407
|
+
type="button"
|
|
408
|
+
>
|
|
409
|
+
{trans.__('... %1 more lines', hiddenCount)}
|
|
410
|
+
</button>
|
|
411
|
+
)}
|
|
412
|
+
{canTruncate && expanded && (
|
|
413
|
+
<button
|
|
414
|
+
className="jp-ai-tool-call-diff-toggle"
|
|
415
|
+
onClick={() => setExpanded(false)}
|
|
416
|
+
type="button"
|
|
417
|
+
>
|
|
418
|
+
{trans.__('Show less')}
|
|
419
|
+
</button>
|
|
420
|
+
)}
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function ToolCallDiffView({
|
|
427
|
+
diffs,
|
|
428
|
+
trans,
|
|
429
|
+
openToolCallPath,
|
|
430
|
+
pendingPermission
|
|
431
|
+
}: {
|
|
432
|
+
diffs: IToolCallDiff[];
|
|
433
|
+
trans: TranslationBundle;
|
|
434
|
+
openToolCallPath?: OpenToolCallPath;
|
|
435
|
+
pendingPermission?: boolean;
|
|
436
|
+
}): JSX.Element {
|
|
437
|
+
return (
|
|
438
|
+
<div className="jp-ai-tool-call-diff-container">
|
|
439
|
+
{diffs.map((diff, index) => (
|
|
440
|
+
<ToolCallDiffBlock
|
|
441
|
+
key={`${diff.path}-${index}`}
|
|
442
|
+
diff={diff}
|
|
443
|
+
trans={trans}
|
|
444
|
+
openToolCallPath={openToolCallPath}
|
|
445
|
+
pendingPermission={pendingPermission}
|
|
446
|
+
/>
|
|
447
|
+
))}
|
|
448
|
+
</div>
|
|
449
|
+
);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function PermissionLabel({
|
|
453
|
+
toolCall
|
|
454
|
+
}: {
|
|
455
|
+
toolCall: IToolCallsEntry;
|
|
456
|
+
}): JSX.Element | null {
|
|
457
|
+
if (toolCall.permissionStatus !== 'resolved' || !toolCall.selectedOptionId) {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const selectedName = toolCall.permissionOptions?.find(
|
|
462
|
+
option => option.optionId === toolCall.selectedOptionId
|
|
463
|
+
)?.name;
|
|
464
|
+
|
|
465
|
+
if (!selectedName) {
|
|
466
|
+
return null;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return (
|
|
470
|
+
<span className="jp-ai-tool-call-permission-label"> - {selectedName}</span>
|
|
471
|
+
);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function PermissionButtons({
|
|
475
|
+
toolCall,
|
|
476
|
+
trans,
|
|
477
|
+
toolCallPermissionDecision
|
|
478
|
+
}: {
|
|
479
|
+
toolCall: IToolCallsEntry;
|
|
480
|
+
trans: TranslationBundle;
|
|
481
|
+
toolCallPermissionDecision?: ToolCallPermissionDecision;
|
|
482
|
+
}): JSX.Element | null {
|
|
483
|
+
const [submitting, setSubmitting] = React.useState(false);
|
|
484
|
+
|
|
485
|
+
if (
|
|
486
|
+
!toolCall.permissionOptions?.length ||
|
|
487
|
+
toolCall.permissionStatus !== 'pending'
|
|
488
|
+
) {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const canSubmit =
|
|
493
|
+
!!toolCallPermissionDecision &&
|
|
494
|
+
!!toolCall.sessionId &&
|
|
495
|
+
!!toolCall.toolCallId;
|
|
496
|
+
|
|
497
|
+
const handleClick = async (optionId: string) => {
|
|
498
|
+
if (!canSubmit) {
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
setSubmitting(true);
|
|
503
|
+
|
|
504
|
+
try {
|
|
505
|
+
await toolCallPermissionDecision!(
|
|
506
|
+
toolCall.sessionId!,
|
|
507
|
+
toolCall.toolCallId,
|
|
508
|
+
optionId
|
|
509
|
+
);
|
|
510
|
+
} catch (error) {
|
|
511
|
+
console.error('Failed to submit tool call permission decision:', error);
|
|
512
|
+
setSubmitting(false);
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
return (
|
|
517
|
+
<div className="jp-ai-tool-call-permission-buttons">
|
|
518
|
+
<span className="jp-ai-tool-call-permission-tree">{'\u2514\u2500'}</span>
|
|
519
|
+
<span>{trans.__('Allow?')}</span>
|
|
520
|
+
{toolCall.permissionOptions.map(option => {
|
|
521
|
+
const optionClass = option.kind
|
|
522
|
+
? ` jp-ai-tool-call-permission-btn-${option.kind.replace(/_/g, '-')}`
|
|
523
|
+
: '';
|
|
524
|
+
|
|
525
|
+
return (
|
|
526
|
+
<button
|
|
527
|
+
key={option.optionId}
|
|
528
|
+
className={`jp-ai-tool-call-permission-btn${optionClass}`}
|
|
529
|
+
onClick={() => handleClick(option.optionId)}
|
|
530
|
+
disabled={submitting || !canSubmit}
|
|
531
|
+
title={option.kind}
|
|
532
|
+
type="button"
|
|
533
|
+
>
|
|
534
|
+
{option.name}
|
|
535
|
+
</button>
|
|
536
|
+
);
|
|
537
|
+
})}
|
|
538
|
+
</div>
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function ToolCallRow({
|
|
543
|
+
toolCall,
|
|
544
|
+
trans,
|
|
545
|
+
openToolCallPath,
|
|
546
|
+
toolCallPermissionDecision
|
|
547
|
+
}: {
|
|
548
|
+
toolCall: IToolCallsEntry;
|
|
549
|
+
trans: TranslationBundle;
|
|
550
|
+
openToolCallPath?: OpenToolCallPath;
|
|
551
|
+
toolCallPermissionDecision?: ToolCallPermissionDecision;
|
|
552
|
+
}): JSX.Element {
|
|
553
|
+
const displayTitle = getToolCallDisplayTitle(toolCall);
|
|
554
|
+
const selectedOption = toolCall.permissionOptions?.find(
|
|
555
|
+
option => option.optionId === toolCall.selectedOptionId
|
|
556
|
+
);
|
|
557
|
+
const isRejected =
|
|
558
|
+
toolCall.permissionStatus === 'resolved' &&
|
|
559
|
+
!!selectedOption?.kind?.includes('reject');
|
|
560
|
+
const hasPendingPermission = toolCall.permissionStatus === 'pending';
|
|
561
|
+
const status = toolCall.status ?? 'in_progress';
|
|
562
|
+
const isInProgress =
|
|
563
|
+
!isRejected &&
|
|
564
|
+
(status === 'in_progress' || status === 'pending' || hasPendingPermission);
|
|
565
|
+
const isCompleted = status === 'completed';
|
|
566
|
+
const isFailed = status === 'failed' || isRejected;
|
|
567
|
+
const icon = isInProgress
|
|
568
|
+
? '\u2022'
|
|
569
|
+
: isCompleted
|
|
570
|
+
? '\u2713'
|
|
571
|
+
: isFailed
|
|
572
|
+
? '\u2717'
|
|
573
|
+
: '\u2022';
|
|
574
|
+
const effectiveStatus = (isRejected ? 'failed' : status).replace(/_/g, '-');
|
|
575
|
+
const cssClass = `jp-ai-tool-call-item jp-ai-tool-call-item-${effectiveStatus}`;
|
|
576
|
+
const hasDiffs = !!toolCall.diffs?.length;
|
|
577
|
+
|
|
578
|
+
if (hasDiffs && hasPendingPermission) {
|
|
579
|
+
return (
|
|
580
|
+
<div className={cssClass}>
|
|
581
|
+
<details open>
|
|
582
|
+
<summary>
|
|
583
|
+
<span className="jp-ai-tool-call-item-icon">{icon}</span>{' '}
|
|
584
|
+
<em>{displayTitle}</em>
|
|
585
|
+
</summary>
|
|
586
|
+
<ToolCallDiffView
|
|
587
|
+
diffs={toolCall.diffs!}
|
|
588
|
+
trans={trans}
|
|
589
|
+
openToolCallPath={openToolCallPath}
|
|
590
|
+
pendingPermission
|
|
591
|
+
/>
|
|
592
|
+
</details>
|
|
593
|
+
<PermissionButtons
|
|
594
|
+
toolCall={toolCall}
|
|
595
|
+
trans={trans}
|
|
596
|
+
toolCallPermissionDecision={toolCallPermissionDecision}
|
|
597
|
+
/>
|
|
598
|
+
</div>
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (!hasDiffs && hasPendingPermission) {
|
|
603
|
+
const permissionDetail = buildPermissionDetail(toolCall);
|
|
604
|
+
|
|
605
|
+
if (permissionDetail !== null) {
|
|
606
|
+
return (
|
|
607
|
+
<div className={cssClass}>
|
|
608
|
+
<details open>
|
|
609
|
+
<summary>
|
|
610
|
+
<span className="jp-ai-tool-call-item-icon">{icon}</span>{' '}
|
|
611
|
+
<em>{displayTitle}</em>
|
|
612
|
+
</summary>
|
|
613
|
+
<div className="jp-ai-tool-call-item-detail">
|
|
614
|
+
{permissionDetail}
|
|
615
|
+
</div>
|
|
616
|
+
</details>
|
|
617
|
+
<PermissionButtons
|
|
618
|
+
toolCall={toolCall}
|
|
619
|
+
trans={trans}
|
|
620
|
+
toolCallPermissionDecision={toolCallPermissionDecision}
|
|
621
|
+
/>
|
|
622
|
+
</div>
|
|
623
|
+
);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const detailLines =
|
|
628
|
+
!hasDiffs && (isCompleted || isFailed)
|
|
629
|
+
? buildToolCallDetailsLines(toolCall)
|
|
630
|
+
: [];
|
|
631
|
+
const hasExpandableContent = hasDiffs || detailLines.length > 0;
|
|
632
|
+
|
|
633
|
+
if ((isCompleted || isFailed) && hasExpandableContent) {
|
|
634
|
+
return (
|
|
635
|
+
<details className={cssClass}>
|
|
636
|
+
<summary>
|
|
637
|
+
<span className="jp-ai-tool-call-item-icon">{icon}</span>{' '}
|
|
638
|
+
{displayTitle}
|
|
639
|
+
<PermissionLabel toolCall={toolCall} />
|
|
640
|
+
</summary>
|
|
641
|
+
{hasDiffs ? (
|
|
642
|
+
<ToolCallDiffView
|
|
643
|
+
diffs={toolCall.diffs!}
|
|
644
|
+
trans={trans}
|
|
645
|
+
openToolCallPath={openToolCallPath}
|
|
646
|
+
/>
|
|
647
|
+
) : (
|
|
648
|
+
<div className="jp-ai-tool-call-item-detail">
|
|
649
|
+
{detailLines.join('\n')}
|
|
650
|
+
</div>
|
|
651
|
+
)}
|
|
652
|
+
</details>
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (isInProgress) {
|
|
657
|
+
return (
|
|
658
|
+
<div className={cssClass}>
|
|
659
|
+
<span className="jp-ai-tool-call-item-icon">{icon}</span>{' '}
|
|
660
|
+
<em>{displayTitle}</em>
|
|
661
|
+
<PermissionButtons
|
|
662
|
+
toolCall={toolCall}
|
|
663
|
+
trans={trans}
|
|
664
|
+
toolCallPermissionDecision={toolCallPermissionDecision}
|
|
665
|
+
/>
|
|
666
|
+
</div>
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return (
|
|
671
|
+
<div className={cssClass}>
|
|
672
|
+
<span className="jp-ai-tool-call-item-icon">{icon}</span> {displayTitle}
|
|
673
|
+
<PermissionLabel toolCall={toolCall} />
|
|
674
|
+
</div>
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* React component for rendering grouped tool calls.
|
|
680
|
+
*/
|
|
681
|
+
export const GroupedToolCalls: React.FC<IGroupedToolCallsProps> = props => {
|
|
682
|
+
const trans = props.trans ?? nullTranslator.load('jupyterlab');
|
|
683
|
+
const { toolCalls } = props;
|
|
684
|
+
|
|
685
|
+
if (!toolCalls.length) {
|
|
686
|
+
return null;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
return (
|
|
690
|
+
<div className="jp-ai-tool-calls">
|
|
691
|
+
{toolCalls.map(toolCall => (
|
|
692
|
+
<ToolCallRow
|
|
693
|
+
key={toolCall.toolCallId}
|
|
694
|
+
toolCall={toolCall}
|
|
695
|
+
trans={trans}
|
|
696
|
+
openToolCallPath={props.openToolCallPath}
|
|
697
|
+
toolCallPermissionDecision={props.toolCallPermissionDecision}
|
|
698
|
+
/>
|
|
699
|
+
))}
|
|
700
|
+
</div>
|
|
701
|
+
);
|
|
702
|
+
};
|
package/src/components/index.ts
CHANGED