@jupyter-ai/acp-client 0.0.3 → 0.0.5
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 +3 -4
- package/lib/diff-view.d.ts +8 -0
- package/lib/diff-view.js +58 -0
- package/lib/request.d.ts +4 -0
- package/lib/request.js +14 -0
- package/lib/tool-calls.js +89 -14
- package/package.json +5 -2
- package/src/diff-view.tsx +125 -0
- package/src/jupyter-chat-augment.d.ts +32 -0
- package/src/request.ts +18 -0
- package/src/tool-calls.tsx +160 -14
- package/style/base.css +145 -4
package/README.md
CHANGED
|
@@ -51,12 +51,11 @@ class ClaudeAcpPersona(BaseAcpPersona):
|
|
|
51
51
|
)
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
Currently, this package provides
|
|
54
|
+
Currently, this package provides 2 personas:
|
|
55
55
|
|
|
56
|
-
1. `@
|
|
57
|
-
2. `@Claude-ACP`
|
|
56
|
+
1. `@Claude-ACP`
|
|
58
57
|
- requires `claude-code-acp`, installed via `npm install -g @zed-industries/claude-code-acp`
|
|
59
|
-
|
|
58
|
+
2. `@Kiro`
|
|
60
59
|
- requires `kiro-cli`, installed from https://kiro.dev
|
|
61
60
|
|
|
62
61
|
## Dependencies
|
package/lib/diff-view.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { structuredPatch } from 'diff';
|
|
3
|
+
/** Maximum number of diff lines shown before truncation. */
|
|
4
|
+
const MAX_DIFF_LINES = 20;
|
|
5
|
+
/**
|
|
6
|
+
* Renders a single file diff block with filename header, line-level
|
|
7
|
+
* highlighting, and click-to-expand truncation.
|
|
8
|
+
*/
|
|
9
|
+
function DiffBlock({ diff, onOpenFile }) {
|
|
10
|
+
var _a, _b;
|
|
11
|
+
const patch = structuredPatch(diff.path, diff.path, (_a = diff.old_text) !== null && _a !== void 0 ? _a : '', diff.new_text, undefined, undefined, { context: Infinity });
|
|
12
|
+
const filename = (_b = diff.path.split('/').pop()) !== null && _b !== void 0 ? _b : diff.path;
|
|
13
|
+
const [expanded, setExpanded] = React.useState(false);
|
|
14
|
+
// Flatten hunks into renderable lines
|
|
15
|
+
const allLines = [];
|
|
16
|
+
for (const hunk of patch.hunks) {
|
|
17
|
+
hunk.lines
|
|
18
|
+
.filter(line => !line.startsWith('\\'))
|
|
19
|
+
.forEach((line, j) => {
|
|
20
|
+
const prefix = line[0];
|
|
21
|
+
const text = line.slice(1);
|
|
22
|
+
const isAdded = prefix === '+';
|
|
23
|
+
const isRemoved = prefix === '-';
|
|
24
|
+
allLines.push({
|
|
25
|
+
cls: isAdded
|
|
26
|
+
? 'jp-jupyter-ai-acp-client-diff-added'
|
|
27
|
+
: isRemoved
|
|
28
|
+
? 'jp-jupyter-ai-acp-client-diff-removed'
|
|
29
|
+
: 'jp-jupyter-ai-acp-client-diff-context',
|
|
30
|
+
prefix,
|
|
31
|
+
text,
|
|
32
|
+
key: `${hunk.oldStart}-${j}`
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
const canTruncate = allLines.length > MAX_DIFF_LINES;
|
|
37
|
+
const visible = canTruncate && !expanded ? allLines.slice(0, MAX_DIFF_LINES) : allLines;
|
|
38
|
+
const hiddenCount = allLines.length - MAX_DIFF_LINES;
|
|
39
|
+
return (React.createElement("div", { className: "jp-jupyter-ai-acp-client-diff-block" },
|
|
40
|
+
React.createElement("div", { className: "jp-jupyter-ai-acp-client-diff-header", onClick: onOpenFile ? () => onOpenFile(diff.path) : undefined, title: diff.path }, filename),
|
|
41
|
+
React.createElement("div", { className: "jp-jupyter-ai-acp-client-diff-content" },
|
|
42
|
+
visible.map((line) => (React.createElement("div", { key: line.key, className: `jp-jupyter-ai-acp-client-diff-line ${line.cls}` },
|
|
43
|
+
React.createElement("span", { className: "jp-jupyter-ai-acp-client-diff-line-text" },
|
|
44
|
+
line.prefix,
|
|
45
|
+
" ",
|
|
46
|
+
line.text)))),
|
|
47
|
+
canTruncate && !expanded && (React.createElement("div", { className: "jp-jupyter-ai-acp-client-diff-toggle", onClick: () => setExpanded(true) },
|
|
48
|
+
"... ",
|
|
49
|
+
hiddenCount,
|
|
50
|
+
" more lines")),
|
|
51
|
+
canTruncate && expanded && (React.createElement("div", { className: "jp-jupyter-ai-acp-client-diff-toggle", onClick: () => setExpanded(false) }, "show less")))));
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Renders one or more file diffs.
|
|
55
|
+
*/
|
|
56
|
+
export function DiffView({ diffs, onOpenFile }) {
|
|
57
|
+
return (React.createElement("div", { className: "jp-jupyter-ai-acp-client-diff-container" }, diffs.map((d, i) => (React.createElement(DiffBlock, { key: i, diff: d, onOpenFile: onOpenFile })))));
|
|
58
|
+
}
|
package/lib/request.d.ts
CHANGED
|
@@ -11,4 +11,8 @@ type AcpSlashCommand = {
|
|
|
11
11
|
description: string;
|
|
12
12
|
};
|
|
13
13
|
export declare function getAcpSlashCommands(chatPath: string, personaMentionName?: string | null): Promise<AcpSlashCommand[]>;
|
|
14
|
+
/**
|
|
15
|
+
* Send the user's permission decision to the backend.
|
|
16
|
+
*/
|
|
17
|
+
export declare function submitPermissionDecision(sessionId: string, toolCallId: string, optionId: string): Promise<void>;
|
|
14
18
|
export {};
|
package/lib/request.js
CHANGED
|
@@ -49,3 +49,17 @@ export async function getAcpSlashCommands(chatPath, personaMentionName = null) {
|
|
|
49
49
|
}
|
|
50
50
|
return response.commands;
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Send the user's permission decision to the backend.
|
|
54
|
+
*/
|
|
55
|
+
export async function submitPermissionDecision(sessionId, toolCallId, optionId) {
|
|
56
|
+
await requestAPI('/permissions', {
|
|
57
|
+
method: 'POST',
|
|
58
|
+
headers: { 'Content-Type': 'application/json' },
|
|
59
|
+
body: JSON.stringify({
|
|
60
|
+
session_id: sessionId,
|
|
61
|
+
tool_call_id: toolCallId,
|
|
62
|
+
option_id: optionId
|
|
63
|
+
})
|
|
64
|
+
});
|
|
65
|
+
}
|
package/lib/tool-calls.js
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { submitPermissionDecision } from './request';
|
|
3
|
+
import clsx from 'clsx';
|
|
4
|
+
import { DiffView } from './diff-view';
|
|
2
5
|
/**
|
|
3
6
|
* Preamble component that renders tool call status lines above message body.
|
|
4
7
|
* Returns null if the message has no tool calls.
|
|
5
8
|
*/
|
|
6
9
|
export function ToolCallsComponent(props) {
|
|
7
10
|
var _a, _b, _c, _d;
|
|
8
|
-
const { message } = props;
|
|
11
|
+
const { message, model } = props;
|
|
9
12
|
if (!((_b = (_a = message.metadata) === null || _a === void 0 ? void 0 : _a.tool_calls) === null || _b === void 0 ? void 0 : _b.length)) {
|
|
10
13
|
return null;
|
|
11
14
|
}
|
|
12
|
-
|
|
15
|
+
const onOpenFile = (path) => {
|
|
16
|
+
var _a;
|
|
17
|
+
(_a = model.documentManager) === null || _a === void 0 ? void 0 : _a.openOrReveal(path);
|
|
18
|
+
};
|
|
19
|
+
return (React.createElement("div", { className: "jp-jupyter-ai-acp-client-tool-calls" }, ((_d = (_c = message.metadata) === null || _c === void 0 ? void 0 : _c.tool_calls) !== null && _d !== void 0 ? _d : []).map((tc) => (React.createElement(ToolCallLine, { key: tc.tool_call_id, toolCall: tc, onOpenFile: onOpenFile })))));
|
|
13
20
|
}
|
|
14
21
|
/**
|
|
15
22
|
* Format raw_output for display. Handles string, object, and array values.
|
|
@@ -56,15 +63,21 @@ function buildDetailsLines(toolCall) {
|
|
|
56
63
|
/**
|
|
57
64
|
* Renders a single tool call line with status icon and optional expandable output.
|
|
58
65
|
*/
|
|
59
|
-
function ToolCallLine({ toolCall }) {
|
|
66
|
+
function ToolCallLine({ toolCall, onOpenFile }) {
|
|
67
|
+
var _a, _b, _c;
|
|
60
68
|
const { title, status, kind } = toolCall;
|
|
61
69
|
const displayTitle = title ||
|
|
62
70
|
(kind
|
|
63
71
|
? `${kind.charAt(0).toUpperCase()}${kind.slice(1)}...`
|
|
64
72
|
: 'Working...');
|
|
65
|
-
const
|
|
73
|
+
const selectedOpt = (_a = toolCall.permission_options) === null || _a === void 0 ? void 0 : _a.find(opt => opt.option_id === toolCall.selected_option_id);
|
|
74
|
+
const isRejected = toolCall.permission_status === 'resolved' &&
|
|
75
|
+
!!((_b = selectedOpt === null || selectedOpt === void 0 ? void 0 : selectedOpt.kind) === null || _b === void 0 ? void 0 : _b.includes('reject'));
|
|
76
|
+
const hasPendingPermission = toolCall.permission_status === 'pending';
|
|
77
|
+
const isInProgress = !isRejected &&
|
|
78
|
+
(status === 'in_progress' || status === 'pending' || hasPendingPermission);
|
|
66
79
|
const isCompleted = status === 'completed';
|
|
67
|
-
const isFailed = status === 'failed';
|
|
80
|
+
const isFailed = status === 'failed' || isRejected;
|
|
68
81
|
// Unicode text glyphs — consistent across OS/browser
|
|
69
82
|
const icon = isInProgress
|
|
70
83
|
? '\u2022'
|
|
@@ -73,28 +86,90 @@ function ToolCallLine({ toolCall }) {
|
|
|
73
86
|
: isFailed
|
|
74
87
|
? '\u2717'
|
|
75
88
|
: '\u2022';
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
|
|
89
|
+
// Force 'failed' class when rejected
|
|
90
|
+
const effectiveStatus = isRejected ? 'failed' : status || 'in_progress';
|
|
91
|
+
const cssClass = clsx('jp-jupyter-ai-acp-client-tool-call', `jp-jupyter-ai-acp-client-tool-call-${effectiveStatus}`);
|
|
92
|
+
const hasDiffs = !!((_c = toolCall.diffs) === null || _c === void 0 ? void 0 : _c.length);
|
|
93
|
+
// Pending permission with diffs: expanded diff + permission buttons outside
|
|
94
|
+
if (hasDiffs && hasPendingPermission) {
|
|
95
|
+
return (React.createElement("div", { className: cssClass },
|
|
96
|
+
React.createElement("details", { open: true },
|
|
97
|
+
React.createElement("summary", null,
|
|
98
|
+
React.createElement("span", { className: "jp-jupyter-ai-acp-client-tool-call-icon" }, icon),
|
|
99
|
+
' ',
|
|
100
|
+
React.createElement("em", null, displayTitle)),
|
|
101
|
+
React.createElement(DiffView, { diffs: toolCall.diffs, onOpenFile: onOpenFile })),
|
|
102
|
+
React.createElement(PermissionButtons, { toolCall: toolCall })));
|
|
103
|
+
}
|
|
104
|
+
// Completed/failed with expandable content (diffs or metadata)
|
|
105
|
+
const detailsLines = !hasDiffs && (isCompleted || isFailed) ? buildDetailsLines(toolCall) : [];
|
|
106
|
+
const hasExpandableContent = hasDiffs || detailsLines.length > 0;
|
|
107
|
+
if ((isCompleted || isFailed) && hasExpandableContent) {
|
|
81
108
|
return (React.createElement("details", { className: cssClass },
|
|
82
109
|
React.createElement("summary", null,
|
|
83
110
|
React.createElement("span", { className: "jp-jupyter-ai-acp-client-tool-call-icon" }, icon),
|
|
84
111
|
' ',
|
|
85
|
-
displayTitle
|
|
86
|
-
|
|
112
|
+
displayTitle,
|
|
113
|
+
React.createElement(PermissionLabel, { toolCall: toolCall })),
|
|
114
|
+
hasDiffs ? (React.createElement(DiffView, { diffs: toolCall.diffs, onOpenFile: onOpenFile })) : (React.createElement("div", { className: "jp-jupyter-ai-acp-client-tool-call-detail" }, detailsLines.join('\n')))));
|
|
87
115
|
}
|
|
88
116
|
// In-progress — italic
|
|
89
117
|
if (isInProgress) {
|
|
90
118
|
return (React.createElement("div", { className: cssClass },
|
|
91
119
|
React.createElement("span", { className: "jp-jupyter-ai-acp-client-tool-call-icon" }, icon),
|
|
92
120
|
' ',
|
|
93
|
-
React.createElement("em", null, displayTitle)
|
|
121
|
+
React.createElement("em", null, displayTitle),
|
|
122
|
+
React.createElement(PermissionButtons, { toolCall: toolCall })));
|
|
94
123
|
}
|
|
95
124
|
// Completed/failed without metadata
|
|
96
125
|
return (React.createElement("div", { className: cssClass },
|
|
97
126
|
React.createElement("span", { className: "jp-jupyter-ai-acp-client-tool-call-icon" }, icon),
|
|
98
127
|
' ',
|
|
99
|
-
displayTitle
|
|
128
|
+
displayTitle,
|
|
129
|
+
React.createElement(PermissionLabel, { toolCall: toolCall })));
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Shows the user's permission selection.
|
|
133
|
+
*/
|
|
134
|
+
function PermissionLabel({ toolCall }) {
|
|
135
|
+
var _a, _b;
|
|
136
|
+
if (toolCall.permission_status !== 'resolved' ||
|
|
137
|
+
!toolCall.selected_option_id) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
const selectedName = (_b = (_a = toolCall.permission_options) === null || _a === void 0 ? void 0 : _a.find(opt => opt.option_id === toolCall.selected_option_id)) === null || _b === void 0 ? void 0 : _b.name;
|
|
141
|
+
if (!selectedName) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
return (React.createElement("span", { className: "jp-jupyter-ai-acp-client-permission-label" },
|
|
145
|
+
' ',
|
|
146
|
+
"\u2014 ",
|
|
147
|
+
selectedName));
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Renders the permission buttons.
|
|
151
|
+
*/
|
|
152
|
+
function PermissionButtons({ toolCall }) {
|
|
153
|
+
var _a;
|
|
154
|
+
const [submitting, setSubmitting] = React.useState(false);
|
|
155
|
+
if (!((_a = toolCall.permission_options) === null || _a === void 0 ? void 0 : _a.length) ||
|
|
156
|
+
toolCall.permission_status !== 'pending' ||
|
|
157
|
+
!toolCall.session_id) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
const handleClick = async (optionId) => {
|
|
161
|
+
setSubmitting(true);
|
|
162
|
+
try {
|
|
163
|
+
await submitPermissionDecision(toolCall.session_id, toolCall.tool_call_id, optionId);
|
|
164
|
+
}
|
|
165
|
+
catch (err) {
|
|
166
|
+
console.error('Failed to submit permission decision:', err);
|
|
167
|
+
setSubmitting(false);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
return (React.createElement("div", { className: "jp-jupyter-ai-acp-client-permission-buttons" },
|
|
171
|
+
React.createElement("span", { className: "jp-jupyter-ai-acp-client-permission-tree" }, "\u2514\u2500"),
|
|
172
|
+
React.createElement("span", null, "Allow?"),
|
|
173
|
+
toolCall.permission_options.map((opt) => (React.createElement("button", { key: opt.option_id, className: clsx('jp-jupyter-ai-acp-client-permission-btn', opt.kind &&
|
|
174
|
+
`jp-jupyter-ai-acp-client-permission-btn-${opt.kind.replace(/_/g, '-')}`), onClick: () => handleClick(opt.option_id), disabled: submitting, title: opt.kind }, opt.name)))));
|
|
100
175
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jupyter-ai/acp-client",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.5",
|
|
4
4
|
"description": "The ACP client for Jupyter AI, allowing for ACP agents to be used in JupyterLab",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -59,11 +59,14 @@
|
|
|
59
59
|
"@jupyter/chat": "0.20.0-alpha.2",
|
|
60
60
|
"@jupyterlab/application": "^4.0.0",
|
|
61
61
|
"@jupyterlab/coreutils": "^6.0.0",
|
|
62
|
-
"@jupyterlab/services": "^7.0.0"
|
|
62
|
+
"@jupyterlab/services": "^7.0.0",
|
|
63
|
+
"clsx": "^2.1.1",
|
|
64
|
+
"diff": "^8.0.0"
|
|
63
65
|
},
|
|
64
66
|
"devDependencies": {
|
|
65
67
|
"@jupyterlab/builder": "^4.0.0",
|
|
66
68
|
"@jupyterlab/testutils": "^4.0.0",
|
|
69
|
+
"@types/diff": "^7.0.0",
|
|
67
70
|
"@types/jest": "^29.2.0",
|
|
68
71
|
"@types/json-schema": "^7.0.11",
|
|
69
72
|
"@types/react": "^18.0.26",
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { IToolCallDiff } from '@jupyter/chat';
|
|
3
|
+
import { structuredPatch } from 'diff';
|
|
4
|
+
|
|
5
|
+
/** Maximum number of diff lines shown before truncation. */
|
|
6
|
+
const MAX_DIFF_LINES = 20;
|
|
7
|
+
|
|
8
|
+
/** A single flattened diff line with its styling metadata. */
|
|
9
|
+
interface IDiffLineInfo {
|
|
10
|
+
cls: string;
|
|
11
|
+
prefix: string;
|
|
12
|
+
text: string;
|
|
13
|
+
key: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Renders a single file diff block with filename header, line-level
|
|
18
|
+
* highlighting, and click-to-expand truncation.
|
|
19
|
+
*/
|
|
20
|
+
function DiffBlock({
|
|
21
|
+
diff,
|
|
22
|
+
onOpenFile
|
|
23
|
+
}: {
|
|
24
|
+
diff: IToolCallDiff;
|
|
25
|
+
onOpenFile?: (path: string) => void;
|
|
26
|
+
}): JSX.Element {
|
|
27
|
+
const patch = structuredPatch(
|
|
28
|
+
diff.path,
|
|
29
|
+
diff.path,
|
|
30
|
+
diff.old_text ?? '',
|
|
31
|
+
diff.new_text,
|
|
32
|
+
undefined,
|
|
33
|
+
undefined,
|
|
34
|
+
{ context: Infinity }
|
|
35
|
+
);
|
|
36
|
+
const filename = diff.path.split('/').pop() ?? diff.path;
|
|
37
|
+
const [expanded, setExpanded] = React.useState(false);
|
|
38
|
+
|
|
39
|
+
// Flatten hunks into renderable lines
|
|
40
|
+
const allLines: IDiffLineInfo[] = [];
|
|
41
|
+
for (const hunk of patch.hunks) {
|
|
42
|
+
hunk.lines
|
|
43
|
+
.filter(line => !line.startsWith('\\'))
|
|
44
|
+
.forEach((line, j) => {
|
|
45
|
+
const prefix = line[0];
|
|
46
|
+
const text = line.slice(1);
|
|
47
|
+
const isAdded = prefix === '+';
|
|
48
|
+
const isRemoved = prefix === '-';
|
|
49
|
+
allLines.push({
|
|
50
|
+
cls: isAdded
|
|
51
|
+
? 'jp-jupyter-ai-acp-client-diff-added'
|
|
52
|
+
: isRemoved
|
|
53
|
+
? 'jp-jupyter-ai-acp-client-diff-removed'
|
|
54
|
+
: 'jp-jupyter-ai-acp-client-diff-context',
|
|
55
|
+
prefix,
|
|
56
|
+
text,
|
|
57
|
+
key: `${hunk.oldStart}-${j}`
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const canTruncate = allLines.length > MAX_DIFF_LINES;
|
|
63
|
+
const visible =
|
|
64
|
+
canTruncate && !expanded ? allLines.slice(0, MAX_DIFF_LINES) : allLines;
|
|
65
|
+
const hiddenCount = allLines.length - MAX_DIFF_LINES;
|
|
66
|
+
|
|
67
|
+
return (
|
|
68
|
+
<div className="jp-jupyter-ai-acp-client-diff-block">
|
|
69
|
+
<div
|
|
70
|
+
className="jp-jupyter-ai-acp-client-diff-header"
|
|
71
|
+
onClick={onOpenFile ? () => onOpenFile(diff.path) : undefined}
|
|
72
|
+
title={diff.path}
|
|
73
|
+
>
|
|
74
|
+
{filename}
|
|
75
|
+
</div>
|
|
76
|
+
<div className="jp-jupyter-ai-acp-client-diff-content">
|
|
77
|
+
{visible.map((line: IDiffLineInfo) => (
|
|
78
|
+
<div
|
|
79
|
+
key={line.key}
|
|
80
|
+
className={`jp-jupyter-ai-acp-client-diff-line ${line.cls}`}
|
|
81
|
+
>
|
|
82
|
+
<span className="jp-jupyter-ai-acp-client-diff-line-text">
|
|
83
|
+
{line.prefix} {line.text}
|
|
84
|
+
</span>
|
|
85
|
+
</div>
|
|
86
|
+
))}
|
|
87
|
+
{canTruncate && !expanded && (
|
|
88
|
+
<div
|
|
89
|
+
className="jp-jupyter-ai-acp-client-diff-toggle"
|
|
90
|
+
onClick={() => setExpanded(true)}
|
|
91
|
+
>
|
|
92
|
+
... {hiddenCount} more lines
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
95
|
+
{canTruncate && expanded && (
|
|
96
|
+
<div
|
|
97
|
+
className="jp-jupyter-ai-acp-client-diff-toggle"
|
|
98
|
+
onClick={() => setExpanded(false)}
|
|
99
|
+
>
|
|
100
|
+
show less
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Renders one or more file diffs.
|
|
110
|
+
*/
|
|
111
|
+
export function DiffView({
|
|
112
|
+
diffs,
|
|
113
|
+
onOpenFile
|
|
114
|
+
}: {
|
|
115
|
+
diffs: IToolCallDiff[];
|
|
116
|
+
onOpenFile?: (path: string) => void;
|
|
117
|
+
}): JSX.Element {
|
|
118
|
+
return (
|
|
119
|
+
<div className="jp-jupyter-ai-acp-client-diff-container">
|
|
120
|
+
{diffs.map((d, i) => (
|
|
121
|
+
<DiffBlock key={i} diff={d} onOpenFile={onOpenFile} />
|
|
122
|
+
))}
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
export {};
|
|
4
4
|
|
|
5
5
|
declare module '@jupyter/chat' {
|
|
6
|
+
export interface IToolCallDiff {
|
|
7
|
+
path: string;
|
|
8
|
+
new_text: string;
|
|
9
|
+
old_text?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
6
12
|
export interface IToolCall {
|
|
7
13
|
/**
|
|
8
14
|
* Unique identifier for this tool call, used to correlate events
|
|
@@ -39,6 +45,32 @@ declare module '@jupyter/chat' {
|
|
|
39
45
|
* File paths or resource URIs involved in this tool call.
|
|
40
46
|
*/
|
|
41
47
|
locations?: string[];
|
|
48
|
+
/**
|
|
49
|
+
* Permission options from the ACP agent.
|
|
50
|
+
*/
|
|
51
|
+
permission_options?: IPermissionOption[];
|
|
52
|
+
/**
|
|
53
|
+
* Whether the permission request is waiting for user.
|
|
54
|
+
*/
|
|
55
|
+
permission_status?: 'pending' | 'resolved';
|
|
56
|
+
/**
|
|
57
|
+
* The option_id the user selected.
|
|
58
|
+
*/
|
|
59
|
+
selected_option_id?: string;
|
|
60
|
+
/**
|
|
61
|
+
* The ACP session ID this tool call belongs to.
|
|
62
|
+
*/
|
|
63
|
+
session_id?: string;
|
|
64
|
+
/**
|
|
65
|
+
* File diffs from ACP FileEditToolCallContent.
|
|
66
|
+
*/
|
|
67
|
+
diffs?: IToolCallDiff[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface IPermissionOption {
|
|
71
|
+
option_id: string;
|
|
72
|
+
name: string;
|
|
73
|
+
kind?: string;
|
|
42
74
|
}
|
|
43
75
|
|
|
44
76
|
export interface IMessageMetadata {
|
package/src/request.ts
CHANGED
|
@@ -74,3 +74,21 @@ export async function getAcpSlashCommands(
|
|
|
74
74
|
|
|
75
75
|
return response.commands;
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Send the user's permission decision to the backend.
|
|
79
|
+
*/
|
|
80
|
+
export async function submitPermissionDecision(
|
|
81
|
+
sessionId: string,
|
|
82
|
+
toolCallId: string,
|
|
83
|
+
optionId: string
|
|
84
|
+
): Promise<void> {
|
|
85
|
+
await requestAPI('/permissions', {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: { 'Content-Type': 'application/json' },
|
|
88
|
+
body: JSON.stringify({
|
|
89
|
+
session_id: sessionId,
|
|
90
|
+
tool_call_id: toolCallId,
|
|
91
|
+
option_id: optionId
|
|
92
|
+
})
|
|
93
|
+
});
|
|
94
|
+
}
|
package/src/tool-calls.tsx
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
IToolCall,
|
|
4
|
+
IPermissionOption,
|
|
5
|
+
MessagePreambleProps
|
|
6
|
+
} from '@jupyter/chat';
|
|
7
|
+
import { submitPermissionDecision } from './request';
|
|
8
|
+
import clsx from 'clsx';
|
|
9
|
+
import { DiffView } from './diff-view';
|
|
3
10
|
|
|
4
11
|
/**
|
|
5
12
|
* Preamble component that renders tool call status lines above message body.
|
|
@@ -8,15 +15,23 @@ import { IToolCall, MessagePreambleProps } from '@jupyter/chat';
|
|
|
8
15
|
export function ToolCallsComponent(
|
|
9
16
|
props: MessagePreambleProps
|
|
10
17
|
): JSX.Element | null {
|
|
11
|
-
const { message } = props;
|
|
18
|
+
const { message, model } = props;
|
|
12
19
|
if (!message.metadata?.tool_calls?.length) {
|
|
13
20
|
return null;
|
|
14
21
|
}
|
|
15
22
|
|
|
23
|
+
const onOpenFile = (path: string) => {
|
|
24
|
+
model.documentManager?.openOrReveal(path);
|
|
25
|
+
};
|
|
26
|
+
|
|
16
27
|
return (
|
|
17
28
|
<div className="jp-jupyter-ai-acp-client-tool-calls">
|
|
18
29
|
{(message.metadata?.tool_calls ?? []).map((tc: IToolCall) => (
|
|
19
|
-
<ToolCallLine
|
|
30
|
+
<ToolCallLine
|
|
31
|
+
key={tc.tool_call_id}
|
|
32
|
+
toolCall={tc}
|
|
33
|
+
onOpenFile={onOpenFile}
|
|
34
|
+
/>
|
|
20
35
|
))}
|
|
21
36
|
</div>
|
|
22
37
|
);
|
|
@@ -70,16 +85,31 @@ function buildDetailsLines(toolCall: IToolCall): string[] {
|
|
|
70
85
|
/**
|
|
71
86
|
* Renders a single tool call line with status icon and optional expandable output.
|
|
72
87
|
*/
|
|
73
|
-
function ToolCallLine({
|
|
88
|
+
function ToolCallLine({
|
|
89
|
+
toolCall,
|
|
90
|
+
onOpenFile
|
|
91
|
+
}: {
|
|
92
|
+
toolCall: IToolCall;
|
|
93
|
+
onOpenFile?: (path: string) => void;
|
|
94
|
+
}): JSX.Element {
|
|
74
95
|
const { title, status, kind } = toolCall;
|
|
75
96
|
const displayTitle =
|
|
76
97
|
title ||
|
|
77
98
|
(kind
|
|
78
99
|
? `${kind.charAt(0).toUpperCase()}${kind.slice(1)}...`
|
|
79
100
|
: 'Working...');
|
|
80
|
-
const
|
|
101
|
+
const selectedOpt = toolCall.permission_options?.find(
|
|
102
|
+
opt => opt.option_id === toolCall.selected_option_id
|
|
103
|
+
);
|
|
104
|
+
const isRejected =
|
|
105
|
+
toolCall.permission_status === 'resolved' &&
|
|
106
|
+
!!selectedOpt?.kind?.includes('reject');
|
|
107
|
+
const hasPendingPermission = toolCall.permission_status === 'pending';
|
|
108
|
+
const isInProgress =
|
|
109
|
+
!isRejected &&
|
|
110
|
+
(status === 'in_progress' || status === 'pending' || hasPendingPermission);
|
|
81
111
|
const isCompleted = status === 'completed';
|
|
82
|
-
const isFailed = status === 'failed';
|
|
112
|
+
const isFailed = status === 'failed' || isRejected;
|
|
83
113
|
|
|
84
114
|
// Unicode text glyphs — consistent across OS/browser
|
|
85
115
|
const icon = isInProgress
|
|
@@ -89,14 +119,40 @@ function ToolCallLine({ toolCall }: { toolCall: IToolCall }): JSX.Element {
|
|
|
89
119
|
: isFailed
|
|
90
120
|
? '\u2717'
|
|
91
121
|
: '\u2022';
|
|
92
|
-
|
|
122
|
+
// Force 'failed' class when rejected
|
|
123
|
+
const effectiveStatus = isRejected ? 'failed' : status || 'in_progress';
|
|
124
|
+
|
|
125
|
+
const cssClass = clsx(
|
|
126
|
+
'jp-jupyter-ai-acp-client-tool-call',
|
|
127
|
+
`jp-jupyter-ai-acp-client-tool-call-${effectiveStatus}`
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const hasDiffs = !!toolCall.diffs?.length;
|
|
131
|
+
|
|
132
|
+
// Pending permission with diffs: expanded diff + permission buttons outside
|
|
133
|
+
if (hasDiffs && hasPendingPermission) {
|
|
134
|
+
return (
|
|
135
|
+
<div className={cssClass}>
|
|
136
|
+
<details open>
|
|
137
|
+
<summary>
|
|
138
|
+
<span className="jp-jupyter-ai-acp-client-tool-call-icon">
|
|
139
|
+
{icon}
|
|
140
|
+
</span>{' '}
|
|
141
|
+
<em>{displayTitle}</em>
|
|
142
|
+
</summary>
|
|
143
|
+
<DiffView diffs={toolCall.diffs!} onOpenFile={onOpenFile} />
|
|
144
|
+
</details>
|
|
145
|
+
<PermissionButtons toolCall={toolCall} />
|
|
146
|
+
</div>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
93
149
|
|
|
94
|
-
//
|
|
150
|
+
// Completed/failed with expandable content (diffs or metadata)
|
|
95
151
|
const detailsLines =
|
|
96
|
-
isCompleted || isFailed ? buildDetailsLines(toolCall) : [];
|
|
97
|
-
const
|
|
152
|
+
!hasDiffs && (isCompleted || isFailed) ? buildDetailsLines(toolCall) : [];
|
|
153
|
+
const hasExpandableContent = hasDiffs || detailsLines.length > 0;
|
|
98
154
|
|
|
99
|
-
if (
|
|
155
|
+
if ((isCompleted || isFailed) && hasExpandableContent) {
|
|
100
156
|
return (
|
|
101
157
|
<details className={cssClass}>
|
|
102
158
|
<summary>
|
|
@@ -104,10 +160,15 @@ function ToolCallLine({ toolCall }: { toolCall: IToolCall }): JSX.Element {
|
|
|
104
160
|
{icon}
|
|
105
161
|
</span>{' '}
|
|
106
162
|
{displayTitle}
|
|
163
|
+
<PermissionLabel toolCall={toolCall} />
|
|
107
164
|
</summary>
|
|
108
|
-
|
|
109
|
-
{
|
|
110
|
-
|
|
165
|
+
{hasDiffs ? (
|
|
166
|
+
<DiffView diffs={toolCall.diffs!} onOpenFile={onOpenFile} />
|
|
167
|
+
) : (
|
|
168
|
+
<div className="jp-jupyter-ai-acp-client-tool-call-detail">
|
|
169
|
+
{detailsLines.join('\n')}
|
|
170
|
+
</div>
|
|
171
|
+
)}
|
|
111
172
|
</details>
|
|
112
173
|
);
|
|
113
174
|
}
|
|
@@ -118,6 +179,7 @@ function ToolCallLine({ toolCall }: { toolCall: IToolCall }): JSX.Element {
|
|
|
118
179
|
<div className={cssClass}>
|
|
119
180
|
<span className="jp-jupyter-ai-acp-client-tool-call-icon">{icon}</span>{' '}
|
|
120
181
|
<em>{displayTitle}</em>
|
|
182
|
+
<PermissionButtons toolCall={toolCall} />
|
|
121
183
|
</div>
|
|
122
184
|
);
|
|
123
185
|
}
|
|
@@ -127,6 +189,90 @@ function ToolCallLine({ toolCall }: { toolCall: IToolCall }): JSX.Element {
|
|
|
127
189
|
<div className={cssClass}>
|
|
128
190
|
<span className="jp-jupyter-ai-acp-client-tool-call-icon">{icon}</span>{' '}
|
|
129
191
|
{displayTitle}
|
|
192
|
+
<PermissionLabel toolCall={toolCall} />
|
|
193
|
+
</div>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Shows the user's permission selection.
|
|
199
|
+
*/
|
|
200
|
+
function PermissionLabel({
|
|
201
|
+
toolCall
|
|
202
|
+
}: {
|
|
203
|
+
toolCall: IToolCall;
|
|
204
|
+
}): JSX.Element | null {
|
|
205
|
+
if (
|
|
206
|
+
toolCall.permission_status !== 'resolved' ||
|
|
207
|
+
!toolCall.selected_option_id
|
|
208
|
+
) {
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
const selectedName = toolCall.permission_options?.find(
|
|
212
|
+
opt => opt.option_id === toolCall.selected_option_id
|
|
213
|
+
)?.name;
|
|
214
|
+
if (!selectedName) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
return (
|
|
218
|
+
<span className="jp-jupyter-ai-acp-client-permission-label">
|
|
219
|
+
{' '}
|
|
220
|
+
— {selectedName}
|
|
221
|
+
</span>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Renders the permission buttons.
|
|
227
|
+
*/
|
|
228
|
+
function PermissionButtons({
|
|
229
|
+
toolCall
|
|
230
|
+
}: {
|
|
231
|
+
toolCall: IToolCall;
|
|
232
|
+
}): JSX.Element | null {
|
|
233
|
+
const [submitting, setSubmitting] = React.useState(false);
|
|
234
|
+
|
|
235
|
+
if (
|
|
236
|
+
!toolCall.permission_options?.length ||
|
|
237
|
+
toolCall.permission_status !== 'pending' ||
|
|
238
|
+
!toolCall.session_id
|
|
239
|
+
) {
|
|
240
|
+
return null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const handleClick = async (optionId: string) => {
|
|
244
|
+
setSubmitting(true);
|
|
245
|
+
try {
|
|
246
|
+
await submitPermissionDecision(
|
|
247
|
+
toolCall.session_id!,
|
|
248
|
+
toolCall.tool_call_id,
|
|
249
|
+
optionId
|
|
250
|
+
);
|
|
251
|
+
} catch (err) {
|
|
252
|
+
console.error('Failed to submit permission decision:', err);
|
|
253
|
+
setSubmitting(false);
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
return (
|
|
258
|
+
<div className="jp-jupyter-ai-acp-client-permission-buttons">
|
|
259
|
+
<span className="jp-jupyter-ai-acp-client-permission-tree">└─</span>
|
|
260
|
+
<span>Allow?</span>
|
|
261
|
+
{toolCall.permission_options.map((opt: IPermissionOption) => (
|
|
262
|
+
<button
|
|
263
|
+
key={opt.option_id}
|
|
264
|
+
className={clsx(
|
|
265
|
+
'jp-jupyter-ai-acp-client-permission-btn',
|
|
266
|
+
opt.kind &&
|
|
267
|
+
`jp-jupyter-ai-acp-client-permission-btn-${opt.kind.replace(/_/g, '-')}`
|
|
268
|
+
)}
|
|
269
|
+
onClick={() => handleClick(opt.option_id)}
|
|
270
|
+
disabled={submitting}
|
|
271
|
+
title={opt.kind}
|
|
272
|
+
>
|
|
273
|
+
{opt.name}
|
|
274
|
+
</button>
|
|
275
|
+
))}
|
|
130
276
|
</div>
|
|
131
277
|
);
|
|
132
278
|
}
|
package/style/base.css
CHANGED
|
@@ -37,16 +37,19 @@
|
|
|
37
37
|
color: var(--jp-error-color1, #d32f2f);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
details.jp-jupyter-ai-acp-client-tool-call summary
|
|
40
|
+
details.jp-jupyter-ai-acp-client-tool-call summary,
|
|
41
|
+
.jp-jupyter-ai-acp-client-tool-call details summary {
|
|
41
42
|
cursor: pointer;
|
|
42
43
|
list-style: none;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
|
-
details.jp-jupyter-ai-acp-client-tool-call summary::-webkit-details-marker
|
|
46
|
+
details.jp-jupyter-ai-acp-client-tool-call summary::-webkit-details-marker,
|
|
47
|
+
.jp-jupyter-ai-acp-client-tool-call details summary::-webkit-details-marker {
|
|
46
48
|
display: none;
|
|
47
49
|
}
|
|
48
50
|
|
|
49
|
-
details.jp-jupyter-ai-acp-client-tool-call summary::after
|
|
51
|
+
details.jp-jupyter-ai-acp-client-tool-call summary::after,
|
|
52
|
+
.jp-jupyter-ai-acp-client-tool-call details summary::after {
|
|
50
53
|
content: '';
|
|
51
54
|
display: inline-block;
|
|
52
55
|
border-left: 4px solid transparent;
|
|
@@ -57,7 +60,8 @@ details.jp-jupyter-ai-acp-client-tool-call summary::after {
|
|
|
57
60
|
opacity: 0.5;
|
|
58
61
|
}
|
|
59
62
|
|
|
60
|
-
details[open].jp-jupyter-ai-acp-client-tool-call summary::after
|
|
63
|
+
details[open].jp-jupyter-ai-acp-client-tool-call summary::after,
|
|
64
|
+
.jp-jupyter-ai-acp-client-tool-call details[open] summary::after {
|
|
61
65
|
border-top: none;
|
|
62
66
|
border-bottom: 5px solid currentcolor;
|
|
63
67
|
}
|
|
@@ -70,3 +74,140 @@ details[open].jp-jupyter-ai-acp-client-tool-call summary::after {
|
|
|
70
74
|
white-space: pre-wrap;
|
|
71
75
|
word-break: break-all;
|
|
72
76
|
}
|
|
77
|
+
|
|
78
|
+
/* Permission approval buttons */
|
|
79
|
+
|
|
80
|
+
.jp-jupyter-ai-acp-client-permission-buttons {
|
|
81
|
+
display: flex;
|
|
82
|
+
gap: 6px;
|
|
83
|
+
margin: 4px 0;
|
|
84
|
+
align-items: center;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.jp-jupyter-ai-acp-client-permission-tree {
|
|
88
|
+
color: var(--jp-ui-font-color2);
|
|
89
|
+
font-family: monospace;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.jp-jupyter-ai-acp-client-permission-btn {
|
|
93
|
+
padding: 2px 6px;
|
|
94
|
+
font-size: var(--jp-ui-font-size1);
|
|
95
|
+
font-family: var(--jp-ui-font-family);
|
|
96
|
+
border-radius: 3px;
|
|
97
|
+
border: 1px solid var(--jp-border-color1);
|
|
98
|
+
cursor: pointer;
|
|
99
|
+
background: var(--jp-layout-color1);
|
|
100
|
+
color: var(--jp-ui-font-color1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.jp-jupyter-ai-acp-client-permission-btn:disabled {
|
|
104
|
+
opacity: 0.5;
|
|
105
|
+
cursor: not-allowed;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.jp-jupyter-ai-acp-client-permission-btn:hover:not(:disabled) {
|
|
109
|
+
background: var(--jp-layout-color2);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.jp-jupyter-ai-acp-client-permission-btn-allow-once,
|
|
113
|
+
.jp-jupyter-ai-acp-client-permission-btn-allow-always {
|
|
114
|
+
border-color: var(--jp-success-color1, #388e3c);
|
|
115
|
+
color: var(--jp-success-color1, #388e3c);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.jp-jupyter-ai-acp-client-permission-btn-reject-once {
|
|
119
|
+
border-color: var(--jp-error-color1, #d32f2f);
|
|
120
|
+
color: var(--jp-error-color1, #d32f2f);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* Permission selection label */
|
|
124
|
+
|
|
125
|
+
.jp-jupyter-ai-acp-client-permission-label {
|
|
126
|
+
font-size: var(--jp-ui-font-size0);
|
|
127
|
+
color: var(--jp-ui-font-color2);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* Diff view */
|
|
131
|
+
|
|
132
|
+
.jp-jupyter-ai-acp-client-diff-container {
|
|
133
|
+
margin: 4px 0 4px 20px;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.jp-jupyter-ai-acp-client-diff-block {
|
|
137
|
+
margin-bottom: 4px;
|
|
138
|
+
border: 1px solid var(--jp-border-color2);
|
|
139
|
+
border-radius: 3px;
|
|
140
|
+
overflow: hidden;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.jp-jupyter-ai-acp-client-diff-header {
|
|
144
|
+
padding: 0 8px;
|
|
145
|
+
font-size: var(--jp-ui-font-size0);
|
|
146
|
+
font-family: var(--jp-code-font-family);
|
|
147
|
+
color: var(--jp-ui-font-color2);
|
|
148
|
+
cursor: pointer;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.jp-jupyter-ai-acp-client-diff-header:hover {
|
|
152
|
+
text-decoration: underline;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.jp-jupyter-ai-acp-client-diff-content {
|
|
156
|
+
margin: 0;
|
|
157
|
+
padding: 0;
|
|
158
|
+
font-size: var(--jp-code-font-size);
|
|
159
|
+
font-family: var(--jp-code-font-family);
|
|
160
|
+
line-height: var(--jp-code-line-height);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.jp-jupyter-ai-acp-client-diff-line {
|
|
164
|
+
display: flex;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.jp-jupyter-ai-acp-client-diff-gutter {
|
|
168
|
+
flex-shrink: 0;
|
|
169
|
+
width: 3ch;
|
|
170
|
+
text-align: right;
|
|
171
|
+
color: var(--jp-ui-font-color3);
|
|
172
|
+
user-select: none;
|
|
173
|
+
padding-right: 2px;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.jp-jupyter-ai-acp-client-diff-line-text {
|
|
177
|
+
flex: 1;
|
|
178
|
+
white-space: pre-wrap;
|
|
179
|
+
word-break: break-all;
|
|
180
|
+
padding-left: 4px;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.jp-jupyter-ai-acp-client-diff-toggle {
|
|
184
|
+
display: block;
|
|
185
|
+
color: var(--jp-ui-font-color3);
|
|
186
|
+
font-style: italic;
|
|
187
|
+
padding: 0 8px;
|
|
188
|
+
cursor: pointer;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.jp-jupyter-ai-acp-client-diff-toggle:hover {
|
|
192
|
+
text-decoration: underline;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.jp-jupyter-ai-acp-client-diff-added {
|
|
196
|
+
background: color-mix(
|
|
197
|
+
in srgb,
|
|
198
|
+
var(--jp-success-color1, #388e3c) 15%,
|
|
199
|
+
transparent
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.jp-jupyter-ai-acp-client-diff-removed {
|
|
204
|
+
background: color-mix(
|
|
205
|
+
in srgb,
|
|
206
|
+
var(--jp-error-color1, #d32f2f) 15%,
|
|
207
|
+
transparent
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.jp-jupyter-ai-acp-client-diff-context {
|
|
212
|
+
color: var(--jp-ui-font-color2);
|
|
213
|
+
}
|