jupyterlab_ai_code_assistants_extension 1.2.199 → 1.2.271
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/core/panel.d.ts +56 -4
- package/lib/core/panel.js +243 -27
- package/lib/core/terminals.d.ts +52 -6
- package/lib/core/terminals.js +171 -24
- package/lib/index.js +29 -15
- package/package.json +10 -10
- package/src/__tests__/index.spec.ts +91 -0
- package/src/__tests__/launcher.spec.ts +155 -3
- package/src/__tests__/panel.spec.ts +1041 -1
- package/src/__tests__/terminals.spec.ts +261 -1
- package/src/core/panel.ts +265 -35
- package/src/core/terminals.ts +197 -25
- package/src/index.ts +30 -15
- package/style/base.css +14 -1
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ Chat-panel extensions re-implement the agent loop and trail the real tool. This
|
|
|
32
32
|
- **One-click resume** - click a row to jump back into that session in a terminal; an open terminal for the project is reused instead of duplicated. Exception: a Kimi or DeepSeek conversation started with `+` runs under an id that never reaches its process command line, so a click on that row while its terminal stays open opens a second terminal. Codex mints the id for a conversation started with `+` and for a fork through its own app-server before the terminal opens, so a click on a Codex row reuses the open terminal instead of opening a second one
|
|
33
33
|
- **File browser follows the opened session** - opening a session moves the JupyterLab file browser to that session's 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
34
|
- **Launcher tiles** - every enabled assistant has a tile in an "AI Assistants" section of the JupyterLab Launcher (after Other, under the extension's own robot-head icon); a click opens the assistant in the file browser's current folder, resuming the folder's conversation when one exists and starting a new one otherwise, through the companion `jupyterlab_basic_terminal_extension` (installed automatically). Disable an assistant and its tile goes; disable them all and the section goes with them
|
|
35
|
-
- **Your terminal's `claude -c` follows the panel** (Claude) -
|
|
35
|
+
- **Your terminal's `claude -c` follows the panel** (Claude) - clicking a conversation row, or opening it with Enter or Space, makes plain `claude -c` in that project resume that same conversation, including one that has been compacted, which Claude Code otherwise refuses to continue; a click that reuses a running terminal counts too. A launch from the context menu repairs a compacted conversation the same way but only opens it, so it does not move what `claude -c` picks
|
|
36
36
|
- **Conversation switcher** - a right-click "Switch and Manage Sessions" submenu lists a project's other conversations by name and short id with last-activity time; "Manage Sessions..." opens a searchable popup over the full list with multi-select delete, a per-row action that switches or opens according to the submenu it was opened from, and a copy-id button
|
|
37
37
|
- **Branch session** - fork the current conversation into a new named session via the right-click menu; each assistant forks its own way (Claude's native `--fork-session`, Codex's app-server `thread/fork`, server-side copies for Kimi, Gemini and DeepSeek) behind the same menu item
|
|
38
38
|
- **Rename session** - name a conversation from the right-click menu, and the name is written where the assistant itself reads it back: Claude gets the record its own `/rename` writes, Kimi a custom title, Gemini the chat's summary, DeepSeek its title event. The panel shows the name the assistant actually kept. Codex's name is set through Codex's own app-server, so its CLI lists the same name the panel shows
|
package/lib/core/panel.d.ts
CHANGED
|
@@ -45,6 +45,9 @@ export declare class AssistantSessionsPanel extends Widget {
|
|
|
45
45
|
protected onBeforeHide(_msg: Message): void;
|
|
46
46
|
protected onCloseRequest(msg: Message): void;
|
|
47
47
|
private _buildShell;
|
|
48
|
+
/** The press in the list has ended. The click it sends is dispatched in the
|
|
49
|
+
* same task as this release, so the waiting redraw runs in the next one. */
|
|
50
|
+
private _onRelease;
|
|
48
51
|
/** Show / hide the filter input. Hiding also clears the active filter, so
|
|
49
52
|
* the user never returns to an "invisible" filter narrowing the rows. */
|
|
50
53
|
private _toggleFilterBar;
|
|
@@ -134,13 +137,19 @@ export declare class AssistantSessionsPanel extends Widget {
|
|
|
134
137
|
* The tile's whole click path lives here rather than in `src/index.ts`
|
|
135
138
|
* because every input it needs is already the panel's: the root join, the
|
|
136
139
|
* listing fetch, the terminal reuse ladder and the settings-resolved launch
|
|
137
|
-
* mode. Nothing here retries
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
+
* mode. Nothing here retries: a guard that refuses shows one notification
|
|
141
|
+
* and launches nothing, and a request that fails writes nothing. Past the
|
|
142
|
+
* guards the path waits for the tile launches of every assistant and the
|
|
143
|
+
* launches of this panel clicked before it, and every launch of any panel
|
|
144
|
+
* clicked after it waits for it (DEF-PANE-345).
|
|
140
145
|
*
|
|
141
146
|
* `cwd` is a Contents-API path, `''` for the root.
|
|
142
147
|
*/
|
|
143
148
|
launchHere(cwd?: string): Promise<any>;
|
|
149
|
+
/** The tile's click path past its guards, run after the tile launches of
|
|
150
|
+
* every assistant and the launches of this panel clicked before it, and
|
|
151
|
+
* before every launch clicked after it (DEF-PANE-345). */
|
|
152
|
+
private _launchIn;
|
|
144
153
|
/** Start a brand-new conversation in the file browser's current folder. */
|
|
145
154
|
private _newSession;
|
|
146
155
|
/** Branch the active row's current conversation. Every `forkStrategy` has
|
|
@@ -180,8 +189,28 @@ export declare class AssistantSessionsPanel extends Widget {
|
|
|
180
189
|
* per conversation may have none to rewrite yet - so it gets words rather
|
|
181
190
|
* than the raw code. */
|
|
182
191
|
private _renameError;
|
|
183
|
-
/** Make another conversation the row's current one.
|
|
192
|
+
/** Make another conversation the row's current one. `click` is the number
|
|
193
|
+
* of the action that asked: a row click carries the number it took when it
|
|
194
|
+
* was pressed, so when its open resolves after a newer row click, an
|
|
195
|
+
* explicit switch or a fork, it leaves that newer decision in place and
|
|
196
|
+
* posts nothing (DEF-PANE-349). The decision is posted through the project's
|
|
197
|
+
* chain, so its write lands after every switch enqueued before it. */
|
|
184
198
|
private _switchBranch;
|
|
199
|
+
/** Queue a switch post for `session`'s project behind every post already
|
|
200
|
+
* queued for it, so the server receives the writes in the order the user
|
|
201
|
+
* acted and the last one to land is the newest decision (DEF-PANE-349).
|
|
202
|
+
*
|
|
203
|
+
* A task reads the newest recorded decision when it runs: a row click's
|
|
204
|
+
* task posts it only if no task has posted that click yet, so clicks that
|
|
205
|
+
* queue behind one another collapse to the newest. A fork correction is
|
|
206
|
+
* forced - the fork's own pin write landed after the last post, so the
|
|
207
|
+
* newest decision has to be written again even when it is already marked
|
|
208
|
+
* posted. A chain entry is removed when its last task settles. */
|
|
209
|
+
private _enqueueSwitch;
|
|
210
|
+
/** Post `sessionId` as the project's current conversation and report the
|
|
211
|
+
* outcome. Neither reads nor writes the project's token: the project's
|
|
212
|
+
* chain chooses the id from the recorded decision - see `_enqueueSwitch`. */
|
|
213
|
+
private _postSwitch;
|
|
185
214
|
/** Delete conversations of the active row's project. Resolves with the
|
|
186
215
|
* removed count, or null on a failure already reported to the user. */
|
|
187
216
|
private _deleteBranches;
|
|
@@ -332,12 +361,35 @@ export declare class AssistantSessionsPanel extends Widget {
|
|
|
332
361
|
/** The last branch listing fetched, and the project it belongs to. Read only
|
|
333
362
|
* when this open's own fetch did not arrive in time. */
|
|
334
363
|
private _branchCache;
|
|
364
|
+
/** The number and session id of the newest action that decided each
|
|
365
|
+
* project's current conversation, by encoded path - see `_switchBranch`
|
|
366
|
+
* (DEF-PANE-349). The id is kept so a fork's late server writes can be
|
|
367
|
+
* corrected by re-posting the newer decision. */
|
|
368
|
+
private _currentTokens;
|
|
369
|
+
/** The switch posts queued per project, by encoded path, and the click
|
|
370
|
+
* number each project's chain posted last - see `_enqueueSwitch`. A chain
|
|
371
|
+
* entry is removed when its last task settles; the posted number stays, so a
|
|
372
|
+
* later task can tell an already-posted decision from a new one. */
|
|
373
|
+
private _switchChains;
|
|
374
|
+
private _postedClicks;
|
|
335
375
|
private _activeSession;
|
|
336
376
|
private _activeRowEl;
|
|
337
377
|
private _pollHandle;
|
|
338
378
|
/** The poll's own in-flight tick. Only the poll reads it - an explicit
|
|
339
379
|
* Refresh must always issue, however long the last poll is taking. */
|
|
340
380
|
private _polling;
|
|
381
|
+
/** Listing requests sent, and the number of the newest one drawn - see
|
|
382
|
+
* `_fetch`. */
|
|
383
|
+
private _fetchSent;
|
|
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;
|
|
389
|
+
/** A button is pressed in the list, and a redraw waits for its release -
|
|
390
|
+
* see `_onRelease`. */
|
|
391
|
+
private _pressed;
|
|
392
|
+
private _redrawWaits;
|
|
341
393
|
private _presentationMode;
|
|
342
394
|
private _recentLimit;
|
|
343
395
|
private _modes;
|
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';
|
|
@@ -71,6 +71,16 @@ export const MOVE_PANELS_COMMAND = 'ai-code-assistants:move-panels';
|
|
|
71
71
|
* order the answers arrived, and the file browser could end on the session
|
|
72
72
|
* opened first. */
|
|
73
73
|
let fileBrowserMoves = Promise.resolve();
|
|
74
|
+
/** The session opens of every panel, numbered in click order, and the number
|
|
75
|
+
* of the newest one whose move ran. An open that finishes after the open of a
|
|
76
|
+
* later click does not move the file browser, so it ends on the folder of the
|
|
77
|
+
* newest click that opened (DEF-PANE-343). The same counter orders the writes
|
|
78
|
+
* that decide a project's current conversation: a row click, an explicit
|
|
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). */
|
|
82
|
+
let openClicks = 0;
|
|
83
|
+
let movedClick = 0;
|
|
74
84
|
function loadExpanded(storageKey) {
|
|
75
85
|
try {
|
|
76
86
|
const raw = window.localStorage.getItem(storageKey);
|
|
@@ -109,6 +119,19 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
109
119
|
constructor(options) {
|
|
110
120
|
var _a, _b, _c, _d, _e;
|
|
111
121
|
super();
|
|
122
|
+
/** The press in the list has ended. The click it sends is dispatched in the
|
|
123
|
+
* same task as this release, so the waiting redraw runs in the next one. */
|
|
124
|
+
this._onRelease = () => {
|
|
125
|
+
if (!this._pressed) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
this._pressed = false;
|
|
129
|
+
window.setTimeout(() => {
|
|
130
|
+
if (this._redrawWaits && !this.isDisposed) {
|
|
131
|
+
this._render();
|
|
132
|
+
}
|
|
133
|
+
}, 0);
|
|
134
|
+
};
|
|
112
135
|
this._rootDir = '';
|
|
113
136
|
this._deleteToTrash = true;
|
|
114
137
|
this._removingPaths = new Set();
|
|
@@ -136,12 +159,35 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
136
159
|
/** The last branch listing fetched, and the project it belongs to. Read only
|
|
137
160
|
* when this open's own fetch did not arrive in time. */
|
|
138
161
|
this._branchCache = null;
|
|
162
|
+
/** The number and session id of the newest action that decided each
|
|
163
|
+
* project's current conversation, by encoded path - see `_switchBranch`
|
|
164
|
+
* (DEF-PANE-349). The id is kept so a fork's late server writes can be
|
|
165
|
+
* corrected by re-posting the newer decision. */
|
|
166
|
+
this._currentTokens = new Map();
|
|
167
|
+
/** The switch posts queued per project, by encoded path, and the click
|
|
168
|
+
* number each project's chain posted last - see `_enqueueSwitch`. A chain
|
|
169
|
+
* entry is removed when its last task settles; the posted number stays, so a
|
|
170
|
+
* later task can tell an already-posted decision from a new one. */
|
|
171
|
+
this._switchChains = new Map();
|
|
172
|
+
this._postedClicks = new Map();
|
|
139
173
|
this._activeSession = null;
|
|
140
174
|
this._activeRowEl = null;
|
|
141
175
|
this._pollHandle = null;
|
|
142
176
|
/** The poll's own in-flight tick. Only the poll reads it - an explicit
|
|
143
177
|
* Refresh must always issue, however long the last poll is taking. */
|
|
144
178
|
this._polling = null;
|
|
179
|
+
/** Listing requests sent, and the number of the newest one drawn - see
|
|
180
|
+
* `_fetch`. */
|
|
181
|
+
this._fetchSent = 0;
|
|
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();
|
|
187
|
+
/** A button is pressed in the list, and a redraw waits for its release -
|
|
188
|
+
* see `_onRelease`. */
|
|
189
|
+
this._pressed = false;
|
|
190
|
+
this._redrawWaits = false;
|
|
145
191
|
this._presentationMode = DEFAULT_PRESENTATION_MODE;
|
|
146
192
|
this._recentLimit = DEFAULT_RECENT_LIMIT;
|
|
147
193
|
this._modes = {};
|
|
@@ -184,6 +230,8 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
184
230
|
return this._descriptor;
|
|
185
231
|
}
|
|
186
232
|
dispose() {
|
|
233
|
+
document.removeEventListener('pointerup', this._onRelease, true);
|
|
234
|
+
document.removeEventListener('pointercancel', this._onRelease, true);
|
|
187
235
|
this._stopPolling();
|
|
188
236
|
// Disabling a provider must leave its running terminals alive and only
|
|
189
237
|
// drop the tint this panel applied.
|
|
@@ -407,6 +455,15 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
407
455
|
this._bodyEl = body;
|
|
408
456
|
this._loadingEl = loading;
|
|
409
457
|
this._errorEl = error;
|
|
458
|
+
// A row drawn again between the press and the release of a click loses
|
|
459
|
+
// the click: the browser sends it to the list the two rows share, not to
|
|
460
|
+
// the row (DEF-PANE-346). A redraw asked for while a button is pressed in
|
|
461
|
+
// the list waits until that click has been sent.
|
|
462
|
+
body.addEventListener('pointerdown', () => {
|
|
463
|
+
this._pressed = true;
|
|
464
|
+
});
|
|
465
|
+
document.addEventListener('pointerup', this._onRelease, true);
|
|
466
|
+
document.addEventListener('pointercancel', this._onRelease, true);
|
|
410
467
|
}
|
|
411
468
|
/** Show / hide the filter input. Hiding also clears the active filter, so
|
|
412
469
|
* the user never returns to an "invisible" filter narrowing the rows. */
|
|
@@ -578,10 +635,19 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
578
635
|
// ------------------------------------------------------------------- data
|
|
579
636
|
async _fetch() {
|
|
580
637
|
var _a;
|
|
638
|
+
// This request's place in the order. Requests answer in any order, and an
|
|
639
|
+
// answer older than the one already drawn is dropped (DEF-PANE-344).
|
|
640
|
+
const request = ++this._fetchSent;
|
|
581
641
|
// `cache: 'no-store'` so a manual refresh always re-reads the server's
|
|
582
642
|
// view rather than a possibly stale browser-cached response.
|
|
583
643
|
const data = await requestProvider(this._descriptor.id, 'sessions', this._serverSettings, { cache: 'no-store' });
|
|
584
|
-
|
|
644
|
+
if (request < this._fetchDrawn) {
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
this._fetchDrawn = request;
|
|
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; });
|
|
585
651
|
this._setInlineError(null);
|
|
586
652
|
this._terminals.setSessions(this._sessions);
|
|
587
653
|
this._render();
|
|
@@ -600,6 +666,10 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
600
666
|
favourite: next
|
|
601
667
|
})
|
|
602
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);
|
|
603
673
|
}
|
|
604
674
|
catch (err) {
|
|
605
675
|
session.favourite = !next; // roll back
|
|
@@ -652,6 +722,11 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
652
722
|
});
|
|
653
723
|
// Drop locally; the next poll reconciles against disk.
|
|
654
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);
|
|
655
730
|
}
|
|
656
731
|
catch (err) {
|
|
657
732
|
this._showActionError(`Could not remove "${name}" - try again.`, err);
|
|
@@ -753,15 +828,32 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
753
828
|
_launchMode(force) {
|
|
754
829
|
return resolveLaunchMode(this._descriptor.launchModes, this._modes, force);
|
|
755
830
|
}
|
|
756
|
-
async _openSession(session, force, sessionId) {
|
|
831
|
+
async _openSession(session, force, sessionId, makeCurrent = false) {
|
|
757
832
|
// The click's own feedback, raised before the terminal walk and the
|
|
758
833
|
// launch - both can take seconds (DEF-PANE-318). The launch's title; the
|
|
759
834
|
// launch joins this dialog through the token.
|
|
760
835
|
const busy = beginSpinner(this._trans.__('Opening %1', this._descriptor.label), this._trans);
|
|
836
|
+
const click = ++openClicks;
|
|
837
|
+
const mode = this._launchMode(force);
|
|
761
838
|
try {
|
|
762
|
-
await this._terminals.
|
|
839
|
+
await this._terminals.inClickOrder(() => this._terminals.openSession(session, mode, sessionId, busy));
|
|
840
|
+
if (makeCurrent) {
|
|
841
|
+
// The row's own controls make that conversation the project's current
|
|
842
|
+
// one - the pin and the `claude -c` mtime lead (DEF-PANE-349). Not
|
|
843
|
+
// awaited: the open has succeeded, and the switch is not part of it.
|
|
844
|
+
// It carries this click's number, so a switch or fork made after this
|
|
845
|
+
// click wins even when this open finishes last. Other launches leave
|
|
846
|
+
// `claude -c` alone.
|
|
847
|
+
void this._switchBranch(session, session.session_id, click);
|
|
848
|
+
}
|
|
763
849
|
// Not awaited: the open has succeeded, and the sync is not part of it.
|
|
764
|
-
fileBrowserMoves = fileBrowserMoves.then(() =>
|
|
850
|
+
fileBrowserMoves = fileBrowserMoves.then(() => {
|
|
851
|
+
if (click < movedClick) {
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
movedClick = click;
|
|
855
|
+
return this._moveFileBrowserTo(session);
|
|
856
|
+
});
|
|
765
857
|
}
|
|
766
858
|
catch (err) {
|
|
767
859
|
if (isResponseStatus(err, 404)) {
|
|
@@ -861,14 +953,15 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
861
953
|
* The tile's whole click path lives here rather than in `src/index.ts`
|
|
862
954
|
* because every input it needs is already the panel's: the root join, the
|
|
863
955
|
* listing fetch, the terminal reuse ladder and the settings-resolved launch
|
|
864
|
-
* mode. Nothing here retries
|
|
865
|
-
*
|
|
866
|
-
*
|
|
956
|
+
* mode. Nothing here retries: a guard that refuses shows one notification
|
|
957
|
+
* and launches nothing, and a request that fails writes nothing. Past the
|
|
958
|
+
* guards the path waits for the tile launches of every assistant and the
|
|
959
|
+
* launches of this panel clicked before it, and every launch of any panel
|
|
960
|
+
* clicked after it waits for it (DEF-PANE-345).
|
|
867
961
|
*
|
|
868
962
|
* `cwd` is a Contents-API path, `''` for the root.
|
|
869
963
|
*/
|
|
870
964
|
async launchHere(cwd) {
|
|
871
|
-
var _a, _b;
|
|
872
965
|
// No roster yet, so the root is unknown. Joining onto an empty one names a
|
|
873
966
|
// folder at the filesystem root, which the server accepts whenever it
|
|
874
967
|
// happens to exist (ACC-LNCH-148, DEF-132).
|
|
@@ -891,7 +984,16 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
891
984
|
return undefined;
|
|
892
985
|
}
|
|
893
986
|
const folder = this._currentFolder(cwd);
|
|
987
|
+
return this._terminals.tileInClickOrder(() => this._launchIn(folder));
|
|
988
|
+
}
|
|
989
|
+
/** The tile's click path past its guards, run after the tile launches of
|
|
990
|
+
* every assistant and the launches of this panel clicked before it, and
|
|
991
|
+
* before every launch clicked after it (DEF-PANE-345). */
|
|
992
|
+
async _launchIn(folder) {
|
|
993
|
+
var _a, _b, _c, _d, _e;
|
|
894
994
|
let argv;
|
|
995
|
+
// The conversation the new terminal runs, when its id is known here.
|
|
996
|
+
let conversation;
|
|
895
997
|
try {
|
|
896
998
|
// Every click asks the server for the listing (ACC-LNCH-151). `_sessions`
|
|
897
999
|
// is written by `_fetch` and by the optimistic remove in
|
|
@@ -910,10 +1012,13 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
910
1012
|
// The folder is required as well: a transcript re-homed into another
|
|
911
1013
|
// project keeps its id, and that terminal is not this folder's
|
|
912
1014
|
// (DEF-PANE-319).
|
|
913
|
-
|
|
1015
|
+
// The terminal a launch before this click opened for it counts too,
|
|
1016
|
+
// before the server can read the conversation off its process
|
|
1017
|
+
// (DEF-PANE-345).
|
|
1018
|
+
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;
|
|
914
1019
|
if (found) {
|
|
915
|
-
this._terminals.focus(found
|
|
916
|
-
return found
|
|
1020
|
+
this._terminals.focus(found);
|
|
1021
|
+
return found;
|
|
917
1022
|
}
|
|
918
1023
|
}
|
|
919
1024
|
const request = {
|
|
@@ -929,6 +1034,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
929
1034
|
};
|
|
930
1035
|
const answer = await requestProvider(this._descriptor.id, 'launch-argv', this._serverSettings, { method: 'POST', body: JSON.stringify(request) });
|
|
931
1036
|
argv = answer.argv;
|
|
1037
|
+
conversation = (_e = request.session_id) !== null && _e !== void 0 ? _e : request.new_session_id;
|
|
932
1038
|
}
|
|
933
1039
|
catch (err) {
|
|
934
1040
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -952,10 +1058,14 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
952
1058
|
// and the Launcher keeps its tab (ACC-LNCH-157 covers the success path
|
|
953
1059
|
// only).
|
|
954
1060
|
try {
|
|
955
|
-
|
|
1061
|
+
const widget = await this._app.commands.execute(BASIC_TERMINAL_LAUNCH, {
|
|
956
1062
|
argv,
|
|
957
1063
|
cwd: folder
|
|
958
1064
|
});
|
|
1065
|
+
if (widget === null || widget === void 0 ? void 0 : widget.id) {
|
|
1066
|
+
this._terminals.remember(folder, widget, conversation);
|
|
1067
|
+
}
|
|
1068
|
+
return widget;
|
|
959
1069
|
}
|
|
960
1070
|
catch (err) {
|
|
961
1071
|
this._notifyLaunchError('Could not open a terminal for this folder', err);
|
|
@@ -977,13 +1087,26 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
977
1087
|
// store key lets the server clear that pin so the new conversation is not
|
|
978
1088
|
// shadowed by a stale one (DEF-9).
|
|
979
1089
|
const existing = ((_a = this._sessions) !== null && _a !== void 0 ? _a : []).find(s => s.project_path === projectPath);
|
|
1090
|
+
if (existing) {
|
|
1091
|
+
// The + click is the project's newest decision: a row open still
|
|
1092
|
+
// finishing posts no switch over it (DEF-PANE-355). It has no id to post;
|
|
1093
|
+
// the launch route clears the pin.
|
|
1094
|
+
this._currentTokens.set(existing.encoded_path, {
|
|
1095
|
+
click: ++openClicks,
|
|
1096
|
+
sessionId: ''
|
|
1097
|
+
});
|
|
1098
|
+
}
|
|
1099
|
+
// Raised on the click, not by the launch: the launch waits for a tile
|
|
1100
|
+
// launch clicked before it (DEF-PANE-345).
|
|
1101
|
+
const busy = beginSpinner(this._trans.__('Opening %1', this._descriptor.label), this._trans);
|
|
1102
|
+
const request = {
|
|
1103
|
+
project_path: projectPath,
|
|
1104
|
+
encoded_path: existing === null || existing === void 0 ? void 0 : existing.encoded_path,
|
|
1105
|
+
new_session_id: newId,
|
|
1106
|
+
mode: this._launchMode(force)
|
|
1107
|
+
};
|
|
980
1108
|
try {
|
|
981
|
-
await this._terminals.launch(
|
|
982
|
-
project_path: projectPath,
|
|
983
|
-
encoded_path: existing === null || existing === void 0 ? void 0 : existing.encoded_path,
|
|
984
|
-
new_session_id: newId,
|
|
985
|
-
mode: this._launchMode(force)
|
|
986
|
-
}, newId);
|
|
1109
|
+
await this._terminals.inClickOrder(() => this._terminals.launch(request, newId, busy));
|
|
987
1110
|
}
|
|
988
1111
|
catch (err) {
|
|
989
1112
|
// Same shape as `_openSession` above - the `finally` refetches, so an
|
|
@@ -991,6 +1114,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
991
1114
|
this._notifyLaunchError('Could not start a new session', err);
|
|
992
1115
|
}
|
|
993
1116
|
finally {
|
|
1117
|
+
endSpinner(busy);
|
|
994
1118
|
void this._fetch().catch(() => undefined);
|
|
995
1119
|
}
|
|
996
1120
|
}
|
|
@@ -1006,8 +1130,9 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1006
1130
|
// (DEF-PANE-318). The menu that ran this command closed before running it,
|
|
1007
1131
|
// so the dialog taking the focus closes nothing.
|
|
1008
1132
|
const busy = beginSpinner(this._trans.__('Branching %1 session', this._descriptor.label), this._trans);
|
|
1133
|
+
const click = ++openClicks;
|
|
1009
1134
|
try {
|
|
1010
|
-
await this._branchByServerCopy(session, undefined, force, busy);
|
|
1135
|
+
await this._terminals.inClickOrder(() => this._branchByServerCopy(session, undefined, force, busy, click));
|
|
1011
1136
|
}
|
|
1012
1137
|
finally {
|
|
1013
1138
|
endSpinner(busy);
|
|
@@ -1017,7 +1142,8 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1017
1142
|
* opens - `server-copy` copies the store entry, `server-native` drives the
|
|
1018
1143
|
* assistant's own non-interactive fork path - so one refresh surfaces the
|
|
1019
1144
|
* branch. `spinner` is the click's token, which the launch joins. */
|
|
1020
|
-
async _branchByServerCopy(session, name, force, spinner) {
|
|
1145
|
+
async _branchByServerCopy(session, name, force, spinner, click = ++openClicks) {
|
|
1146
|
+
var _a;
|
|
1021
1147
|
// Let the parent's colour settle before the server reads it (DEF-74). The
|
|
1022
1148
|
// server copies the store entry while minting the fork, so a colour picked
|
|
1023
1149
|
// on the parent's tab moments earlier - still queued in this store's
|
|
@@ -1045,6 +1171,32 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1045
1171
|
})
|
|
1046
1172
|
});
|
|
1047
1173
|
minted = true;
|
|
1174
|
+
// The fork route has already written the child's pin, so a row click
|
|
1175
|
+
// still opening from before the Branch must not put the parent back
|
|
1176
|
+
// (DEF-PANE-349). The number is the Branch click's: a row clicked after
|
|
1177
|
+
// it outranks it and a row clicked before it stays suppressed. A newer
|
|
1178
|
+
// click has already switched the project, and the fork's own writes
|
|
1179
|
+
// landed after the user chose that conversation, so the correction is
|
|
1180
|
+
// queued on the project's chain - forced, because that decision may
|
|
1181
|
+
// already be marked posted - and it re-posts the newest decision after
|
|
1182
|
+
// the fork's writes. Otherwise the guard stops a fork that answers
|
|
1183
|
+
// after that later row's switch from lowering the recorded number.
|
|
1184
|
+
const recorded = this._currentTokens.get(session.encoded_path);
|
|
1185
|
+
if (recorded && recorded.click > click) {
|
|
1186
|
+
void this._enqueueSwitch(session, true);
|
|
1187
|
+
}
|
|
1188
|
+
else if (click > ((_a = recorded === null || recorded === void 0 ? void 0 : recorded.click) !== null && _a !== void 0 ? _a : 0)) {
|
|
1189
|
+
this._currentTokens.set(session.encoded_path, {
|
|
1190
|
+
click,
|
|
1191
|
+
sessionId: fork.session_id
|
|
1192
|
+
});
|
|
1193
|
+
// A switch post dispatched before this fork can still land after the
|
|
1194
|
+
// fork route's pin write, so the fork is queued on the project's chain
|
|
1195
|
+
// behind it and written again once that post settles. The chain reads
|
|
1196
|
+
// the newest recorded decision when it runs, so a row clicked later
|
|
1197
|
+
// still wins.
|
|
1198
|
+
void this._enqueueSwitch(session, true);
|
|
1199
|
+
}
|
|
1048
1200
|
// No colour write here. `BranchHandler` already inherited it while
|
|
1049
1201
|
// minting this fork, from a read taken server-side at request time -
|
|
1050
1202
|
// strictly fresher than the click-time read this would repeat, and the
|
|
@@ -1172,8 +1324,57 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1172
1324
|
}
|
|
1173
1325
|
return `Rename failed: ${err}`;
|
|
1174
1326
|
}
|
|
1175
|
-
/** Make another conversation the row's current one.
|
|
1176
|
-
|
|
1327
|
+
/** Make another conversation the row's current one. `click` is the number
|
|
1328
|
+
* of the action that asked: a row click carries the number it took when it
|
|
1329
|
+
* was pressed, so when its open resolves after a newer row click, an
|
|
1330
|
+
* explicit switch or a fork, it leaves that newer decision in place and
|
|
1331
|
+
* posts nothing (DEF-PANE-349). The decision is posted through the project's
|
|
1332
|
+
* chain, so its write lands after every switch enqueued before it. */
|
|
1333
|
+
async _switchBranch(session, sessionId, click = ++openClicks) {
|
|
1334
|
+
var _a, _b;
|
|
1335
|
+
if (click <= ((_b = (_a = this._currentTokens.get(session.encoded_path)) === null || _a === void 0 ? void 0 : _a.click) !== null && _b !== void 0 ? _b : 0)) {
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
this._currentTokens.set(session.encoded_path, { click, sessionId });
|
|
1339
|
+
await this._enqueueSwitch(session);
|
|
1340
|
+
}
|
|
1341
|
+
/** Queue a switch post for `session`'s project behind every post already
|
|
1342
|
+
* queued for it, so the server receives the writes in the order the user
|
|
1343
|
+
* acted and the last one to land is the newest decision (DEF-PANE-349).
|
|
1344
|
+
*
|
|
1345
|
+
* A task reads the newest recorded decision when it runs: a row click's
|
|
1346
|
+
* task posts it only if no task has posted that click yet, so clicks that
|
|
1347
|
+
* queue behind one another collapse to the newest. A fork correction is
|
|
1348
|
+
* forced - the fork's own pin write landed after the last post, so the
|
|
1349
|
+
* newest decision has to be written again even when it is already marked
|
|
1350
|
+
* posted. A chain entry is removed when its last task settles. */
|
|
1351
|
+
_enqueueSwitch(session, force = false) {
|
|
1352
|
+
var _a;
|
|
1353
|
+
const key = session.encoded_path;
|
|
1354
|
+
const run = ((_a = this._switchChains.get(key)) !== null && _a !== void 0 ? _a : Promise.resolve()).then(async () => {
|
|
1355
|
+
const newest = this._currentTokens.get(key);
|
|
1356
|
+
// An empty id is a New Session (+) decision, which posts nothing.
|
|
1357
|
+
if (!(newest === null || newest === void 0 ? void 0 : newest.sessionId)) {
|
|
1358
|
+
return;
|
|
1359
|
+
}
|
|
1360
|
+
if (!force && this._postedClicks.get(key) === newest.click) {
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
this._postedClicks.set(key, newest.click);
|
|
1364
|
+
await this._postSwitch(session, newest.sessionId);
|
|
1365
|
+
});
|
|
1366
|
+
this._switchChains.set(key, run);
|
|
1367
|
+
void run.then(() => {
|
|
1368
|
+
if (this._switchChains.get(key) === run) {
|
|
1369
|
+
this._switchChains.delete(key);
|
|
1370
|
+
}
|
|
1371
|
+
});
|
|
1372
|
+
return run;
|
|
1373
|
+
}
|
|
1374
|
+
/** Post `sessionId` as the project's current conversation and report the
|
|
1375
|
+
* outcome. Neither reads nor writes the project's token: the project's
|
|
1376
|
+
* chain chooses the id from the recorded decision - see `_enqueueSwitch`. */
|
|
1377
|
+
async _postSwitch(session, sessionId) {
|
|
1177
1378
|
try {
|
|
1178
1379
|
const result = await requestProvider(this._descriptor.id, 'switch', this._serverSettings, {
|
|
1179
1380
|
method: 'POST',
|
|
@@ -1192,8 +1393,8 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1192
1393
|
: `Switch failed: ${err}`, { autoClose: 4000 });
|
|
1193
1394
|
}
|
|
1194
1395
|
finally {
|
|
1195
|
-
// Best-effort refresh - never let a transient failure reject
|
|
1196
|
-
//
|
|
1396
|
+
// Best-effort refresh - never let a transient failure reject the switch:
|
|
1397
|
+
// the user action that asked for it is fire-and-forget.
|
|
1197
1398
|
await this._fetch().catch(() => undefined);
|
|
1198
1399
|
}
|
|
1199
1400
|
}
|
|
@@ -1299,6 +1500,11 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1299
1500
|
}
|
|
1300
1501
|
_render() {
|
|
1301
1502
|
var _a, _b, _c, _d, _e;
|
|
1503
|
+
if (this._pressed) {
|
|
1504
|
+
this._redrawWaits = true;
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
this._redrawWaits = false;
|
|
1302
1508
|
const sessions = (_a = this._sessions) !== null && _a !== void 0 ? _a : [];
|
|
1303
1509
|
// Capture scrollTop per section so a polling refresh does not reset the
|
|
1304
1510
|
// user's place. The body scrolls too in short windows - clearing innerHTML
|
|
@@ -1542,7 +1748,9 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1542
1748
|
if (removing) {
|
|
1543
1749
|
return;
|
|
1544
1750
|
}
|
|
1545
|
-
|
|
1751
|
+
// The row's own controls also make the conversation current, rather
|
|
1752
|
+
// than only opening it (DEF-PANE-349).
|
|
1753
|
+
void this._openSession(session, undefined, undefined, true);
|
|
1546
1754
|
});
|
|
1547
1755
|
row.addEventListener('contextmenu', e => {
|
|
1548
1756
|
e.preventDefault();
|
|
@@ -1591,7 +1799,9 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1591
1799
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
1592
1800
|
// Space would scroll the list otherwise.
|
|
1593
1801
|
e.preventDefault();
|
|
1594
|
-
|
|
1802
|
+
// The keyboard's row click, so it makes the conversation current
|
|
1803
|
+
// exactly as the pointer's does (DEF-PANE-349).
|
|
1804
|
+
void this._openSession(session, undefined, undefined, true);
|
|
1595
1805
|
return;
|
|
1596
1806
|
}
|
|
1597
1807
|
// The two keyboard routes to a context menu. Anchored on the row's own
|
|
@@ -1913,6 +2123,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1913
2123
|
});
|
|
1914
2124
|
this._commands.addCommand(this._cmd('copy-path'), {
|
|
1915
2125
|
label: this._trans.__('Copy Path'),
|
|
2126
|
+
icon: copyIcon,
|
|
1916
2127
|
execute: () => {
|
|
1917
2128
|
const s = active();
|
|
1918
2129
|
if (s) {
|
|
@@ -1922,6 +2133,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1922
2133
|
});
|
|
1923
2134
|
this._commands.addCommand(this._cmd('copy-session-id'), {
|
|
1924
2135
|
label: this._trans.__('Copy Session ID'),
|
|
2136
|
+
icon: copyIcon,
|
|
1925
2137
|
execute: () => {
|
|
1926
2138
|
var _a;
|
|
1927
2139
|
const id = (_a = active()) === null || _a === void 0 ? void 0 : _a.session_id;
|
|
@@ -1961,6 +2173,9 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1961
2173
|
// item beside an ordinary one (DEF-45). The sweep variant of the trash
|
|
1962
2174
|
// glyph, because this one deletes several at once.
|
|
1963
2175
|
icon: cleanupIcon,
|
|
2176
|
+
// Both history-deleting entries draw their icon in the error colour
|
|
2177
|
+
// (base.css).
|
|
2178
|
+
className: 'jp-AiAssistantsContextMenu-danger',
|
|
1964
2179
|
isVisible: () => { var _a, _b; return ((_b = (_a = active()) === null || _a === void 0 ? void 0 : _a.extra_sessions) !== null && _b !== void 0 ? _b : 0) > 0; },
|
|
1965
2180
|
execute: () => {
|
|
1966
2181
|
const s = active();
|
|
@@ -2057,6 +2272,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2057
2272
|
this._commands.addCommand(this._cmd('remove'), {
|
|
2058
2273
|
label: `Remove from ${this._descriptor.label}`,
|
|
2059
2274
|
icon: removeIcon,
|
|
2275
|
+
className: 'jp-AiAssistantsContextMenu-danger',
|
|
2060
2276
|
execute: () => {
|
|
2061
2277
|
const s = active();
|
|
2062
2278
|
if (s) {
|