jupyterlab_ai_code_assistants_extension 1.0.32 → 1.0.39
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/lib/core/panel.d.ts +14 -2
- package/lib/core/panel.js +34 -8
- package/lib/index.js +38 -30
- package/package.json +1 -1
- package/src/__tests__/panel.spec.ts +40 -0
- package/src/core/panel.ts +34 -10
- package/src/index.ts +41 -30
- package/style/base.css +8 -1
package/lib/core/panel.d.ts
CHANGED
|
@@ -85,6 +85,18 @@ export declare class AssistantSessionsPanel extends Widget {
|
|
|
85
85
|
* `resolveLaunchMode` for the precedence rule. */
|
|
86
86
|
private _launchMode;
|
|
87
87
|
private _openSession;
|
|
88
|
+
/** The server root every path is shown and resolved against, plus
|
|
89
|
+
* JupyterLab's move-to-trash setting. Settable because a panel docked before
|
|
90
|
+
* the server has answered its first status probe is built without either
|
|
91
|
+
* (DEF-132); the action paths read them lazily, so a late value applies on
|
|
92
|
+
* the next click, while rows already rendered keep absolute paths until the
|
|
93
|
+
* next poll.
|
|
94
|
+
*
|
|
95
|
+
* Trailing slashes go, but never the root itself: serving JupyterLab at
|
|
96
|
+
* `/` used to normalise to the empty string, which reads as "no root" -
|
|
97
|
+
* the + button then did nothing at all, and Open Terminal and Show in File
|
|
98
|
+
* Browser answered "outside the JupyterLab root" for every row inside it. */
|
|
99
|
+
setRoot(rootDir: string, deleteToTrash: boolean): void;
|
|
88
100
|
/** Absolute path of the file browser's current folder; the server root when
|
|
89
101
|
* no file browser is available. */
|
|
90
102
|
private _currentFolder;
|
|
@@ -271,8 +283,8 @@ export declare class AssistantSessionsPanel extends Widget {
|
|
|
271
283
|
private readonly _descriptor;
|
|
272
284
|
private readonly _hooks;
|
|
273
285
|
private readonly _serverSettings;
|
|
274
|
-
private
|
|
275
|
-
private
|
|
286
|
+
private _rootDir;
|
|
287
|
+
private _deleteToTrash;
|
|
276
288
|
private readonly _fileBrowser;
|
|
277
289
|
private readonly _colours;
|
|
278
290
|
private readonly _terminals;
|
package/lib/core/panel.js
CHANGED
|
@@ -94,6 +94,8 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
94
94
|
constructor(options) {
|
|
95
95
|
var _a, _b, _c, _d;
|
|
96
96
|
super();
|
|
97
|
+
this._rootDir = '';
|
|
98
|
+
this._deleteToTrash = true;
|
|
97
99
|
this._removingPaths = new Set();
|
|
98
100
|
this._loadingEl = null;
|
|
99
101
|
this._errorEl = null;
|
|
@@ -132,13 +134,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
132
134
|
this._descriptor = options.descriptor;
|
|
133
135
|
this._hooks = (_a = options.hooks) !== null && _a !== void 0 ? _a : {};
|
|
134
136
|
this._serverSettings = options.app.serviceManager.serverSettings;
|
|
135
|
-
|
|
136
|
-
// `/` used to normalise to the empty string, which reads as "no root" -
|
|
137
|
-
// the + button then did nothing at all, and Open Terminal and Show in File
|
|
138
|
-
// Browser answered "outside the JupyterLab root" for every row inside it.
|
|
139
|
-
const root = (options.rootDir || '').replace(/\/+$/, '');
|
|
140
|
-
this._rootDir = root || (options.rootDir ? '/' : '');
|
|
141
|
-
this._deleteToTrash = options.deleteToTrash !== false;
|
|
137
|
+
this.setRoot(options.rootDir, options.deleteToTrash !== false);
|
|
142
138
|
this._fileBrowser = (_b = options.fileBrowser) !== null && _b !== void 0 ? _b : null;
|
|
143
139
|
this._expandedKey = `jupyterlab_ai_code_assistants_extension:${this._descriptor.id}:expanded`;
|
|
144
140
|
this._expanded = loadExpanded(this._expandedKey);
|
|
@@ -259,8 +255,15 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
259
255
|
// and this button LAUNCHES on click instead of dropping a menu, so without
|
|
260
256
|
// the suffix the one surface that starts an approval-free session is the
|
|
261
257
|
// only one that never says so (DEF-36, DEF-113, DEF-114).
|
|
258
|
+
// Disabled, not silent, while the server root is unknown (a panel docked
|
|
259
|
+
// before the first status roster, DEF-132): a healthy-looking button that
|
|
260
|
+
// does nothing on click cannot be diagnosed by the user. `setRoot` re-runs
|
|
261
|
+
// this once the root lands, so the state is data-driven, not timed.
|
|
262
262
|
const nameNewButton = () => {
|
|
263
|
-
newBtn.
|
|
263
|
+
newBtn.disabled = !this._rootDir;
|
|
264
|
+
newBtn.title = this._rootDir
|
|
265
|
+
? `New session in current folder${this._variantSuffix()}`
|
|
266
|
+
: 'Waiting for the server root';
|
|
264
267
|
};
|
|
265
268
|
// Focus as well as hover, or the icon-only button's accessible name is the
|
|
266
269
|
// generic one for everybody not using a mouse.
|
|
@@ -707,10 +710,33 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
707
710
|
void this._fetch().catch(() => undefined);
|
|
708
711
|
}
|
|
709
712
|
}
|
|
713
|
+
/** The server root every path is shown and resolved against, plus
|
|
714
|
+
* JupyterLab's move-to-trash setting. Settable because a panel docked before
|
|
715
|
+
* the server has answered its first status probe is built without either
|
|
716
|
+
* (DEF-132); the action paths read them lazily, so a late value applies on
|
|
717
|
+
* the next click, while rows already rendered keep absolute paths until the
|
|
718
|
+
* next poll.
|
|
719
|
+
*
|
|
720
|
+
* Trailing slashes go, but never the root itself: serving JupyterLab at
|
|
721
|
+
* `/` used to normalise to the empty string, which reads as "no root" -
|
|
722
|
+
* the + button then did nothing at all, and Open Terminal and Show in File
|
|
723
|
+
* Browser answered "outside the JupyterLab root" for every row inside it. */
|
|
724
|
+
setRoot(rootDir, deleteToTrash) {
|
|
725
|
+
var _a;
|
|
726
|
+
const root = (rootDir || '').replace(/\/+$/, '');
|
|
727
|
+
this._rootDir = root || (rootDir ? '/' : '');
|
|
728
|
+
this._deleteToTrash = deleteToTrash;
|
|
729
|
+
(_a = this._renameNewButton) === null || _a === void 0 ? void 0 : _a.call(this);
|
|
730
|
+
}
|
|
710
731
|
/** Absolute path of the file browser's current folder; the server root when
|
|
711
732
|
* no file browser is available. */
|
|
712
733
|
_currentFolder() {
|
|
713
734
|
var _a, _b, _c;
|
|
735
|
+
// No roster yet (DEF-132): a relative folder joined to an empty root
|
|
736
|
+
// would be an absolute path at the filesystem root.
|
|
737
|
+
if (!this._rootDir) {
|
|
738
|
+
return '';
|
|
739
|
+
}
|
|
714
740
|
const rel = ((_c = (_b = (_a = this._fileBrowser) === null || _a === void 0 ? void 0 : _a.model) === null || _b === void 0 ? void 0 : _b.path) !== null && _c !== void 0 ? _c : '').replace(/^\/+/, '');
|
|
715
741
|
if (!rel) {
|
|
716
742
|
return this._rootDir;
|
package/lib/index.js
CHANGED
|
@@ -64,34 +64,27 @@ const plugin = {
|
|
|
64
64
|
let status = null;
|
|
65
65
|
let settings = null;
|
|
66
66
|
let sidebar = DEFAULT_SIDEBAR;
|
|
67
|
-
let probeGeneration = 0;
|
|
68
67
|
/** Ask the server which providers exist and which of their binaries are on
|
|
69
|
-
* PATH.
|
|
70
|
-
* panels until a later probe succeeds - never a dialog, never a thrown
|
|
71
|
-
* activation.
|
|
68
|
+
* PATH.
|
|
72
69
|
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
70
|
+
* `status` is the last roster the server gave, and null means it has never
|
|
71
|
+
* given one - two different facts that used to share one value. A probe
|
|
72
|
+
* that FAILS writes nothing: not answering is not an answer, and reading it
|
|
73
|
+
* as "no assistant is installed" is what emptied the sidebar on every
|
|
74
|
+
* reload whose first request landed before the server was listening
|
|
75
|
+
* (DEF-132), and on every wake whose probe fired before the network was
|
|
76
|
+
* back (DEF-119). No request is suppressed, and a failure never needs
|
|
77
|
+
* ordering against a success, because it has nothing to write. */
|
|
80
78
|
const probeStatus = async () => {
|
|
81
|
-
const mine = ++probeGeneration;
|
|
82
79
|
try {
|
|
83
80
|
status = await requestAPI('status', serverSettings, {
|
|
84
81
|
cache: 'no-store'
|
|
85
82
|
});
|
|
86
83
|
}
|
|
87
84
|
catch (err) {
|
|
88
|
-
// No cadence named
|
|
89
|
-
//
|
|
90
|
-
|
|
91
|
-
console.warn(`${LOG_PREFIX} status probe failed; panels dock again once a later probe succeeds.`, err);
|
|
92
|
-
if (mine === probeGeneration) {
|
|
93
|
-
status = null;
|
|
94
|
-
}
|
|
85
|
+
// No cadence named: this also prints from the activation probe, before
|
|
86
|
+
// the interval and the wake listeners are armed (DEF-122).
|
|
87
|
+
console.warn(`${LOG_PREFIX} status probe failed; the last known roster stands (every enabled assistant, when there is none yet) until a later probe answers.`, err);
|
|
95
88
|
}
|
|
96
89
|
};
|
|
97
90
|
const statusFor = (id) => { var _a, _b; return (_b = (_a = status === null || status === void 0 ? void 0 : status.providers) === null || _a === void 0 ? void 0 : _a.find(p => p.id === id)) !== null && _b !== void 0 ? _b : null; };
|
|
@@ -153,7 +146,10 @@ const plugin = {
|
|
|
153
146
|
descriptor: module.descriptor,
|
|
154
147
|
hooks: module.hooks,
|
|
155
148
|
rootDir: (_a = status === null || status === void 0 ? void 0 : status.root_dir) !== null && _a !== void 0 ? _a : '',
|
|
156
|
-
|
|
149
|
+
// Unknown reads as "not trash": the popup then arms its Delete
|
|
150
|
+
// button before it executes, which is the only confirmation a
|
|
151
|
+
// permanent bulk delete has (DEF-135).
|
|
152
|
+
deleteToTrash: status ? status.delete_to_trash !== false : false,
|
|
157
153
|
terminalTracker,
|
|
158
154
|
fileBrowser,
|
|
159
155
|
colourfulTabs
|
|
@@ -188,7 +184,7 @@ const plugin = {
|
|
|
188
184
|
/** Bring the docked panels in line with settings and CLI availability.
|
|
189
185
|
* Both must hold for a panel to be present. */
|
|
190
186
|
const reconcile = () => {
|
|
191
|
-
var _a, _b, _c, _d;
|
|
187
|
+
var _a, _b, _c, _d, _e;
|
|
192
188
|
for (const descriptor of registry.descriptors) {
|
|
193
189
|
const id = descriptor.id;
|
|
194
190
|
if (suppressed.has(id)) {
|
|
@@ -196,16 +192,22 @@ const plugin = {
|
|
|
196
192
|
}
|
|
197
193
|
const enabled = setting(`providers.${id}.enabled`, true) !== false;
|
|
198
194
|
const probe = statusFor(id);
|
|
199
|
-
|
|
195
|
+
// Unknown is not absent. With no roster yet, an enabled assistant is
|
|
196
|
+
// docked: JupyterLab restores the sidebar tab it had open only if that
|
|
197
|
+
// tab exists when activation ends, and it waits for activation but
|
|
198
|
+
// not for a later probe - so a panel that arrives a minute late is one
|
|
199
|
+
// the layout has already given up on (DEF-132). Once a roster exists
|
|
200
|
+
// the server's word on each binary is final.
|
|
201
|
+
const roster = status;
|
|
202
|
+
const available = roster === null ? true : probe !== null && probe.available !== false;
|
|
200
203
|
// Enabled but binary absent must not be a silent state: the user sees
|
|
201
204
|
// a missing panel with no clue that PATH under the Jupyter server
|
|
202
205
|
// differs from their shell. Once per id, not per reconcile.
|
|
203
206
|
// Only when the probe SUCCEEDED and this provider is the one missing.
|
|
204
|
-
//
|
|
205
|
-
//
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
// user has never heard of. The console line stays for that case.
|
|
207
|
+
// Blaming PATH for a server the frontend never reached names the wrong
|
|
208
|
+
// cause; and every provider defaults to enabled, so a machine with one
|
|
209
|
+
// assistant installed would otherwise raise three toasts about assistants
|
|
210
|
+
// the user has never heard of. The console line stays for that case.
|
|
209
211
|
const chosen = (settings === null || settings === void 0 ? void 0 : settings.get(`providers.${id}.enabled`).user) !== undefined;
|
|
210
212
|
if (probe !== null &&
|
|
211
213
|
probe.available === false &&
|
|
@@ -226,7 +228,13 @@ const plugin = {
|
|
|
226
228
|
}
|
|
227
229
|
if (enabled && available) {
|
|
228
230
|
if (live.has(id)) {
|
|
229
|
-
|
|
231
|
+
const panel = live.get(id).panel;
|
|
232
|
+
applySharedSettings(panel);
|
|
233
|
+
// A panel docked before the first roster was built without the
|
|
234
|
+
// server root, and cannot resolve paths until it has one.
|
|
235
|
+
if (roster !== null) {
|
|
236
|
+
panel.setRoot((_e = roster.root_dir) !== null && _e !== void 0 ? _e : '', roster.delete_to_trash !== false);
|
|
237
|
+
}
|
|
230
238
|
}
|
|
231
239
|
else {
|
|
232
240
|
start(id);
|
|
@@ -323,8 +331,8 @@ const plugin = {
|
|
|
323
331
|
void probeStatus().then(reconcile);
|
|
324
332
|
}, STATUS_INTERVAL_MS);
|
|
325
333
|
// A suspended network stack - laptop asleep, tab backgrounded - is exactly
|
|
326
|
-
// when the probe fails, and the slow cadence above would leave the
|
|
327
|
-
//
|
|
334
|
+
// when the probe fails, and the slow cadence above would leave the roster
|
|
335
|
+
// stale for up to a minute after the machine comes back. A wake fires
|
|
328
336
|
// both events, so it costs two probes; deliberately, because a guard that
|
|
329
337
|
// let the second join the first answered the wake with a request issued
|
|
330
338
|
// before it, and starved the interval above of its own tick (DEF-117).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jupyterlab_ai_code_assistants_extension",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.39",
|
|
4
4
|
"description": "Jupyterlab extension to replace individual jupyterlab_*_code_extension and integrate into one, that offers joint capability, joint settings page and individual right toolbar panels per code assistant, with the ability in settings to decide which code assistant support is on (default - all are enabled)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -622,3 +622,43 @@ describe('DEF-46 - the row tooltip names only a mode the row launches with', ()
|
|
|
622
622
|
expect(text).not.toContain('Launch mode');
|
|
623
623
|
});
|
|
624
624
|
});
|
|
625
|
+
|
|
626
|
+
describe('DEF-132 - a panel docked before the first roster has no root', () => {
|
|
627
|
+
function rootless(): AssistantSessionsPanel {
|
|
628
|
+
return new AssistantSessionsPanel({
|
|
629
|
+
app: {
|
|
630
|
+
serviceManager: { serverSettings: {} },
|
|
631
|
+
commands: { execute: jest.fn() }
|
|
632
|
+
} as any,
|
|
633
|
+
descriptor: { ...DESCRIPTOR, iconName: 'testbed-rootless-panel-spec' },
|
|
634
|
+
rootDir: '',
|
|
635
|
+
fileBrowser: { model: { path: 'data/raw' } } as any
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
it('the + button is inert until setRoot supplies the root', async () => {
|
|
640
|
+
const p = rootless();
|
|
641
|
+
await flush();
|
|
642
|
+
request.mockClear();
|
|
643
|
+
|
|
644
|
+
// Without the guard this is `/data/raw` - a folder at the filesystem
|
|
645
|
+
// root, which the server accepts whenever it happens to exist.
|
|
646
|
+
expect((p as any)._currentFolder()).toBe('');
|
|
647
|
+
await (p as any)._newSession();
|
|
648
|
+
expect(request).not.toHaveBeenCalled();
|
|
649
|
+
// And the button says so, rather than looking live and doing nothing.
|
|
650
|
+
const newBtn = p.node.querySelector<HTMLButtonElement>(
|
|
651
|
+
'.jp-AiAssistantsPanel-iconButton'
|
|
652
|
+
)!;
|
|
653
|
+
expect(newBtn.disabled).toBe(true);
|
|
654
|
+
expect(newBtn.title).toBe('Waiting for the server root');
|
|
655
|
+
|
|
656
|
+
p.setRoot('/srv/lab/', true);
|
|
657
|
+
expect((p as any)._currentFolder()).toBe('/srv/lab/data/raw');
|
|
658
|
+
expect(newBtn.disabled).toBe(false);
|
|
659
|
+
expect(newBtn.title).toMatch(/^New session in current folder/);
|
|
660
|
+
p.setRoot('/', true);
|
|
661
|
+
expect((p as any)._currentFolder()).toBe('/data/raw');
|
|
662
|
+
p.dispose();
|
|
663
|
+
});
|
|
664
|
+
});
|
package/src/core/panel.ts
CHANGED
|
@@ -171,13 +171,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
171
171
|
this._descriptor = options.descriptor;
|
|
172
172
|
this._hooks = options.hooks ?? {};
|
|
173
173
|
this._serverSettings = options.app.serviceManager.serverSettings;
|
|
174
|
-
|
|
175
|
-
// `/` used to normalise to the empty string, which reads as "no root" -
|
|
176
|
-
// the + button then did nothing at all, and Open Terminal and Show in File
|
|
177
|
-
// Browser answered "outside the JupyterLab root" for every row inside it.
|
|
178
|
-
const root = (options.rootDir || '').replace(/\/+$/, '');
|
|
179
|
-
this._rootDir = root || (options.rootDir ? '/' : '');
|
|
180
|
-
this._deleteToTrash = options.deleteToTrash !== false;
|
|
174
|
+
this.setRoot(options.rootDir, options.deleteToTrash !== false);
|
|
181
175
|
this._fileBrowser = options.fileBrowser ?? null;
|
|
182
176
|
this._expandedKey = `jupyterlab_ai_code_assistants_extension:${this._descriptor.id}:expanded`;
|
|
183
177
|
this._expanded = loadExpanded(this._expandedKey);
|
|
@@ -323,8 +317,15 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
323
317
|
// and this button LAUNCHES on click instead of dropping a menu, so without
|
|
324
318
|
// the suffix the one surface that starts an approval-free session is the
|
|
325
319
|
// only one that never says so (DEF-36, DEF-113, DEF-114).
|
|
320
|
+
// Disabled, not silent, while the server root is unknown (a panel docked
|
|
321
|
+
// before the first status roster, DEF-132): a healthy-looking button that
|
|
322
|
+
// does nothing on click cannot be diagnosed by the user. `setRoot` re-runs
|
|
323
|
+
// this once the root lands, so the state is data-driven, not timed.
|
|
326
324
|
const nameNewButton = (): void => {
|
|
327
|
-
newBtn.
|
|
325
|
+
newBtn.disabled = !this._rootDir;
|
|
326
|
+
newBtn.title = this._rootDir
|
|
327
|
+
? `New session in current folder${this._variantSuffix()}`
|
|
328
|
+
: 'Waiting for the server root';
|
|
328
329
|
};
|
|
329
330
|
// Focus as well as hover, or the icon-only button's accessible name is the
|
|
330
331
|
// generic one for everybody not using a mouse.
|
|
@@ -841,9 +842,32 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
841
842
|
}
|
|
842
843
|
}
|
|
843
844
|
|
|
845
|
+
/** The server root every path is shown and resolved against, plus
|
|
846
|
+
* JupyterLab's move-to-trash setting. Settable because a panel docked before
|
|
847
|
+
* the server has answered its first status probe is built without either
|
|
848
|
+
* (DEF-132); the action paths read them lazily, so a late value applies on
|
|
849
|
+
* the next click, while rows already rendered keep absolute paths until the
|
|
850
|
+
* next poll.
|
|
851
|
+
*
|
|
852
|
+
* Trailing slashes go, but never the root itself: serving JupyterLab at
|
|
853
|
+
* `/` used to normalise to the empty string, which reads as "no root" -
|
|
854
|
+
* the + button then did nothing at all, and Open Terminal and Show in File
|
|
855
|
+
* Browser answered "outside the JupyterLab root" for every row inside it. */
|
|
856
|
+
setRoot(rootDir: string, deleteToTrash: boolean): void {
|
|
857
|
+
const root = (rootDir || '').replace(/\/+$/, '');
|
|
858
|
+
this._rootDir = root || (rootDir ? '/' : '');
|
|
859
|
+
this._deleteToTrash = deleteToTrash;
|
|
860
|
+
this._renameNewButton?.();
|
|
861
|
+
}
|
|
862
|
+
|
|
844
863
|
/** Absolute path of the file browser's current folder; the server root when
|
|
845
864
|
* no file browser is available. */
|
|
846
865
|
private _currentFolder(): string {
|
|
866
|
+
// No roster yet (DEF-132): a relative folder joined to an empty root
|
|
867
|
+
// would be an absolute path at the filesystem root.
|
|
868
|
+
if (!this._rootDir) {
|
|
869
|
+
return '';
|
|
870
|
+
}
|
|
847
871
|
const rel = (this._fileBrowser?.model?.path ?? '').replace(/^\/+/, '');
|
|
848
872
|
if (!rel) {
|
|
849
873
|
return this._rootDir;
|
|
@@ -2603,8 +2627,8 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2603
2627
|
private readonly _descriptor: IProviderDescriptor;
|
|
2604
2628
|
private readonly _hooks: IProviderHooks;
|
|
2605
2629
|
private readonly _serverSettings: ServerConnection.ISettings;
|
|
2606
|
-
private
|
|
2607
|
-
private
|
|
2630
|
+
private _rootDir = '';
|
|
2631
|
+
private _deleteToTrash = true;
|
|
2608
2632
|
private readonly _fileBrowser: IDefaultFileBrowser | null;
|
|
2609
2633
|
private readonly _colours: ColourStore;
|
|
2610
2634
|
private readonly _terminals: TerminalManager;
|
package/src/index.ts
CHANGED
|
@@ -116,37 +116,29 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
116
116
|
let settings: ISettingRegistry.ISettings | null = null;
|
|
117
117
|
let sidebar: Sidebar = DEFAULT_SIDEBAR;
|
|
118
118
|
|
|
119
|
-
let probeGeneration = 0;
|
|
120
|
-
|
|
121
119
|
/** Ask the server which providers exist and which of their binaries are on
|
|
122
|
-
* PATH.
|
|
123
|
-
* panels until a later probe succeeds - never a dialog, never a thrown
|
|
124
|
-
* activation.
|
|
120
|
+
* PATH.
|
|
125
121
|
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
122
|
+
* `status` is the last roster the server gave, and null means it has never
|
|
123
|
+
* given one - two different facts that used to share one value. A probe
|
|
124
|
+
* that FAILS writes nothing: not answering is not an answer, and reading it
|
|
125
|
+
* as "no assistant is installed" is what emptied the sidebar on every
|
|
126
|
+
* reload whose first request landed before the server was listening
|
|
127
|
+
* (DEF-132), and on every wake whose probe fired before the network was
|
|
128
|
+
* back (DEF-119). No request is suppressed, and a failure never needs
|
|
129
|
+
* ordering against a success, because it has nothing to write. */
|
|
133
130
|
const probeStatus = async (): Promise<void> => {
|
|
134
|
-
const mine = ++probeGeneration;
|
|
135
131
|
try {
|
|
136
132
|
status = await requestAPI<IStatusResponse>('status', serverSettings, {
|
|
137
133
|
cache: 'no-store'
|
|
138
134
|
});
|
|
139
135
|
} catch (err) {
|
|
140
|
-
// No cadence named
|
|
141
|
-
//
|
|
142
|
-
// after `await migrate()` below (DEF-122).
|
|
136
|
+
// No cadence named: this also prints from the activation probe, before
|
|
137
|
+
// the interval and the wake listeners are armed (DEF-122).
|
|
143
138
|
console.warn(
|
|
144
|
-
`${LOG_PREFIX} status probe failed;
|
|
139
|
+
`${LOG_PREFIX} status probe failed; the last known roster stands (every enabled assistant, when there is none yet) until a later probe answers.`,
|
|
145
140
|
err
|
|
146
141
|
);
|
|
147
|
-
if (mine === probeGeneration) {
|
|
148
|
-
status = null;
|
|
149
|
-
}
|
|
150
142
|
}
|
|
151
143
|
};
|
|
152
144
|
|
|
@@ -214,7 +206,10 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
214
206
|
descriptor: module.descriptor,
|
|
215
207
|
hooks: module.hooks,
|
|
216
208
|
rootDir: status?.root_dir ?? '',
|
|
217
|
-
|
|
209
|
+
// Unknown reads as "not trash": the popup then arms its Delete
|
|
210
|
+
// button before it executes, which is the only confirmation a
|
|
211
|
+
// permanent bulk delete has (DEF-135).
|
|
212
|
+
deleteToTrash: status ? status.delete_to_trash !== false : false,
|
|
218
213
|
terminalTracker,
|
|
219
214
|
fileBrowser,
|
|
220
215
|
colourfulTabs
|
|
@@ -258,16 +253,23 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
258
253
|
const enabled =
|
|
259
254
|
setting<boolean>(`providers.${id}.enabled`, true) !== false;
|
|
260
255
|
const probe = statusFor(id);
|
|
261
|
-
|
|
256
|
+
// Unknown is not absent. With no roster yet, an enabled assistant is
|
|
257
|
+
// docked: JupyterLab restores the sidebar tab it had open only if that
|
|
258
|
+
// tab exists when activation ends, and it waits for activation but
|
|
259
|
+
// not for a later probe - so a panel that arrives a minute late is one
|
|
260
|
+
// the layout has already given up on (DEF-132). Once a roster exists
|
|
261
|
+
// the server's word on each binary is final.
|
|
262
|
+
const roster = status;
|
|
263
|
+
const available =
|
|
264
|
+
roster === null ? true : probe !== null && probe.available !== false;
|
|
262
265
|
// Enabled but binary absent must not be a silent state: the user sees
|
|
263
266
|
// a missing panel with no clue that PATH under the Jupyter server
|
|
264
267
|
// differs from their shell. Once per id, not per reconcile.
|
|
265
268
|
// Only when the probe SUCCEEDED and this provider is the one missing.
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
// user has never heard of. The console line stays for that case.
|
|
269
|
+
// Blaming PATH for a server the frontend never reached names the wrong
|
|
270
|
+
// cause; and every provider defaults to enabled, so a machine with one
|
|
271
|
+
// assistant installed would otherwise raise three toasts about assistants
|
|
272
|
+
// the user has never heard of. The console line stays for that case.
|
|
271
273
|
const chosen =
|
|
272
274
|
settings?.get(`providers.${id}.enabled`).user !== undefined;
|
|
273
275
|
if (
|
|
@@ -296,7 +298,16 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
296
298
|
}
|
|
297
299
|
if (enabled && available) {
|
|
298
300
|
if (live.has(id)) {
|
|
299
|
-
|
|
301
|
+
const panel = live.get(id)!.panel;
|
|
302
|
+
applySharedSettings(panel);
|
|
303
|
+
// A panel docked before the first roster was built without the
|
|
304
|
+
// server root, and cannot resolve paths until it has one.
|
|
305
|
+
if (roster !== null) {
|
|
306
|
+
panel.setRoot(
|
|
307
|
+
roster.root_dir ?? '',
|
|
308
|
+
roster.delete_to_trash !== false
|
|
309
|
+
);
|
|
310
|
+
}
|
|
300
311
|
} else {
|
|
301
312
|
start(id);
|
|
302
313
|
}
|
|
@@ -410,8 +421,8 @@ const plugin: JupyterFrontEndPlugin<void> = {
|
|
|
410
421
|
}, STATUS_INTERVAL_MS);
|
|
411
422
|
|
|
412
423
|
// A suspended network stack - laptop asleep, tab backgrounded - is exactly
|
|
413
|
-
// when the probe fails, and the slow cadence above would leave the
|
|
414
|
-
//
|
|
424
|
+
// when the probe fails, and the slow cadence above would leave the roster
|
|
425
|
+
// stale for up to a minute after the machine comes back. A wake fires
|
|
415
426
|
// both events, so it costs two probes; deliberately, because a guard that
|
|
416
427
|
// let the second join the first answered the wake with a request issued
|
|
417
428
|
// before it, and starved the interval above of its own tick (DEF-117).
|
package/style/base.css
CHANGED
|
@@ -61,7 +61,14 @@
|
|
|
61
61
|
color: var(--jp-ui-font-color2);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
/* The + button while the server root is unknown (DEF-132): the glyph is an
|
|
65
|
+
icon fill, not `color`, so the UA's disabled styling never reaches it. */
|
|
66
|
+
.jp-AiAssistantsPanel-iconButton:disabled {
|
|
67
|
+
cursor: default;
|
|
68
|
+
opacity: 0.4;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.jp-AiAssistantsPanel-iconButton:hover:not(:disabled) {
|
|
65
72
|
background: var(--jp-layout-color3);
|
|
66
73
|
color: var(--jp-ui-font-color1);
|
|
67
74
|
}
|