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
package/README.md
CHANGED
|
@@ -32,7 +32,32 @@ Other JupyterLab extensions can consume the `IComponentsRendererFactory` token a
|
|
|
32
32
|
|
|
33
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
34
|
|
|
35
|
-
|
|
35
|
+
### `grouped-tool-calls`
|
|
36
|
+
|
|
37
|
+
Renders grouped tool calls, including:
|
|
38
|
+
|
|
39
|
+
- grouped in-progress, completed, and failed tool rows
|
|
40
|
+
- expandable raw output and file path details
|
|
41
|
+
- inline file diffs for edit operations
|
|
42
|
+
- permission options for approval flows
|
|
43
|
+
|
|
44
|
+
The component uses a camelCase metadata API.
|
|
45
|
+
|
|
46
|
+
If your host extension wants the approval buttons to be interactive, set the renderer factory callbacks:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
rendererFactory.toolCallPermissionDecision = async (
|
|
50
|
+
sessionId,
|
|
51
|
+
toolCallId,
|
|
52
|
+
optionId
|
|
53
|
+
) => {
|
|
54
|
+
// submit the decision to your backend
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
rendererFactory.openToolCallPath = path => {
|
|
58
|
+
// optionally open the referenced file or resource
|
|
59
|
+
};
|
|
60
|
+
```
|
|
36
61
|
|
|
37
62
|
## Requirements
|
|
38
63
|
|
|
@@ -0,0 +1,38 @@
|
|
|
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 output for display.
|
|
16
|
+
*/
|
|
17
|
+
export declare function formatToolCallOutput(rawOutput: unknown): string;
|
|
18
|
+
/**
|
|
19
|
+
* Format tool input for display.
|
|
20
|
+
*/
|
|
21
|
+
export declare function formatToolCallInput(input: unknown): string;
|
|
22
|
+
/**
|
|
23
|
+
* Compute the line title shown for a tool call.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getToolCallDisplayTitle(toolCall: IToolCallsEntry): string;
|
|
26
|
+
/**
|
|
27
|
+
* Compute the pre-permission detail text for a tool call, or null if the
|
|
28
|
+
* title alone is enough.
|
|
29
|
+
*/
|
|
30
|
+
export declare function buildPermissionDetail(toolCall: IToolCallsEntry): string | null;
|
|
31
|
+
/**
|
|
32
|
+
* Build the expandable detail lines shown for completed and failed tool calls.
|
|
33
|
+
*/
|
|
34
|
+
export declare function buildToolCallDetailsLines(toolCall: IToolCallsEntry): string[];
|
|
35
|
+
/**
|
|
36
|
+
* React component for rendering grouped tool calls.
|
|
37
|
+
*/
|
|
38
|
+
export declare const GroupedToolCalls: React.FC<IGroupedToolCallsProps>;
|
|
@@ -0,0 +1,381 @@
|
|
|
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
|
+
/** Tool kinds where expanded view shows file paths from locations. */
|
|
8
|
+
const FILE_KINDS = new Set(['read', 'edit', 'delete', 'move']);
|
|
9
|
+
/** Tool kinds where expanded view shows raw output. */
|
|
10
|
+
const OUTPUT_KINDS = new Set(['search', 'execute', 'think', 'fetch']);
|
|
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 output for display.
|
|
51
|
+
*/
|
|
52
|
+
export function formatToolCallOutput(rawOutput) {
|
|
53
|
+
if (typeof rawOutput === 'string') {
|
|
54
|
+
return rawOutput;
|
|
55
|
+
}
|
|
56
|
+
if (Array.isArray(rawOutput) &&
|
|
57
|
+
rawOutput.every(item => typeof item === 'object' &&
|
|
58
|
+
item !== null &&
|
|
59
|
+
typeof item.text === 'string')) {
|
|
60
|
+
return rawOutput.map(item => item.text).join('\n');
|
|
61
|
+
}
|
|
62
|
+
return JSON.stringify(rawOutput, null, 2);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Format tool input for display.
|
|
66
|
+
*/
|
|
67
|
+
export function formatToolCallInput(input) {
|
|
68
|
+
if (typeof input === 'string') {
|
|
69
|
+
return input;
|
|
70
|
+
}
|
|
71
|
+
if (typeof input !== 'object' || input === null || Array.isArray(input)) {
|
|
72
|
+
return JSON.stringify(input, null, 2);
|
|
73
|
+
}
|
|
74
|
+
const entries = Object.entries(input);
|
|
75
|
+
const isFlat = entries.every(([, value]) => typeof value === 'string' ||
|
|
76
|
+
typeof value === 'number' ||
|
|
77
|
+
typeof value === 'boolean');
|
|
78
|
+
if (isFlat) {
|
|
79
|
+
return entries.map(([key, value]) => `${key}: ${value}`).join('\n');
|
|
80
|
+
}
|
|
81
|
+
return JSON.stringify(input, null, 2);
|
|
82
|
+
}
|
|
83
|
+
function getLocationSummary(toolCall) {
|
|
84
|
+
var _a;
|
|
85
|
+
const firstLocation = (_a = toolCall.locations) === null || _a === void 0 ? void 0 : _a[0];
|
|
86
|
+
if (!firstLocation) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
return PathExt.basename(firstLocation) || firstLocation;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Compute the line title shown for a tool call.
|
|
93
|
+
*/
|
|
94
|
+
export function getToolCallDisplayTitle(toolCall) {
|
|
95
|
+
var _a, _b, _c;
|
|
96
|
+
const title = (_a = toolCall.title) === null || _a === void 0 ? void 0 : _a.trim();
|
|
97
|
+
if (title) {
|
|
98
|
+
return title;
|
|
99
|
+
}
|
|
100
|
+
const verb = (_c = TOOL_KIND_LABELS[(_b = toolCall.kind) !== null && _b !== void 0 ? _b : '']) !== null && _c !== void 0 ? _c : 'Working';
|
|
101
|
+
const location = getLocationSummary(toolCall);
|
|
102
|
+
if (location) {
|
|
103
|
+
return `${verb} ${location}`;
|
|
104
|
+
}
|
|
105
|
+
return `${verb}...`;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Compute the pre-permission detail text for a tool call, or null if the
|
|
109
|
+
* title alone is enough.
|
|
110
|
+
*/
|
|
111
|
+
export function buildPermissionDetail(toolCall) {
|
|
112
|
+
const { kind, title, locations, rawInput } = toolCall;
|
|
113
|
+
if (kind === 'execute') {
|
|
114
|
+
const rawObject = typeof rawInput === 'object' &&
|
|
115
|
+
rawInput !== null &&
|
|
116
|
+
!Array.isArray(rawInput)
|
|
117
|
+
? rawInput
|
|
118
|
+
: null;
|
|
119
|
+
const command = rawObject && typeof rawObject.command === 'string'
|
|
120
|
+
? rawObject.command
|
|
121
|
+
: (title === null || title === void 0 ? void 0 : title.replace(/^Running:\s*/i, '').replace(/\.\.\.$/, '').trim()) || null;
|
|
122
|
+
if (!command || command === title) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
return '$ ' + command;
|
|
126
|
+
}
|
|
127
|
+
if ((kind === 'delete' || kind === 'move' || kind === 'read') &&
|
|
128
|
+
(locations === null || locations === void 0 ? void 0 : locations.length)) {
|
|
129
|
+
return kind === 'move' && locations.length >= 2
|
|
130
|
+
? `${toServerRelativePath(locations[0])} \u2192 ${toServerRelativePath(locations[1])}`
|
|
131
|
+
: locations.map(toServerRelativePath).join('\n');
|
|
132
|
+
}
|
|
133
|
+
if (rawInput !== null &&
|
|
134
|
+
rawInput !== undefined &&
|
|
135
|
+
typeof rawInput === 'object' &&
|
|
136
|
+
!Array.isArray(rawInput)) {
|
|
137
|
+
const rawObject = rawInput;
|
|
138
|
+
const purpose = typeof rawObject.__tool_use_purpose === 'string'
|
|
139
|
+
? rawObject.__tool_use_purpose
|
|
140
|
+
: null;
|
|
141
|
+
const paramEntries = Object.entries(rawObject).filter(([key]) => !key.startsWith('__'));
|
|
142
|
+
const filteredParams = paramEntries.reduce((result, [key, value]) => {
|
|
143
|
+
result[key] = value;
|
|
144
|
+
return result;
|
|
145
|
+
}, {});
|
|
146
|
+
const params = paramEntries.length > 0 ? formatToolCallInput(filteredParams) : null;
|
|
147
|
+
if (purpose && params) {
|
|
148
|
+
return `${purpose}\n${params}`;
|
|
149
|
+
}
|
|
150
|
+
if (purpose) {
|
|
151
|
+
return purpose;
|
|
152
|
+
}
|
|
153
|
+
if (params) {
|
|
154
|
+
return params;
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
if (rawInput !== null && rawInput !== undefined) {
|
|
159
|
+
return formatToolCallInput(rawInput);
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Build the expandable detail lines shown for completed and failed tool calls.
|
|
165
|
+
*/
|
|
166
|
+
export function buildToolCallDetailsLines(toolCall) {
|
|
167
|
+
const lines = [];
|
|
168
|
+
const { kind, locations, rawOutput } = toolCall;
|
|
169
|
+
if (kind && FILE_KINDS.has(kind) && (locations === null || locations === void 0 ? void 0 : locations.length)) {
|
|
170
|
+
lines.push(...locations.map(toServerRelativePath));
|
|
171
|
+
}
|
|
172
|
+
else if (kind &&
|
|
173
|
+
OUTPUT_KINDS.has(kind) &&
|
|
174
|
+
rawOutput !== null &&
|
|
175
|
+
rawOutput !== undefined) {
|
|
176
|
+
lines.push(formatToolCallOutput(rawOutput));
|
|
177
|
+
}
|
|
178
|
+
else if (typeof rawOutput === 'string') {
|
|
179
|
+
lines.push(rawOutput);
|
|
180
|
+
}
|
|
181
|
+
return lines;
|
|
182
|
+
}
|
|
183
|
+
function toDiffLineInfo(type, text, key) {
|
|
184
|
+
switch (type) {
|
|
185
|
+
case 'added':
|
|
186
|
+
return {
|
|
187
|
+
cssClass: 'jp-mod-added',
|
|
188
|
+
prefix: '+',
|
|
189
|
+
text,
|
|
190
|
+
key
|
|
191
|
+
};
|
|
192
|
+
case 'removed':
|
|
193
|
+
return {
|
|
194
|
+
cssClass: 'jp-mod-removed',
|
|
195
|
+
prefix: '-',
|
|
196
|
+
text,
|
|
197
|
+
key
|
|
198
|
+
};
|
|
199
|
+
case 'context':
|
|
200
|
+
return {
|
|
201
|
+
cssClass: 'jp-mod-context',
|
|
202
|
+
prefix: ' ',
|
|
203
|
+
text,
|
|
204
|
+
key
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function buildDiffLinesFromHunk(hunk, hunkIndex) {
|
|
209
|
+
return hunk.lines
|
|
210
|
+
.filter(line => !line.startsWith('\\'))
|
|
211
|
+
.map((line, lineIndex) => {
|
|
212
|
+
var _a;
|
|
213
|
+
const prefix = (_a = line[0]) !== null && _a !== void 0 ? _a : ' ';
|
|
214
|
+
const text = line.slice(1);
|
|
215
|
+
const key = `${hunkIndex}-${hunk.oldStart}-${hunk.newStart}-${lineIndex}`;
|
|
216
|
+
if (prefix === '+') {
|
|
217
|
+
return toDiffLineInfo('added', text, key);
|
|
218
|
+
}
|
|
219
|
+
if (prefix === '-') {
|
|
220
|
+
return toDiffLineInfo('removed', text, key);
|
|
221
|
+
}
|
|
222
|
+
return toDiffLineInfo('context', text, key);
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
function buildDiffLines(diff) {
|
|
226
|
+
var _a;
|
|
227
|
+
const patch = structuredPatch(diff.path, diff.path, (_a = diff.oldText) !== null && _a !== void 0 ? _a : '', diff.newText, undefined, undefined, { context: Infinity });
|
|
228
|
+
return patch.hunks.reduce((lines, hunk, index) => {
|
|
229
|
+
lines.push(...buildDiffLinesFromHunk(hunk, index));
|
|
230
|
+
return lines;
|
|
231
|
+
}, []);
|
|
232
|
+
}
|
|
233
|
+
function ToolCallDiffBlock({ diff, trans, openToolCallPath, pendingPermission }) {
|
|
234
|
+
const [expanded, setExpanded] = React.useState(false);
|
|
235
|
+
const allLines = React.useMemo(() => buildDiffLines(diff), [diff]);
|
|
236
|
+
const canTruncate = allLines.length > MAX_DIFF_LINES;
|
|
237
|
+
const visibleLines = canTruncate && !expanded ? allLines.slice(0, MAX_DIFF_LINES) : allLines;
|
|
238
|
+
const hiddenCount = allLines.length - MAX_DIFF_LINES;
|
|
239
|
+
const displayPath = toServerRelativePath(diff.path);
|
|
240
|
+
const canOpenPath = !!openToolCallPath && !(pendingPermission && !diff.oldText);
|
|
241
|
+
return (React.createElement("div", { className: "jp-ai-tool-call-diff-block" },
|
|
242
|
+
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),
|
|
243
|
+
React.createElement("div", { className: "jp-ai-tool-call-diff-content" },
|
|
244
|
+
visibleLines.length ? (visibleLines.map(line => (React.createElement("div", { key: line.key, className: `jp-ai-tool-call-diff-line ${line.cssClass}` },
|
|
245
|
+
React.createElement("span", { className: "jp-ai-tool-call-diff-line-prefix" }, line.prefix),
|
|
246
|
+
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'))),
|
|
247
|
+
canTruncate && !expanded && (React.createElement("button", { className: "jp-ai-tool-call-diff-toggle", onClick: () => setExpanded(true), type: "button" }, trans.__('... %1 more lines', hiddenCount))),
|
|
248
|
+
canTruncate && expanded && (React.createElement("button", { className: "jp-ai-tool-call-diff-toggle", onClick: () => setExpanded(false), type: "button" }, trans.__('Show less'))))));
|
|
249
|
+
}
|
|
250
|
+
function ToolCallDiffView({ diffs, trans, openToolCallPath, pendingPermission }) {
|
|
251
|
+
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 })))));
|
|
252
|
+
}
|
|
253
|
+
function PermissionLabel({ toolCall }) {
|
|
254
|
+
var _a, _b;
|
|
255
|
+
if (toolCall.permissionStatus !== 'resolved' || !toolCall.selectedOptionId) {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
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;
|
|
259
|
+
if (!selectedName) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
return (React.createElement("span", { className: "jp-ai-tool-call-permission-label" },
|
|
263
|
+
" - ",
|
|
264
|
+
selectedName));
|
|
265
|
+
}
|
|
266
|
+
function PermissionButtons({ toolCall, trans, toolCallPermissionDecision }) {
|
|
267
|
+
var _a;
|
|
268
|
+
const [submitting, setSubmitting] = React.useState(false);
|
|
269
|
+
if (!((_a = toolCall.permissionOptions) === null || _a === void 0 ? void 0 : _a.length) ||
|
|
270
|
+
toolCall.permissionStatus !== 'pending') {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
const canSubmit = !!toolCallPermissionDecision &&
|
|
274
|
+
!!toolCall.sessionId &&
|
|
275
|
+
!!toolCall.toolCallId;
|
|
276
|
+
const handleClick = async (optionId) => {
|
|
277
|
+
if (!canSubmit) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
setSubmitting(true);
|
|
281
|
+
try {
|
|
282
|
+
await toolCallPermissionDecision(toolCall.sessionId, toolCall.toolCallId, optionId);
|
|
283
|
+
}
|
|
284
|
+
catch (error) {
|
|
285
|
+
console.error('Failed to submit tool call permission decision:', error);
|
|
286
|
+
setSubmitting(false);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
return (React.createElement("div", { className: "jp-ai-tool-call-permission-buttons" },
|
|
290
|
+
React.createElement("span", { className: "jp-ai-tool-call-permission-tree" }, '\u2514\u2500'),
|
|
291
|
+
React.createElement("span", null, trans.__('Allow?')),
|
|
292
|
+
toolCall.permissionOptions.map(option => {
|
|
293
|
+
const optionClass = option.kind
|
|
294
|
+
? ` jp-ai-tool-call-permission-btn-${option.kind.replace(/_/g, '-')}`
|
|
295
|
+
: '';
|
|
296
|
+
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));
|
|
297
|
+
})));
|
|
298
|
+
}
|
|
299
|
+
function ToolCallRow({ toolCall, trans, openToolCallPath, toolCallPermissionDecision }) {
|
|
300
|
+
var _a, _b, _c, _d;
|
|
301
|
+
const displayTitle = getToolCallDisplayTitle(toolCall);
|
|
302
|
+
const selectedOption = (_a = toolCall.permissionOptions) === null || _a === void 0 ? void 0 : _a.find(option => option.optionId === toolCall.selectedOptionId);
|
|
303
|
+
const isRejected = toolCall.permissionStatus === 'resolved' &&
|
|
304
|
+
!!((_b = selectedOption === null || selectedOption === void 0 ? void 0 : selectedOption.kind) === null || _b === void 0 ? void 0 : _b.includes('reject'));
|
|
305
|
+
const hasPendingPermission = toolCall.permissionStatus === 'pending';
|
|
306
|
+
const status = (_c = toolCall.status) !== null && _c !== void 0 ? _c : 'in_progress';
|
|
307
|
+
const isInProgress = !isRejected &&
|
|
308
|
+
(status === 'in_progress' || status === 'pending' || hasPendingPermission);
|
|
309
|
+
const isCompleted = status === 'completed';
|
|
310
|
+
const isFailed = status === 'failed' || isRejected;
|
|
311
|
+
const icon = isInProgress
|
|
312
|
+
? '\u2022'
|
|
313
|
+
: isCompleted
|
|
314
|
+
? '\u2713'
|
|
315
|
+
: isFailed
|
|
316
|
+
? '\u2717'
|
|
317
|
+
: '\u2022';
|
|
318
|
+
const effectiveStatus = (isRejected ? 'failed' : status).replace(/_/g, '-');
|
|
319
|
+
const cssClass = `jp-ai-tool-call-item jp-ai-tool-call-item-${effectiveStatus}`;
|
|
320
|
+
const hasDiffs = !!((_d = toolCall.diffs) === null || _d === void 0 ? void 0 : _d.length);
|
|
321
|
+
if (hasDiffs && hasPendingPermission) {
|
|
322
|
+
return (React.createElement("div", { className: cssClass },
|
|
323
|
+
React.createElement("details", { open: true },
|
|
324
|
+
React.createElement("summary", null,
|
|
325
|
+
React.createElement("span", { className: "jp-ai-tool-call-item-icon" }, icon),
|
|
326
|
+
' ',
|
|
327
|
+
React.createElement("em", null, displayTitle)),
|
|
328
|
+
React.createElement(ToolCallDiffView, { diffs: toolCall.diffs, trans: trans, openToolCallPath: openToolCallPath, pendingPermission: true })),
|
|
329
|
+
React.createElement(PermissionButtons, { toolCall: toolCall, trans: trans, toolCallPermissionDecision: toolCallPermissionDecision })));
|
|
330
|
+
}
|
|
331
|
+
if (!hasDiffs && hasPendingPermission) {
|
|
332
|
+
const permissionDetail = buildPermissionDetail(toolCall);
|
|
333
|
+
if (permissionDetail !== null) {
|
|
334
|
+
return (React.createElement("div", { className: cssClass },
|
|
335
|
+
React.createElement("details", { open: true },
|
|
336
|
+
React.createElement("summary", null,
|
|
337
|
+
React.createElement("span", { className: "jp-ai-tool-call-item-icon" }, icon),
|
|
338
|
+
' ',
|
|
339
|
+
React.createElement("em", null, displayTitle)),
|
|
340
|
+
React.createElement("div", { className: "jp-ai-tool-call-item-detail" }, permissionDetail)),
|
|
341
|
+
React.createElement(PermissionButtons, { toolCall: toolCall, trans: trans, toolCallPermissionDecision: toolCallPermissionDecision })));
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const detailLines = !hasDiffs && (isCompleted || isFailed)
|
|
345
|
+
? buildToolCallDetailsLines(toolCall)
|
|
346
|
+
: [];
|
|
347
|
+
const hasExpandableContent = hasDiffs || detailLines.length > 0;
|
|
348
|
+
if ((isCompleted || isFailed) && hasExpandableContent) {
|
|
349
|
+
return (React.createElement("details", { className: cssClass },
|
|
350
|
+
React.createElement("summary", null,
|
|
351
|
+
React.createElement("span", { className: "jp-ai-tool-call-item-icon" }, icon),
|
|
352
|
+
' ',
|
|
353
|
+
displayTitle,
|
|
354
|
+
React.createElement(PermissionLabel, { toolCall: toolCall })),
|
|
355
|
+
hasDiffs ? (React.createElement(ToolCallDiffView, { diffs: toolCall.diffs, trans: trans, openToolCallPath: openToolCallPath })) : (React.createElement("div", { className: "jp-ai-tool-call-item-detail" }, detailLines.join('\n')))));
|
|
356
|
+
}
|
|
357
|
+
if (isInProgress) {
|
|
358
|
+
return (React.createElement("div", { className: cssClass },
|
|
359
|
+
React.createElement("span", { className: "jp-ai-tool-call-item-icon" }, icon),
|
|
360
|
+
' ',
|
|
361
|
+
React.createElement("em", null, displayTitle),
|
|
362
|
+
React.createElement(PermissionButtons, { toolCall: toolCall, trans: trans, toolCallPermissionDecision: toolCallPermissionDecision })));
|
|
363
|
+
}
|
|
364
|
+
return (React.createElement("div", { className: cssClass },
|
|
365
|
+
React.createElement("span", { className: "jp-ai-tool-call-item-icon" }, icon),
|
|
366
|
+
" ",
|
|
367
|
+
displayTitle,
|
|
368
|
+
React.createElement(PermissionLabel, { toolCall: toolCall })));
|
|
369
|
+
}
|
|
370
|
+
/**
|
|
371
|
+
* React component for rendering grouped tool calls.
|
|
372
|
+
*/
|
|
373
|
+
export const GroupedToolCalls = props => {
|
|
374
|
+
var _a;
|
|
375
|
+
const trans = (_a = props.trans) !== null && _a !== void 0 ? _a : nullTranslator.load('jupyterlab');
|
|
376
|
+
const { toolCalls } = props;
|
|
377
|
+
if (!toolCalls.length) {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
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 })))));
|
|
381
|
+
};
|
package/lib/components/index.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { TranslationBundle } from '@jupyterlab/translation';
|
|
2
2
|
import * as React from 'react';
|
|
3
|
-
import { IInlineDiffMetadata } from '../token';
|
|
3
|
+
import { IInlineDiff, IInlineDiffMetadata } from '../token';
|
|
4
4
|
export interface IInlineDiffProps extends IInlineDiffMetadata {
|
|
5
5
|
trans?: TranslationBundle;
|
|
6
6
|
}
|
|
7
7
|
export declare function getDiffFilename(path: string): string;
|
|
8
|
+
export declare function getInlineDiffLabel(diff: IInlineDiff): string;
|
|
9
|
+
export declare function getInlineDiffTitle(diff: IInlineDiff): string;
|
|
8
10
|
/**
|
|
9
|
-
* React component for rendering one or more inline
|
|
11
|
+
* React component for rendering one or more inline diffs.
|
|
10
12
|
*/
|
|
11
13
|
export declare const InlineDiff: React.FC<IInlineDiffProps>;
|
|
@@ -7,6 +7,50 @@ const MAX_DIFF_LINES = 20;
|
|
|
7
7
|
export function getDiffFilename(path) {
|
|
8
8
|
return PathExt.basename(path);
|
|
9
9
|
}
|
|
10
|
+
function getNotebookCellLabel(target) {
|
|
11
|
+
if (typeof target.cellIndex === 'number') {
|
|
12
|
+
return `Cell ${target.cellIndex + 1}`;
|
|
13
|
+
}
|
|
14
|
+
if (target.cellId) {
|
|
15
|
+
return `Cell ${target.cellId}`;
|
|
16
|
+
}
|
|
17
|
+
return 'Notebook Cell';
|
|
18
|
+
}
|
|
19
|
+
function getInlineDiffPatchPath(diff) {
|
|
20
|
+
var _a;
|
|
21
|
+
const target = diff.target;
|
|
22
|
+
if (target.kind === 'file') {
|
|
23
|
+
return target.path;
|
|
24
|
+
}
|
|
25
|
+
return [target.notebookPath, (_a = target.cellId) !== null && _a !== void 0 ? _a : target.cellIndex]
|
|
26
|
+
.filter(value => value !== undefined && value !== '')
|
|
27
|
+
.join('#');
|
|
28
|
+
}
|
|
29
|
+
export function getInlineDiffLabel(diff) {
|
|
30
|
+
if (diff.label) {
|
|
31
|
+
return diff.label;
|
|
32
|
+
}
|
|
33
|
+
const target = diff.target;
|
|
34
|
+
if (target.kind === 'file') {
|
|
35
|
+
return getDiffFilename(target.path);
|
|
36
|
+
}
|
|
37
|
+
const notebookName = getDiffFilename(target.notebookPath);
|
|
38
|
+
const cellLabel = getNotebookCellLabel(target);
|
|
39
|
+
return [notebookName, cellLabel].join(' · ');
|
|
40
|
+
}
|
|
41
|
+
export function getInlineDiffTitle(diff) {
|
|
42
|
+
const target = diff.target;
|
|
43
|
+
if (target.kind === 'file') {
|
|
44
|
+
return target.path;
|
|
45
|
+
}
|
|
46
|
+
const cellLabel = getNotebookCellLabel(target);
|
|
47
|
+
const cellIdLabel = typeof target.cellIndex === 'number' && target.cellId
|
|
48
|
+
? `Cell ID ${target.cellId}`
|
|
49
|
+
: null;
|
|
50
|
+
return [target.notebookPath, cellLabel, cellIdLabel]
|
|
51
|
+
.filter(part => part !== null && part !== undefined && part !== '')
|
|
52
|
+
.join(' · ');
|
|
53
|
+
}
|
|
10
54
|
function toLineInfo(type, text, key) {
|
|
11
55
|
switch (type) {
|
|
12
56
|
case 'added':
|
|
@@ -51,21 +95,23 @@ function buildDiffLinesFromHunk(hunk, hunkIndex) {
|
|
|
51
95
|
}
|
|
52
96
|
function buildDiffLines(diff) {
|
|
53
97
|
var _a;
|
|
54
|
-
const
|
|
98
|
+
const patchPath = getInlineDiffPatchPath(diff);
|
|
99
|
+
const patch = structuredPatch(patchPath, patchPath, (_a = diff.oldText) !== null && _a !== void 0 ? _a : '', diff.newText, undefined, undefined, { context: Infinity });
|
|
55
100
|
return patch.hunks.reduce((lines, hunk, index) => {
|
|
56
101
|
lines.push(...buildDiffLinesFromHunk(hunk, index));
|
|
57
102
|
return lines;
|
|
58
103
|
}, []);
|
|
59
104
|
}
|
|
60
105
|
function DiffBlock({ diff, trans }) {
|
|
61
|
-
const filename =
|
|
106
|
+
const filename = getInlineDiffLabel(diff);
|
|
107
|
+
const title = getInlineDiffTitle(diff);
|
|
62
108
|
const [expanded, setExpanded] = React.useState(false);
|
|
63
109
|
const allLines = React.useMemo(() => buildDiffLines(diff), [diff]);
|
|
64
110
|
const canTruncate = allLines.length > MAX_DIFF_LINES;
|
|
65
111
|
const visibleLines = canTruncate && !expanded ? allLines.slice(0, MAX_DIFF_LINES) : allLines;
|
|
66
112
|
const hiddenCount = allLines.length - MAX_DIFF_LINES;
|
|
67
113
|
return (React.createElement("div", { className: "jp-ai-inline-diff-block" },
|
|
68
|
-
React.createElement("div", { className: "jp-ai-inline-diff-header", title:
|
|
114
|
+
React.createElement("div", { className: "jp-ai-inline-diff-header", title: title }, filename),
|
|
69
115
|
React.createElement("div", { className: "jp-ai-inline-diff-content" },
|
|
70
116
|
visibleLines.length ? (visibleLines.map(line => (React.createElement("div", { key: line.key, className: `jp-ai-inline-diff-line ${line.cssClass}` },
|
|
71
117
|
React.createElement("span", { className: "jp-ai-inline-diff-line-prefix" }, line.prefix),
|
|
@@ -74,9 +120,9 @@ function DiffBlock({ diff, trans }) {
|
|
|
74
120
|
canTruncate && expanded && (React.createElement("button", { className: "jp-ai-inline-diff-toggle", onClick: () => setExpanded(false), type: "button" }, trans.__('Show less'))))));
|
|
75
121
|
}
|
|
76
122
|
/**
|
|
77
|
-
* React component for rendering one or more inline
|
|
123
|
+
* React component for rendering one or more inline diffs.
|
|
78
124
|
*/
|
|
79
125
|
export const InlineDiff = ({ diffs, trans }) => {
|
|
80
126
|
const transBundle = trans !== null && trans !== void 0 ? trans : nullTranslator.load('jupyterlab');
|
|
81
|
-
return (React.createElement("div", { className: "jp-ai-inline-diff-container" }, diffs.map((diff, index) => (React.createElement(DiffBlock, { key: `${diff
|
|
127
|
+
return (React.createElement("div", { className: "jp-ai-inline-diff-container" }, diffs.map((diff, index) => (React.createElement(DiffBlock, { key: `${getInlineDiffPatchPath(diff)}-${index}`, diff: diff, trans: transBundle })))));
|
|
82
128
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { IComponentProps, IMessageQueueMetadata, RemoveQueuedMessage } from '../token';
|
|
3
|
+
/**
|
|
4
|
+
* Props for the MessageQueue component.
|
|
5
|
+
*/
|
|
6
|
+
export interface IMessageQueueProps extends IComponentProps, IMessageQueueMetadata {
|
|
7
|
+
removeQueuedMessage?: RemoveQueuedMessage;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* React component that displays a list of queued messages by
|
|
11
|
+
* showing each pending message as a bubble in the chat
|
|
12
|
+
*/
|
|
13
|
+
export declare const MessageQueue: React.FC<IMessageQueueProps>;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* React component that displays a list of queued messages by
|
|
4
|
+
* showing each pending message as a bubble in the chat
|
|
5
|
+
*/
|
|
6
|
+
export const MessageQueue = ({ messages, targetId, trans, removeQueuedMessage }) => {
|
|
7
|
+
if (!messages || messages.length === 0) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
return (React.createElement("div", { className: "jp-chat-message-queue" }, messages.map(msg => (React.createElement("div", { key: msg.id, className: "jp-chat-message-queue-bubble" },
|
|
11
|
+
React.createElement("span", { className: "jp-chat-message-queue-text" }, msg.body),
|
|
12
|
+
removeQueuedMessage && targetId && (React.createElement("button", { className: "jp-chat-message-queue-remove", onClick: () => removeQueuedMessage(targetId, msg.id), title: trans.__('Remove from queue'), type: "button" }, "\u2715")))))));
|
|
13
|
+
};
|
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, 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.
|
|
@@ -12,6 +12,18 @@ interface IComponentsRendererOptions extends IRenderMime.IRendererOptions {
|
|
|
12
12
|
* The callback to approve or reject a tool.
|
|
13
13
|
*/
|
|
14
14
|
toolCallApproval?: ToolCallApproval;
|
|
15
|
+
/**
|
|
16
|
+
* The callback to remove a queued message.
|
|
17
|
+
*/
|
|
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;
|
|
15
27
|
/**
|
|
16
28
|
* The component registry.
|
|
17
29
|
*/
|
|
@@ -33,6 +45,9 @@ export declare class ComponentsRenderer extends ReactWidget implements IRenderMi
|
|
|
33
45
|
private _trans;
|
|
34
46
|
private _mimeType;
|
|
35
47
|
private _toolCallApproval?;
|
|
48
|
+
private _removeQueuedMessage?;
|
|
49
|
+
private _toolCallPermissionDecision?;
|
|
50
|
+
private _openToolCallPath?;
|
|
36
51
|
private _registry;
|
|
37
52
|
private _data;
|
|
38
53
|
private _metadata;
|
|
@@ -46,6 +61,9 @@ export declare class RendererFactory implements IComponentsRendererFactory {
|
|
|
46
61
|
readonly defaultRank = 100;
|
|
47
62
|
readonly registry: ComponentRegistry;
|
|
48
63
|
toolCallApproval: ToolCallApproval;
|
|
64
|
+
removeQueuedMessage: RemoveQueuedMessage;
|
|
65
|
+
toolCallPermissionDecision: ToolCallPermissionDecision;
|
|
66
|
+
openToolCallPath: OpenToolCallPath;
|
|
49
67
|
constructor();
|
|
50
68
|
createRenderer: (options: IRenderMime.IRendererOptions) => ComponentsRenderer;
|
|
51
69
|
}
|