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.
@@ -5,7 +5,11 @@ import * as React from 'react';
5
5
  import { structuredPatch } from 'diff';
6
6
  import type { StructuredPatchHunk } from 'diff';
7
7
 
8
- import { IInlineDiff, IInlineDiffMetadata } from '../token';
8
+ import {
9
+ IInlineDiff,
10
+ IInlineDiffMetadata,
11
+ IInlineDiffNotebookCellTarget
12
+ } from '../token';
9
13
 
10
14
  /** Maximum number of rendered lines before truncation. */
11
15
  const MAX_DIFF_LINES = 20;
@@ -25,6 +29,65 @@ export function getDiffFilename(path: string): string {
25
29
  return PathExt.basename(path);
26
30
  }
27
31
 
32
+ function getNotebookCellLabel(target: IInlineDiffNotebookCellTarget): string {
33
+ if (typeof target.cellIndex === 'number') {
34
+ return `Cell ${target.cellIndex + 1}`;
35
+ }
36
+
37
+ if (target.cellId) {
38
+ return `Cell ${target.cellId}`;
39
+ }
40
+
41
+ return 'Notebook Cell';
42
+ }
43
+
44
+ function getInlineDiffPatchPath(diff: IInlineDiff): string {
45
+ const target = diff.target;
46
+
47
+ if (target.kind === 'file') {
48
+ return target.path;
49
+ }
50
+
51
+ return [target.notebookPath, target.cellId ?? target.cellIndex]
52
+ .filter(value => value !== undefined && value !== '')
53
+ .join('#');
54
+ }
55
+
56
+ export function getInlineDiffLabel(diff: IInlineDiff): string {
57
+ if (diff.label) {
58
+ return diff.label;
59
+ }
60
+
61
+ const target = diff.target;
62
+
63
+ if (target.kind === 'file') {
64
+ return getDiffFilename(target.path);
65
+ }
66
+
67
+ const notebookName = getDiffFilename(target.notebookPath);
68
+ const cellLabel = getNotebookCellLabel(target);
69
+
70
+ return [notebookName, cellLabel].join(' · ');
71
+ }
72
+
73
+ export function getInlineDiffTitle(diff: IInlineDiff): string {
74
+ const target = diff.target;
75
+
76
+ if (target.kind === 'file') {
77
+ return target.path;
78
+ }
79
+
80
+ const cellLabel = getNotebookCellLabel(target);
81
+ const cellIdLabel =
82
+ typeof target.cellIndex === 'number' && target.cellId
83
+ ? `Cell ID ${target.cellId}`
84
+ : null;
85
+
86
+ return [target.notebookPath, cellLabel, cellIdLabel]
87
+ .filter(part => part !== null && part !== undefined && part !== '')
88
+ .join(' · ');
89
+ }
90
+
28
91
  function toLineInfo(
29
92
  type: 'added' | 'removed' | 'context',
30
93
  text: string,
@@ -79,9 +142,10 @@ function buildDiffLinesFromHunk(
79
142
  }
80
143
 
81
144
  function buildDiffLines(diff: IInlineDiff): IDiffLineInfo[] {
145
+ const patchPath = getInlineDiffPatchPath(diff);
82
146
  const patch = structuredPatch(
83
- diff.path,
84
- diff.path,
147
+ patchPath,
148
+ patchPath,
85
149
  diff.oldText ?? '',
86
150
  diff.newText,
87
151
  undefined,
@@ -102,7 +166,8 @@ function DiffBlock({
102
166
  diff: IInlineDiff;
103
167
  trans: TranslationBundle;
104
168
  }): JSX.Element {
105
- const filename = getDiffFilename(diff.path);
169
+ const filename = getInlineDiffLabel(diff);
170
+ const title = getInlineDiffTitle(diff);
106
171
  const [expanded, setExpanded] = React.useState(false);
107
172
  const allLines = React.useMemo(() => buildDiffLines(diff), [diff]);
108
173
  const canTruncate = allLines.length > MAX_DIFF_LINES;
@@ -112,7 +177,7 @@ function DiffBlock({
112
177
 
113
178
  return (
114
179
  <div className="jp-ai-inline-diff-block">
115
- <div className="jp-ai-inline-diff-header" title={diff.path}>
180
+ <div className="jp-ai-inline-diff-header" title={title}>
116
181
  {filename}
117
182
  </div>
118
183
  <div className="jp-ai-inline-diff-content">
@@ -157,7 +222,7 @@ function DiffBlock({
157
222
  }
158
223
 
159
224
  /**
160
- * React component for rendering one or more inline file diffs.
225
+ * React component for rendering one or more inline diffs.
161
226
  */
162
227
  export const InlineDiff: React.FC<IInlineDiffProps> = ({ diffs, trans }) => {
163
228
  const transBundle = trans ?? nullTranslator.load('jupyterlab');
@@ -166,7 +231,7 @@ export const InlineDiff: React.FC<IInlineDiffProps> = ({ diffs, trans }) => {
166
231
  <div className="jp-ai-inline-diff-container">
167
232
  {diffs.map((diff, index) => (
168
233
  <DiffBlock
169
- key={`${diff.path}-${index}`}
234
+ key={`${getInlineDiffPatchPath(diff)}-${index}`}
170
235
  diff={diff}
171
236
  trans={transBundle}
172
237
  />
@@ -0,0 +1,50 @@
1
+ import * as React from 'react';
2
+
3
+ import {
4
+ IComponentProps,
5
+ IMessageQueueMetadata,
6
+ RemoveQueuedMessage
7
+ } from '../token';
8
+
9
+ /**
10
+ * Props for the MessageQueue component.
11
+ */
12
+ export interface IMessageQueueProps
13
+ extends IComponentProps, IMessageQueueMetadata {
14
+ removeQueuedMessage?: RemoveQueuedMessage;
15
+ }
16
+
17
+ /**
18
+ * React component that displays a list of queued messages by
19
+ * showing each pending message as a bubble in the chat
20
+ */
21
+ export const MessageQueue: React.FC<IMessageQueueProps> = ({
22
+ messages,
23
+ targetId,
24
+ trans,
25
+ removeQueuedMessage
26
+ }) => {
27
+ if (!messages || messages.length === 0) {
28
+ return null;
29
+ }
30
+
31
+ return (
32
+ <div className="jp-chat-message-queue">
33
+ {messages.map(msg => (
34
+ <div key={msg.id} className="jp-chat-message-queue-bubble">
35
+ <span className="jp-chat-message-queue-text">{msg.body}</span>
36
+ {removeQueuedMessage && targetId && (
37
+ <button
38
+ className="jp-chat-message-queue-remove"
39
+ onClick={() => removeQueuedMessage(targetId, msg.id)}
40
+ title={trans.__('Remove from queue')}
41
+ type="button"
42
+ >
43
+
44
+ </button>
45
+ )}
46
+ </div>
47
+ ))}
48
+ </div>
49
+ );
50
+ };
package/src/factory.tsx CHANGED
@@ -8,14 +8,22 @@ import { ReadonlyPartialJSONValue } from '@lumino/coreutils';
8
8
 
9
9
  import * as React from 'react';
10
10
 
11
- import { InlineDiff, ToolCall } from './components';
11
+ import {
12
+ GroupedToolCalls,
13
+ InlineDiff,
14
+ MessageQueue,
15
+ ToolCall
16
+ } from './components';
12
17
 
13
18
  import { ComponentRegistry } from './registry';
14
19
 
15
20
  import {
16
21
  IComponentRegistry,
17
22
  IComponentsRendererFactory,
18
- ToolCallApproval
23
+ OpenToolCallPath,
24
+ RemoveQueuedMessage,
25
+ ToolCallApproval,
26
+ ToolCallPermissionDecision
19
27
  } from './token';
20
28
 
21
29
  /**
@@ -41,6 +49,21 @@ interface IComponentsRendererOptions extends IRenderMime.IRendererOptions {
41
49
  */
42
50
  toolCallApproval?: ToolCallApproval;
43
51
 
52
+ /**
53
+ * The callback to remove a queued message.
54
+ */
55
+ removeQueuedMessage?: RemoveQueuedMessage;
56
+
57
+ /**
58
+ * The callback to submit a permission decision for grouped tool calls.
59
+ */
60
+ toolCallPermissionDecision?: ToolCallPermissionDecision;
61
+
62
+ /**
63
+ * The callback to open a path referenced by grouped tool calls.
64
+ */
65
+ openToolCallPath?: OpenToolCallPath;
66
+
44
67
  /**
45
68
  * The component registry.
46
69
  */
@@ -62,6 +85,9 @@ export class ComponentsRenderer
62
85
  this._trans = (options.translator ?? nullTranslator).load('jupyterlab');
63
86
  this._mimeType = options.mimeType;
64
87
  this._toolCallApproval = options.toolCallApproval;
88
+ this._removeQueuedMessage = options.removeQueuedMessage;
89
+ this._toolCallPermissionDecision = options.toolCallPermissionDecision;
90
+ this._openToolCallPath = options.openToolCallPath;
65
91
  this._registry = options.registry;
66
92
  this.addClass(CLASS_NAME);
67
93
  }
@@ -93,12 +119,25 @@ export class ComponentsRenderer
93
119
  componentsProps.toolCallApproval = this._toolCallApproval;
94
120
  }
95
121
 
122
+ if (this._data === 'message-queue') {
123
+ componentsProps.removeQueuedMessage = this._removeQueuedMessage;
124
+ }
125
+
126
+ if (this._data === 'grouped-tool-calls') {
127
+ componentsProps.toolCallPermissionDecision =
128
+ this._toolCallPermissionDecision;
129
+ componentsProps.openToolCallPath = this._openToolCallPath;
130
+ }
131
+
96
132
  return <Component {...componentsProps} trans={this._trans} />;
97
133
  }
98
134
 
99
135
  private _trans: TranslationBundle;
100
136
  private _mimeType: string;
101
137
  private _toolCallApproval?: ToolCallApproval;
138
+ private _removeQueuedMessage?: RemoveQueuedMessage;
139
+ private _toolCallPermissionDecision?: ToolCallPermissionDecision;
140
+ private _openToolCallPath?: OpenToolCallPath;
102
141
  private _registry: IComponentRegistry;
103
142
  private _data: string | null = null;
104
143
  private _metadata: ReadonlyPartialJSONValue | null = null;
@@ -113,17 +152,25 @@ export class RendererFactory implements IComponentsRendererFactory {
113
152
  readonly defaultRank = 100;
114
153
  readonly registry: ComponentRegistry;
115
154
  toolCallApproval: ToolCallApproval = null;
155
+ removeQueuedMessage: RemoveQueuedMessage = null;
156
+ toolCallPermissionDecision: ToolCallPermissionDecision = null;
157
+ openToolCallPath: OpenToolCallPath = null;
116
158
 
117
159
  constructor() {
118
160
  this.registry = new ComponentRegistry();
119
161
  this.registry.add('tool-call', ToolCall);
162
+ this.registry.add('grouped-tool-calls', GroupedToolCalls);
120
163
  this.registry.add('inline-diff', InlineDiff);
164
+ this.registry.add('message-queue', MessageQueue);
121
165
  }
122
166
 
123
167
  createRenderer = (options: IRenderMime.IRendererOptions) => {
124
168
  return new ComponentsRenderer({
125
169
  ...options,
126
170
  toolCallApproval: this.toolCallApproval,
171
+ removeQueuedMessage: this.removeQueuedMessage,
172
+ toolCallPermissionDecision: this.toolCallPermissionDecision,
173
+ openToolCallPath: this.openToolCallPath,
127
174
  registry: this.registry
128
175
  });
129
176
  };
package/src/token.ts CHANGED
@@ -21,6 +21,29 @@ export type ToolCallApproval =
21
21
  | ((targetId: string, approvalId: string, approve: boolean) => void)
22
22
  | null;
23
23
 
24
+ /**
25
+ * The callback to submit a tool-call permission decision.
26
+ */
27
+ export type ToolCallPermissionDecision =
28
+ | ((
29
+ sessionId: string,
30
+ toolCallId: string,
31
+ optionId: string
32
+ ) => Promise<void> | void)
33
+ | null;
34
+
35
+ /**
36
+ * The callback to remove a queued message.
37
+ */
38
+ export type RemoveQueuedMessage =
39
+ | ((targetId: string, messageId: string) => void)
40
+ | null;
41
+
42
+ /**
43
+ * The callback to open a file or resource path referenced by a tool call.
44
+ */
45
+ export type OpenToolCallPath = ((path: string) => void) | null;
46
+
24
47
  /**
25
48
  * The interface for components renderer factory.
26
49
  */
@@ -35,6 +58,21 @@ export interface IComponentsRendererFactory
35
58
  * The callback to approve or reject a tool.
36
59
  */
37
60
  toolCallApproval: ToolCallApproval;
61
+
62
+ /**
63
+ * The callback to remove a queued message.
64
+ */
65
+ removeQueuedMessage: RemoveQueuedMessage;
66
+
67
+ /**
68
+ * The callback to submit a permission decision for grouped tool calls.
69
+ */
70
+ toolCallPermissionDecision: ToolCallPermissionDecision;
71
+
72
+ /**
73
+ * The callback to open a path referenced by grouped tool calls.
74
+ */
75
+ openToolCallPath: OpenToolCallPath;
38
76
  }
39
77
 
40
78
  /**
@@ -84,11 +122,167 @@ export interface IComponentProps {
84
122
  }
85
123
 
86
124
  /**
87
- * A single file diff entry.
125
+ * A file diff entry for grouped tool calls.
88
126
  */
89
- export interface IInlineDiff {
127
+ export interface IToolCallDiff {
128
+ /**
129
+ * Path of the file being diffed.
130
+ */
131
+ path: string;
132
+ /**
133
+ * Updated text content.
134
+ */
135
+ newText: string;
136
+ /**
137
+ * Previous text content.
138
+ */
139
+ oldText?: string;
140
+ }
141
+
142
+ /**
143
+ * A permission option for grouped tool calls.
144
+ */
145
+ export interface IToolCallPermissionOption {
146
+ /**
147
+ * Stable permission option identifier.
148
+ */
149
+ optionId: string;
150
+ /**
151
+ * Human-readable button label.
152
+ */
153
+ name: string;
154
+ /**
155
+ * Optional semantic option kind, such as allow_once or reject_once.
156
+ */
157
+ kind?: string;
158
+ }
159
+
160
+ /**
161
+ * A single grouped tool call entry.
162
+ */
163
+ export interface IToolCallsEntry {
164
+ /**
165
+ * Unique tool call identifier.
166
+ */
167
+ toolCallId: string;
168
+ /**
169
+ * Human-readable title displayed in the UI.
170
+ */
171
+ title?: string;
172
+ /**
173
+ * Tool operation category.
174
+ */
175
+ kind?: string;
176
+ /**
177
+ * Current tool call status.
178
+ */
179
+ status?: string;
180
+ /**
181
+ * Tool input payload.
182
+ */
183
+ rawInput?: unknown;
184
+ /**
185
+ * Tool output payload.
186
+ */
187
+ rawOutput?: unknown;
188
+ /**
189
+ * File paths or resource locations referenced by the tool call.
190
+ */
191
+ locations?: string[];
192
+ /**
193
+ * Permission options presented to the user.
194
+ */
195
+ permissionOptions?: IToolCallPermissionOption[];
196
+ /**
197
+ * Permission request lifecycle state.
198
+ */
199
+ permissionStatus?: 'pending' | 'resolved';
200
+ /**
201
+ * Selected permission option identifier.
202
+ */
203
+ selectedOptionId?: string;
204
+ /**
205
+ * Session identifier used to route permission decisions.
206
+ */
207
+ sessionId?: string;
208
+ /**
209
+ * Optional inline file diffs associated with the tool call.
210
+ */
211
+ diffs?: IToolCallDiff[];
212
+ }
213
+
214
+ /**
215
+ * Metadata for rendering grouped tool calls.
216
+ */
217
+ export interface IToolCallsMetadata {
218
+ /**
219
+ * List of tool call entries.
220
+ */
221
+ toolCalls: IToolCallsEntry[];
222
+ }
223
+
224
+ /**
225
+ * A file diff target.
226
+ */
227
+ export interface IInlineDiffFileTarget {
228
+ /**
229
+ * Discriminator for a regular file diff target.
230
+ */
231
+ kind: 'file';
232
+ /**
233
+ * Path of the file being diffed.
234
+ */
90
235
  path: string;
236
+ }
237
+
238
+ /**
239
+ * A notebook cell diff target.
240
+ */
241
+ export interface IInlineDiffNotebookCellTarget {
242
+ /**
243
+ * Discriminator for a notebook cell source diff target.
244
+ */
245
+ kind: 'cell';
246
+ /**
247
+ * Path of the notebook containing the cell.
248
+ */
249
+ notebookPath: string;
250
+ /**
251
+ * Stable cell identifier, when available from the producer.
252
+ */
253
+ cellId?: string;
254
+ /**
255
+ * Zero-based notebook cell index used for the default display label.
256
+ */
257
+ cellIndex?: number;
258
+ }
259
+
260
+ /**
261
+ * A supported inline diff target.
262
+ */
263
+ export type IInlineDiffTarget =
264
+ | IInlineDiffFileTarget
265
+ | IInlineDiffNotebookCellTarget;
266
+
267
+ /**
268
+ * A single inline diff entry.
269
+ */
270
+ export interface IInlineDiff {
271
+ /**
272
+ * Structured target metadata.
273
+ */
274
+ target: IInlineDiffTarget;
275
+ /**
276
+ * Optional explicit label for the diff header.
277
+ */
278
+ label?: string;
279
+ /**
280
+ * Updated text content for the diff target.
281
+ */
91
282
  newText: string;
283
+ /**
284
+ * Previous text content for the diff target.
285
+ */
92
286
  oldText?: string;
93
287
  }
94
288
 
@@ -96,5 +290,24 @@ export interface IInlineDiff {
96
290
  * Metadata for rendering inline diffs.
97
291
  */
98
292
  export interface IInlineDiffMetadata {
293
+ /**
294
+ * List of inline diff entries to render.
295
+ */
99
296
  diffs: IInlineDiff[];
100
297
  }
298
+
299
+ /**
300
+ * A single queued message entry.
301
+ */
302
+ export interface IQueuedMessage {
303
+ id: string;
304
+ body: string;
305
+ }
306
+
307
+ /**
308
+ * Metadata for the message queue component.
309
+ */
310
+ export interface IMessageQueueMetadata {
311
+ messages: IQueuedMessage[];
312
+ targetId?: string;
313
+ }