@jupyter-ai/acp-client 0.0.7 → 0.0.9

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 CHANGED
@@ -28,13 +28,13 @@ package also provides a default ACP client implementation as `JaiAcpClient`.
28
28
  the persona name, the persona avatar, and the `executable` starting the ACP
29
29
  agent server.
30
30
 
31
- For example, the `@Claude-ACP` persona is defined in `claude.py` using less than
31
+ For example, the `@Claude` persona is defined in `claude.py` using less than
32
32
  20 lines of code:
33
33
 
34
34
  ```py
35
35
  class ClaudeAcpPersona(BaseAcpPersona):
36
36
  def __init__(self, *args, **kwargs):
37
- executable = ["claude-code-acp"]
37
+ executable = ["claude-agent-acp"]
38
38
  super().__init__(*args, executable=executable, **kwargs)
39
39
 
40
40
  @property
@@ -44,19 +44,24 @@ class ClaudeAcpPersona(BaseAcpPersona):
44
44
  ))
45
45
 
46
46
  return PersonaDefaults(
47
- name="Claude-ACP",
47
+ name="Claude",
48
48
  description="Claude Code as an ACP agent persona.",
49
49
  avatar_path=avatar_path,
50
50
  system_prompt="unused"
51
51
  )
52
52
  ```
53
53
 
54
- Currently, this package provides 2 personas:
54
+ Currently, this package provides 4 personas:
55
55
 
56
- 1. `@Claude-ACP`
57
- - requires `claude-code-acp`, installed via `npm install -g @zed-industries/claude-code-acp`
58
- 2. `@Kiro`
59
- - requires `kiro-cli`, installed from https://kiro.dev
56
+ - `@Claude`
57
+ - requires `claude-agent-acp`, installed via `npm install -g @zed-industries/claude-agent-acp`
58
+ - optional env variable `CLAUDE_CODE_EXECUTABLE` points to your custom-installed Claude executable location. By default, claude-agent-acp uses Claude packaged in `@zed-industries/claude-agent-acp`.
59
+ - `@Gemini`
60
+ - requires `gemini` CLI (>= 0.34.0), installed via https://geminicli.com/
61
+ - `@Kiro`
62
+ - requires `kiro-cli` (>= 1.25.0, < 2), installed via https://kiro.dev
63
+ - `@Mistral-Vibe`
64
+ - requires `vibe-acp`, installed via `uv tool install mistral-vibe` or `pip install mistral-vibe`
60
65
 
61
66
  ## Dependencies
62
67
 
@@ -68,8 +73,10 @@ Currently, this package provides 2 personas:
68
73
 
69
74
  **Optional**
70
75
 
71
- - `claude-code-acp` (enables `@Claude-ACP`)
76
+ - `claude-agent-acp` (enables `@Claude`)
77
+ - `gemini` (enables `@Gemini`)
72
78
  - `kiro-cli` (enables `@Kiro`)
79
+ - `mistral-vibe` (enables `@Mistral-Vibe` via the `vibe-acp` command)
73
80
 
74
81
  ## Install
75
82
 
@@ -2,7 +2,9 @@ import { IToolCallDiff } from '@jupyter/chat';
2
2
  /**
3
3
  * Renders one or more file diffs.
4
4
  */
5
- export declare function DiffView({ diffs, onOpenFile }: {
5
+ export declare function DiffView({ diffs, onOpenFile, toDisplayPath, pendingPermission }: {
6
6
  diffs: IToolCallDiff[];
7
7
  onOpenFile?: (path: string) => void;
8
+ toDisplayPath?: (path: string) => string;
9
+ pendingPermission?: boolean;
8
10
  }): JSX.Element;
package/lib/diff-view.js CHANGED
@@ -1,15 +1,25 @@
1
1
  import React from 'react';
2
+ import { PathExt } from '@jupyterlab/coreutils';
2
3
  import { structuredPatch } from 'diff';
4
+ import clsx from 'clsx';
3
5
  /** Maximum number of diff lines shown before truncation. */
4
6
  const MAX_DIFF_LINES = 20;
5
7
  /**
6
8
  * Renders a single file diff block with filename header, line-level
7
9
  * highlighting, and click-to-expand truncation.
8
10
  */
9
- function DiffBlock({ diff, onOpenFile }) {
10
- var _a, _b;
11
+ function DiffBlock({ diff, onOpenFile, toDisplayPath, pendingPermission }) {
12
+ var _a;
11
13
  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;
14
+ const displayPath = toDisplayPath
15
+ ? toDisplayPath(diff.path)
16
+ : PathExt.basename(diff.path);
17
+ // toDisplayPath makes paths inside the server root relative. A leading '/'
18
+ // means the file is outside it and cannot be opened via the Contents API.
19
+ const isOutsideRoot = displayPath.startsWith('/');
20
+ const isClickable = !!onOpenFile &&
21
+ !isOutsideRoot &&
22
+ !(pendingPermission && diff.old_text === undefined);
13
23
  const [expanded, setExpanded] = React.useState(false);
14
24
  // Flatten hunks into renderable lines
15
25
  const allLines = [];
@@ -37,7 +47,9 @@ function DiffBlock({ diff, onOpenFile }) {
37
47
  const visible = canTruncate && !expanded ? allLines.slice(0, MAX_DIFF_LINES) : allLines;
38
48
  const hiddenCount = allLines.length - MAX_DIFF_LINES;
39
49
  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),
50
+ React.createElement("div", { className: clsx('jp-jupyter-ai-acp-client-diff-header', {
51
+ 'jp-jupyter-ai-acp-client-diff-header-clickable': isClickable
52
+ }), onClick: isClickable ? () => onOpenFile(diff.path) : undefined, title: diff.path }, displayPath),
41
53
  React.createElement("div", { className: "jp-jupyter-ai-acp-client-diff-content" },
42
54
  visible.map((line) => (React.createElement("div", { key: line.key, className: `jp-jupyter-ai-acp-client-diff-line ${line.cls}` },
43
55
  React.createElement("span", { className: "jp-jupyter-ai-acp-client-diff-line-text" },
@@ -53,6 +65,6 @@ function DiffBlock({ diff, onOpenFile }) {
53
65
  /**
54
66
  * Renders one or more file diffs.
55
67
  */
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 })))));
68
+ export function DiffView({ diffs, onOpenFile, toDisplayPath, pendingPermission }) {
69
+ 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, toDisplayPath: toDisplayPath, pendingPermission: pendingPermission })))));
58
70
  }
package/lib/tool-calls.js CHANGED
@@ -1,7 +1,28 @@
1
1
  import React from 'react';
2
+ import { PageConfig, PathExt } from '@jupyterlab/coreutils';
2
3
  import { submitPermissionDecision } from './request';
3
4
  import clsx from 'clsx';
4
5
  import { DiffView } from './diff-view';
6
+ /**
7
+ * Convert an absolute filesystem path to a server-relative path.
8
+ * Returns the path unchanged if the server root is not set or the path
9
+ * is outside it.
10
+ */
11
+ function toServerRelativePath(absolutePath) {
12
+ const rootUri = PageConfig.getOption('rootUri');
13
+ const serverRoot = rootUri
14
+ ? new URL(rootUri).pathname
15
+ : PageConfig.getOption('serverRoot');
16
+ if (!serverRoot) {
17
+ return absolutePath;
18
+ }
19
+ const relativePath = PathExt.relative(serverRoot, absolutePath);
20
+ // Path is outside server root — keep absolute
21
+ if (relativePath.startsWith('..')) {
22
+ return absolutePath;
23
+ }
24
+ return relativePath;
25
+ }
5
26
  /**
6
27
  * Preamble component that renders tool call status lines above message body.
7
28
  * Returns null if the message has no tool calls.
@@ -14,7 +35,7 @@ export function ToolCallsComponent(props) {
14
35
  }
15
36
  const onOpenFile = (path) => {
16
37
  var _a;
17
- (_a = model.documentManager) === null || _a === void 0 ? void 0 : _a.openOrReveal(path);
38
+ (_a = model.documentManager) === null || _a === void 0 ? void 0 : _a.openOrReveal(toServerRelativePath(path));
18
39
  };
19
40
  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 })))));
20
41
  }
@@ -51,7 +72,7 @@ function formatToolInput(input) {
51
72
  }
52
73
  /**
53
74
  * Compute the pre-permission detail text for a tool call, or null if nothing
54
- * to show beyond the title. Returns a plain string so callers can check null.
75
+ * to show beyond the title.
55
76
  */
56
77
  function buildPermissionDetail(toolCall) {
57
78
  const { kind, title, locations, raw_input } = toolCall;
@@ -72,8 +93,10 @@ function buildPermissionDetail(toolCall) {
72
93
  if ((kind === 'delete' || kind === 'move' || kind === 'read') &&
73
94
  (locations === null || locations === void 0 ? void 0 : locations.length)) {
74
95
  return kind === 'move' && locations.length >= 2
75
- ? locations[0] + ' \u2192 ' + locations[1]
76
- : locations.join('\n');
96
+ ? toServerRelativePath(locations[0]) +
97
+ ' \u2192 ' +
98
+ toServerRelativePath(locations[1])
99
+ : locations.map(toServerRelativePath).join('\n');
77
100
  }
78
101
  // Generic fallback for unknown/MCP kinds with raw_input.
79
102
  if (raw_input !== null &&
@@ -105,7 +128,7 @@ function buildPermissionDetail(toolCall) {
105
128
  }
106
129
  return null;
107
130
  }
108
- /** Tool kinds where expanded view shows full file path(s) from locations. */
131
+ /** Tool kinds where expanded view shows file path(s) from locations. */
109
132
  const FILE_KINDS = new Set(['read', 'edit', 'delete', 'move']);
110
133
  /** Tool kinds where expanded view shows raw_output (stdout, search results, etc.). */
111
134
  const OUTPUT_KINDS = new Set(['search', 'execute', 'think', 'fetch']);
@@ -113,7 +136,7 @@ const OUTPUT_KINDS = new Set(['search', 'execute', 'think', 'fetch']);
113
136
  * Build the expandable details content for a tool call.
114
137
  * Returns lines of metadata to display, or empty array if nothing to show.
115
138
  *
116
- * File operations show full paths; output operations show raw_output;
139
+ * File operations show server-relative paths; output operations show raw_output;
117
140
  * switch_mode/other/None show nothing (clean title only).
118
141
  */
119
142
  function buildDetailsLines(toolCall) {
@@ -122,7 +145,7 @@ function buildDetailsLines(toolCall) {
122
145
  const kind = toolCall.kind;
123
146
  if (kind && FILE_KINDS.has(kind) && ((_a = toolCall.locations) === null || _a === void 0 ? void 0 : _a.length)) {
124
147
  for (const loc of toolCall.locations) {
125
- lines.push(loc);
148
+ lines.push(toServerRelativePath(loc));
126
149
  }
127
150
  }
128
151
  else if (kind && OUTPUT_KINDS.has(kind) && toolCall.raw_output) {
@@ -172,7 +195,7 @@ function ToolCallLine({ toolCall, onOpenFile }) {
172
195
  React.createElement("span", { className: "jp-jupyter-ai-acp-client-tool-call-icon" }, icon),
173
196
  ' ',
174
197
  React.createElement("em", null, displayTitle)),
175
- React.createElement(DiffView, { diffs: toolCall.diffs, onOpenFile: onOpenFile })),
198
+ React.createElement(DiffView, { diffs: toolCall.diffs, onOpenFile: toolCall.kind === 'edit' ? onOpenFile : undefined, toDisplayPath: toServerRelativePath, pendingPermission: true })),
176
199
  React.createElement(PermissionButtons, { toolCall: toolCall })));
177
200
  }
178
201
  // Pending permission without diffs: show kind-specific detail if available
@@ -199,7 +222,7 @@ function ToolCallLine({ toolCall, onOpenFile }) {
199
222
  ' ',
200
223
  displayTitle,
201
224
  React.createElement(PermissionLabel, { toolCall: toolCall })),
202
- hasDiffs ? (React.createElement(DiffView, { diffs: toolCall.diffs, onOpenFile: onOpenFile })) : (React.createElement("div", { className: "jp-jupyter-ai-acp-client-tool-call-detail" }, detailsLines.join('\n')))));
225
+ hasDiffs ? (React.createElement(DiffView, { diffs: toolCall.diffs, onOpenFile: onOpenFile, toDisplayPath: toServerRelativePath })) : (React.createElement("div", { className: "jp-jupyter-ai-acp-client-tool-call-detail" }, detailsLines.join('\n')))));
203
226
  }
204
227
  // In-progress — italic
205
228
  if (isInProgress) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jupyter-ai/acp-client",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "The ACP client for Jupyter AI, allowing for ACP agents to be used in JupyterLab",
5
5
  "keywords": [
6
6
  "jupyter",
package/src/diff-view.tsx CHANGED
@@ -1,6 +1,8 @@
1
1
  import React from 'react';
2
2
  import { IToolCallDiff } from '@jupyter/chat';
3
+ import { PathExt } from '@jupyterlab/coreutils';
3
4
  import { structuredPatch } from 'diff';
5
+ import clsx from 'clsx';
4
6
 
5
7
  /** Maximum number of diff lines shown before truncation. */
6
8
  const MAX_DIFF_LINES = 20;
@@ -19,10 +21,14 @@ interface IDiffLineInfo {
19
21
  */
20
22
  function DiffBlock({
21
23
  diff,
22
- onOpenFile
24
+ onOpenFile,
25
+ toDisplayPath,
26
+ pendingPermission
23
27
  }: {
24
28
  diff: IToolCallDiff;
25
29
  onOpenFile?: (path: string) => void;
30
+ toDisplayPath?: (path: string) => string;
31
+ pendingPermission?: boolean;
26
32
  }): JSX.Element {
27
33
  const patch = structuredPatch(
28
34
  diff.path,
@@ -33,7 +39,16 @@ function DiffBlock({
33
39
  undefined,
34
40
  { context: Infinity }
35
41
  );
36
- const filename = diff.path.split('/').pop() ?? diff.path;
42
+ const displayPath = toDisplayPath
43
+ ? toDisplayPath(diff.path)
44
+ : PathExt.basename(diff.path);
45
+ // toDisplayPath makes paths inside the server root relative. A leading '/'
46
+ // means the file is outside it and cannot be opened via the Contents API.
47
+ const isOutsideRoot = displayPath.startsWith('/');
48
+ const isClickable =
49
+ !!onOpenFile &&
50
+ !isOutsideRoot &&
51
+ !(pendingPermission && diff.old_text === undefined);
37
52
  const [expanded, setExpanded] = React.useState(false);
38
53
 
39
54
  // Flatten hunks into renderable lines
@@ -67,11 +82,13 @@ function DiffBlock({
67
82
  return (
68
83
  <div className="jp-jupyter-ai-acp-client-diff-block">
69
84
  <div
70
- className="jp-jupyter-ai-acp-client-diff-header"
71
- onClick={onOpenFile ? () => onOpenFile(diff.path) : undefined}
85
+ className={clsx('jp-jupyter-ai-acp-client-diff-header', {
86
+ 'jp-jupyter-ai-acp-client-diff-header-clickable': isClickable
87
+ })}
88
+ onClick={isClickable ? () => onOpenFile!(diff.path) : undefined}
72
89
  title={diff.path}
73
90
  >
74
- {filename}
91
+ {displayPath}
75
92
  </div>
76
93
  <div className="jp-jupyter-ai-acp-client-diff-content">
77
94
  {visible.map((line: IDiffLineInfo) => (
@@ -110,15 +127,25 @@ function DiffBlock({
110
127
  */
111
128
  export function DiffView({
112
129
  diffs,
113
- onOpenFile
130
+ onOpenFile,
131
+ toDisplayPath,
132
+ pendingPermission
114
133
  }: {
115
134
  diffs: IToolCallDiff[];
116
135
  onOpenFile?: (path: string) => void;
136
+ toDisplayPath?: (path: string) => string;
137
+ pendingPermission?: boolean;
117
138
  }): JSX.Element {
118
139
  return (
119
140
  <div className="jp-jupyter-ai-acp-client-diff-container">
120
141
  {diffs.map((d, i) => (
121
- <DiffBlock key={i} diff={d} onOpenFile={onOpenFile} />
142
+ <DiffBlock
143
+ key={i}
144
+ diff={d}
145
+ onOpenFile={onOpenFile}
146
+ toDisplayPath={toDisplayPath}
147
+ pendingPermission={pendingPermission}
148
+ />
122
149
  ))}
123
150
  </div>
124
151
  );
@@ -4,10 +4,32 @@ import {
4
4
  IPermissionOption,
5
5
  MessagePreambleProps
6
6
  } from '@jupyter/chat';
7
+ import { PageConfig, PathExt } from '@jupyterlab/coreutils';
7
8
  import { submitPermissionDecision } from './request';
8
9
  import clsx from 'clsx';
9
10
  import { DiffView } from './diff-view';
10
11
 
12
+ /**
13
+ * Convert an absolute filesystem path to a server-relative path.
14
+ * Returns the path unchanged if the server root is not set or the path
15
+ * is outside it.
16
+ */
17
+ function toServerRelativePath(absolutePath: string): string {
18
+ const rootUri = PageConfig.getOption('rootUri');
19
+ const serverRoot = rootUri
20
+ ? new URL(rootUri).pathname
21
+ : PageConfig.getOption('serverRoot');
22
+ if (!serverRoot) {
23
+ return absolutePath;
24
+ }
25
+ const relativePath = PathExt.relative(serverRoot, absolutePath);
26
+ // Path is outside server root — keep absolute
27
+ if (relativePath.startsWith('..')) {
28
+ return absolutePath;
29
+ }
30
+ return relativePath;
31
+ }
32
+
11
33
  /**
12
34
  * Preamble component that renders tool call status lines above message body.
13
35
  * Returns null if the message has no tool calls.
@@ -21,7 +43,7 @@ export function ToolCallsComponent(
21
43
  }
22
44
 
23
45
  const onOpenFile = (path: string) => {
24
- model.documentManager?.openOrReveal(path);
46
+ model.documentManager?.openOrReveal(toServerRelativePath(path));
25
47
  };
26
48
 
27
49
  return (
@@ -75,7 +97,7 @@ function formatToolInput(input: unknown): string {
75
97
 
76
98
  /**
77
99
  * Compute the pre-permission detail text for a tool call, or null if nothing
78
- * to show beyond the title. Returns a plain string so callers can check null.
100
+ * to show beyond the title.
79
101
  */
80
102
  function buildPermissionDetail(toolCall: IToolCall): string | null {
81
103
  const { kind, title, locations, raw_input } = toolCall;
@@ -105,8 +127,10 @@ function buildPermissionDetail(toolCall: IToolCall): string | null {
105
127
  locations?.length
106
128
  ) {
107
129
  return kind === 'move' && locations.length >= 2
108
- ? locations[0] + ' \u2192 ' + locations[1]
109
- : locations.join('\n');
130
+ ? toServerRelativePath(locations[0]) +
131
+ ' \u2192 ' +
132
+ toServerRelativePath(locations[1])
133
+ : locations.map(toServerRelativePath).join('\n');
110
134
  }
111
135
 
112
136
  // Generic fallback for unknown/MCP kinds with raw_input.
@@ -151,7 +175,7 @@ function buildPermissionDetail(toolCall: IToolCall): string | null {
151
175
  return null;
152
176
  }
153
177
 
154
- /** Tool kinds where expanded view shows full file path(s) from locations. */
178
+ /** Tool kinds where expanded view shows file path(s) from locations. */
155
179
  const FILE_KINDS = new Set(['read', 'edit', 'delete', 'move']);
156
180
 
157
181
  /** Tool kinds where expanded view shows raw_output (stdout, search results, etc.). */
@@ -161,7 +185,7 @@ const OUTPUT_KINDS = new Set(['search', 'execute', 'think', 'fetch']);
161
185
  * Build the expandable details content for a tool call.
162
186
  * Returns lines of metadata to display, or empty array if nothing to show.
163
187
  *
164
- * File operations show full paths; output operations show raw_output;
188
+ * File operations show server-relative paths; output operations show raw_output;
165
189
  * switch_mode/other/None show nothing (clean title only).
166
190
  */
167
191
  function buildDetailsLines(toolCall: IToolCall): string[] {
@@ -170,7 +194,7 @@ function buildDetailsLines(toolCall: IToolCall): string[] {
170
194
 
171
195
  if (kind && FILE_KINDS.has(kind) && toolCall.locations?.length) {
172
196
  for (const loc of toolCall.locations) {
173
- lines.push(loc);
197
+ lines.push(toServerRelativePath(loc));
174
198
  }
175
199
  } else if (kind && OUTPUT_KINDS.has(kind) && toolCall.raw_output) {
176
200
  lines.push(formatOutput(toolCall.raw_output));
@@ -240,7 +264,12 @@ function ToolCallLine({
240
264
  </span>{' '}
241
265
  <em>{displayTitle}</em>
242
266
  </summary>
243
- <DiffView diffs={toolCall.diffs!} onOpenFile={onOpenFile} />
267
+ <DiffView
268
+ diffs={toolCall.diffs!}
269
+ onOpenFile={toolCall.kind === 'edit' ? onOpenFile : undefined}
270
+ toDisplayPath={toServerRelativePath}
271
+ pendingPermission
272
+ />
244
273
  </details>
245
274
  <PermissionButtons toolCall={toolCall} />
246
275
  </div>
@@ -286,7 +315,11 @@ function ToolCallLine({
286
315
  <PermissionLabel toolCall={toolCall} />
287
316
  </summary>
288
317
  {hasDiffs ? (
289
- <DiffView diffs={toolCall.diffs!} onOpenFile={onOpenFile} />
318
+ <DiffView
319
+ diffs={toolCall.diffs!}
320
+ onOpenFile={onOpenFile}
321
+ toDisplayPath={toServerRelativePath}
322
+ />
290
323
  ) : (
291
324
  <div className="jp-jupyter-ai-acp-client-tool-call-detail">
292
325
  {detailsLines.join('\n')}
package/style/base.css CHANGED
@@ -145,10 +145,13 @@ details[open].jp-jupyter-ai-acp-client-tool-call summary::after,
145
145
  font-size: var(--jp-ui-font-size0);
146
146
  font-family: var(--jp-code-font-family);
147
147
  color: var(--jp-ui-font-color2);
148
+ }
149
+
150
+ .jp-jupyter-ai-acp-client-diff-header-clickable {
148
151
  cursor: pointer;
149
152
  }
150
153
 
151
- .jp-jupyter-ai-acp-client-diff-header:hover {
154
+ .jp-jupyter-ai-acp-client-diff-header-clickable:hover {
152
155
  text-decoration: underline;
153
156
  }
154
157