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 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
 
@@ -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 - and
58
- * a refused link replaced - in the same place. Close and Escape end it. */
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._sending = false;
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._sending) {
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._sending = true;
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.50",
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
- const settle = (): Promise<void> =>
33
- new Promise(resolve => setTimeout(resolve, 0));
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 and writes the result under the field, window still open', async () => {
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
- popup.close.click();
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 is replaced and sent again in the same window', async () => {
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
- popup.input.value = LINK.replace('one-time', 'second');
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
- expect(request).toHaveBeenCalledTimes(2);
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 () => {
@@ -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 - and
95
- * a refused link replaced - in the same place. Close and Escape end it. */
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._sending) {
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._sending = true;
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 _sending = false;
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 title, the search field, the caret column and the
19
- status-dot column keep one left edge. 4px, not 8px: the lists reserve a
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;