jupyterlab_ai_code_assistants_extension 1.2.236 → 1.2.282
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 +23 -23
- package/lib/core/panel.d.ts +4 -0
- package/lib/core/panel.js +136 -17
- package/lib/core/terminals.d.ts +4 -2
- package/lib/core/terminals.js +18 -6
- package/lib/index.js +29 -15
- package/package.json +1 -1
- package/src/__tests__/index.spec.ts +91 -0
- package/src/__tests__/panel.spec.ts +590 -13
- package/src/__tests__/terminals.spec.ts +20 -0
- package/src/core/panel.ts +149 -15
- package/src/core/terminals.ts +18 -6
- package/src/index.ts +30 -15
- package/style/base.css +42 -0
package/README.md
CHANGED
|
@@ -25,30 +25,30 @@ Chat-panel extensions re-implement the agent loop and trail the real tool. This
|
|
|
25
25
|
|
|
26
26
|
## Features
|
|
27
27
|
|
|
28
|
-
- **One install, every assistant** - Claude Code, Codex, Kimi, Gemini and DeepSeek
|
|
29
|
-
- **Provider registry** - assistant-specific behaviour lives in one module per assistant
|
|
30
|
-
- **Joint settings page** - one settings section
|
|
31
|
-
- **Three-section side panel** - Favorites, Recent
|
|
32
|
-
- **One-click resume** - click a row to
|
|
33
|
-
- **File browser follows the opened session** - opening a session moves the
|
|
34
|
-
- **Launcher tiles** -
|
|
35
|
-
- **Your terminal's `claude -c` follows the panel** (Claude) -
|
|
36
|
-
- **Conversation switcher** - a right-click "Switch and Manage Sessions" submenu lists a project's other conversations by name and short id
|
|
37
|
-
- **Branch session** - fork the current conversation into a new named session
|
|
38
|
-
- **Rename session** - name a conversation from the right-click menu, and the
|
|
39
|
-
- **Launch modes under each assistant's own name** - Claude's skip-permissions, Codex's approval bypass, Kimi's `--yolo
|
|
40
|
-
- **Coloured terminal tabs** - each session's colour tints its terminal tab
|
|
41
|
-
- **Your newest colour choice wins** -
|
|
42
|
-
- **Favorites** - star projects
|
|
43
|
-
- **Remove and clean up** -
|
|
44
|
-
- **Activity at a glance** - each row shows its last activity in an aligned column
|
|
45
|
-
- **Remote control indicator and background agents** (Claude) - a green dot marks sessions
|
|
46
|
-
- **Search** - fuzzy filter per panel, toggled by the funnel button
|
|
28
|
+
- **One install, every assistant** - Claude Code, Codex, Kimi, Gemini and DeepSeek in one package, each with its own right-side panel and official mark
|
|
29
|
+
- **Provider registry** - assistant-specific behaviour lives in one module per assistant, so adding one changes no core file
|
|
30
|
+
- **Joint settings page** - one settings section covers all assistants with a per-assistant toggle, all on by default; toggling takes effect without a JupyterLab reload
|
|
31
|
+
- **Three-section side panel** - Favorites, Recent and All projects, each scrolling independently
|
|
32
|
+
- **One-click resume** - click a row to resume its session in a terminal, reusing an open terminal for the project. A Kimi or DeepSeek conversation started with `+` opens a second terminal when its first is still open
|
|
33
|
+
- **File browser follows the opened session** - opening a session moves the file browser to its project folder, a setting on by default; a right-click on a panel header toggles this and moves the panels between the left and right sidebars
|
|
34
|
+
- **Launcher tiles** - each enabled assistant has a tile in the Launcher's AI Assistants section; a click opens the assistant in the file browser's current folder, resuming that folder's conversation when one exists
|
|
35
|
+
- **Your terminal's `claude -c` follows the panel** (Claude) - selecting a conversation row makes plain `claude -c` in that project resume the same conversation, even one that has been compacted
|
|
36
|
+
- **Conversation switcher** - a right-click "Switch and Manage Sessions" submenu lists a project's other conversations by name and short id. "Manage Sessions..." opens a searchable list with multi-select delete and copy-id
|
|
37
|
+
- **Branch session** - fork the current conversation into a new named session from the right-click menu
|
|
38
|
+
- **Rename session** - name a conversation from the right-click menu, and the panel shows the name the assistant itself kept
|
|
39
|
+
- **Launch modes under each assistant's own name** - Claude's skip-permissions, Codex's approval bypass, Kimi's `--yolo` and Gemini's YOLO; unsafe variants carry a shield glyph
|
|
40
|
+
- **Coloured terminal tabs** - each session's colour tints its terminal tab, and a branched session inherits its parent's colour. Claude's `/color` supplies a default and Kimi derives a stable one; Codex, Gemini and DeepSeek have no colour until you set one
|
|
41
|
+
- **Your newest colour choice wins** - a colour set on a terminal tab is remembered for that conversation; a Kimi or DeepSeek conversation that has never been resumed, including one started with `+`, is the exception, because its id cannot be read yet. In Claude Code whichever of the tab colour and `/color` you used last wins, and Reset Tab Colour in the right-click menu hands back the colours you set by hand
|
|
42
|
+
- **Favorites** - star projects from the right-click menu; favourites from the standalone extensions are migrated on first run
|
|
43
|
+
- **Remove and clean up** - remove a project's history or its extra parallel sessions from the right-click menu, with a confirmation. Removed files honour JupyterLab's "move files to trash" setting
|
|
44
|
+
- **Activity at a glance** - each row shows its last activity in an aligned column. Recent rows light up, stale rows dim, and parallel conversations show a branch icon and count
|
|
45
|
+
- **Remote control indicator and background agents** (Claude) - a green dot marks sessions under remote control, and a `bg` chip marks a conversation held by a background agent. Clicking stops the agent and resumes the conversation in your terminal
|
|
46
|
+
- **Search** - a fuzzy filter per panel, toggled by the funnel button
|
|
47
47
|
- **Presentation modes** - label rows by session name or by path relative to the JupyterLab root
|
|
48
|
-
- **Conflict-safe upgrade** - if a retired standalone extension is still installed, its panel wins and this extension stands down for that assistant
|
|
49
|
-
- **DeepSeek in the terminal** - the
|
|
50
|
-
- **Auto-disabled when absent** - an assistant whose CLI is not on `PATH`
|
|
51
|
-
- **Open Callback** - when an assistant's login
|
|
48
|
+
- **Conflict-safe upgrade** - if a retired standalone extension is still installed, its panel wins and this extension stands down for that assistant
|
|
49
|
+
- **DeepSeek in the terminal** - the panel runs the community TUI bundle, because the DeepSeek Harness ships no terminal chat. Without the bundle a launch is refused before a terminal opens
|
|
50
|
+
- **Auto-disabled when absent** - an assistant whose CLI is not on `PATH` shows no panel
|
|
51
|
+
- **Open Callback** - when an assistant's login redirects to a loopback address that will not load, run Open Callback from the command palette and paste the link. The server makes the request and says whether the login accepted it
|
|
52
52
|
|
|
53
53
|
## Requirements
|
|
54
54
|
|
package/lib/core/panel.d.ts
CHANGED
|
@@ -382,6 +382,10 @@ export declare class AssistantSessionsPanel extends Widget {
|
|
|
382
382
|
* `_fetch`. */
|
|
383
383
|
private _fetchSent;
|
|
384
384
|
private _fetchDrawn;
|
|
385
|
+
/** The `_fetchSent` number at each removal's DELETE, by encoded path. A
|
|
386
|
+
* listing sent before that number may still carry the removed project, so
|
|
387
|
+
* `_fetch` drops it (DEF-PANE-351-WRITE-ORDER). */
|
|
388
|
+
private readonly _removedAt;
|
|
385
389
|
/** A button is pressed in the list, and a redraw waits for its release -
|
|
386
390
|
* see `_onRelease`. */
|
|
387
391
|
private _pressed;
|
package/lib/core/panel.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Clipboard, Dialog, InputDialog, Notification, showDialog } from '@jupyterlab/apputils';
|
|
2
|
-
import { MenuSvg, closeIcon, editIcon, folderIcon, terminalIcon } from '@jupyterlab/ui-components';
|
|
2
|
+
import { MenuSvg, closeIcon, copyIcon, editIcon, folderIcon, terminalIcon } from '@jupyterlab/ui-components';
|
|
3
3
|
import { nullTranslator } from '@jupyterlab/translation';
|
|
4
4
|
import { CommandRegistry } from '@lumino/commands';
|
|
5
5
|
import { UUID } from '@lumino/coreutils';
|
|
@@ -76,8 +76,9 @@ let fileBrowserMoves = Promise.resolve();
|
|
|
76
76
|
* later click does not move the file browser, so it ends on the folder of the
|
|
77
77
|
* newest click that opened (DEF-PANE-343). The same counter orders the writes
|
|
78
78
|
* that decide a project's current conversation: a row click, an explicit
|
|
79
|
-
* switch and a
|
|
80
|
-
* switch only while it is the newest (DEF-PANE-349
|
|
79
|
+
* switch, a fork and a New Session (+) each take a number, and a row click
|
|
80
|
+
* posts its deferred switch only while it is the newest (DEF-PANE-349,
|
|
81
|
+
* DEF-PANE-355). */
|
|
81
82
|
let openClicks = 0;
|
|
82
83
|
let movedClick = 0;
|
|
83
84
|
function loadExpanded(storageKey) {
|
|
@@ -179,6 +180,10 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
179
180
|
* `_fetch`. */
|
|
180
181
|
this._fetchSent = 0;
|
|
181
182
|
this._fetchDrawn = 0;
|
|
183
|
+
/** The `_fetchSent` number at each removal's DELETE, by encoded path. A
|
|
184
|
+
* listing sent before that number may still carry the removed project, so
|
|
185
|
+
* `_fetch` drops it (DEF-PANE-351-WRITE-ORDER). */
|
|
186
|
+
this._removedAt = new Map();
|
|
182
187
|
/** A button is pressed in the list, and a redraw waits for its release -
|
|
183
188
|
* see `_onRelease`. */
|
|
184
189
|
this._pressed = false;
|
|
@@ -640,7 +645,9 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
640
645
|
return;
|
|
641
646
|
}
|
|
642
647
|
this._fetchDrawn = request;
|
|
643
|
-
|
|
648
|
+
// A project removed at or after this listing was sent must not be drawn
|
|
649
|
+
// back from the answer (DEF-PANE-351-WRITE-ORDER).
|
|
650
|
+
this._sessions = ((_a = data.sessions) !== null && _a !== void 0 ? _a : []).filter(s => { var _a; return ((_a = this._removedAt.get(s.encoded_path)) !== null && _a !== void 0 ? _a : 0) < request; });
|
|
644
651
|
this._setInlineError(null);
|
|
645
652
|
this._terminals.setSessions(this._sessions);
|
|
646
653
|
this._render();
|
|
@@ -659,6 +666,10 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
659
666
|
favourite: next
|
|
660
667
|
})
|
|
661
668
|
});
|
|
669
|
+
// A listing sent before the write may still be drawn, and a poll may
|
|
670
|
+
// have replaced the row this wrote; this one is answered after the write
|
|
671
|
+
// (DEF-PANE-351).
|
|
672
|
+
void this._fetch().catch(() => undefined);
|
|
662
673
|
}
|
|
663
674
|
catch (err) {
|
|
664
675
|
session.favourite = !next; // roll back
|
|
@@ -711,6 +722,11 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
711
722
|
});
|
|
712
723
|
// Drop locally; the next poll reconciles against disk.
|
|
713
724
|
this._sessions = ((_a = this._sessions) !== null && _a !== void 0 ? _a : []).filter(s => s.encoded_path !== session.encoded_path);
|
|
725
|
+
// A listing sent before the delete may still be drawn (DEF-PANE-351),
|
|
726
|
+
// and it carries the removed project: the number taken here marks the
|
|
727
|
+
// answer for `_fetch` to drop (DEF-PANE-351-WRITE-ORDER).
|
|
728
|
+
this._removedAt.set(session.encoded_path, this._fetchSent);
|
|
729
|
+
void this._fetch().catch(() => undefined);
|
|
714
730
|
}
|
|
715
731
|
catch (err) {
|
|
716
732
|
this._showActionError(`Could not remove "${name}" - try again.`, err);
|
|
@@ -721,14 +737,44 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
721
737
|
}
|
|
722
738
|
}
|
|
723
739
|
async _cleanupParallel(session) {
|
|
724
|
-
var _a;
|
|
725
|
-
const extra = session.extra_sessions;
|
|
740
|
+
var _a, _b, _c, _d;
|
|
726
741
|
const name = this._lookupName(session);
|
|
727
|
-
const
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
742
|
+
const confirmBody = new Widget();
|
|
743
|
+
confirmBody.node.className = 'jp-AiAssistantsPanel-cleanupConfirm';
|
|
744
|
+
const question = document.createElement('p');
|
|
745
|
+
// Announced politely when the fetch restates it; the warning keeps
|
|
746
|
+
// role=alert for the case that needs attention.
|
|
747
|
+
question.setAttribute('role', 'status');
|
|
748
|
+
let extra = session.extra_sessions;
|
|
749
|
+
const askCount = (count) => {
|
|
750
|
+
question.textContent =
|
|
751
|
+
`Remove ${count} parallel session${count === 1 ? '' : 's'} from ` +
|
|
752
|
+
`"${name}"?`;
|
|
753
|
+
};
|
|
754
|
+
askCount(extra);
|
|
755
|
+
confirmBody.node.appendChild(question);
|
|
756
|
+
// One sentence, twelve words or fewer: what happens, not the detail.
|
|
757
|
+
const hint = document.createElement('p');
|
|
758
|
+
hint.className = 'jp-AiAssistantsPanel-cleanupHint';
|
|
759
|
+
hint.textContent = this._deleteToTrash
|
|
760
|
+
? 'The current conversation is kept; the rest go to the trash.'
|
|
761
|
+
: 'The current conversation is kept; the rest are deleted permanently.';
|
|
762
|
+
confirmBody.node.appendChild(hint);
|
|
763
|
+
// The irreversible note is a safety statement, not the hint, so it keeps
|
|
764
|
+
// its own line. With the trash on it names the refusal: a refused move
|
|
765
|
+
// deletes permanently, while the hint promises the trash
|
|
766
|
+
// (DEF-PANE-363-CAUTION-REFUSAL).
|
|
767
|
+
const caution = document.createElement('p');
|
|
768
|
+
caution.className = 'jp-AiAssistantsPanel-cleanupCaution';
|
|
769
|
+
caution.textContent = this._deleteToTrash
|
|
770
|
+
? 'A refused trash move deletes permanently and cannot be undone.'
|
|
771
|
+
: this._irreversibleNote.trim();
|
|
772
|
+
confirmBody.node.appendChild(caution);
|
|
773
|
+
const title = this._trans.__('Clean Up Parallel Sessions');
|
|
774
|
+
let warning = null;
|
|
775
|
+
const confirmDialog = new Dialog({
|
|
776
|
+
title,
|
|
777
|
+
body: confirmBody,
|
|
732
778
|
buttons: [
|
|
733
779
|
Dialog.cancelButton(),
|
|
734
780
|
Dialog.warnButton({ label: this._trans.__('Remove') })
|
|
@@ -737,6 +783,58 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
737
783
|
defaultButton: 0,
|
|
738
784
|
renderer: dialogRenderer
|
|
739
785
|
});
|
|
786
|
+
// showDialog names a dialog only when its body is a string, and this body
|
|
787
|
+
// is a Widget: name it here, and again when the question is restated and
|
|
788
|
+
// the warning joins it.
|
|
789
|
+
const nameDialog = () => {
|
|
790
|
+
var _a, _b, _c, _d;
|
|
791
|
+
confirmDialog.node.setAttribute('aria-label', [
|
|
792
|
+
title,
|
|
793
|
+
(_a = question.textContent) !== null && _a !== void 0 ? _a : '',
|
|
794
|
+
(_b = hint.textContent) !== null && _b !== void 0 ? _b : '',
|
|
795
|
+
(_c = caution.textContent) !== null && _c !== void 0 ? _c : '',
|
|
796
|
+
(_d = warning === null || warning === void 0 ? void 0 : warning.textContent) !== null && _d !== void 0 ? _d : ''
|
|
797
|
+
]
|
|
798
|
+
.filter(part => part)
|
|
799
|
+
.join(' '));
|
|
800
|
+
};
|
|
801
|
+
nameDialog();
|
|
802
|
+
// Alongside the question, never in front of it: the request layer allows
|
|
803
|
+
// 60 s, and the confirmation must open at once. When the list lands the
|
|
804
|
+
// question states its length and a conversation newer than the kept one
|
|
805
|
+
// adds the warning, drawn last so it reads after the caution. The same
|
|
806
|
+
// list is what the removal deletes. A failed fetch leaves the question as
|
|
807
|
+
// the row reported it, with no warning, and the removal fetches again
|
|
808
|
+
// below.
|
|
809
|
+
const branchesReady = this._fetchBranches(session).then(fetched => {
|
|
810
|
+
const list = fetched === null || fetched === void 0 ? void 0 : fetched.branches;
|
|
811
|
+
if (list && list.length === 0) {
|
|
812
|
+
// The row is up to one poll old, and a second client may have
|
|
813
|
+
// cleaned the project since (DEF-GUARD-96): a red Remove with zero
|
|
814
|
+
// targets is a dead end. End the confirmation through the accept
|
|
815
|
+
// path, whose zero-id guard states the empty outcome and refreshes
|
|
816
|
+
// the list (DEF-PANE-365); resolving after the user already
|
|
817
|
+
// accepted or cancelled is a no-op.
|
|
818
|
+
confirmDialog.resolve(1);
|
|
819
|
+
}
|
|
820
|
+
else if (list) {
|
|
821
|
+
extra = list.length;
|
|
822
|
+
askCount(extra);
|
|
823
|
+
const newer = list.filter(branch => branch.file_mtime > session.file_mtime).length;
|
|
824
|
+
if (newer > 0) {
|
|
825
|
+
warning = document.createElement('div');
|
|
826
|
+
warning.className = 'jp-AiAssistantsPanel-cleanupWarning';
|
|
827
|
+
warning.setAttribute('role', 'alert');
|
|
828
|
+
warning.textContent =
|
|
829
|
+
`${newer} parallel session${newer === 1 ? ' was' : 's were'} ` +
|
|
830
|
+
'active after the one kept.';
|
|
831
|
+
confirmBody.node.appendChild(warning);
|
|
832
|
+
}
|
|
833
|
+
nameDialog();
|
|
834
|
+
}
|
|
835
|
+
return fetched;
|
|
836
|
+
}, () => null);
|
|
837
|
+
const confirm = await confirmDialog.launch();
|
|
740
838
|
if (!confirm.button.accept) {
|
|
741
839
|
return;
|
|
742
840
|
}
|
|
@@ -744,6 +842,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
744
842
|
body.node.className = 'jp-AiAssistantsPanel-cleanupBody';
|
|
745
843
|
const message = document.createElement('div');
|
|
746
844
|
message.className = 'jp-AiAssistantsPanel-cleanupMessage';
|
|
845
|
+
message.setAttribute('role', 'status');
|
|
747
846
|
message.textContent = `Removing ${extra} parallel session${extra === 1 ? '' : 's'}...`;
|
|
748
847
|
body.node.appendChild(message);
|
|
749
848
|
// No `value` attribute -> indeterminate (animated) while the request is in
|
|
@@ -758,6 +857,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
758
857
|
buttons: [Dialog.okButton({ label: this._trans.__('Close') })],
|
|
759
858
|
renderer: dialogRenderer
|
|
760
859
|
});
|
|
860
|
+
dialog.node.setAttribute('aria-label', title);
|
|
761
861
|
// Close stays live while the work runs. Hiding it took away the only exit
|
|
762
862
|
// a keyboard user has: the dialog listens for Escape on its own node, it
|
|
763
863
|
// puts focus on this button and nowhere else, and focusing a hidden button
|
|
@@ -768,9 +868,11 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
768
868
|
void dialog.launch();
|
|
769
869
|
try {
|
|
770
870
|
// The extras ARE the branch list - the current conversation is not in
|
|
771
|
-
// it - so cleaning up is deleting exactly those ids.
|
|
772
|
-
|
|
773
|
-
|
|
871
|
+
// it - so cleaning up is deleting exactly those ids. Reuse the list the
|
|
872
|
+
// question was built from, waiting for it here where the progress
|
|
873
|
+
// dialog is already up; only a failed fetch re-reads.
|
|
874
|
+
const fetched = (_a = (await branchesReady)) !== null && _a !== void 0 ? _a : (await this._fetchBranches(session));
|
|
875
|
+
const ids = ((_b = fetched === null || fetched === void 0 ? void 0 : fetched.branches) !== null && _b !== void 0 ? _b : []).map(b => b.session_id);
|
|
774
876
|
if (ids.length === 0) {
|
|
775
877
|
// The menu item is gated on a row up to one poll old, so by the time
|
|
776
878
|
// the refetch lands another tab may have cleaned the project already.
|
|
@@ -779,6 +881,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
779
881
|
// to remove N.
|
|
780
882
|
bar.remove();
|
|
781
883
|
message.textContent = 'Nothing left to clean up.';
|
|
884
|
+
dialog.node.setAttribute('aria-label', `${title}. ${(_c = message.textContent) !== null && _c !== void 0 ? _c : ''}`);
|
|
782
885
|
this._branchCache = null;
|
|
783
886
|
await this._fetch();
|
|
784
887
|
return;
|
|
@@ -792,7 +895,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
792
895
|
});
|
|
793
896
|
// Only the conversations that ACTUALLY went lose their colour - one the
|
|
794
897
|
// server could not delete keeps its tint.
|
|
795
|
-
await this._colours.forget((
|
|
898
|
+
await this._colours.forget((_d = data.removed_ids) !== null && _d !== void 0 ? _d : ids);
|
|
796
899
|
// The kept listing names conversations that may have just gone.
|
|
797
900
|
this._branchCache = null;
|
|
798
901
|
bar.value = 1;
|
|
@@ -999,7 +1102,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
999
1102
|
// The terminal a launch before this click opened for it counts too,
|
|
1000
1103
|
// before the server can read the conversation off its process
|
|
1001
1104
|
// (DEF-PANE-345).
|
|
1002
|
-
const found = (_c = this._terminals.cached(folder, row.session_id)) !== null && _c !== void 0 ? _c : (_d = (await this._terminals.findForSession(row.session_id, folder))) === null || _d === void 0 ? void 0 : _d.widget;
|
|
1105
|
+
const found = (_c = (await this._terminals.cached(folder, row.session_id))) !== null && _c !== void 0 ? _c : (_d = (await this._terminals.findForSession(row.session_id, folder))) === null || _d === void 0 ? void 0 : _d.widget;
|
|
1003
1106
|
if (found) {
|
|
1004
1107
|
this._terminals.focus(found);
|
|
1005
1108
|
return found;
|
|
@@ -1071,6 +1174,15 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1071
1174
|
// store key lets the server clear that pin so the new conversation is not
|
|
1072
1175
|
// shadowed by a stale one (DEF-9).
|
|
1073
1176
|
const existing = ((_a = this._sessions) !== null && _a !== void 0 ? _a : []).find(s => s.project_path === projectPath);
|
|
1177
|
+
if (existing) {
|
|
1178
|
+
// The + click is the project's newest decision: a row open still
|
|
1179
|
+
// finishing posts no switch over it (DEF-PANE-355). It has no id to post;
|
|
1180
|
+
// the launch route clears the pin.
|
|
1181
|
+
this._currentTokens.set(existing.encoded_path, {
|
|
1182
|
+
click: ++openClicks,
|
|
1183
|
+
sessionId: ''
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1074
1186
|
// Raised on the click, not by the launch: the launch waits for a tile
|
|
1075
1187
|
// launch clicked before it (DEF-PANE-345).
|
|
1076
1188
|
const busy = beginSpinner(this._trans.__('Opening %1', this._descriptor.label), this._trans);
|
|
@@ -1328,7 +1440,8 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1328
1440
|
const key = session.encoded_path;
|
|
1329
1441
|
const run = ((_a = this._switchChains.get(key)) !== null && _a !== void 0 ? _a : Promise.resolve()).then(async () => {
|
|
1330
1442
|
const newest = this._currentTokens.get(key);
|
|
1331
|
-
|
|
1443
|
+
// An empty id is a New Session (+) decision, which posts nothing.
|
|
1444
|
+
if (!(newest === null || newest === void 0 ? void 0 : newest.sessionId)) {
|
|
1332
1445
|
return;
|
|
1333
1446
|
}
|
|
1334
1447
|
if (!force && this._postedClicks.get(key) === newest.click) {
|
|
@@ -2097,6 +2210,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2097
2210
|
});
|
|
2098
2211
|
this._commands.addCommand(this._cmd('copy-path'), {
|
|
2099
2212
|
label: this._trans.__('Copy Path'),
|
|
2213
|
+
icon: copyIcon,
|
|
2100
2214
|
execute: () => {
|
|
2101
2215
|
const s = active();
|
|
2102
2216
|
if (s) {
|
|
@@ -2106,6 +2220,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2106
2220
|
});
|
|
2107
2221
|
this._commands.addCommand(this._cmd('copy-session-id'), {
|
|
2108
2222
|
label: this._trans.__('Copy Session ID'),
|
|
2223
|
+
icon: copyIcon,
|
|
2109
2224
|
execute: () => {
|
|
2110
2225
|
var _a;
|
|
2111
2226
|
const id = (_a = active()) === null || _a === void 0 ? void 0 : _a.session_id;
|
|
@@ -2145,6 +2260,9 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2145
2260
|
// item beside an ordinary one (DEF-45). The sweep variant of the trash
|
|
2146
2261
|
// glyph, because this one deletes several at once.
|
|
2147
2262
|
icon: cleanupIcon,
|
|
2263
|
+
// Both history-deleting entries draw their icon in the error colour
|
|
2264
|
+
// (base.css).
|
|
2265
|
+
className: 'jp-AiAssistantsContextMenu-danger',
|
|
2148
2266
|
isVisible: () => { var _a, _b; return ((_b = (_a = active()) === null || _a === void 0 ? void 0 : _a.extra_sessions) !== null && _b !== void 0 ? _b : 0) > 0; },
|
|
2149
2267
|
execute: () => {
|
|
2150
2268
|
const s = active();
|
|
@@ -2241,6 +2359,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2241
2359
|
this._commands.addCommand(this._cmd('remove'), {
|
|
2242
2360
|
label: `Remove from ${this._descriptor.label}`,
|
|
2243
2361
|
icon: removeIcon,
|
|
2362
|
+
className: 'jp-AiAssistantsContextMenu-danger',
|
|
2244
2363
|
execute: () => {
|
|
2245
2364
|
const s = active();
|
|
2246
2365
|
if (s) {
|
package/lib/core/terminals.d.ts
CHANGED
|
@@ -86,8 +86,10 @@ export declare class TerminalManager {
|
|
|
86
86
|
*/
|
|
87
87
|
launch(request: ILaunchRequest, tagSessionId?: string, spinner?: number): Promise<any>;
|
|
88
88
|
/** The terminal this manager last opened for `projectPath`, when it is
|
|
89
|
-
* still open
|
|
90
|
-
|
|
89
|
+
* still open, was opened for the conversation `sessionId`, and the server
|
|
90
|
+
* does not read another conversation off its process - which /clear starts
|
|
91
|
+
* in the same process (DEF-PANE-350); else null. */
|
|
92
|
+
cached(projectPath: string, sessionId: string): Promise<any | null>;
|
|
91
93
|
/** Remember `widget` as the newest terminal of `projectPath`, opened for
|
|
92
94
|
* the conversation `sessionId`. A launch is remembered before the server
|
|
93
95
|
* can read the conversation off the new process, so the next open of the
|
package/lib/core/terminals.js
CHANGED
|
@@ -383,7 +383,7 @@ export class TerminalManager {
|
|
|
383
383
|
var _a;
|
|
384
384
|
// 1. Microcache - the most recent terminal of this project, reused only
|
|
385
385
|
// when it carries the wanted conversation.
|
|
386
|
-
const cached = this.cached(session.project_path, wanted);
|
|
386
|
+
const cached = await this.cached(session.project_path, wanted);
|
|
387
387
|
if (cached) {
|
|
388
388
|
this.focus(cached, spinner);
|
|
389
389
|
return;
|
|
@@ -473,12 +473,24 @@ export class TerminalManager {
|
|
|
473
473
|
}
|
|
474
474
|
}
|
|
475
475
|
/** The terminal this manager last opened for `projectPath`, when it is
|
|
476
|
-
* still open
|
|
477
|
-
|
|
476
|
+
* still open, was opened for the conversation `sessionId`, and the server
|
|
477
|
+
* does not read another conversation off its process - which /clear starts
|
|
478
|
+
* in the same process (DEF-PANE-350); else null. */
|
|
479
|
+
async cached(projectPath, sessionId) {
|
|
478
480
|
const cached = this._byProject.get(projectPath);
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
481
|
+
if (!cached || cached.widget.isDisposed || cached.sessionId !== sessionId) {
|
|
482
|
+
return null;
|
|
483
|
+
}
|
|
484
|
+
// No id read is the window before the new process is readable, which is
|
|
485
|
+
// what this cache covers (DEF-PANE-345).
|
|
486
|
+
const info = await this.probe(cached.widget);
|
|
487
|
+
if ((info === null || info === void 0 ? void 0 : info.session_id) && info.session_id !== sessionId) {
|
|
488
|
+
if (this._byProject.get(projectPath) === cached) {
|
|
489
|
+
this._byProject.delete(projectPath);
|
|
490
|
+
}
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
return cached.widget;
|
|
482
494
|
}
|
|
483
495
|
/** Remember `widget` as the newest terminal of `projectPath`, opened for
|
|
484
496
|
* the conversation `sessionId`. A launch is remembered before the server
|
package/lib/index.js
CHANGED
|
@@ -413,6 +413,31 @@ const plugin = {
|
|
|
413
413
|
}
|
|
414
414
|
}
|
|
415
415
|
};
|
|
416
|
+
// A suspended network stack - laptop asleep, tab backgrounded - is exactly
|
|
417
|
+
// when the probe fails, and the slow cadence below would leave the roster
|
|
418
|
+
// stale for up to a minute after the machine comes back. A wake fires
|
|
419
|
+
// both events, so it costs two probes; deliberately, because a guard that
|
|
420
|
+
// let the second join the first answered the wake with a request issued
|
|
421
|
+
// before it, and starved the interval below of its own tick (DEF-117).
|
|
422
|
+
// Heard from here, before the first await: a wake before the listeners are
|
|
423
|
+
// armed below is only remembered, because a reconcile before the settings
|
|
424
|
+
// load reads the enabled-by-default fallback and docks assistants the user
|
|
425
|
+
// disabled; one status check answers it once they are armed (DEF-GUARD-122).
|
|
426
|
+
let armed = false;
|
|
427
|
+
let wokeDuringActivation = false;
|
|
428
|
+
const reprobe = () => {
|
|
429
|
+
if (!armed) {
|
|
430
|
+
wokeDuringActivation = true;
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
void probeStatus().then(reconcile);
|
|
434
|
+
};
|
|
435
|
+
window.addEventListener('online', reprobe);
|
|
436
|
+
document.addEventListener('visibilitychange', () => {
|
|
437
|
+
if (document.visibilityState === 'visible') {
|
|
438
|
+
reprobe();
|
|
439
|
+
}
|
|
440
|
+
});
|
|
416
441
|
await probeStatus();
|
|
417
442
|
if (settingRegistry) {
|
|
418
443
|
try {
|
|
@@ -490,21 +515,10 @@ const plugin = {
|
|
|
490
515
|
window.setInterval(() => {
|
|
491
516
|
void probeStatus().then(reconcile);
|
|
492
517
|
}, STATUS_INTERVAL_MS);
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
// let the second join the first answered the wake with a request issued
|
|
498
|
-
// before it, and starved the interval above of its own tick (DEF-117).
|
|
499
|
-
const reprobe = () => {
|
|
500
|
-
void probeStatus().then(reconcile);
|
|
501
|
-
};
|
|
502
|
-
window.addEventListener('online', reprobe);
|
|
503
|
-
document.addEventListener('visibilitychange', () => {
|
|
504
|
-
if (document.visibilityState === 'visible') {
|
|
505
|
-
reprobe();
|
|
506
|
-
}
|
|
507
|
-
});
|
|
518
|
+
armed = true;
|
|
519
|
+
if (wokeDuringActivation) {
|
|
520
|
+
reprobe();
|
|
521
|
+
}
|
|
508
522
|
// Each JupyterLab window keeps its own copy of the settings and never
|
|
509
523
|
// reads them again, and every save writes that whole copy. So a window
|
|
510
524
|
// loaded before another window changed a setting put the old value back
|
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.282",
|
|
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",
|
|
@@ -168,6 +168,97 @@ describe('DEF-GUARD-139 - reconcile hands a late roster to a docked panel', () =
|
|
|
168
168
|
});
|
|
169
169
|
});
|
|
170
170
|
|
|
171
|
+
describe('DEF-GUARD-122 - a wake during activation', () => {
|
|
172
|
+
it('is remembered, reconciles nothing before the settings load, and is answered by one status check once the listeners are armed', async () => {
|
|
173
|
+
// Identity of this activation's probes; earlier activations' listeners
|
|
174
|
+
// reach the same mock (see the activation-contract harness below).
|
|
175
|
+
const serverSettings = {};
|
|
176
|
+
let probes = 0;
|
|
177
|
+
request.mockImplementation((path: string, settings: unknown) => {
|
|
178
|
+
if (path !== 'status' || settings !== serverSettings) {
|
|
179
|
+
return Promise.reject(new Error('not this activation'));
|
|
180
|
+
}
|
|
181
|
+
probes += 1;
|
|
182
|
+
return Promise.resolve({
|
|
183
|
+
root_dir: '/srv/lab',
|
|
184
|
+
delete_to_trash: true,
|
|
185
|
+
providers: PROVIDERS.map(m => ({
|
|
186
|
+
id: m.descriptor.id,
|
|
187
|
+
available: true
|
|
188
|
+
}))
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
const docked: AssistantSessionsPanel[] = [];
|
|
192
|
+
const labShell = {
|
|
193
|
+
add: (w: AssistantSessionsPanel) => {
|
|
194
|
+
docked.push(w);
|
|
195
|
+
}
|
|
196
|
+
} as any;
|
|
197
|
+
const app = {
|
|
198
|
+
serviceManager: { serverSettings },
|
|
199
|
+
commands: new CommandRegistry(),
|
|
200
|
+
hasPlugin: () => false
|
|
201
|
+
} as any;
|
|
202
|
+
// Every assistant off, so a reconcile on the enabled-by-default fallback
|
|
203
|
+
// would show as a docked panel.
|
|
204
|
+
const composite: Record<string, unknown> = Object.fromEntries(
|
|
205
|
+
PROVIDERS.map(m => [`providers.${m.descriptor.id}.enabled`, false])
|
|
206
|
+
);
|
|
207
|
+
const settings = {
|
|
208
|
+
raw: '{}',
|
|
209
|
+
composite,
|
|
210
|
+
get: (key: string) => ({
|
|
211
|
+
composite: composite[key],
|
|
212
|
+
user: composite[key]
|
|
213
|
+
}),
|
|
214
|
+
set: async () => undefined,
|
|
215
|
+
changed: new Signal<unknown, void>({})
|
|
216
|
+
};
|
|
217
|
+
let finishLoad!: () => void;
|
|
218
|
+
const loading = new Promise<void>(resolve => {
|
|
219
|
+
finishLoad = resolve;
|
|
220
|
+
});
|
|
221
|
+
const settingRegistry = {
|
|
222
|
+
connector: { fetch: async () => ({ raw: settings.raw }) },
|
|
223
|
+
load: async () => {
|
|
224
|
+
await loading;
|
|
225
|
+
return settings;
|
|
226
|
+
}
|
|
227
|
+
} as any;
|
|
228
|
+
|
|
229
|
+
const activation = (plugin.activate as any)(
|
|
230
|
+
app,
|
|
231
|
+
labShell,
|
|
232
|
+
null,
|
|
233
|
+
settingRegistry,
|
|
234
|
+
null,
|
|
235
|
+
null,
|
|
236
|
+
null
|
|
237
|
+
);
|
|
238
|
+
await flush();
|
|
239
|
+
expect(probes).toBe(1); // the activation probe; the settings still load
|
|
240
|
+
|
|
241
|
+
window.dispatchEvent(new Event('online'));
|
|
242
|
+
Object.defineProperty(document, 'visibilityState', {
|
|
243
|
+
configurable: true,
|
|
244
|
+
get: () => 'visible'
|
|
245
|
+
});
|
|
246
|
+
document.dispatchEvent(new Event('visibilitychange'));
|
|
247
|
+
await flush();
|
|
248
|
+
expect(probes).toBe(1);
|
|
249
|
+
expect(docked).toHaveLength(0);
|
|
250
|
+
|
|
251
|
+
finishLoad();
|
|
252
|
+
await activation;
|
|
253
|
+
await flush();
|
|
254
|
+
// How this fails: without the remembered wake the count stays 1 until the
|
|
255
|
+
// 60 s tick; a listener that probes before the settings load docks every
|
|
256
|
+
// assistant above, and one probe per wake makes it 3.
|
|
257
|
+
expect(probes).toBe(2);
|
|
258
|
+
expect(docked).toHaveLength(0);
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
171
262
|
describe('ACC-LNCH-143..146 - the Launcher tile has the panel lifecycle', () => {
|
|
172
263
|
/** A roster in which every assistant is present, or every one but `absent`. */
|
|
173
264
|
const roster = (absent?: string): unknown => ({
|