jupyterlab_ai_code_assistants_extension 1.2.50 → 1.2.55
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 +1 -1
- package/lib/core/callback.js +12 -6
- package/package.json +1 -1
- package/src/__tests__/callback.spec.ts +29 -11
- package/src/core/callback.ts +13 -6
- package/style/base.css +7 -3
package/README.md
CHANGED
|
@@ -47,7 +47,7 @@ Chat-panel extensions re-implement the agent loop and trail the real tool. This
|
|
|
47
47
|
- **Conflict-safe upgrade** - if a retired standalone extension is still installed, its panel wins and this extension stands down for that assistant instead of showing a duplicate
|
|
48
48
|
- **DeepSeek in the browser** - the DeepSeek Harness has no terminal chat; its panel lists the projects under `~/.dsh/sessions` (or `$DSH_HOME`) and `Open Web UI` starts `dsh --profile web --no-open --port 0` in the project's terminal, which prints the URL to open. The conversation is then chosen in the harness's own browser UI; branching and deleting work from the panel as for every other assistant. The URL is on the machine running JupyterLab, so a remote server needs a port forward to reach it
|
|
49
49
|
- **Auto-disabled when absent** - an assistant whose CLI is not on `PATH` does not show a panel
|
|
50
|
-
- **Open Callback** - when an assistant's login sends your browser to an `http://127.0.0.1:...` link that fails to load, because the login is waiting on the machine running JupyterLab, run `Open Callback` from the command palette and paste the link; the Jupyter server requests it for you, and the popup says whether the login accepted it. Only `http` links to that machine's own loopback are accepted
|
|
50
|
+
- **Open Callback** - when an assistant's login sends your browser to an `http://127.0.0.1:...` link that fails to load, because the login is waiting on the machine running JupyterLab, run `Open Callback` from the command palette and paste the link; the Jupyter server requests it for you, and the popup says whether the login accepted it, then closes by itself 3 seconds later. Only `http` links to that machine's own loopback are accepted
|
|
51
51
|
|
|
52
52
|
## Requirements
|
|
53
53
|
|
package/lib/core/callback.js
CHANGED
|
@@ -3,6 +3,8 @@ import { Widget } from '@lumino/widgets';
|
|
|
3
3
|
import { requestAPI } from './request';
|
|
4
4
|
/** The command that delivers a pasted login callback link. */
|
|
5
5
|
export const CALLBACK_COMMAND = 'open:callback';
|
|
6
|
+
/** How long the popup stays open after it has shown what a send came to. */
|
|
7
|
+
const CLOSE_DELAY_MS = 3000;
|
|
6
8
|
async function deliver(url, app, trans) {
|
|
7
9
|
try {
|
|
8
10
|
const answer = await requestAPI('callback', app.serviceManager.serverSettings, { method: 'POST', body: JSON.stringify({ url }) });
|
|
@@ -54,8 +56,11 @@ class CallbackForm extends Widget {
|
|
|
54
56
|
this._status.textContent = message;
|
|
55
57
|
}
|
|
56
58
|
}
|
|
57
|
-
/** OK sends the link and leaves the window open, so the result is read
|
|
58
|
-
*
|
|
59
|
+
/** OK sends the link and leaves the window open, so the result is read in the
|
|
60
|
+
* same place. The window then closes by itself after `CLOSE_DELAY_MS`. One
|
|
61
|
+
* window sends one link: the login code in it works once, and a second send
|
|
62
|
+
* of the same link fails a login the first send completed. Close and Escape
|
|
63
|
+
* end the window at once. */
|
|
59
64
|
class CallbackDialog extends Dialog {
|
|
60
65
|
constructor(_form, _send, _trans) {
|
|
61
66
|
super({
|
|
@@ -71,7 +76,7 @@ class CallbackDialog extends Dialog {
|
|
|
71
76
|
this._form = _form;
|
|
72
77
|
this._send = _send;
|
|
73
78
|
this._trans = _trans;
|
|
74
|
-
this.
|
|
79
|
+
this._sent = false;
|
|
75
80
|
}
|
|
76
81
|
resolve(index) {
|
|
77
82
|
if (index === 0) {
|
|
@@ -82,19 +87,20 @@ class CallbackDialog extends Dialog {
|
|
|
82
87
|
}
|
|
83
88
|
async _deliver() {
|
|
84
89
|
const url = this._form.url;
|
|
85
|
-
if (this.
|
|
90
|
+
if (this._sent) {
|
|
86
91
|
return;
|
|
87
92
|
}
|
|
88
93
|
if (!url) {
|
|
89
94
|
this._form.report('error', this._trans.__('Paste the link first.'));
|
|
90
95
|
return;
|
|
91
96
|
}
|
|
92
|
-
this.
|
|
97
|
+
this._sent = true;
|
|
93
98
|
this._form.report('pending', this._trans.__('Sending the link...'));
|
|
94
99
|
const outcome = await this._send(url);
|
|
95
|
-
this._sending = false;
|
|
96
100
|
if (!this.isDisposed) {
|
|
97
101
|
this._form.report(outcome.level, outcome.message);
|
|
102
|
+
// A no-op when Close has already ended the window.
|
|
103
|
+
window.setTimeout(() => this.reject(), CLOSE_DELAY_MS);
|
|
98
104
|
}
|
|
99
105
|
}
|
|
100
106
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jupyterlab_ai_code_assistants_extension",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.55",
|
|
4
4
|
"description": "A full launcher and manager for every AI code assistant in JupyterLab - Claude Code, Codex, Kimi, Gemini and DeepSeek. Start, resume, fork, switch, and clean up CLI sessions from a side panel per assistant - one click lands you in the right terminal with the assistant already running, no duplicate tabs, no UUID hunting.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -28,9 +28,10 @@ import { requestAPI } from '../core/request';
|
|
|
28
28
|
const request = requestAPI as jest.Mock;
|
|
29
29
|
const LINK = 'http://127.0.0.1:1455/auth/callback?code=one-time&state=s';
|
|
30
30
|
|
|
31
|
-
/** One macrotask: `Dialog.launch` attaches the popup after its own awaits.
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
/** One macrotask: `Dialog.launch` attaches the popup after its own awaits.
|
|
32
|
+
* The timers are jest's, so that the popup's 3 s close is stepped, not waited
|
|
33
|
+
* for. */
|
|
34
|
+
const settle = (): Promise<void> => jest.advanceTimersByTimeAsync(0);
|
|
34
35
|
|
|
35
36
|
function setup(): {
|
|
36
37
|
commands: CommandRegistry;
|
|
@@ -88,6 +89,7 @@ describe('the Open Callback command', () => {
|
|
|
88
89
|
let error: jest.SpyInstance;
|
|
89
90
|
|
|
90
91
|
beforeEach(() => {
|
|
92
|
+
jest.useFakeTimers();
|
|
91
93
|
request.mockReset();
|
|
92
94
|
success = jest.spyOn(Notification, 'success').mockReturnValue('' as any);
|
|
93
95
|
error = jest.spyOn(Notification, 'error').mockReturnValue('' as any);
|
|
@@ -100,6 +102,7 @@ describe('the Open Callback command', () => {
|
|
|
100
102
|
)?.click();
|
|
101
103
|
await settle();
|
|
102
104
|
jest.restoreAllMocks();
|
|
105
|
+
jest.useRealTimers();
|
|
103
106
|
});
|
|
104
107
|
|
|
105
108
|
it('is on the command palette under the assistants category', () => {
|
|
@@ -122,7 +125,7 @@ describe('the Open Callback command', () => {
|
|
|
122
125
|
expect(commands.hasCommand(CALLBACK_COMMAND)).toBe(true);
|
|
123
126
|
});
|
|
124
127
|
|
|
125
|
-
it('OK sends the trimmed link
|
|
128
|
+
it('OK sends the trimmed link, writes the result under the field and the window closes 3 s later', async () => {
|
|
126
129
|
const { commands, serverSettings } = setup();
|
|
127
130
|
request.mockResolvedValue({ status: 302 });
|
|
128
131
|
const popup = await openPopup(commands);
|
|
@@ -140,9 +143,10 @@ describe('the Open Callback command', () => {
|
|
|
140
143
|
expectPlainWords(popup.status.textContent ?? '');
|
|
141
144
|
// Read in the popup, not in a toast, and the popup is still there.
|
|
142
145
|
expect(success).not.toHaveBeenCalled();
|
|
146
|
+
await jest.advanceTimersByTimeAsync(2999);
|
|
143
147
|
expect(document.querySelector('.jp-Dialog')).not.toBeNull();
|
|
144
148
|
|
|
145
|
-
|
|
149
|
+
await jest.advanceTimersByTimeAsync(1);
|
|
146
150
|
await popup.closed;
|
|
147
151
|
expect(document.querySelector('.jp-Dialog')).toBeNull();
|
|
148
152
|
});
|
|
@@ -162,11 +166,9 @@ describe('the Open Callback command', () => {
|
|
|
162
166
|
expect(popup.status.dataset.level).toBe('success');
|
|
163
167
|
});
|
|
164
168
|
|
|
165
|
-
it('a refused link
|
|
169
|
+
it('a refused link also closes the window 3 s after it is reported', async () => {
|
|
166
170
|
const { commands } = setup();
|
|
167
|
-
request
|
|
168
|
-
.mockRejectedValueOnce(new Error('callback_unreachable'))
|
|
169
|
-
.mockResolvedValueOnce({ status: 302 });
|
|
171
|
+
request.mockRejectedValue(new Error('callback_unreachable'));
|
|
170
172
|
const popup = await openPopup(commands);
|
|
171
173
|
|
|
172
174
|
popup.input.value = LINK;
|
|
@@ -174,10 +176,23 @@ describe('the Open Callback command', () => {
|
|
|
174
176
|
await settle();
|
|
175
177
|
expect(popup.status.dataset.level).toBe('error');
|
|
176
178
|
|
|
177
|
-
|
|
179
|
+
await jest.advanceTimersByTimeAsync(3000);
|
|
180
|
+
await popup.closed;
|
|
181
|
+
expect(document.querySelector('.jp-Dialog')).toBeNull();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('a second OK in the same window sends nothing: the login code works once', async () => {
|
|
185
|
+
const { commands } = setup();
|
|
186
|
+
request.mockResolvedValue({ status: 200 });
|
|
187
|
+
const popup = await openPopup(commands);
|
|
188
|
+
|
|
189
|
+
popup.input.value = LINK;
|
|
190
|
+
popup.ok.click();
|
|
191
|
+
await settle();
|
|
178
192
|
popup.ok.click();
|
|
179
193
|
await settle();
|
|
180
|
-
|
|
194
|
+
|
|
195
|
+
expect(request).toHaveBeenCalledTimes(1);
|
|
181
196
|
expect(popup.status.dataset.level).toBe('success');
|
|
182
197
|
});
|
|
183
198
|
|
|
@@ -191,6 +206,9 @@ describe('the Open Callback command', () => {
|
|
|
191
206
|
|
|
192
207
|
expect(request).not.toHaveBeenCalled();
|
|
193
208
|
expect(popup.status.textContent).toBe('Paste the link first.');
|
|
209
|
+
// Nothing was sent, so there is no result to close after.
|
|
210
|
+
await jest.advanceTimersByTimeAsync(3000);
|
|
211
|
+
expect(document.querySelector('.jp-Dialog')).not.toBeNull();
|
|
194
212
|
});
|
|
195
213
|
|
|
196
214
|
it('Close sends nothing', async () => {
|
package/src/core/callback.ts
CHANGED
|
@@ -8,6 +8,9 @@ import { requestAPI } from './request';
|
|
|
8
8
|
/** The command that delivers a pasted login callback link. */
|
|
9
9
|
export const CALLBACK_COMMAND = 'open:callback';
|
|
10
10
|
|
|
11
|
+
/** How long the popup stays open after it has shown what a send came to. */
|
|
12
|
+
const CLOSE_DELAY_MS = 3000;
|
|
13
|
+
|
|
11
14
|
/** What a delivery came to, in words for the user. Never the link, which
|
|
12
15
|
* carries a login code, and never a status code. */
|
|
13
16
|
interface IOutcome {
|
|
@@ -91,8 +94,11 @@ class CallbackForm extends Widget {
|
|
|
91
94
|
private _status: HTMLDivElement;
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
/** OK sends the link and leaves the window open, so the result is read
|
|
95
|
-
*
|
|
97
|
+
/** OK sends the link and leaves the window open, so the result is read in the
|
|
98
|
+
* same place. The window then closes by itself after `CLOSE_DELAY_MS`. One
|
|
99
|
+
* window sends one link: the login code in it works once, and a second send
|
|
100
|
+
* of the same link fails a login the first send completed. Close and Escape
|
|
101
|
+
* end the window at once. */
|
|
96
102
|
class CallbackDialog extends Dialog<void> {
|
|
97
103
|
constructor(
|
|
98
104
|
private _form: CallbackForm,
|
|
@@ -121,23 +127,24 @@ class CallbackDialog extends Dialog<void> {
|
|
|
121
127
|
|
|
122
128
|
private async _deliver(): Promise<void> {
|
|
123
129
|
const url = this._form.url;
|
|
124
|
-
if (this.
|
|
130
|
+
if (this._sent) {
|
|
125
131
|
return;
|
|
126
132
|
}
|
|
127
133
|
if (!url) {
|
|
128
134
|
this._form.report('error', this._trans.__('Paste the link first.'));
|
|
129
135
|
return;
|
|
130
136
|
}
|
|
131
|
-
this.
|
|
137
|
+
this._sent = true;
|
|
132
138
|
this._form.report('pending', this._trans.__('Sending the link...'));
|
|
133
139
|
const outcome = await this._send(url);
|
|
134
|
-
this._sending = false;
|
|
135
140
|
if (!this.isDisposed) {
|
|
136
141
|
this._form.report(outcome.level, outcome.message);
|
|
142
|
+
// A no-op when Close has already ended the window.
|
|
143
|
+
window.setTimeout(() => this.reject(), CLOSE_DELAY_MS);
|
|
137
144
|
}
|
|
138
145
|
}
|
|
139
146
|
|
|
140
|
-
private
|
|
147
|
+
private _sent = false;
|
|
141
148
|
}
|
|
142
149
|
|
|
143
150
|
/**
|
package/style/base.css
CHANGED
|
@@ -15,8 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
/* Single source for the gap between the panel border and its content, on
|
|
17
17
|
both sides: the header, search field, error line, section headers and rows
|
|
18
|
-
all read it, so the
|
|
19
|
-
|
|
18
|
+
all read it, so the search field, the caret column and the status-dot
|
|
19
|
+
column keep one left edge. 4px, not 8px: the lists reserve a
|
|
20
20
|
scrollbar gutter as well, so 8px put the time column 23px from the right
|
|
21
21
|
border where the file browser's sits 12px from it. */
|
|
22
22
|
--aica-inset: 4px;
|
|
@@ -43,9 +43,13 @@
|
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
/* The panel title and section headers mirror the file browser's headers so the
|
|
46
|
-
sidebar reads uniformly: 600 weight, uppercase, ui-font-size0, 1px tracking.
|
|
46
|
+
sidebar reads uniformly: 600 weight, uppercase, ui-font-size0, 1px tracking.
|
|
47
|
+
The margin is the caret column's width: the title starts where the section
|
|
48
|
+
labels and the row names start, not on the inset the carets and the status
|
|
49
|
+
dots stand on (DEF-PANE-285). */
|
|
47
50
|
.jp-AiAssistantsPanel-title {
|
|
48
51
|
flex: 1 1 auto;
|
|
52
|
+
margin-left: 14px;
|
|
49
53
|
font-weight: 600;
|
|
50
54
|
letter-spacing: 1px;
|
|
51
55
|
text-transform: uppercase;
|