jupyterlab_ai_code_assistants_extension 0.6.15
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/LICENSE +29 -0
- package/README.md +44 -0
- package/lib/core/colour.d.ts +57 -0
- package/lib/core/colour.js +190 -0
- package/lib/core/icons.d.ts +12 -0
- package/lib/core/icons.js +99 -0
- package/lib/core/modes.d.ts +13 -0
- package/lib/core/modes.js +36 -0
- package/lib/core/panel.d.ts +233 -0
- package/lib/core/panel.js +1673 -0
- package/lib/core/popup.d.ts +40 -0
- package/lib/core/popup.js +404 -0
- package/lib/core/registry.d.ts +24 -0
- package/lib/core/registry.js +48 -0
- package/lib/core/request.d.ts +25 -0
- package/lib/core/request.js +64 -0
- package/lib/core/terminals.d.ts +129 -0
- package/lib/core/terminals.js +344 -0
- package/lib/core/types.d.ts +269 -0
- package/lib/core/types.js +6 -0
- package/lib/index.d.ts +3 -0
- package/lib/index.js +277 -0
- package/lib/providers/claude.d.ts +3 -0
- package/lib/providers/claude.js +88 -0
- package/lib/providers/codex.d.ts +3 -0
- package/lib/providers/codex.js +79 -0
- package/lib/providers/gemini.d.ts +2 -0
- package/lib/providers/gemini.js +73 -0
- package/lib/providers/index.d.ts +3 -0
- package/lib/providers/index.js +16 -0
- package/lib/providers/kimi.d.ts +23 -0
- package/lib/providers/kimi.js +139 -0
- package/package.json +169 -0
- package/schema/plugin.json +93 -0
- package/src/__tests__/colour.spec.ts +153 -0
- package/src/__tests__/core-neutrality.spec.ts +86 -0
- package/src/__tests__/labels.spec.ts +89 -0
- package/src/__tests__/launch-mode.spec.ts +53 -0
- package/src/__tests__/registry.spec.ts +178 -0
- package/src/core/colour.ts +224 -0
- package/src/core/icons.ts +121 -0
- package/src/core/modes.ts +45 -0
- package/src/core/panel.ts +2020 -0
- package/src/core/popup.ts +475 -0
- package/src/core/registry.ts +60 -0
- package/src/core/request.ts +96 -0
- package/src/core/terminals.ts +446 -0
- package/src/core/types.ts +305 -0
- package/src/index.ts +356 -0
- package/src/providers/claude.ts +102 -0
- package/src/providers/codex.ts +89 -0
- package/src/providers/gemini.ts +80 -0
- package/src/providers/index.ts +21 -0
- package/src/providers/kimi.ts +155 -0
- package/style/base.css +888 -0
- package/style/index.css +1 -0
- package/style/index.js +1 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { IBranch } from './types';
|
|
2
|
+
/**
|
|
3
|
+
* The Manage Sessions popup: a searchable, scrollable table over every
|
|
4
|
+
* conversation of one project, with the current one pinned at the top,
|
|
5
|
+
* checkbox multi-select and a two-step delete.
|
|
6
|
+
*
|
|
7
|
+
* Its own module because it is a screen, not a panel detail - the panel hands
|
|
8
|
+
* it a branch list and four callbacks and never sees its DOM.
|
|
9
|
+
*/
|
|
10
|
+
export declare namespace ManageSessionsPopup {
|
|
11
|
+
interface IOptions {
|
|
12
|
+
/** Conversations other than the current one. */
|
|
13
|
+
branches: IBranch[];
|
|
14
|
+
/** The project's current conversation id, pinned at the top. */
|
|
15
|
+
current: string;
|
|
16
|
+
/** Display name of the row this popup was opened from. */
|
|
17
|
+
projectName: string;
|
|
18
|
+
/** Whether deletions move to trash - the popup says which it means. */
|
|
19
|
+
deleteToTrash: boolean;
|
|
20
|
+
/** Display name for one branch, `<label> (<short id>)` by default. */
|
|
21
|
+
branchName: (branch: IBranch) => string;
|
|
22
|
+
/** Relative-time label for a branch. */
|
|
23
|
+
formatTime: (epochMs: number) => string;
|
|
24
|
+
/** Provider chip for a branch row, or null. */
|
|
25
|
+
branchBadge?: (branch: IBranch) => HTMLElement | null;
|
|
26
|
+
/** Make a conversation the row's current one. */
|
|
27
|
+
onSwitch: (sessionId: string) => void;
|
|
28
|
+
/** Open a conversation in its own terminal. */
|
|
29
|
+
onOpen: (sessionId: string) => void;
|
|
30
|
+
/** Delete conversations; resolves with the removed count, or null on a
|
|
31
|
+
* failure the caller has already reported. */
|
|
32
|
+
onDelete: (sessionIds: string[]) => Promise<number | null>;
|
|
33
|
+
/** Re-read the branch list from the server. */
|
|
34
|
+
onRefetch: () => Promise<IBranch[]>;
|
|
35
|
+
/** Called with the fresh list whenever the popup re-syncs, so the panel's
|
|
36
|
+
* own cached branch list does not drift from what is on screen. */
|
|
37
|
+
onSync?: (branches: IBranch[]) => void;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export declare function showManageSessionsPopup(options: ManageSessionsPopup.IOptions): void;
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
import { Clipboard, Dialog } from '@jupyterlab/apputils';
|
|
2
|
+
import { copyIcon } from '@jupyterlab/ui-components';
|
|
3
|
+
import { Widget } from '@lumino/widgets';
|
|
4
|
+
import { branchIcon } from './icons';
|
|
5
|
+
/** Compact copy button for a popup row. `stopPropagation` keeps the click from
|
|
6
|
+
* switching or selecting the row it sits in. */
|
|
7
|
+
function copyButton(sessionId) {
|
|
8
|
+
const btn = document.createElement('button');
|
|
9
|
+
btn.type = 'button';
|
|
10
|
+
btn.className = 'jp-AiAssistantsPanel-branchCopy';
|
|
11
|
+
btn.title = 'Copy session id';
|
|
12
|
+
copyIcon.element({ container: btn });
|
|
13
|
+
btn.addEventListener('click', e => {
|
|
14
|
+
e.stopPropagation();
|
|
15
|
+
Clipboard.copyToSystem(sessionId);
|
|
16
|
+
});
|
|
17
|
+
return btn;
|
|
18
|
+
}
|
|
19
|
+
export function showManageSessionsPopup(options) {
|
|
20
|
+
// Local working copy so deletions can refresh the list in place.
|
|
21
|
+
let items = [...options.branches];
|
|
22
|
+
const selected = new Set();
|
|
23
|
+
let deleting = false; // guards the async delete against double invocation
|
|
24
|
+
// Permanent deletion arms first and executes on the second click; a trashed
|
|
25
|
+
// deletion is recoverable and goes straight through.
|
|
26
|
+
const armsFirst = !options.deleteToTrash;
|
|
27
|
+
let armed = false;
|
|
28
|
+
let armTimer = 0;
|
|
29
|
+
const body = document.createElement('div');
|
|
30
|
+
body.className = 'jp-AiAssistantsPanel-branchPopup';
|
|
31
|
+
const search = document.createElement('input');
|
|
32
|
+
search.type = 'search';
|
|
33
|
+
search.placeholder = 'Filter sessions...';
|
|
34
|
+
search.setAttribute('aria-label', 'Filter sessions');
|
|
35
|
+
search.className = 'jp-AiAssistantsPanel-branchSearch';
|
|
36
|
+
body.appendChild(search);
|
|
37
|
+
// Header strip: select-all left, conversation count right.
|
|
38
|
+
const header = document.createElement('div');
|
|
39
|
+
header.className = 'jp-AiAssistantsPanel-branchHeader';
|
|
40
|
+
const selectAllBar = document.createElement('label');
|
|
41
|
+
selectAllBar.className = 'jp-AiAssistantsPanel-branchSelectAll';
|
|
42
|
+
const selectAll = document.createElement('input');
|
|
43
|
+
selectAll.type = 'checkbox';
|
|
44
|
+
selectAllBar.appendChild(selectAll);
|
|
45
|
+
selectAllBar.appendChild(document.createTextNode('Select all'));
|
|
46
|
+
header.appendChild(selectAllBar);
|
|
47
|
+
const countEl = document.createElement('span');
|
|
48
|
+
countEl.className = 'jp-AiAssistantsPanel-branchHeaderCount';
|
|
49
|
+
header.appendChild(countEl);
|
|
50
|
+
body.appendChild(header);
|
|
51
|
+
const list = document.createElement('div');
|
|
52
|
+
list.className = 'jp-AiAssistantsPanel-branchList';
|
|
53
|
+
// role=group makes the aria-label apply to the conversation list region.
|
|
54
|
+
list.setAttribute('role', 'group');
|
|
55
|
+
list.setAttribute('aria-label', 'Conversations');
|
|
56
|
+
body.appendChild(list);
|
|
57
|
+
const footer = document.createElement('div');
|
|
58
|
+
footer.className = 'jp-AiAssistantsPanel-branchFooter';
|
|
59
|
+
// Plain visual counter (selection, or the last action). The screen-reader
|
|
60
|
+
// announcement lives in its own live region so per-checkbox ticks are not
|
|
61
|
+
// announced on top of the native checkbox.
|
|
62
|
+
const selCount = document.createElement('span');
|
|
63
|
+
selCount.className = 'jp-AiAssistantsPanel-branchSelCount';
|
|
64
|
+
footer.appendChild(selCount);
|
|
65
|
+
const deleteBtn = document.createElement('button');
|
|
66
|
+
deleteBtn.type = 'button';
|
|
67
|
+
deleteBtn.className = 'jp-AiAssistantsPanel-branchDelete';
|
|
68
|
+
deleteBtn.title = options.deleteToTrash
|
|
69
|
+
? 'Deleted conversations move to the trash (recoverable)'
|
|
70
|
+
: 'Deleted conversations are removed permanently';
|
|
71
|
+
footer.appendChild(deleteBtn);
|
|
72
|
+
body.appendChild(footer);
|
|
73
|
+
// Visually hidden polite live region, announced only on delete.
|
|
74
|
+
const srLive = document.createElement('div');
|
|
75
|
+
srLive.className = 'jp-AiAssistantsPanel-srOnly';
|
|
76
|
+
srLive.setAttribute('role', 'status');
|
|
77
|
+
srLive.setAttribute('aria-live', 'polite');
|
|
78
|
+
body.appendChild(srLive);
|
|
79
|
+
const bodyWidget = new Widget({ node: body });
|
|
80
|
+
const dialog = new Dialog({
|
|
81
|
+
title: 'Manage Sessions',
|
|
82
|
+
body: bodyWidget,
|
|
83
|
+
buttons: [Dialog.cancelButton()]
|
|
84
|
+
});
|
|
85
|
+
// Per-row "Open" launches that conversation in its own terminal and closes
|
|
86
|
+
// the popup. stopPropagation keeps the click from toggling selection or
|
|
87
|
+
// switching the row.
|
|
88
|
+
const openButton = (sessionId) => {
|
|
89
|
+
const btn = document.createElement('button');
|
|
90
|
+
btn.type = 'button';
|
|
91
|
+
btn.className = 'jp-AiAssistantsPanel-branchOpen';
|
|
92
|
+
btn.textContent = 'Open';
|
|
93
|
+
btn.title = 'Open this conversation in its own terminal';
|
|
94
|
+
btn.addEventListener('click', e => {
|
|
95
|
+
e.stopPropagation();
|
|
96
|
+
dialog.dispose();
|
|
97
|
+
options.onOpen(sessionId);
|
|
98
|
+
});
|
|
99
|
+
return btn;
|
|
100
|
+
};
|
|
101
|
+
const visibleMatches = () => {
|
|
102
|
+
const needle = search.value.trim().toLowerCase();
|
|
103
|
+
return items.filter(b => !needle ||
|
|
104
|
+
b.label.toLowerCase().includes(needle) ||
|
|
105
|
+
b.session_id.toLowerCase().includes(needle));
|
|
106
|
+
};
|
|
107
|
+
/** Drop the armed state - the count an armed button names must never outlive
|
|
108
|
+
* the selection it was armed for. Callers refresh the button themselves. */
|
|
109
|
+
const disarm = () => {
|
|
110
|
+
armed = false;
|
|
111
|
+
window.clearTimeout(armTimer);
|
|
112
|
+
};
|
|
113
|
+
const updateControls = () => {
|
|
114
|
+
deleteBtn.disabled = deleting || selected.size === 0;
|
|
115
|
+
deleteBtn.textContent = deleting
|
|
116
|
+
? 'Deleting...'
|
|
117
|
+
: armed
|
|
118
|
+
? `Confirm delete ${selected.size}`
|
|
119
|
+
: `Delete (${selected.size})`;
|
|
120
|
+
deleteBtn.classList.toggle('jp-mod-armed', armed && !deleting);
|
|
121
|
+
selCount.textContent = selected.size ? `${selected.size} selected` : '';
|
|
122
|
+
selCount.classList.remove('jp-mod-deleted');
|
|
123
|
+
const visible = visibleMatches();
|
|
124
|
+
const total = items.length + 1; // + the pinned current conversation
|
|
125
|
+
const shown = visible.length + 1; // the current row is always shown
|
|
126
|
+
countEl.textContent = search.value.trim()
|
|
127
|
+
? `${shown} of ${total}`
|
|
128
|
+
: `${total} conversation${total === 1 ? '' : 's'}`;
|
|
129
|
+
const visibleSelected = visible.filter(b => selected.has(b.session_id)).length;
|
|
130
|
+
selectAll.checked =
|
|
131
|
+
visible.length > 0 && visibleSelected === visible.length;
|
|
132
|
+
selectAll.indeterminate =
|
|
133
|
+
visibleSelected > 0 && visibleSelected < visible.length;
|
|
134
|
+
};
|
|
135
|
+
const render = () => {
|
|
136
|
+
var _a, _b;
|
|
137
|
+
list.replaceChildren();
|
|
138
|
+
// The current conversation leads the list - badged, unselectable,
|
|
139
|
+
// undeletable; only the extras below it are manageable.
|
|
140
|
+
const currentRow = document.createElement('div');
|
|
141
|
+
currentRow.className = 'jp-AiAssistantsPanel-branchRow jp-mod-current';
|
|
142
|
+
currentRow.title = `Session id: ${options.current}`;
|
|
143
|
+
// The brand left-bar, tint and plain "current" text are visual-only cues.
|
|
144
|
+
currentRow.setAttribute('aria-current', 'true');
|
|
145
|
+
// Empty select cell keeps the name column aligned with branch rows.
|
|
146
|
+
const currentSelect = document.createElement('span');
|
|
147
|
+
currentSelect.className = 'jp-AiAssistantsPanel-branchSelectCell';
|
|
148
|
+
currentRow.appendChild(currentSelect);
|
|
149
|
+
const currentLabel = document.createElement('span');
|
|
150
|
+
currentLabel.className = 'jp-AiAssistantsPanel-branchLabel';
|
|
151
|
+
// Text in its own ellipsising span so any chip survives truncation - the
|
|
152
|
+
// same structure the branch rows below use.
|
|
153
|
+
const currentText = document.createElement('span');
|
|
154
|
+
currentText.className = 'jp-AiAssistantsPanel-branchLabelText';
|
|
155
|
+
currentText.textContent = `${options.projectName} (${options.current.slice(0, 8)})`;
|
|
156
|
+
currentLabel.appendChild(currentText);
|
|
157
|
+
currentRow.appendChild(currentLabel);
|
|
158
|
+
const badge = document.createElement('span');
|
|
159
|
+
badge.className = 'jp-AiAssistantsPanel-branchCurrentBadge';
|
|
160
|
+
badge.textContent = 'current';
|
|
161
|
+
currentRow.appendChild(badge);
|
|
162
|
+
currentRow.appendChild(openButton(options.current));
|
|
163
|
+
currentRow.appendChild(copyButton(options.current));
|
|
164
|
+
list.appendChild(currentRow);
|
|
165
|
+
const matches = visibleMatches();
|
|
166
|
+
if (matches.length === 0) {
|
|
167
|
+
const empty = document.createElement('div');
|
|
168
|
+
empty.className = 'jp-AiAssistantsPanel-emptySection';
|
|
169
|
+
empty.textContent = items.length
|
|
170
|
+
? 'No matching sessions.'
|
|
171
|
+
: 'No other conversations.';
|
|
172
|
+
list.appendChild(empty);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
for (const b of matches) {
|
|
176
|
+
const row = document.createElement('div');
|
|
177
|
+
row.className = 'jp-AiAssistantsPanel-branchRow';
|
|
178
|
+
row.title = `Session id: ${b.session_id}`;
|
|
179
|
+
const selectCell = document.createElement('span');
|
|
180
|
+
selectCell.className = 'jp-AiAssistantsPanel-branchSelectCell';
|
|
181
|
+
const check = document.createElement('input');
|
|
182
|
+
check.type = 'checkbox';
|
|
183
|
+
check.checked = selected.has(b.session_id);
|
|
184
|
+
check.setAttribute('aria-label', `Select ${options.branchName(b)}`);
|
|
185
|
+
// The checkbox is its own click zone - ticking must not switch.
|
|
186
|
+
check.addEventListener('click', e => {
|
|
187
|
+
e.stopPropagation();
|
|
188
|
+
if (deleting) {
|
|
189
|
+
// Keyboard Space is not blocked by the busy scrim's pointer-events;
|
|
190
|
+
// re-sync the native toggle instead.
|
|
191
|
+
check.checked = selected.has(b.session_id);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (check.checked) {
|
|
195
|
+
selected.add(b.session_id);
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
selected.delete(b.session_id);
|
|
199
|
+
}
|
|
200
|
+
disarm();
|
|
201
|
+
updateControls();
|
|
202
|
+
});
|
|
203
|
+
selectCell.appendChild(check);
|
|
204
|
+
// The whole cell is a select target (>=24px), not just the checkbox.
|
|
205
|
+
selectCell.addEventListener('click', e => {
|
|
206
|
+
if (e.target !== check) {
|
|
207
|
+
e.stopPropagation();
|
|
208
|
+
check.click();
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
row.appendChild(selectCell);
|
|
212
|
+
const label = document.createElement('span');
|
|
213
|
+
label.className = 'jp-AiAssistantsPanel-branchLabel';
|
|
214
|
+
// The text gets its own ellipsising span so the marker after it can
|
|
215
|
+
// never be clipped away: branch labels are titles that routinely fill
|
|
216
|
+
// the label column, and a marker inside the ellipsised span vanishes on
|
|
217
|
+
// exactly those rows.
|
|
218
|
+
const labelText = document.createElement('span');
|
|
219
|
+
labelText.className = 'jp-AiAssistantsPanel-branchLabelText';
|
|
220
|
+
labelText.textContent = options.branchName(b);
|
|
221
|
+
label.appendChild(labelText);
|
|
222
|
+
const providerBadge = (_b = (_a = options.branchBadge) === null || _a === void 0 ? void 0 : _a.call(options, b)) !== null && _b !== void 0 ? _b : null;
|
|
223
|
+
if (providerBadge) {
|
|
224
|
+
label.appendChild(providerBadge);
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
const fork = document.createElement('span');
|
|
228
|
+
fork.className = 'jp-AiAssistantsPanel-branchBadge';
|
|
229
|
+
const icon = document.createElement('span');
|
|
230
|
+
icon.className = 'jp-AiAssistantsPanel-branchBadgeIcon';
|
|
231
|
+
branchIcon.element({ container: icon });
|
|
232
|
+
fork.appendChild(icon);
|
|
233
|
+
label.appendChild(fork);
|
|
234
|
+
}
|
|
235
|
+
row.appendChild(label);
|
|
236
|
+
const time = document.createElement('span');
|
|
237
|
+
time.className = 'jp-AiAssistantsPanel-branchTime';
|
|
238
|
+
time.textContent = options.formatTime(b.file_mtime);
|
|
239
|
+
row.appendChild(time);
|
|
240
|
+
row.appendChild(openButton(b.session_id));
|
|
241
|
+
row.appendChild(copyButton(b.session_id));
|
|
242
|
+
const activate = () => {
|
|
243
|
+
// Selection mode: while anything is ticked, row clicks toggle
|
|
244
|
+
// selection - no accidental switch mid-selection.
|
|
245
|
+
if (selected.size > 0) {
|
|
246
|
+
if (selected.has(b.session_id)) {
|
|
247
|
+
selected.delete(b.session_id);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
selected.add(b.session_id);
|
|
251
|
+
}
|
|
252
|
+
check.checked = selected.has(b.session_id);
|
|
253
|
+
disarm();
|
|
254
|
+
updateControls();
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
dialog.dispose();
|
|
258
|
+
options.onSwitch(b.session_id);
|
|
259
|
+
};
|
|
260
|
+
row.addEventListener('click', activate);
|
|
261
|
+
// Switch has no button of its own, so it needs a role and a key route or
|
|
262
|
+
// it is reachable by mouse only. The affordance sits on the label span,
|
|
263
|
+
// not the row: the row also hosts a checkbox and the Open/Copy buttons,
|
|
264
|
+
// and a role="button" wrapping them both swallows their own Enter/Space
|
|
265
|
+
// and makes them presentational children (ARIA nested-interactive).
|
|
266
|
+
label.setAttribute('role', 'button');
|
|
267
|
+
label.tabIndex = 0;
|
|
268
|
+
label.addEventListener('keydown', e => {
|
|
269
|
+
// The busy scrim is pointer-events CSS and does not block keyboard.
|
|
270
|
+
if (deleting) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
274
|
+
// Space would scroll the list otherwise.
|
|
275
|
+
e.preventDefault();
|
|
276
|
+
activate();
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
list.appendChild(row);
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
selectAll.addEventListener('change', () => {
|
|
283
|
+
if (deleting) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
// Select-all acts on the visible (filtered) rows only.
|
|
287
|
+
const visible = visibleMatches();
|
|
288
|
+
if (selectAll.checked) {
|
|
289
|
+
visible.forEach(b => selected.add(b.session_id));
|
|
290
|
+
}
|
|
291
|
+
else {
|
|
292
|
+
visible.forEach(b => selected.delete(b.session_id));
|
|
293
|
+
}
|
|
294
|
+
disarm();
|
|
295
|
+
render();
|
|
296
|
+
updateControls();
|
|
297
|
+
});
|
|
298
|
+
// Busy-lock the whole body during the async delete, so a slow backend cannot
|
|
299
|
+
// be double-clicked into deleting the same set twice and a mid-flight
|
|
300
|
+
// selection cannot be silently discarded. The Dialog's Cancel button sits
|
|
301
|
+
// outside the body and stays usable.
|
|
302
|
+
const setDeleting = (on) => {
|
|
303
|
+
deleting = on;
|
|
304
|
+
body.classList.toggle('jp-mod-busy', on);
|
|
305
|
+
if (on) {
|
|
306
|
+
list.setAttribute('aria-busy', 'true');
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
list.removeAttribute('aria-busy');
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
deleteBtn.addEventListener('click', () => {
|
|
313
|
+
if (deleting || selected.size === 0) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
// No confirmation DIALOG here: a second Lumino dialog stacked on this popup
|
|
317
|
+
// renders detached. A permanent deletion still confirms, in the footer
|
|
318
|
+
// itself - the first click arms the button, the second executes, and any
|
|
319
|
+
// selection change or four seconds of hesitation disarms it. A trashed
|
|
320
|
+
// deletion is recoverable, so it keeps going through on one click, with
|
|
321
|
+
// the live "N deleted" status and a toast from the caller on failure.
|
|
322
|
+
if (armsFirst && !armed) {
|
|
323
|
+
armed = true;
|
|
324
|
+
window.clearTimeout(armTimer);
|
|
325
|
+
armTimer = window.setTimeout(() => {
|
|
326
|
+
disarm();
|
|
327
|
+
updateControls();
|
|
328
|
+
}, 4000);
|
|
329
|
+
updateControls();
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
disarm();
|
|
333
|
+
setDeleting(true);
|
|
334
|
+
updateControls();
|
|
335
|
+
void options
|
|
336
|
+
.onDelete([...selected])
|
|
337
|
+
.then(async (deleted) => {
|
|
338
|
+
var _a;
|
|
339
|
+
if (deleted === null) {
|
|
340
|
+
setDeleting(false);
|
|
341
|
+
updateControls(); // re-enable; selection unchanged
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
// Re-sync from disk truth, not an optimistic splice: a conversation
|
|
345
|
+
// the server skipped (it became the resolved-current, or was already
|
|
346
|
+
// gone) must not vanish from the list while it still exists.
|
|
347
|
+
try {
|
|
348
|
+
items = await options.onRefetch();
|
|
349
|
+
}
|
|
350
|
+
catch (_b) {
|
|
351
|
+
// Refetch failed (rare): optimistic removal. The announced count may
|
|
352
|
+
// then differ from the rows removed; the panel's own poll reconciles.
|
|
353
|
+
items = items.filter(b => !selected.has(b.session_id));
|
|
354
|
+
}
|
|
355
|
+
// The user may have closed the popup during the refetch await - do not
|
|
356
|
+
// touch shared state or a detached DOM in that case.
|
|
357
|
+
if (!bodyWidget.isAttached) {
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
setDeleting(false);
|
|
361
|
+
selected.clear();
|
|
362
|
+
(_a = options.onSync) === null || _a === void 0 ? void 0 : _a.call(options, items);
|
|
363
|
+
render();
|
|
364
|
+
updateControls();
|
|
365
|
+
const moved = options.deleteToTrash
|
|
366
|
+
? `${deleted} moved to trash`
|
|
367
|
+
: `${deleted} deleted`;
|
|
368
|
+
selCount.textContent = moved;
|
|
369
|
+
selCount.classList.add('jp-mod-deleted');
|
|
370
|
+
// Clear then re-set on the next tick so an identical count (two single
|
|
371
|
+
// deletes in a row) is still re-announced by aria-live.
|
|
372
|
+
srLive.textContent = '';
|
|
373
|
+
window.setTimeout(() => {
|
|
374
|
+
srLive.textContent = moved;
|
|
375
|
+
}, 60);
|
|
376
|
+
// Keep focus inside the dialog - render() destroyed the focused row.
|
|
377
|
+
// The select-all checkbox is a real control with a reliable keyboard
|
|
378
|
+
// ring, unless the user is still typing in the search box.
|
|
379
|
+
if (document.activeElement !== search) {
|
|
380
|
+
selectAll.focus();
|
|
381
|
+
}
|
|
382
|
+
})
|
|
383
|
+
.catch(() => {
|
|
384
|
+
// Defensive: never leave the button stuck disabled.
|
|
385
|
+
if (bodyWidget.isAttached) {
|
|
386
|
+
setDeleting(false);
|
|
387
|
+
updateControls();
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
search.addEventListener('input', () => {
|
|
392
|
+
if (deleting) {
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
render();
|
|
396
|
+
updateControls();
|
|
397
|
+
});
|
|
398
|
+
render();
|
|
399
|
+
updateControls();
|
|
400
|
+
// dialog.dispose() (on open/switch) rejects this promise with `undefined`;
|
|
401
|
+
// catch it so the teardown never surfaces as an unhandled rejection.
|
|
402
|
+
dialog.launch().catch(() => undefined);
|
|
403
|
+
search.focus();
|
|
404
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { IProviderDescriptor, IProviderModule } from './types';
|
|
2
|
+
/** Console prefix for everything this extension logs. One spelling, so a
|
|
3
|
+
* reader filtering the console on it sees every message the extension wrote. */
|
|
4
|
+
export declare const LOG_PREFIX = "[jupyterlab_ai_code_assistants_extension]";
|
|
5
|
+
/**
|
|
6
|
+
* The frontend provider registry.
|
|
7
|
+
*
|
|
8
|
+
* Built once from the `providers/index.ts` barrel. Registration is the only
|
|
9
|
+
* place a provider id is ever asserted, so a duplicate fails loudly here
|
|
10
|
+
* rather than silently at render time - two panels sharing a widget id, a
|
|
11
|
+
* command namespace and a settings key is not a state worth recovering from.
|
|
12
|
+
*/
|
|
13
|
+
export declare class ProviderRegistry {
|
|
14
|
+
constructor(modules: IProviderModule[]);
|
|
15
|
+
/** Every registered module, in barrel order. */
|
|
16
|
+
get modules(): IProviderModule[];
|
|
17
|
+
get descriptors(): IProviderDescriptor[];
|
|
18
|
+
get ids(): string[];
|
|
19
|
+
has(id: string): boolean;
|
|
20
|
+
/** The module for an id, or null. A stale id in saved settings is a warning
|
|
21
|
+
* and a null, never a hard failure. */
|
|
22
|
+
get(id: string): IProviderModule | null;
|
|
23
|
+
private readonly _modules;
|
|
24
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/** Console prefix for everything this extension logs. One spelling, so a
|
|
2
|
+
* reader filtering the console on it sees every message the extension wrote. */
|
|
3
|
+
export const LOG_PREFIX = '[jupyterlab_ai_code_assistants_extension]';
|
|
4
|
+
/**
|
|
5
|
+
* The frontend provider registry.
|
|
6
|
+
*
|
|
7
|
+
* Built once from the `providers/index.ts` barrel. Registration is the only
|
|
8
|
+
* place a provider id is ever asserted, so a duplicate fails loudly here
|
|
9
|
+
* rather than silently at render time - two panels sharing a widget id, a
|
|
10
|
+
* command namespace and a settings key is not a state worth recovering from.
|
|
11
|
+
*/
|
|
12
|
+
export class ProviderRegistry {
|
|
13
|
+
constructor(modules) {
|
|
14
|
+
// Insertion-ordered, so `modules` and `ids` come back in barrel order.
|
|
15
|
+
this._modules = new Map();
|
|
16
|
+
for (const module of modules) {
|
|
17
|
+
const descriptor = module === null || module === void 0 ? void 0 : module.descriptor;
|
|
18
|
+
if (!(descriptor === null || descriptor === void 0 ? void 0 : descriptor.id)) {
|
|
19
|
+
console.warn(`${LOG_PREFIX} ignoring a provider module with no descriptor id`);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (this._modules.has(descriptor.id)) {
|
|
23
|
+
throw new Error(`Duplicate AI code assistant provider id "${descriptor.id}" - ` +
|
|
24
|
+
'two descriptors cannot share an id.');
|
|
25
|
+
}
|
|
26
|
+
this._modules.set(descriptor.id, module);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** Every registered module, in barrel order. */
|
|
30
|
+
get modules() {
|
|
31
|
+
return [...this._modules.values()];
|
|
32
|
+
}
|
|
33
|
+
get descriptors() {
|
|
34
|
+
return this.modules.map(m => m.descriptor);
|
|
35
|
+
}
|
|
36
|
+
get ids() {
|
|
37
|
+
return [...this._modules.keys()];
|
|
38
|
+
}
|
|
39
|
+
has(id) {
|
|
40
|
+
return this._modules.has(id);
|
|
41
|
+
}
|
|
42
|
+
/** The module for an id, or null. A stale id in saved settings is a warning
|
|
43
|
+
* and a null, never a hard failure. */
|
|
44
|
+
get(id) {
|
|
45
|
+
var _a;
|
|
46
|
+
return (_a = this._modules.get(id)) !== null && _a !== void 0 ? _a : null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { ServerConnection } from '@jupyterlab/services';
|
|
2
|
+
/** The server extension's API namespace. Every route lives under it. */
|
|
3
|
+
export declare const API_NAMESPACE = "jupyterlab-ai-code-assistants-extension";
|
|
4
|
+
/**
|
|
5
|
+
* Call the server extension.
|
|
6
|
+
*
|
|
7
|
+
* @param endPoint API REST end point for the extension, relative to the
|
|
8
|
+
* extension namespace
|
|
9
|
+
* @param serverSettings The server settings to use for the request
|
|
10
|
+
* @param init Initial values for the request
|
|
11
|
+
* @returns The response body interpreted as JSON
|
|
12
|
+
*/
|
|
13
|
+
export declare function requestAPI<T>(endPoint: string, serverSettings: ServerConnection.ISettings, init?: RequestInit): Promise<T>;
|
|
14
|
+
/** Build a provider-scoped path: `providers/<id>/<path>`. The id is encoded so
|
|
15
|
+
* a malformed registration can never escape the namespace. */
|
|
16
|
+
export declare function providerPath(providerId: string, path: string): string;
|
|
17
|
+
/** Call a provider-scoped route. Every session, launch, colour and disposal
|
|
18
|
+
* call goes through here, which is what keeps one provider's traffic out of
|
|
19
|
+
* another's namespace. */
|
|
20
|
+
export declare function requestProvider<T>(providerId: string, path: string, serverSettings: ServerConnection.ISettings, init?: RequestInit): Promise<T>;
|
|
21
|
+
/** Append a query string to a route path, skipping empty values. */
|
|
22
|
+
export declare function withQuery(path: string, params: Record<string, string | undefined>): string;
|
|
23
|
+
/** True when the error is a server response carrying this status code - used
|
|
24
|
+
* to tell "the conversation is gone" apart from "the server is unreachable". */
|
|
25
|
+
export declare function isResponseStatus(err: unknown, status: number): boolean;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { URLExt } from '@jupyterlab/coreutils';
|
|
2
|
+
import { ServerConnection } from '@jupyterlab/services';
|
|
3
|
+
/** The server extension's API namespace. Every route lives under it. */
|
|
4
|
+
export const API_NAMESPACE = 'jupyterlab-ai-code-assistants-extension';
|
|
5
|
+
/**
|
|
6
|
+
* Call the server extension.
|
|
7
|
+
*
|
|
8
|
+
* @param endPoint API REST end point for the extension, relative to the
|
|
9
|
+
* extension namespace
|
|
10
|
+
* @param serverSettings The server settings to use for the request
|
|
11
|
+
* @param init Initial values for the request
|
|
12
|
+
* @returns The response body interpreted as JSON
|
|
13
|
+
*/
|
|
14
|
+
export async function requestAPI(endPoint, serverSettings, init = {}) {
|
|
15
|
+
const requestUrl = URLExt.join(serverSettings.baseUrl, API_NAMESPACE, endPoint);
|
|
16
|
+
let response;
|
|
17
|
+
try {
|
|
18
|
+
response = await ServerConnection.makeRequest(requestUrl, init, serverSettings);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
throw new ServerConnection.NetworkError(error);
|
|
22
|
+
}
|
|
23
|
+
let data = await response.text();
|
|
24
|
+
if (data.length > 0) {
|
|
25
|
+
try {
|
|
26
|
+
data = JSON.parse(data);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
console.log('Not a JSON response body.', response);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
throw new ServerConnection.ResponseError(response, data.message || data);
|
|
34
|
+
}
|
|
35
|
+
return data;
|
|
36
|
+
}
|
|
37
|
+
/** Build a provider-scoped path: `providers/<id>/<path>`. The id is encoded so
|
|
38
|
+
* a malformed registration can never escape the namespace. */
|
|
39
|
+
export function providerPath(providerId, path) {
|
|
40
|
+
return `providers/${encodeURIComponent(providerId)}/${path}`;
|
|
41
|
+
}
|
|
42
|
+
/** Call a provider-scoped route. Every session, launch, colour and disposal
|
|
43
|
+
* call goes through here, which is what keeps one provider's traffic out of
|
|
44
|
+
* another's namespace. */
|
|
45
|
+
export async function requestProvider(providerId, path, serverSettings, init = {}) {
|
|
46
|
+
return requestAPI(providerPath(providerId, path), serverSettings, init);
|
|
47
|
+
}
|
|
48
|
+
/** Append a query string to a route path, skipping empty values. */
|
|
49
|
+
export function withQuery(path, params) {
|
|
50
|
+
const parts = [];
|
|
51
|
+
for (const [key, value] of Object.entries(params)) {
|
|
52
|
+
if (value === undefined || value === '') {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
|
|
56
|
+
}
|
|
57
|
+
return parts.length ? `${path}?${parts.join('&')}` : path;
|
|
58
|
+
}
|
|
59
|
+
/** True when the error is a server response carrying this status code - used
|
|
60
|
+
* to tell "the conversation is gone" apart from "the server is unreachable". */
|
|
61
|
+
export function isResponseStatus(err, status) {
|
|
62
|
+
return (err instanceof ServerConnection.ResponseError &&
|
|
63
|
+
err.response.status === status);
|
|
64
|
+
}
|