jupyter-chat-components 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Project Jupyter
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # jupyter_chat_components
2
+
3
+ [![Github Actions Status](https://github.com/brichet/jupyter-chat-components/workflows/Build/badge.svg)](https://github.com/brichet/jupyter-chat-components/actions/workflows/build.yml)
4
+
5
+ Components to displayed in jupyter chat
6
+
7
+ ## Requirements
8
+
9
+ - JupyterLab >= 4.0.0
10
+
11
+ ## Install
12
+
13
+ To install the extension, execute:
14
+
15
+ ```bash
16
+ pip install jupyter_chat_components
17
+ ```
18
+
19
+ ## Uninstall
20
+
21
+ To remove the extension, execute:
22
+
23
+ ```bash
24
+ pip uninstall jupyter_chat_components
25
+ ```
26
+
27
+ ## Contributing
28
+
29
+ ### Development install
30
+
31
+ Note: You will need NodeJS to build the extension package.
32
+
33
+ The `jlpm` command is JupyterLab's pinned version of
34
+ [yarn](https://yarnpkg.com/) that is installed with JupyterLab. You may use
35
+ `yarn` or `npm` in lieu of `jlpm` below.
36
+
37
+ ```bash
38
+ # Clone the repo to your local environment
39
+ # Change directory to the jupyter_chat_components directory
40
+
41
+ # Set up a virtual environment and install package in development mode
42
+ python -m venv .venv
43
+ source .venv/bin/activate
44
+ pip install --editable "."
45
+
46
+ # Link your development version of the extension with JupyterLab
47
+ jupyter labextension develop . --overwrite
48
+
49
+ # Rebuild extension Typescript source after making changes
50
+ # IMPORTANT: Unlike the steps above which are performed only once, do this step
51
+ # every time you make a change.
52
+ jlpm build
53
+ ```
54
+
55
+ You can watch the source directory and run JupyterLab at the same time in different terminals to watch for changes in the extension's source and automatically rebuild the extension.
56
+
57
+ ```bash
58
+ # Watch the source directory in one terminal, automatically rebuilding when needed
59
+ jlpm watch
60
+ # Run JupyterLab in another terminal
61
+ jupyter lab
62
+ ```
63
+
64
+ With the watch command running, every saved change will immediately be built locally and available in your running JupyterLab. Refresh JupyterLab to load the change in your browser (you may need to wait several seconds for the extension to be rebuilt).
65
+
66
+ By default, the `jlpm build` command generates the source maps for this extension to make it easier to debug using the browser dev tools. To also generate source maps for the JupyterLab core extensions, you can run the following command:
67
+
68
+ ```bash
69
+ jupyter lab build --minimize=False
70
+ ```
71
+
72
+ ### Development uninstall
73
+
74
+ ```bash
75
+ pip uninstall jupyter_chat_components
76
+ ```
77
+
78
+ In development mode, you will also need to remove the symlink created by `jupyter labextension develop`
79
+ command. To find its location, you can run `jupyter labextension list` to figure out where the `labextensions`
80
+ folder is located. Then you can remove the symlink named `jupyter-chat-components` within that folder.
81
+
82
+ ### Testing the extension
83
+
84
+ #### Frontend tests
85
+
86
+ This extension is using [Jest](https://jestjs.io/) for JavaScript code testing.
87
+
88
+ To execute them, execute:
89
+
90
+ ```sh
91
+ jlpm
92
+ jlpm test
93
+ ```
94
+
95
+ #### Integration tests
96
+
97
+ This extension uses [Playwright](https://playwright.dev/docs/intro) for the integration tests (aka user level tests).
98
+ More precisely, the JupyterLab helper [Galata](https://github.com/jupyterlab/jupyterlab/tree/master/galata) is used to handle testing the extension in JupyterLab.
99
+
100
+ More information are provided within the [ui-tests](./ui-tests/README.md) README.
101
+
102
+ ## AI Coding Assistant Support
103
+
104
+ This project includes an `AGENTS.md` file with coding standards and best practices for JupyterLab extension development. The file follows the [AGENTS.md standard](https://agents.md) for cross-tool compatibility.
105
+
106
+ ### Compatible AI Tools
107
+
108
+ `AGENTS.md` works with AI coding assistants that support the standard, including Cursor, GitHub Copilot, Windsurf, Aider, and others. For a current list of compatible tools, see [the AGENTS.md standard](https://agents.md).
109
+
110
+ Other conventions you might encounter:
111
+
112
+ - `.cursorrules` - Cursor's YAML/JSON format (Cursor also supports AGENTS.md natively)
113
+ - `CONVENTIONS.md` / `CONTRIBUTING.md` - For CodeConventions.ai and GitHub bots
114
+ - Project-specific rules in JetBrains AI Assistant settings
115
+
116
+ All tool-specific files should be symlinks to `AGENTS.md` as the single source of truth.
117
+
118
+ ### What's Included
119
+
120
+ The `AGENTS.md` file provides guidance on:
121
+
122
+ - Code quality rules and file-scoped validation commands
123
+ - Naming conventions for packages, plugins, and files
124
+ - Coding standards (TypeScript)
125
+ - Development workflow and debugging
126
+ - Common pitfalls and how to avoid them
127
+
128
+ ### Customization
129
+
130
+ You can edit `AGENTS.md` to add project-specific conventions or adjust guidelines to match your team's practices. The file uses plain Markdown with Do/Don't patterns and references to actual project files.
131
+
132
+ **Note**: `AGENTS.md` is living documentation. Update it when you change conventions, add dependencies, or discover new patterns. Include `AGENTS.md` updates in commits that modify workflows or coding standards.
133
+
134
+ ### Packaging the extension
135
+
136
+ See [RELEASE](RELEASE.md)
package/lib/index.d.ts ADDED
@@ -0,0 +1,32 @@
1
+ import { JupyterFrontEndPlugin } from '@jupyterlab/application';
2
+ import { IRenderMime } from '@jupyterlab/rendermime-interfaces';
3
+ import { Widget } from '@lumino/widgets';
4
+ import { IComponentsRendererFactory, ToolCallApproval } from './token';
5
+ /**
6
+ * The options for the chat components renderer.
7
+ */
8
+ interface IComponentsRendererOptions extends IRenderMime.IRendererOptions {
9
+ /**
10
+ * The callback to approve or reject a tool.
11
+ */
12
+ toolCallApproval: ToolCallApproval;
13
+ }
14
+ /**
15
+ * A widget for rendering .
16
+ */
17
+ export declare class ComponentsRenderer extends Widget implements IRenderMime.IRenderer {
18
+ /**
19
+ * Construct a new output widget.
20
+ */
21
+ constructor(options: IComponentsRendererOptions);
22
+ /**
23
+ * Render into this widget's node.
24
+ */
25
+ renderModel(model: IRenderMime.IMimeModel): Promise<void>;
26
+ private _trans;
27
+ private _mimeType;
28
+ private _toolCallApproval;
29
+ }
30
+ declare const plugin: JupyterFrontEndPlugin<IComponentsRendererFactory>;
31
+ export * from './token';
32
+ export default plugin;
package/lib/index.js ADDED
@@ -0,0 +1,76 @@
1
+ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
2
+ import { nullTranslator } from '@jupyterlab/translation';
3
+ import { Widget } from '@lumino/widgets';
4
+ import { IComponentsRendererFactory } from './token';
5
+ import { buildToolCallHtml } from './tool-call';
6
+ /**
7
+ * The default mime type for the extension.
8
+ */
9
+ const MIME_TYPE = 'application/vnd.jupyter.chat.components';
10
+ /**
11
+ * The class name added to the extension.
12
+ */
13
+ const CLASS_NAME = 'jp-RenderedChatComponents';
14
+ /**
15
+ * A widget for rendering .
16
+ */
17
+ export class ComponentsRenderer extends Widget {
18
+ /**
19
+ * Construct a new output widget.
20
+ */
21
+ constructor(options) {
22
+ var _a;
23
+ super();
24
+ this._trans = ((_a = options.translator) !== null && _a !== void 0 ? _a : nullTranslator).load('jupyterlab');
25
+ this._mimeType = options.mimeType;
26
+ this._toolCallApproval = options.toolCallApproval;
27
+ this.addClass(CLASS_NAME);
28
+ }
29
+ /**
30
+ * Render into this widget's node.
31
+ */
32
+ renderModel(model) {
33
+ const data = model.data[this._mimeType];
34
+ const metadata = { ...model.metadata };
35
+ if (data === 'tool-call') {
36
+ const toolCallOptions = {
37
+ ...metadata,
38
+ trans: this._trans,
39
+ toolCallApproval: this._toolCallApproval
40
+ };
41
+ this.node.appendChild(buildToolCallHtml(toolCallOptions));
42
+ }
43
+ return Promise.resolve();
44
+ }
45
+ }
46
+ /**
47
+ * A mime renderer factory for chat components.
48
+ */
49
+ class RendererFactory {
50
+ constructor() {
51
+ this.safe = true;
52
+ this.mimeTypes = [MIME_TYPE];
53
+ this.defaultRank = 100;
54
+ this.toolCallApproval = null;
55
+ this.createRenderer = (options) => {
56
+ return new ComponentsRenderer({
57
+ ...options,
58
+ toolCallApproval: this.toolCallApproval
59
+ });
60
+ };
61
+ }
62
+ }
63
+ const plugin = {
64
+ id: 'jupyter-chat-components:plugin',
65
+ description: 'Adds MIME type renderer for chat components',
66
+ autoStart: true,
67
+ provides: IComponentsRendererFactory,
68
+ requires: [IRenderMimeRegistry],
69
+ activate: (app, rendermime) => {
70
+ const rendererFactory = new RendererFactory();
71
+ rendermime.addFactory(rendererFactory);
72
+ return rendererFactory;
73
+ }
74
+ };
75
+ export * from './token';
76
+ export default plugin;
package/lib/token.d.ts ADDED
@@ -0,0 +1,35 @@
1
+ import { IRenderMime } from '@jupyterlab/rendermime-interfaces';
2
+ import { Token } from '@lumino/coreutils';
3
+ /**
4
+ * The token providing the chat components renderer.
5
+ */
6
+ export declare const IComponentsRendererFactory: Token<IComponentsRendererFactory>;
7
+ /**
8
+ * The callback to approve or reject a tool.
9
+ */
10
+ export type ToolCallApproval = ((targetId: string, approvalId: string, approve: boolean) => void) | null;
11
+ /**
12
+ * The interface for components renderer factory.
13
+ */
14
+ export interface IComponentsRendererFactory extends IRenderMime.IRendererFactory {
15
+ /**
16
+ * The callback to approve or reject a tool.
17
+ */
18
+ toolCallApproval: ToolCallApproval;
19
+ }
20
+ /**
21
+ * Tool call status types.
22
+ */
23
+ export type ToolCallStatus = 'pending' | 'awaiting_approval' | 'approved' | 'rejected' | 'completed' | 'error';
24
+ /**
25
+ * Options for building tool call HTML.
26
+ */
27
+ export interface IToolCallMetadata {
28
+ toolName: string;
29
+ input: string;
30
+ status: ToolCallStatus;
31
+ summary?: string;
32
+ output?: string;
33
+ targetId?: string;
34
+ approvalId?: string;
35
+ }
package/lib/token.js ADDED
@@ -0,0 +1,5 @@
1
+ import { Token } from '@lumino/coreutils';
2
+ /**
3
+ * The token providing the chat components renderer.
4
+ */
5
+ export const IComponentsRendererFactory = new Token('jupyter-chat-components:IComponentsRendererFactory', 'The chat components renderer factory');
@@ -0,0 +1,14 @@
1
+ import { TranslationBundle } from '@jupyterlab/translation';
2
+ import { IToolCallMetadata, ToolCallApproval } from './token';
3
+ /**
4
+ * Options for building tool call HTML.
5
+ */
6
+ export interface IToolCallHtmlOptions extends IToolCallMetadata {
7
+ trans: TranslationBundle;
8
+ toolCallApproval: ToolCallApproval;
9
+ }
10
+ export declare function escapeHtml(value: string): string;
11
+ /**
12
+ * Builds HTML for a tool call display.
13
+ */
14
+ export declare function buildToolCallHtml(options: IToolCallHtmlOptions): HTMLDetailsElement;
@@ -0,0 +1,152 @@
1
+ const STATUS_CONFIG = {
2
+ pending: {
3
+ cssClass: 'jp-ai-tool-pending',
4
+ statusClass: 'jp-ai-tool-status-pending'
5
+ },
6
+ awaiting_approval: {
7
+ cssClass: 'jp-ai-tool-pending',
8
+ statusClass: 'jp-ai-tool-status-approval',
9
+ open: true
10
+ },
11
+ approved: {
12
+ cssClass: 'jp-ai-tool-pending',
13
+ statusClass: 'jp-ai-tool-status-completed'
14
+ },
15
+ rejected: {
16
+ cssClass: 'jp-ai-tool-error',
17
+ statusClass: 'jp-ai-tool-status-error'
18
+ },
19
+ completed: {
20
+ cssClass: 'jp-ai-tool-completed',
21
+ statusClass: 'jp-ai-tool-status-completed'
22
+ },
23
+ error: {
24
+ cssClass: 'jp-ai-tool-error',
25
+ statusClass: 'jp-ai-tool-status-error'
26
+ }
27
+ };
28
+ export function escapeHtml(value) {
29
+ // Prefer the same native escaping approach used in JupyterLab itself
30
+ // (e.g. `@jupyterlab/completer`).
31
+ if (typeof document !== 'undefined') {
32
+ const node = document.createElement('span');
33
+ node.textContent = value;
34
+ return node.innerHTML;
35
+ }
36
+ // Fallback
37
+ return value
38
+ .replace(/&/g, '&amp;')
39
+ .replace(/</g, '&lt;')
40
+ .replace(/>/g, '&gt;')
41
+ .replace(/"/g, '&quot;')
42
+ .replace(/'/g, '&#39;');
43
+ }
44
+ /**
45
+ * Returns the translated status text for a given tool status.
46
+ */
47
+ const getStatusText = (status, trans) => {
48
+ switch (status) {
49
+ case 'pending':
50
+ return trans.__('Running...');
51
+ case 'awaiting_approval':
52
+ return trans.__('Awaiting Approval');
53
+ case 'approved':
54
+ return trans.__('Approved - Executing...');
55
+ case 'rejected':
56
+ return trans.__('Rejected');
57
+ case 'completed':
58
+ return trans.__('Completed');
59
+ case 'error':
60
+ return trans.__('Error');
61
+ }
62
+ };
63
+ /**
64
+ * Builds HTML for a tool call display.
65
+ */
66
+ export function buildToolCallHtml(options) {
67
+ const { toolName, input, status, summary, output, targetId, approvalId, trans } = options;
68
+ const config = STATUS_CONFIG[status];
69
+ const statusText = getStatusText(status, trans);
70
+ const escapedToolName = escapeHtml(toolName);
71
+ const escapedInput = escapeHtml(input);
72
+ const details = document.createElement('details');
73
+ details.classList.add('jp-ai-tool-call', config.cssClass);
74
+ if (config.open) {
75
+ details.setAttribute('open', 'true');
76
+ }
77
+ const summaryElement = document.createElement('summary');
78
+ summaryElement.classList.add('jp-ai-tool-header');
79
+ // Build summary header
80
+ const icon = document.createElement('div');
81
+ icon.classList.add('jp-ai-tool-icon');
82
+ icon.textContent = '⚡';
83
+ const title = document.createElement('div');
84
+ title.classList.add('jp-ai-tool-title');
85
+ title.textContent = escapedToolName;
86
+ if (summary) {
87
+ const summarySpan = document.createElement('span');
88
+ summarySpan.classList.add('jp-ai-tool-summary');
89
+ summarySpan.textContent = summary;
90
+ title.appendChild(summarySpan);
91
+ }
92
+ const statusDiv = document.createElement('div');
93
+ statusDiv.classList.add('jp-ai-tool-status', config.statusClass);
94
+ statusDiv.textContent = statusText;
95
+ summaryElement.appendChild(icon);
96
+ summaryElement.appendChild(title);
97
+ summaryElement.appendChild(statusDiv);
98
+ // Build body
99
+ const body = document.createElement('div');
100
+ body.classList.add('jp-ai-tool-body');
101
+ // Add input section
102
+ const inputSection = document.createElement('div');
103
+ inputSection.classList.add('jp-ai-tool-section');
104
+ const inputLabel = document.createElement('div');
105
+ inputLabel.classList.add('jp-ai-tool-label');
106
+ inputLabel.textContent = trans.__('Input');
107
+ const inputPre = document.createElement('pre');
108
+ inputPre.classList.add('jp-ai-tool-code');
109
+ const inputCode = document.createElement('code');
110
+ inputCode.textContent = escapedInput;
111
+ inputPre.appendChild(inputCode);
112
+ inputSection.appendChild(inputLabel);
113
+ inputSection.appendChild(inputPre);
114
+ body.appendChild(inputSection);
115
+ // Add approval buttons if awaiting approval
116
+ if (status === 'awaiting_approval' && approvalId && targetId) {
117
+ const approvalButtonsDiv = document.createElement('div');
118
+ approvalButtonsDiv.classList.add('jp-ai-tool-approval-buttons', `jp-ai-approval-id--${approvalId}`);
119
+ const approveBtn = document.createElement('button');
120
+ approveBtn.classList.add('jp-ai-approval-btn', 'jp-ai-approval-approve');
121
+ approveBtn.textContent = trans.__('Approve');
122
+ const rejectBtn = document.createElement('button');
123
+ rejectBtn.classList.add('jp-ai-approval-btn', 'jp-ai-approval-reject');
124
+ rejectBtn.textContent = trans.__('Reject');
125
+ approvalButtonsDiv.appendChild(approveBtn);
126
+ approvalButtonsDiv.appendChild(rejectBtn);
127
+ body.appendChild(approvalButtonsDiv);
128
+ approveBtn.addEventListener('click', () => { var _a; return (_a = options.toolCallApproval) === null || _a === void 0 ? void 0 : _a.call(options, targetId, approvalId, true); });
129
+ rejectBtn.addEventListener('click', () => { var _a; return (_a = options.toolCallApproval) === null || _a === void 0 ? void 0 : _a.call(options, targetId, approvalId, false); });
130
+ }
131
+ // Add output/result section if provided
132
+ if (output !== undefined) {
133
+ const escapedOutput = escapeHtml(output);
134
+ const label = status === 'error' ? trans.__('Error') : trans.__('Result');
135
+ const outputSection = document.createElement('div');
136
+ outputSection.classList.add('jp-ai-tool-section');
137
+ const outputLabel = document.createElement('div');
138
+ outputLabel.classList.add('jp-ai-tool-label');
139
+ outputLabel.textContent = label;
140
+ const outputPre = document.createElement('pre');
141
+ outputPre.classList.add('jp-ai-tool-code');
142
+ const outputCode = document.createElement('code');
143
+ outputCode.textContent = escapedOutput;
144
+ outputPre.appendChild(outputCode);
145
+ outputSection.appendChild(outputLabel);
146
+ outputSection.appendChild(outputPre);
147
+ body.appendChild(outputSection);
148
+ }
149
+ details.appendChild(summaryElement);
150
+ details.appendChild(body);
151
+ return details;
152
+ }
package/package.json ADDED
@@ -0,0 +1,202 @@
1
+ {
2
+ "name": "jupyter-chat-components",
3
+ "version": "0.1.1",
4
+ "description": "Components to displayed in jupyter chat",
5
+ "keywords": [
6
+ "jupyter",
7
+ "jupyterlab",
8
+ "jupyterlab-extension"
9
+ ],
10
+ "homepage": "https://github.com/brichet/jupyter-chat-components",
11
+ "bugs": {
12
+ "url": "https://github.com/brichet/jupyter-chat-components/issues"
13
+ },
14
+ "license": "BSD-3-Clause",
15
+ "author": {
16
+ "name": "Project Jupyter",
17
+ "email": "jupyter@googlegroups.com"
18
+ },
19
+ "files": [
20
+ "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
21
+ "style/**/*.{css,js,eot,gif,html,jpg,json,png,svg,woff2,ttf}",
22
+ "src/**/*.{ts,tsx}"
23
+ ],
24
+ "main": "lib/index.js",
25
+ "types": "lib/index.d.ts",
26
+ "style": "style/index.css",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/brichet/jupyter-chat-components.git"
30
+ },
31
+ "scripts": {
32
+ "build": "jlpm build:lib && jlpm build:labextension:dev",
33
+ "build:prod": "jlpm clean && jlpm build:lib:prod && jlpm build:labextension",
34
+ "build:labextension": "jupyter labextension build .",
35
+ "build:labextension:dev": "jupyter labextension build --development True .",
36
+ "build:lib": "tsc --sourceMap",
37
+ "build:lib:prod": "tsc",
38
+ "clean": "jlpm clean:lib",
39
+ "clean:lib": "rimraf lib tsconfig.tsbuildinfo",
40
+ "clean:lintcache": "rimraf .eslintcache .stylelintcache",
41
+ "clean:labextension": "rimraf jupyter_chat_components/labextension jupyter_chat_components/_version.py",
42
+ "clean:all": "jlpm clean:lib && jlpm clean:labextension && jlpm clean:lintcache",
43
+ "eslint": "jlpm eslint:check --fix",
44
+ "eslint:check": "eslint . --cache --ext .ts,.tsx",
45
+ "install:extension": "jlpm build",
46
+ "lint": "jlpm stylelint && jlpm prettier && jlpm eslint",
47
+ "lint:check": "jlpm stylelint:check && jlpm prettier:check && jlpm eslint:check",
48
+ "prettier": "jlpm prettier:base --write --list-different",
49
+ "prettier:base": "prettier \"**/*{.ts,.tsx,.js,.jsx,.css,.json,.md}\"",
50
+ "prettier:check": "jlpm prettier:base --check",
51
+ "stylelint": "jlpm stylelint:check --fix",
52
+ "stylelint:check": "stylelint --cache \"style/**/*.css\"",
53
+ "test": "jest --coverage",
54
+ "watch": "run-p watch:src watch:labextension",
55
+ "watch:src": "tsc -w --sourceMap",
56
+ "watch:labextension": "jupyter labextension watch ."
57
+ },
58
+ "dependencies": {
59
+ "@jupyterlab/application": "^4.5.0",
60
+ "@jupyterlab/rendermime": "^4.5.0",
61
+ "@jupyterlab/rendermime-interfaces": "^3.8.0",
62
+ "@jupyterlab/translation": "^4.5.0",
63
+ "@lumino/coreutils": "^2.2.2",
64
+ "@lumino/widgets": "^2.1.0"
65
+ },
66
+ "devDependencies": {
67
+ "@jupyterlab/builder": "^4.0.0",
68
+ "@jupyterlab/testutils": "^4.0.0",
69
+ "@types/jest": "^29.2.0",
70
+ "@types/json-schema": "^7.0.11",
71
+ "@types/react": "^18.0.26",
72
+ "@types/react-addons-linked-state-mixin": "^0.14.22",
73
+ "@typescript-eslint/eslint-plugin": "^6.1.0",
74
+ "@typescript-eslint/parser": "^6.1.0",
75
+ "css-loader": "^6.7.1",
76
+ "eslint": "^8.36.0",
77
+ "eslint-config-prettier": "^8.8.0",
78
+ "eslint-plugin-prettier": "^5.0.0",
79
+ "jest": "^29.2.0",
80
+ "npm-run-all2": "^7.0.1",
81
+ "prettier": "^3.0.0",
82
+ "rimraf": "^5.0.1",
83
+ "source-map-loader": "^1.0.2",
84
+ "style-loader": "^3.3.1",
85
+ "stylelint": "^15.10.1",
86
+ "stylelint-config-recommended": "^13.0.0",
87
+ "stylelint-config-standard": "^34.0.0",
88
+ "stylelint-csstree-validator": "^3.0.0",
89
+ "stylelint-prettier": "^4.0.0",
90
+ "typescript": "~5.5.4",
91
+ "yjs": "^13.5.0"
92
+ },
93
+ "resolutions": {
94
+ "lib0": "0.2.111"
95
+ },
96
+ "sideEffects": [
97
+ "style/*.css",
98
+ "style/index.js"
99
+ ],
100
+ "styleModule": "style/index.js",
101
+ "publishConfig": {
102
+ "access": "public"
103
+ },
104
+ "jupyterlab": {
105
+ "extension": true,
106
+ "outputDir": "jupyter_chat_components/labextension"
107
+ },
108
+ "eslintIgnore": [
109
+ "node_modules",
110
+ "dist",
111
+ "coverage",
112
+ "**/*.d.ts",
113
+ "tests",
114
+ "**/__tests__",
115
+ "ui-tests"
116
+ ],
117
+ "eslintConfig": {
118
+ "extends": [
119
+ "eslint:recommended",
120
+ "plugin:@typescript-eslint/eslint-recommended",
121
+ "plugin:@typescript-eslint/recommended",
122
+ "plugin:prettier/recommended"
123
+ ],
124
+ "parser": "@typescript-eslint/parser",
125
+ "parserOptions": {
126
+ "project": "tsconfig.json",
127
+ "sourceType": "module"
128
+ },
129
+ "plugins": [
130
+ "@typescript-eslint"
131
+ ],
132
+ "rules": {
133
+ "@typescript-eslint/naming-convention": [
134
+ "error",
135
+ {
136
+ "selector": "interface",
137
+ "format": [
138
+ "PascalCase"
139
+ ],
140
+ "custom": {
141
+ "regex": "^I[A-Z]",
142
+ "match": true
143
+ }
144
+ }
145
+ ],
146
+ "@typescript-eslint/no-unused-vars": [
147
+ "warn",
148
+ {
149
+ "args": "none"
150
+ }
151
+ ],
152
+ "@typescript-eslint/no-explicit-any": "off",
153
+ "@typescript-eslint/no-namespace": "off",
154
+ "@typescript-eslint/no-use-before-define": "off",
155
+ "@typescript-eslint/quotes": [
156
+ "error",
157
+ "single",
158
+ {
159
+ "avoidEscape": true,
160
+ "allowTemplateLiterals": false
161
+ }
162
+ ],
163
+ "curly": [
164
+ "error",
165
+ "all"
166
+ ],
167
+ "eqeqeq": "error",
168
+ "prefer-arrow-callback": "error"
169
+ }
170
+ },
171
+ "prettier": {
172
+ "singleQuote": true,
173
+ "trailingComma": "none",
174
+ "arrowParens": "avoid",
175
+ "endOfLine": "auto",
176
+ "overrides": [
177
+ {
178
+ "files": "package.json",
179
+ "options": {
180
+ "tabWidth": 4
181
+ }
182
+ }
183
+ ]
184
+ },
185
+ "stylelint": {
186
+ "extends": [
187
+ "stylelint-config-recommended",
188
+ "stylelint-config-standard",
189
+ "stylelint-prettier/recommended"
190
+ ],
191
+ "plugins": [
192
+ "stylelint-csstree-validator"
193
+ ],
194
+ "rules": {
195
+ "csstree/validator": true,
196
+ "property-no-vendor-prefix": null,
197
+ "selector-class-pattern": "^([a-z][A-z\\d]*)(-[A-z\\d]+)*$",
198
+ "selector-no-vendor-prefix": null,
199
+ "value-no-vendor-prefix": null
200
+ }
201
+ }
202
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Example of [Jest](https://jestjs.io/docs/getting-started) unit tests
3
+ */
4
+
5
+ describe('jupyter-chat-components', () => {
6
+ it('should be tested', () => {
7
+ expect(1 + 1).toEqual(2);
8
+ });
9
+ });
package/src/index.ts ADDED
@@ -0,0 +1,115 @@
1
+ import {
2
+ JupyterFrontEnd,
3
+ JupyterFrontEndPlugin
4
+ } from '@jupyterlab/application';
5
+
6
+ import { IRenderMimeRegistry } from '@jupyterlab/rendermime';
7
+
8
+ import { IRenderMime } from '@jupyterlab/rendermime-interfaces';
9
+
10
+ import { nullTranslator, TranslationBundle } from '@jupyterlab/translation';
11
+
12
+ import { Widget } from '@lumino/widgets';
13
+
14
+ import {
15
+ IComponentsRendererFactory,
16
+ IToolCallMetadata,
17
+ ToolCallApproval
18
+ } from './token';
19
+
20
+ import { buildToolCallHtml, IToolCallHtmlOptions } from './tool-call';
21
+
22
+ /**
23
+ * The default mime type for the extension.
24
+ */
25
+ const MIME_TYPE = 'application/vnd.jupyter.chat.components';
26
+
27
+ /**
28
+ * The class name added to the extension.
29
+ */
30
+ const CLASS_NAME = 'jp-RenderedChatComponents';
31
+
32
+ /**
33
+ * The options for the chat components renderer.
34
+ */
35
+ interface IComponentsRendererOptions extends IRenderMime.IRendererOptions {
36
+ /**
37
+ * The callback to approve or reject a tool.
38
+ */
39
+ toolCallApproval: ToolCallApproval;
40
+ }
41
+
42
+ /**
43
+ * A widget for rendering .
44
+ */
45
+ export class ComponentsRenderer
46
+ extends Widget
47
+ implements IRenderMime.IRenderer
48
+ {
49
+ /**
50
+ * Construct a new output widget.
51
+ */
52
+ constructor(options: IComponentsRendererOptions) {
53
+ super();
54
+ this._trans = (options.translator ?? nullTranslator).load('jupyterlab');
55
+ this._mimeType = options.mimeType;
56
+ this._toolCallApproval = options.toolCallApproval;
57
+ this.addClass(CLASS_NAME);
58
+ }
59
+
60
+ /**
61
+ * Render into this widget's node.
62
+ */
63
+ renderModel(model: IRenderMime.IMimeModel): Promise<void> {
64
+ const data = model.data[this._mimeType] as string;
65
+ const metadata = { ...model.metadata };
66
+ if (data === 'tool-call') {
67
+ const toolCallOptions: IToolCallHtmlOptions = {
68
+ ...(metadata as unknown as IToolCallMetadata),
69
+ trans: this._trans,
70
+ toolCallApproval: this._toolCallApproval
71
+ };
72
+ this.node.appendChild(buildToolCallHtml(toolCallOptions));
73
+ }
74
+ return Promise.resolve();
75
+ }
76
+
77
+ private _trans: TranslationBundle;
78
+ private _mimeType: string;
79
+ private _toolCallApproval: ToolCallApproval;
80
+ }
81
+
82
+ /**
83
+ * A mime renderer factory for chat components.
84
+ */
85
+ class RendererFactory implements IComponentsRendererFactory {
86
+ readonly safe = true;
87
+ readonly mimeTypes = [MIME_TYPE];
88
+ readonly defaultRank = 100;
89
+ toolCallApproval: ToolCallApproval = null;
90
+ createRenderer = (options: IRenderMime.IRendererOptions) => {
91
+ return new ComponentsRenderer({
92
+ ...options,
93
+ toolCallApproval: this.toolCallApproval
94
+ });
95
+ };
96
+ }
97
+
98
+ const plugin: JupyterFrontEndPlugin<IComponentsRendererFactory> = {
99
+ id: 'jupyter-chat-components:plugin',
100
+ description: 'Adds MIME type renderer for chat components',
101
+ autoStart: true,
102
+ provides: IComponentsRendererFactory,
103
+ requires: [IRenderMimeRegistry],
104
+ activate: (
105
+ app: JupyterFrontEnd,
106
+ rendermime: IRenderMimeRegistry
107
+ ): IComponentsRendererFactory => {
108
+ const rendererFactory = new RendererFactory();
109
+ rendermime.addFactory(rendererFactory);
110
+ return rendererFactory;
111
+ }
112
+ };
113
+
114
+ export * from './token';
115
+ export default plugin;
package/src/token.ts ADDED
@@ -0,0 +1,53 @@
1
+ import { IRenderMime } from '@jupyterlab/rendermime-interfaces';
2
+
3
+ import { Token } from '@lumino/coreutils';
4
+
5
+ /**
6
+ * The token providing the chat components renderer.
7
+ */
8
+ export const IComponentsRendererFactory = new Token<IComponentsRendererFactory>(
9
+ 'jupyter-chat-components:IComponentsRendererFactory',
10
+ 'The chat components renderer factory'
11
+ );
12
+
13
+ /**
14
+ * The callback to approve or reject a tool.
15
+ */
16
+ export type ToolCallApproval =
17
+ | ((targetId: string, approvalId: string, approve: boolean) => void)
18
+ | null;
19
+
20
+ /**
21
+ * The interface for components renderer factory.
22
+ */
23
+ export interface IComponentsRendererFactory
24
+ extends IRenderMime.IRendererFactory {
25
+ /**
26
+ * The callback to approve or reject a tool.
27
+ */
28
+ toolCallApproval: ToolCallApproval;
29
+ }
30
+
31
+ /**
32
+ * Tool call status types.
33
+ */
34
+ export type ToolCallStatus =
35
+ | 'pending'
36
+ | 'awaiting_approval'
37
+ | 'approved'
38
+ | 'rejected'
39
+ | 'completed'
40
+ | 'error';
41
+
42
+ /**
43
+ * Options for building tool call HTML.
44
+ */
45
+ export interface IToolCallMetadata {
46
+ toolName: string;
47
+ input: string;
48
+ status: ToolCallStatus;
49
+ summary?: string;
50
+ output?: string;
51
+ targetId?: string;
52
+ approvalId?: string;
53
+ }
@@ -0,0 +1,224 @@
1
+ import { TranslationBundle } from '@jupyterlab/translation';
2
+
3
+ import { IToolCallMetadata, ToolCallApproval, ToolCallStatus } from './token';
4
+
5
+ /**
6
+ * Configuration for rendering tool call status.
7
+ */
8
+ interface IStatusConfig {
9
+ cssClass: string;
10
+ statusClass: string;
11
+ open?: boolean;
12
+ }
13
+
14
+ const STATUS_CONFIG: Record<ToolCallStatus, IStatusConfig> = {
15
+ pending: {
16
+ cssClass: 'jp-ai-tool-pending',
17
+ statusClass: 'jp-ai-tool-status-pending'
18
+ },
19
+ awaiting_approval: {
20
+ cssClass: 'jp-ai-tool-pending',
21
+ statusClass: 'jp-ai-tool-status-approval',
22
+ open: true
23
+ },
24
+ approved: {
25
+ cssClass: 'jp-ai-tool-pending',
26
+ statusClass: 'jp-ai-tool-status-completed'
27
+ },
28
+ rejected: {
29
+ cssClass: 'jp-ai-tool-error',
30
+ statusClass: 'jp-ai-tool-status-error'
31
+ },
32
+ completed: {
33
+ cssClass: 'jp-ai-tool-completed',
34
+ statusClass: 'jp-ai-tool-status-completed'
35
+ },
36
+ error: {
37
+ cssClass: 'jp-ai-tool-error',
38
+ statusClass: 'jp-ai-tool-status-error'
39
+ }
40
+ };
41
+
42
+ /**
43
+ * Options for building tool call HTML.
44
+ */
45
+ export interface IToolCallHtmlOptions extends IToolCallMetadata {
46
+ trans: TranslationBundle;
47
+ toolCallApproval: ToolCallApproval;
48
+ }
49
+
50
+ export function escapeHtml(value: string): string {
51
+ // Prefer the same native escaping approach used in JupyterLab itself
52
+ // (e.g. `@jupyterlab/completer`).
53
+ if (typeof document !== 'undefined') {
54
+ const node = document.createElement('span');
55
+ node.textContent = value;
56
+ return node.innerHTML;
57
+ }
58
+
59
+ // Fallback
60
+ return value
61
+ .replace(/&/g, '&amp;')
62
+ .replace(/</g, '&lt;')
63
+ .replace(/>/g, '&gt;')
64
+ .replace(/"/g, '&quot;')
65
+ .replace(/'/g, '&#39;');
66
+ }
67
+
68
+ /**
69
+ * Returns the translated status text for a given tool status.
70
+ */
71
+ const getStatusText = (
72
+ status: ToolCallStatus,
73
+ trans: TranslationBundle
74
+ ): string => {
75
+ switch (status) {
76
+ case 'pending':
77
+ return trans.__('Running...');
78
+ case 'awaiting_approval':
79
+ return trans.__('Awaiting Approval');
80
+ case 'approved':
81
+ return trans.__('Approved - Executing...');
82
+ case 'rejected':
83
+ return trans.__('Rejected');
84
+ case 'completed':
85
+ return trans.__('Completed');
86
+ case 'error':
87
+ return trans.__('Error');
88
+ }
89
+ };
90
+
91
+ /**
92
+ * Builds HTML for a tool call display.
93
+ */
94
+ export function buildToolCallHtml(
95
+ options: IToolCallHtmlOptions
96
+ ): HTMLDetailsElement {
97
+ const {
98
+ toolName,
99
+ input,
100
+ status,
101
+ summary,
102
+ output,
103
+ targetId,
104
+ approvalId,
105
+ trans
106
+ } = options;
107
+ const config = STATUS_CONFIG[status];
108
+ const statusText = getStatusText(status, trans);
109
+ const escapedToolName = escapeHtml(toolName);
110
+ const escapedInput = escapeHtml(input);
111
+
112
+ const details = document.createElement('details');
113
+ details.classList.add('jp-ai-tool-call', config.cssClass);
114
+ if (config.open) {
115
+ details.setAttribute('open', 'true');
116
+ }
117
+
118
+ const summaryElement = document.createElement('summary');
119
+ summaryElement.classList.add('jp-ai-tool-header');
120
+
121
+ // Build summary header
122
+ const icon = document.createElement('div');
123
+ icon.classList.add('jp-ai-tool-icon');
124
+ icon.textContent = '⚡';
125
+
126
+ const title = document.createElement('div');
127
+ title.classList.add('jp-ai-tool-title');
128
+ title.textContent = escapedToolName;
129
+
130
+ if (summary) {
131
+ const summarySpan = document.createElement('span');
132
+ summarySpan.classList.add('jp-ai-tool-summary');
133
+ summarySpan.textContent = summary;
134
+ title.appendChild(summarySpan);
135
+ }
136
+
137
+ const statusDiv = document.createElement('div');
138
+ statusDiv.classList.add('jp-ai-tool-status', config.statusClass);
139
+ statusDiv.textContent = statusText;
140
+
141
+ summaryElement.appendChild(icon);
142
+ summaryElement.appendChild(title);
143
+ summaryElement.appendChild(statusDiv);
144
+
145
+ // Build body
146
+ const body = document.createElement('div');
147
+ body.classList.add('jp-ai-tool-body');
148
+
149
+ // Add input section
150
+ const inputSection = document.createElement('div');
151
+ inputSection.classList.add('jp-ai-tool-section');
152
+
153
+ const inputLabel = document.createElement('div');
154
+ inputLabel.classList.add('jp-ai-tool-label');
155
+ inputLabel.textContent = trans.__('Input');
156
+
157
+ const inputPre = document.createElement('pre');
158
+ inputPre.classList.add('jp-ai-tool-code');
159
+
160
+ const inputCode = document.createElement('code');
161
+ inputCode.textContent = escapedInput;
162
+
163
+ inputPre.appendChild(inputCode);
164
+ inputSection.appendChild(inputLabel);
165
+ inputSection.appendChild(inputPre);
166
+ body.appendChild(inputSection);
167
+
168
+ // Add approval buttons if awaiting approval
169
+ if (status === 'awaiting_approval' && approvalId && targetId) {
170
+ const approvalButtonsDiv = document.createElement('div');
171
+ approvalButtonsDiv.classList.add(
172
+ 'jp-ai-tool-approval-buttons',
173
+ `jp-ai-approval-id--${approvalId}`
174
+ );
175
+
176
+ const approveBtn = document.createElement('button');
177
+ approveBtn.classList.add('jp-ai-approval-btn', 'jp-ai-approval-approve');
178
+ approveBtn.textContent = trans.__('Approve');
179
+
180
+ const rejectBtn = document.createElement('button');
181
+ rejectBtn.classList.add('jp-ai-approval-btn', 'jp-ai-approval-reject');
182
+ rejectBtn.textContent = trans.__('Reject');
183
+
184
+ approvalButtonsDiv.appendChild(approveBtn);
185
+ approvalButtonsDiv.appendChild(rejectBtn);
186
+ body.appendChild(approvalButtonsDiv);
187
+
188
+ approveBtn.addEventListener('click', () =>
189
+ options.toolCallApproval?.(targetId, approvalId, true)
190
+ );
191
+ rejectBtn.addEventListener('click', () =>
192
+ options.toolCallApproval?.(targetId, approvalId, false)
193
+ );
194
+ }
195
+
196
+ // Add output/result section if provided
197
+ if (output !== undefined) {
198
+ const escapedOutput = escapeHtml(output);
199
+ const label = status === 'error' ? trans.__('Error') : trans.__('Result');
200
+
201
+ const outputSection = document.createElement('div');
202
+ outputSection.classList.add('jp-ai-tool-section');
203
+
204
+ const outputLabel = document.createElement('div');
205
+ outputLabel.classList.add('jp-ai-tool-label');
206
+ outputLabel.textContent = label;
207
+
208
+ const outputPre = document.createElement('pre');
209
+ outputPre.classList.add('jp-ai-tool-code');
210
+
211
+ const outputCode = document.createElement('code');
212
+ outputCode.textContent = escapedOutput;
213
+
214
+ outputPre.appendChild(outputCode);
215
+ outputSection.appendChild(outputLabel);
216
+ outputSection.appendChild(outputPre);
217
+ body.appendChild(outputSection);
218
+ }
219
+
220
+ details.appendChild(summaryElement);
221
+ details.appendChild(body);
222
+
223
+ return details;
224
+ }
package/style/base.css ADDED
@@ -0,0 +1,207 @@
1
+ /*
2
+ See the JupyterLab Developer Guide for useful CSS Patterns:
3
+
4
+ https://jupyterlab.readthedocs.io/en/stable/developer/css.html
5
+ */
6
+
7
+ /* Modern Tool Call Card Styling */
8
+ .jp-ai-tool-call {
9
+ margin: 8px 0;
10
+ border: 1px solid var(--jp-border-color1);
11
+ border-radius: 6px;
12
+ background: var(--jp-layout-color0);
13
+ box-shadow: var(--jp-elevation-z2);
14
+ transition: all 0.2s ease;
15
+ overflow: hidden;
16
+ }
17
+
18
+ .jp-ai-tool-call:hover {
19
+ border-color: var(--jp-border-color2);
20
+ box-shadow: var(--jp-elevation-z4);
21
+ }
22
+
23
+ /* Tool Header - clickable summary */
24
+ .jp-ai-tool-header {
25
+ display: flex;
26
+ align-items: center;
27
+ padding: 4px 8px;
28
+ background: var(--jp-layout-color1);
29
+ cursor: pointer;
30
+ user-select: none;
31
+ gap: 8px;
32
+ transition: background-color 0.2s ease;
33
+ }
34
+
35
+ .jp-ai-tool-header:hover {
36
+ background: var(--jp-layout-color2);
37
+ }
38
+
39
+ .jp-ai-tool-header::before {
40
+ content: '';
41
+ width: 0;
42
+ height: 0;
43
+ border-left: 5px solid var(--jp-ui-font-color2);
44
+ border-top: 3px solid transparent;
45
+ border-bottom: 3px solid transparent;
46
+ transition: transform 0.2s ease;
47
+ }
48
+
49
+ .jp-ai-tool-call[open] .jp-ai-tool-header::before {
50
+ transform: rotate(90deg);
51
+ }
52
+
53
+ .jp-ai-tool-icon {
54
+ font-size: 14px;
55
+ opacity: 0.8;
56
+ }
57
+
58
+ .jp-ai-tool-title {
59
+ font-family: var(--jp-ui-font-family);
60
+ font-size: var(--jp-ui-font-size1);
61
+ font-weight: 500;
62
+ color: var(--jp-ui-font-color1);
63
+ flex: 1;
64
+ }
65
+
66
+ .jp-ai-tool-summary {
67
+ font-weight: 400;
68
+ opacity: 0.7;
69
+ font-size: var(--jp-ui-font-size0);
70
+ }
71
+
72
+ .jp-ai-tool-summary::before {
73
+ content: ' ';
74
+ white-space: pre;
75
+ }
76
+
77
+ .jp-ai-tool-status {
78
+ font-size: var(--jp-ui-font-size0);
79
+ font-weight: 500;
80
+ padding: 2px 6px;
81
+ border-radius: 3px;
82
+ }
83
+
84
+ .jp-ai-tool-status-pending {
85
+ background: rgb(var(--jp-warn-color1-rgb) / 15%);
86
+ color: var(--jp-warn-color1);
87
+ }
88
+
89
+ .jp-ai-tool-status-completed {
90
+ background: rgb(var(--jp-success-color1-rgb) / 15%);
91
+ color: var(--jp-success-color1);
92
+ }
93
+
94
+ .jp-ai-tool-status-error {
95
+ background: rgb(var(--jp-error-color1-rgb) / 15%);
96
+ color: var(--jp-error-color1);
97
+ }
98
+
99
+ .jp-ai-tool-status-approval {
100
+ background: rgb(var(--jp-warn-color1-rgb) / 15%);
101
+ color: var(--jp-warn-color1);
102
+ }
103
+
104
+ /* Tool Body */
105
+ .jp-ai-tool-body {
106
+ padding: 8px 12px 12px;
107
+ }
108
+
109
+ .jp-ai-tool-section {
110
+ margin-bottom: 8px;
111
+ }
112
+
113
+ .jp-ai-tool-section:last-child {
114
+ margin-bottom: 0;
115
+ }
116
+
117
+ .jp-ai-tool-label {
118
+ font-family: var(--jp-ui-font-family);
119
+ font-size: var(--jp-ui-font-size0);
120
+ font-weight: 600;
121
+ color: var(--jp-ui-font-color2);
122
+ margin-bottom: 4px;
123
+ text-transform: uppercase;
124
+ letter-spacing: 0.5px;
125
+ }
126
+
127
+ .jp-ai-tool-code {
128
+ background: var(--jp-layout-color2);
129
+ border: 1px solid var(--jp-border-color1);
130
+ border-radius: 4px;
131
+ padding: 8px;
132
+ margin: 0;
133
+ font-family: var(--jp-code-font-family);
134
+ font-size: var(--jp-code-font-size);
135
+ line-height: 1.4;
136
+ overflow: auto auto;
137
+ max-height: 200px;
138
+ }
139
+
140
+ .jp-ai-tool-code code {
141
+ background: none;
142
+ padding: 0;
143
+ border: none;
144
+ font-family: inherit;
145
+ font-size: inherit;
146
+ }
147
+
148
+ /* State-specific styling */
149
+ .jp-ai-tool-pending {
150
+ border-left: 4px solid var(--jp-warn-color1);
151
+ }
152
+
153
+ .jp-ai-tool-completed {
154
+ border-left: 4px solid var(--jp-success-color1);
155
+ }
156
+
157
+ .jp-ai-tool-error {
158
+ border-left: 4px solid var(--jp-error-color1);
159
+ }
160
+
161
+ /* Tool Approval Button Styles */
162
+ .jp-ai-tool-approval-buttons {
163
+ display: flex;
164
+ gap: 8px;
165
+ margin-top: 12px;
166
+ justify-content: flex-end;
167
+ }
168
+
169
+ .jp-ai-approval-btn {
170
+ padding: 6px 12px;
171
+ border: none;
172
+ border-radius: 4px;
173
+ font-family: var(--jp-ui-font-family);
174
+ font-size: var(--jp-ui-font-size0);
175
+ font-weight: 500;
176
+ cursor: pointer;
177
+ transition: all 0.2s ease;
178
+ min-width: 70px;
179
+ display: inline-block;
180
+ }
181
+
182
+ .jp-ai-approval-approve {
183
+ background: var(--jp-success-color1);
184
+ color: var(--jp-ui-inverse-font-color1);
185
+ }
186
+
187
+ .jp-ai-approval-approve:hover:not(:disabled) {
188
+ background: var(--jp-success-color0);
189
+ transform: translateY(-1px);
190
+ box-shadow: var(--jp-elevation-z4);
191
+ }
192
+
193
+ .jp-ai-approval-reject {
194
+ background: var(--jp-error-color1);
195
+ color: var(--jp-ui-inverse-font-color1);
196
+ }
197
+
198
+ .jp-ai-approval-reject:hover:not(:disabled) {
199
+ background: var(--jp-error-color0);
200
+ transform: translateY(-1px);
201
+ box-shadow: var(--jp-elevation-z4);
202
+ }
203
+
204
+ .jp-ai-approval-btn:disabled {
205
+ cursor: not-allowed;
206
+ opacity: 0.5;
207
+ }
@@ -0,0 +1 @@
1
+ @import url('base.css');
package/style/index.js ADDED
@@ -0,0 +1 @@
1
+ import './base.css';