jupyter-chat-components 0.2.0 → 0.3.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.
@@ -1,2 +1,3 @@
1
1
  export * from './inline-diff';
2
+ export * from './message-queue';
2
3
  export * from './tool-call';
@@ -1,2 +1,3 @@
1
1
  export * from './inline-diff';
2
+ export * from './message-queue';
2
3
  export * from './tool-call';
@@ -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 file diffs.
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 patch = structuredPatch(diff.path, diff.path, (_a = diff.oldText) !== null && _a !== void 0 ? _a : '', diff.newText, undefined, undefined, { context: Infinity });
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 = getDiffFilename(diff.path);
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: diff.path }, filename),
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 file diffs.
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.path}-${index}`, diff: diff, trans: transBundle })))));
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, RemoveQueuedMessage, ToolCallApproval } 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,10 @@ 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;
15
19
  /**
16
20
  * The component registry.
17
21
  */
@@ -33,6 +37,7 @@ export declare class ComponentsRenderer extends ReactWidget implements IRenderMi
33
37
  private _trans;
34
38
  private _mimeType;
35
39
  private _toolCallApproval?;
40
+ private _removeQueuedMessage?;
36
41
  private _registry;
37
42
  private _data;
38
43
  private _metadata;
@@ -46,6 +51,7 @@ export declare class RendererFactory implements IComponentsRendererFactory {
46
51
  readonly defaultRank = 100;
47
52
  readonly registry: ComponentRegistry;
48
53
  toolCallApproval: ToolCallApproval;
54
+ removeQueuedMessage: RemoveQueuedMessage;
49
55
  constructor();
50
56
  createRenderer: (options: IRenderMime.IRendererOptions) => ComponentsRenderer;
51
57
  }
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, ToolCall } from './components';
4
+ import { InlineDiff, MessageQueue, ToolCall } from './components';
5
5
  import { ComponentRegistry } from './registry';
6
6
  /**
7
7
  * The default mime type for the extension.
@@ -26,6 +26,7 @@ export class ComponentsRenderer extends ReactWidget {
26
26
  this._trans = ((_a = options.translator) !== null && _a !== void 0 ? _a : nullTranslator).load('jupyterlab');
27
27
  this._mimeType = options.mimeType;
28
28
  this._toolCallApproval = options.toolCallApproval;
29
+ this._removeQueuedMessage = options.removeQueuedMessage;
29
30
  this._registry = options.registry;
30
31
  this.addClass(CLASS_NAME);
31
32
  }
@@ -53,6 +54,9 @@ export class ComponentsRenderer extends ReactWidget {
53
54
  if (this._data === 'tool-call') {
54
55
  componentsProps.toolCallApproval = this._toolCallApproval;
55
56
  }
57
+ if (this._data === 'message-queue') {
58
+ componentsProps.removeQueuedMessage = this._removeQueuedMessage;
59
+ }
56
60
  return React.createElement(Component, { ...componentsProps, trans: this._trans });
57
61
  }
58
62
  }
@@ -65,15 +69,18 @@ export class RendererFactory {
65
69
  this.mimeTypes = [MIME_TYPE];
66
70
  this.defaultRank = 100;
67
71
  this.toolCallApproval = null;
72
+ this.removeQueuedMessage = null;
68
73
  this.createRenderer = (options) => {
69
74
  return new ComponentsRenderer({
70
75
  ...options,
71
76
  toolCallApproval: this.toolCallApproval,
77
+ removeQueuedMessage: this.removeQueuedMessage,
72
78
  registry: this.registry
73
79
  });
74
80
  };
75
81
  this.registry = new ComponentRegistry();
76
82
  this.registry.add('tool-call', ToolCall);
77
83
  this.registry.add('inline-diff', InlineDiff);
84
+ this.registry.add('message-queue', MessageQueue);
78
85
  }
79
86
  }
package/lib/token.d.ts CHANGED
@@ -10,6 +10,10 @@ export declare const IComponentsRendererFactory: Token<IComponentsRendererFactor
10
10
  * The callback to approve or reject a tool.
11
11
  */
12
12
  export type ToolCallApproval = ((targetId: string, approvalId: string, approve: boolean) => void) | null;
13
+ /**
14
+ * The callback to remove a queued message.
15
+ */
16
+ export type RemoveQueuedMessage = ((targetId: string, messageId: string) => void) | null;
13
17
  /**
14
18
  * The interface for components renderer factory.
15
19
  */
@@ -22,6 +26,10 @@ export interface IComponentsRendererFactory extends IRenderMime.IRendererFactory
22
26
  * The callback to approve or reject a tool.
23
27
  */
24
28
  toolCallApproval: ToolCallApproval;
29
+ /**
30
+ * The callback to remove a queued message.
31
+ */
32
+ removeQueuedMessage: RemoveQueuedMessage;
25
33
  }
26
34
  /**
27
35
  * The interface for the component registry.
@@ -65,16 +73,84 @@ export interface IComponentProps {
65
73
  trans: TranslationBundle;
66
74
  }
67
75
  /**
68
- * A single file diff entry.
76
+ * A file diff target.
69
77
  */
70
- export interface IInlineDiff {
78
+ export interface IInlineDiffFileTarget {
79
+ /**
80
+ * Discriminator for a regular file diff target.
81
+ */
82
+ kind: 'file';
83
+ /**
84
+ * Path of the file being diffed.
85
+ */
71
86
  path: string;
87
+ }
88
+ /**
89
+ * A notebook cell diff target.
90
+ */
91
+ export interface IInlineDiffNotebookCellTarget {
92
+ /**
93
+ * Discriminator for a notebook cell source diff target.
94
+ */
95
+ kind: 'cell';
96
+ /**
97
+ * Path of the notebook containing the cell.
98
+ */
99
+ notebookPath: string;
100
+ /**
101
+ * Stable cell identifier, when available from the producer.
102
+ */
103
+ cellId?: string;
104
+ /**
105
+ * Zero-based notebook cell index used for the default display label.
106
+ */
107
+ cellIndex?: number;
108
+ }
109
+ /**
110
+ * A supported inline diff target.
111
+ */
112
+ export type IInlineDiffTarget = IInlineDiffFileTarget | IInlineDiffNotebookCellTarget;
113
+ /**
114
+ * A single inline diff entry.
115
+ */
116
+ export interface IInlineDiff {
117
+ /**
118
+ * Structured target metadata.
119
+ */
120
+ target: IInlineDiffTarget;
121
+ /**
122
+ * Optional explicit label for the diff header.
123
+ */
124
+ label?: string;
125
+ /**
126
+ * Updated text content for the diff target.
127
+ */
72
128
  newText: string;
129
+ /**
130
+ * Previous text content for the diff target.
131
+ */
73
132
  oldText?: string;
74
133
  }
75
134
  /**
76
135
  * Metadata for rendering inline diffs.
77
136
  */
78
137
  export interface IInlineDiffMetadata {
138
+ /**
139
+ * List of inline diff entries to render.
140
+ */
79
141
  diffs: IInlineDiff[];
80
142
  }
143
+ /**
144
+ * A single queued message entry.
145
+ */
146
+ export interface IQueuedMessage {
147
+ id: string;
148
+ body: string;
149
+ }
150
+ /**
151
+ * Metadata for the message queue component.
152
+ */
153
+ export interface IMessageQueueMetadata {
154
+ messages: IQueuedMessage[];
155
+ targetId?: string;
156
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyter-chat-components",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Components to displayed in jupyter chat",
5
5
  "keywords": [
6
6
  "jupyter",
@@ -1,2 +1,3 @@
1
1
  export * from './inline-diff';
2
+ export * from './message-queue';
2
3
  export * from './tool-call';
@@ -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,13 +8,14 @@ import { ReadonlyPartialJSONValue } from '@lumino/coreutils';
8
8
 
9
9
  import * as React from 'react';
10
10
 
11
- import { InlineDiff, ToolCall } from './components';
11
+ import { InlineDiff, MessageQueue, ToolCall } from './components';
12
12
 
13
13
  import { ComponentRegistry } from './registry';
14
14
 
15
15
  import {
16
16
  IComponentRegistry,
17
17
  IComponentsRendererFactory,
18
+ RemoveQueuedMessage,
18
19
  ToolCallApproval
19
20
  } from './token';
20
21
 
@@ -41,6 +42,11 @@ interface IComponentsRendererOptions extends IRenderMime.IRendererOptions {
41
42
  */
42
43
  toolCallApproval?: ToolCallApproval;
43
44
 
45
+ /**
46
+ * The callback to remove a queued message.
47
+ */
48
+ removeQueuedMessage?: RemoveQueuedMessage;
49
+
44
50
  /**
45
51
  * The component registry.
46
52
  */
@@ -62,6 +68,7 @@ export class ComponentsRenderer
62
68
  this._trans = (options.translator ?? nullTranslator).load('jupyterlab');
63
69
  this._mimeType = options.mimeType;
64
70
  this._toolCallApproval = options.toolCallApproval;
71
+ this._removeQueuedMessage = options.removeQueuedMessage;
65
72
  this._registry = options.registry;
66
73
  this.addClass(CLASS_NAME);
67
74
  }
@@ -93,12 +100,17 @@ export class ComponentsRenderer
93
100
  componentsProps.toolCallApproval = this._toolCallApproval;
94
101
  }
95
102
 
103
+ if (this._data === 'message-queue') {
104
+ componentsProps.removeQueuedMessage = this._removeQueuedMessage;
105
+ }
106
+
96
107
  return <Component {...componentsProps} trans={this._trans} />;
97
108
  }
98
109
 
99
110
  private _trans: TranslationBundle;
100
111
  private _mimeType: string;
101
112
  private _toolCallApproval?: ToolCallApproval;
113
+ private _removeQueuedMessage?: RemoveQueuedMessage;
102
114
  private _registry: IComponentRegistry;
103
115
  private _data: string | null = null;
104
116
  private _metadata: ReadonlyPartialJSONValue | null = null;
@@ -113,17 +125,20 @@ export class RendererFactory implements IComponentsRendererFactory {
113
125
  readonly defaultRank = 100;
114
126
  readonly registry: ComponentRegistry;
115
127
  toolCallApproval: ToolCallApproval = null;
128
+ removeQueuedMessage: RemoveQueuedMessage = null;
116
129
 
117
130
  constructor() {
118
131
  this.registry = new ComponentRegistry();
119
132
  this.registry.add('tool-call', ToolCall);
120
133
  this.registry.add('inline-diff', InlineDiff);
134
+ this.registry.add('message-queue', MessageQueue);
121
135
  }
122
136
 
123
137
  createRenderer = (options: IRenderMime.IRendererOptions) => {
124
138
  return new ComponentsRenderer({
125
139
  ...options,
126
140
  toolCallApproval: this.toolCallApproval,
141
+ removeQueuedMessage: this.removeQueuedMessage,
127
142
  registry: this.registry
128
143
  });
129
144
  };
package/src/token.ts CHANGED
@@ -21,6 +21,13 @@ export type ToolCallApproval =
21
21
  | ((targetId: string, approvalId: string, approve: boolean) => void)
22
22
  | null;
23
23
 
24
+ /**
25
+ * The callback to remove a queued message.
26
+ */
27
+ export type RemoveQueuedMessage =
28
+ | ((targetId: string, messageId: string) => void)
29
+ | null;
30
+
24
31
  /**
25
32
  * The interface for components renderer factory.
26
33
  */
@@ -35,6 +42,11 @@ export interface IComponentsRendererFactory
35
42
  * The callback to approve or reject a tool.
36
43
  */
37
44
  toolCallApproval: ToolCallApproval;
45
+
46
+ /**
47
+ * The callback to remove a queued message.
48
+ */
49
+ removeQueuedMessage: RemoveQueuedMessage;
38
50
  }
39
51
 
40
52
  /**
@@ -84,11 +96,67 @@ export interface IComponentProps {
84
96
  }
85
97
 
86
98
  /**
87
- * A single file diff entry.
99
+ * A file diff target.
88
100
  */
89
- export interface IInlineDiff {
101
+ export interface IInlineDiffFileTarget {
102
+ /**
103
+ * Discriminator for a regular file diff target.
104
+ */
105
+ kind: 'file';
106
+ /**
107
+ * Path of the file being diffed.
108
+ */
90
109
  path: string;
110
+ }
111
+
112
+ /**
113
+ * A notebook cell diff target.
114
+ */
115
+ export interface IInlineDiffNotebookCellTarget {
116
+ /**
117
+ * Discriminator for a notebook cell source diff target.
118
+ */
119
+ kind: 'cell';
120
+ /**
121
+ * Path of the notebook containing the cell.
122
+ */
123
+ notebookPath: string;
124
+ /**
125
+ * Stable cell identifier, when available from the producer.
126
+ */
127
+ cellId?: string;
128
+ /**
129
+ * Zero-based notebook cell index used for the default display label.
130
+ */
131
+ cellIndex?: number;
132
+ }
133
+
134
+ /**
135
+ * A supported inline diff target.
136
+ */
137
+ export type IInlineDiffTarget =
138
+ | IInlineDiffFileTarget
139
+ | IInlineDiffNotebookCellTarget;
140
+
141
+ /**
142
+ * A single inline diff entry.
143
+ */
144
+ export interface IInlineDiff {
145
+ /**
146
+ * Structured target metadata.
147
+ */
148
+ target: IInlineDiffTarget;
149
+ /**
150
+ * Optional explicit label for the diff header.
151
+ */
152
+ label?: string;
153
+ /**
154
+ * Updated text content for the diff target.
155
+ */
91
156
  newText: string;
157
+ /**
158
+ * Previous text content for the diff target.
159
+ */
92
160
  oldText?: string;
93
161
  }
94
162
 
@@ -96,5 +164,24 @@ export interface IInlineDiff {
96
164
  * Metadata for rendering inline diffs.
97
165
  */
98
166
  export interface IInlineDiffMetadata {
167
+ /**
168
+ * List of inline diff entries to render.
169
+ */
99
170
  diffs: IInlineDiff[];
100
171
  }
172
+
173
+ /**
174
+ * A single queued message entry.
175
+ */
176
+ export interface IQueuedMessage {
177
+ id: string;
178
+ body: string;
179
+ }
180
+
181
+ /**
182
+ * Metadata for the message queue component.
183
+ */
184
+ export interface IMessageQueueMetadata {
185
+ messages: IQueuedMessage[];
186
+ targetId?: string;
187
+ }
package/style/base.css CHANGED
@@ -323,3 +323,61 @@
323
323
  color: var(--jp-ui-font-color1);
324
324
  text-decoration: underline;
325
325
  }
326
+
327
+ /* Message Queue Styling */
328
+ .jp-chat-message-queue {
329
+ display: flex;
330
+ flex-direction: column;
331
+ align-items: flex-end;
332
+ gap: 8px;
333
+ pointer-events: none;
334
+ width: 100%;
335
+ }
336
+
337
+ .jp-chat-message-queue-bubble {
338
+ position: relative;
339
+ display: inline-flex;
340
+ align-items: center;
341
+ gap: 6px;
342
+ background-color: var(--jp-layout-color2);
343
+ border: 1px solid var(--jp-border-color1);
344
+ border-radius: 16px;
345
+ padding: 4px 28px 4px 12px;
346
+ pointer-events: auto;
347
+ }
348
+
349
+ .jp-chat-message-queue-text {
350
+ font-size: var(--jp-ui-font-size0);
351
+ overflow: hidden;
352
+ text-overflow: ellipsis;
353
+ white-space: nowrap;
354
+ }
355
+
356
+ .jp-chat-message-queue-remove {
357
+ position: absolute;
358
+ right: 6px;
359
+ top: 50%;
360
+ transform: translateY(-50%);
361
+ display: inline-flex;
362
+ align-items: center;
363
+ justify-content: center;
364
+ width: 16px;
365
+ height: 16px;
366
+ padding: 0;
367
+ border: none;
368
+ border-radius: 50%;
369
+ background: transparent;
370
+ color: var(--jp-ui-font-color2);
371
+ font-size: 10px;
372
+ cursor: pointer;
373
+ opacity: 0;
374
+ }
375
+
376
+ .jp-chat-message-queue-remove:hover {
377
+ background: var(--jp-layout-color3);
378
+ color: var(--jp-ui-font-color1);
379
+ }
380
+
381
+ .jp-chat-message-queue-bubble:hover .jp-chat-message-queue-remove {
382
+ opacity: 1;
383
+ }