jupyterlab_ai_code_assistants_extension 1.2.163 → 1.2.236
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/dialogs.d.ts +4 -2
- package/lib/core/dialogs.js +4 -2
- package/lib/core/icons.d.ts +5 -1
- package/lib/core/icons.js +13 -3
- package/lib/core/panel.d.ts +52 -4
- package/lib/core/panel.js +213 -29
- package/lib/core/popup.js +12 -1
- package/lib/core/terminals.d.ts +61 -8
- package/lib/core/terminals.js +233 -31
- package/package.json +13 -10
- package/src/__tests__/icon-size.spec.ts +4 -2
- package/src/__tests__/launcher-icon.spec.ts +9 -3
- package/src/__tests__/launcher.spec.ts +155 -3
- package/src/__tests__/panel.spec.ts +835 -73
- package/src/__tests__/popup.spec.ts +47 -0
- package/src/__tests__/terminals.spec.ts +357 -1
- package/src/core/dialogs.ts +4 -2
- package/src/core/icons.ts +16 -3
- package/src/core/panel.ts +235 -40
- package/src/core/popup.ts +12 -1
- package/src/core/terminals.ts +266 -33
- package/style/base.css +21 -8
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/dialogs.d.ts
CHANGED
|
@@ -2,8 +2,10 @@ import { Dialog } from '@jupyterlab/apputils';
|
|
|
2
2
|
import { Widget } from '@lumino/widgets';
|
|
3
3
|
/**
|
|
4
4
|
* The renderer every dialog of this extension is built with. It marks the
|
|
5
|
-
* footer, and `style/base.css` scopes its dialog rules - no focus rim on
|
|
6
|
-
* button
|
|
5
|
+
* footer, and `style/base.css` scopes its dialog rules - no focus rim on a
|
|
6
|
+
* button focused by the mouse or by a dialog opened with the mouse (keyboard
|
|
7
|
+
* focus keeps JupyterLab's rim), the reject buttons (Cancel, and Close in the
|
|
8
|
+
* Open Callback popup) as
|
|
7
9
|
* secondary surface buttons so the fill alone marks the primary - to a dialog
|
|
8
10
|
* that holds the mark, so JupyterLab's own
|
|
9
11
|
* dialogs keep their look. A renderer is the one hook that also reaches the
|
package/lib/core/dialogs.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { Dialog } from '@jupyterlab/apputils';
|
|
2
2
|
/**
|
|
3
3
|
* The renderer every dialog of this extension is built with. It marks the
|
|
4
|
-
* footer, and `style/base.css` scopes its dialog rules - no focus rim on
|
|
5
|
-
* button
|
|
4
|
+
* footer, and `style/base.css` scopes its dialog rules - no focus rim on a
|
|
5
|
+
* button focused by the mouse or by a dialog opened with the mouse (keyboard
|
|
6
|
+
* focus keeps JupyterLab's rim), the reject buttons (Cancel, and Close in the
|
|
7
|
+
* Open Callback popup) as
|
|
6
8
|
* secondary surface buttons so the fill alone marks the primary - to a dialog
|
|
7
9
|
* that holds the mark, so JupyterLab's own
|
|
8
10
|
* dialogs keep their look. A renderer is the one hook that also reaches the
|
package/lib/core/icons.d.ts
CHANGED
|
@@ -28,7 +28,11 @@ export declare function providerIcon(name: string, svgstr: string): LabIcon;
|
|
|
28
28
|
* preset, and each tile under `launcherCard`. This view of `tile` renders the
|
|
29
29
|
* extension's joint icon under the section preset and `tile` everywhere else,
|
|
30
30
|
* so the header carries no vendor's mark while every tile keeps its own
|
|
31
|
-
* (ACC-LNCH-163).
|
|
31
|
+
* (ACC-LNCH-163). Under `launcherCard` it also adds the class
|
|
32
|
+
* `jp-AiAssistantsLauncher-tileGlyph`, which `style/base.css` scales to 75%,
|
|
33
|
+
* so the tile glyph is drawn smaller than JupyterLab's own tile glyphs while
|
|
34
|
+
* the sidebar tab, panel header and menus keep the standard size
|
|
35
|
+
* (ACC-LNCH-215).
|
|
32
36
|
*
|
|
33
37
|
* Built the way `LabIcon.bindprops` builds its views - a prototype child of
|
|
34
38
|
* the tile with its own `react` - so `LabIcon.resolve` still recognises it.
|
package/lib/core/icons.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { LabIcon } from '@jupyterlab/ui-components';
|
|
1
|
+
import { classes, LabIcon } from '@jupyterlab/ui-components';
|
|
2
2
|
import * as React from 'react';
|
|
3
3
|
// Shared panel chrome icons. Provider icons are NOT here - each provider
|
|
4
4
|
// carries its own SVG on its descriptor and the registry builds the LabIcon
|
|
@@ -148,7 +148,11 @@ export function providerIcon(name, svgstr) {
|
|
|
148
148
|
* preset, and each tile under `launcherCard`. This view of `tile` renders the
|
|
149
149
|
* extension's joint icon under the section preset and `tile` everywhere else,
|
|
150
150
|
* so the header carries no vendor's mark while every tile keeps its own
|
|
151
|
-
* (ACC-LNCH-163).
|
|
151
|
+
* (ACC-LNCH-163). Under `launcherCard` it also adds the class
|
|
152
|
+
* `jp-AiAssistantsLauncher-tileGlyph`, which `style/base.css` scales to 75%,
|
|
153
|
+
* so the tile glyph is drawn smaller than JupyterLab's own tile glyphs while
|
|
154
|
+
* the sidebar tab, panel header and menus keep the standard size
|
|
155
|
+
* (ACC-LNCH-215).
|
|
152
156
|
*
|
|
153
157
|
* Built the way `LabIcon.bindprops` builds its views - a prototype child of
|
|
154
158
|
* the tile with its own `react` - so `LabIcon.resolve` still recognises it.
|
|
@@ -157,7 +161,13 @@ export function launcherTileIcon(tile) {
|
|
|
157
161
|
const view = Object.create(tile);
|
|
158
162
|
const react = React.forwardRef((props, ref) => React.createElement(props.stylesheet === 'launcherSection'
|
|
159
163
|
? assistantsIcon.react
|
|
160
|
-
: tile.react, {
|
|
164
|
+
: tile.react, {
|
|
165
|
+
...props,
|
|
166
|
+
...(props.stylesheet === 'launcherCard' && {
|
|
167
|
+
className: classes(props.className, 'jp-AiAssistantsLauncher-tileGlyph')
|
|
168
|
+
}),
|
|
169
|
+
ref
|
|
170
|
+
}));
|
|
161
171
|
// `react` is readonly on the type; the view shadows the tile's own.
|
|
162
172
|
Object.defineProperty(view, 'react', { value: react });
|
|
163
173
|
return view;
|
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,31 @@ 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
|
+
/** A button is pressed in the list, and a redraw waits for its release -
|
|
386
|
+
* see `_onRelease`. */
|
|
387
|
+
private _pressed;
|
|
388
|
+
private _redrawWaits;
|
|
341
389
|
private _presentationMode;
|
|
342
390
|
private _recentLimit;
|
|
343
391
|
private _modes;
|
package/lib/core/panel.js
CHANGED
|
@@ -71,6 +71,15 @@ 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 and a fork each take a number, and a row click posts its deferred
|
|
80
|
+
* switch only while it is the newest (DEF-PANE-349). */
|
|
81
|
+
let openClicks = 0;
|
|
82
|
+
let movedClick = 0;
|
|
74
83
|
function loadExpanded(storageKey) {
|
|
75
84
|
try {
|
|
76
85
|
const raw = window.localStorage.getItem(storageKey);
|
|
@@ -109,6 +118,19 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
109
118
|
constructor(options) {
|
|
110
119
|
var _a, _b, _c, _d, _e;
|
|
111
120
|
super();
|
|
121
|
+
/** The press in the list has ended. The click it sends is dispatched in the
|
|
122
|
+
* same task as this release, so the waiting redraw runs in the next one. */
|
|
123
|
+
this._onRelease = () => {
|
|
124
|
+
if (!this._pressed) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this._pressed = false;
|
|
128
|
+
window.setTimeout(() => {
|
|
129
|
+
if (this._redrawWaits && !this.isDisposed) {
|
|
130
|
+
this._render();
|
|
131
|
+
}
|
|
132
|
+
}, 0);
|
|
133
|
+
};
|
|
112
134
|
this._rootDir = '';
|
|
113
135
|
this._deleteToTrash = true;
|
|
114
136
|
this._removingPaths = new Set();
|
|
@@ -136,12 +158,31 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
136
158
|
/** The last branch listing fetched, and the project it belongs to. Read only
|
|
137
159
|
* when this open's own fetch did not arrive in time. */
|
|
138
160
|
this._branchCache = null;
|
|
161
|
+
/** The number and session id of the newest action that decided each
|
|
162
|
+
* project's current conversation, by encoded path - see `_switchBranch`
|
|
163
|
+
* (DEF-PANE-349). The id is kept so a fork's late server writes can be
|
|
164
|
+
* corrected by re-posting the newer decision. */
|
|
165
|
+
this._currentTokens = new Map();
|
|
166
|
+
/** The switch posts queued per project, by encoded path, and the click
|
|
167
|
+
* number each project's chain posted last - see `_enqueueSwitch`. A chain
|
|
168
|
+
* entry is removed when its last task settles; the posted number stays, so a
|
|
169
|
+
* later task can tell an already-posted decision from a new one. */
|
|
170
|
+
this._switchChains = new Map();
|
|
171
|
+
this._postedClicks = new Map();
|
|
139
172
|
this._activeSession = null;
|
|
140
173
|
this._activeRowEl = null;
|
|
141
174
|
this._pollHandle = null;
|
|
142
175
|
/** The poll's own in-flight tick. Only the poll reads it - an explicit
|
|
143
176
|
* Refresh must always issue, however long the last poll is taking. */
|
|
144
177
|
this._polling = null;
|
|
178
|
+
/** Listing requests sent, and the number of the newest one drawn - see
|
|
179
|
+
* `_fetch`. */
|
|
180
|
+
this._fetchSent = 0;
|
|
181
|
+
this._fetchDrawn = 0;
|
|
182
|
+
/** A button is pressed in the list, and a redraw waits for its release -
|
|
183
|
+
* see `_onRelease`. */
|
|
184
|
+
this._pressed = false;
|
|
185
|
+
this._redrawWaits = false;
|
|
145
186
|
this._presentationMode = DEFAULT_PRESENTATION_MODE;
|
|
146
187
|
this._recentLimit = DEFAULT_RECENT_LIMIT;
|
|
147
188
|
this._modes = {};
|
|
@@ -184,6 +225,8 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
184
225
|
return this._descriptor;
|
|
185
226
|
}
|
|
186
227
|
dispose() {
|
|
228
|
+
document.removeEventListener('pointerup', this._onRelease, true);
|
|
229
|
+
document.removeEventListener('pointercancel', this._onRelease, true);
|
|
187
230
|
this._stopPolling();
|
|
188
231
|
// Disabling a provider must leave its running terminals alive and only
|
|
189
232
|
// drop the tint this panel applied.
|
|
@@ -407,6 +450,15 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
407
450
|
this._bodyEl = body;
|
|
408
451
|
this._loadingEl = loading;
|
|
409
452
|
this._errorEl = error;
|
|
453
|
+
// A row drawn again between the press and the release of a click loses
|
|
454
|
+
// the click: the browser sends it to the list the two rows share, not to
|
|
455
|
+
// the row (DEF-PANE-346). A redraw asked for while a button is pressed in
|
|
456
|
+
// the list waits until that click has been sent.
|
|
457
|
+
body.addEventListener('pointerdown', () => {
|
|
458
|
+
this._pressed = true;
|
|
459
|
+
});
|
|
460
|
+
document.addEventListener('pointerup', this._onRelease, true);
|
|
461
|
+
document.addEventListener('pointercancel', this._onRelease, true);
|
|
410
462
|
}
|
|
411
463
|
/** Show / hide the filter input. Hiding also clears the active filter, so
|
|
412
464
|
* the user never returns to an "invisible" filter narrowing the rows. */
|
|
@@ -578,9 +630,16 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
578
630
|
// ------------------------------------------------------------------- data
|
|
579
631
|
async _fetch() {
|
|
580
632
|
var _a;
|
|
633
|
+
// This request's place in the order. Requests answer in any order, and an
|
|
634
|
+
// answer older than the one already drawn is dropped (DEF-PANE-344).
|
|
635
|
+
const request = ++this._fetchSent;
|
|
581
636
|
// `cache: 'no-store'` so a manual refresh always re-reads the server's
|
|
582
637
|
// view rather than a possibly stale browser-cached response.
|
|
583
638
|
const data = await requestProvider(this._descriptor.id, 'sessions', this._serverSettings, { cache: 'no-store' });
|
|
639
|
+
if (request < this._fetchDrawn) {
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
this._fetchDrawn = request;
|
|
584
643
|
this._sessions = (_a = data.sessions) !== null && _a !== void 0 ? _a : [];
|
|
585
644
|
this._setInlineError(null);
|
|
586
645
|
this._terminals.setSessions(this._sessions);
|
|
@@ -753,15 +812,32 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
753
812
|
_launchMode(force) {
|
|
754
813
|
return resolveLaunchMode(this._descriptor.launchModes, this._modes, force);
|
|
755
814
|
}
|
|
756
|
-
async _openSession(session, force, sessionId) {
|
|
815
|
+
async _openSession(session, force, sessionId, makeCurrent = false) {
|
|
757
816
|
// The click's own feedback, raised before the terminal walk and the
|
|
758
817
|
// launch - both can take seconds (DEF-PANE-318). The launch's title; the
|
|
759
818
|
// launch joins this dialog through the token.
|
|
760
819
|
const busy = beginSpinner(this._trans.__('Opening %1', this._descriptor.label), this._trans);
|
|
820
|
+
const click = ++openClicks;
|
|
821
|
+
const mode = this._launchMode(force);
|
|
761
822
|
try {
|
|
762
|
-
await this._terminals.
|
|
823
|
+
await this._terminals.inClickOrder(() => this._terminals.openSession(session, mode, sessionId, busy));
|
|
824
|
+
if (makeCurrent) {
|
|
825
|
+
// The row's own controls make that conversation the project's current
|
|
826
|
+
// one - the pin and the `claude -c` mtime lead (DEF-PANE-349). Not
|
|
827
|
+
// awaited: the open has succeeded, and the switch is not part of it.
|
|
828
|
+
// It carries this click's number, so a switch or fork made after this
|
|
829
|
+
// click wins even when this open finishes last. Other launches leave
|
|
830
|
+
// `claude -c` alone.
|
|
831
|
+
void this._switchBranch(session, session.session_id, click);
|
|
832
|
+
}
|
|
763
833
|
// Not awaited: the open has succeeded, and the sync is not part of it.
|
|
764
|
-
fileBrowserMoves = fileBrowserMoves.then(() =>
|
|
834
|
+
fileBrowserMoves = fileBrowserMoves.then(() => {
|
|
835
|
+
if (click < movedClick) {
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
movedClick = click;
|
|
839
|
+
return this._moveFileBrowserTo(session);
|
|
840
|
+
});
|
|
765
841
|
}
|
|
766
842
|
catch (err) {
|
|
767
843
|
if (isResponseStatus(err, 404)) {
|
|
@@ -861,14 +937,15 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
861
937
|
* The tile's whole click path lives here rather than in `src/index.ts`
|
|
862
938
|
* because every input it needs is already the panel's: the root join, the
|
|
863
939
|
* listing fetch, the terminal reuse ladder and the settings-resolved launch
|
|
864
|
-
* mode. Nothing here retries
|
|
865
|
-
*
|
|
866
|
-
*
|
|
940
|
+
* mode. Nothing here retries: a guard that refuses shows one notification
|
|
941
|
+
* and launches nothing, and a request that fails writes nothing. Past the
|
|
942
|
+
* guards the path waits for the tile launches of every assistant and the
|
|
943
|
+
* launches of this panel clicked before it, and every launch of any panel
|
|
944
|
+
* clicked after it waits for it (DEF-PANE-345).
|
|
867
945
|
*
|
|
868
946
|
* `cwd` is a Contents-API path, `''` for the root.
|
|
869
947
|
*/
|
|
870
948
|
async launchHere(cwd) {
|
|
871
|
-
var _a, _b;
|
|
872
949
|
// No roster yet, so the root is unknown. Joining onto an empty one names a
|
|
873
950
|
// folder at the filesystem root, which the server accepts whenever it
|
|
874
951
|
// happens to exist (ACC-LNCH-148, DEF-132).
|
|
@@ -891,7 +968,16 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
891
968
|
return undefined;
|
|
892
969
|
}
|
|
893
970
|
const folder = this._currentFolder(cwd);
|
|
971
|
+
return this._terminals.tileInClickOrder(() => this._launchIn(folder));
|
|
972
|
+
}
|
|
973
|
+
/** The tile's click path past its guards, run after the tile launches of
|
|
974
|
+
* every assistant and the launches of this panel clicked before it, and
|
|
975
|
+
* before every launch clicked after it (DEF-PANE-345). */
|
|
976
|
+
async _launchIn(folder) {
|
|
977
|
+
var _a, _b, _c, _d, _e;
|
|
894
978
|
let argv;
|
|
979
|
+
// The conversation the new terminal runs, when its id is known here.
|
|
980
|
+
let conversation;
|
|
895
981
|
try {
|
|
896
982
|
// Every click asks the server for the listing (ACC-LNCH-151). `_sessions`
|
|
897
983
|
// is written by `_fetch` and by the optimistic remove in
|
|
@@ -910,10 +996,13 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
910
996
|
// The folder is required as well: a transcript re-homed into another
|
|
911
997
|
// project keeps its id, and that terminal is not this folder's
|
|
912
998
|
// (DEF-PANE-319).
|
|
913
|
-
|
|
999
|
+
// The terminal a launch before this click opened for it counts too,
|
|
1000
|
+
// before the server can read the conversation off its process
|
|
1001
|
+
// (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;
|
|
914
1003
|
if (found) {
|
|
915
|
-
this._terminals.focus(found
|
|
916
|
-
return found
|
|
1004
|
+
this._terminals.focus(found);
|
|
1005
|
+
return found;
|
|
917
1006
|
}
|
|
918
1007
|
}
|
|
919
1008
|
const request = {
|
|
@@ -929,6 +1018,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
929
1018
|
};
|
|
930
1019
|
const answer = await requestProvider(this._descriptor.id, 'launch-argv', this._serverSettings, { method: 'POST', body: JSON.stringify(request) });
|
|
931
1020
|
argv = answer.argv;
|
|
1021
|
+
conversation = (_e = request.session_id) !== null && _e !== void 0 ? _e : request.new_session_id;
|
|
932
1022
|
}
|
|
933
1023
|
catch (err) {
|
|
934
1024
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -952,10 +1042,14 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
952
1042
|
// and the Launcher keeps its tab (ACC-LNCH-157 covers the success path
|
|
953
1043
|
// only).
|
|
954
1044
|
try {
|
|
955
|
-
|
|
1045
|
+
const widget = await this._app.commands.execute(BASIC_TERMINAL_LAUNCH, {
|
|
956
1046
|
argv,
|
|
957
1047
|
cwd: folder
|
|
958
1048
|
});
|
|
1049
|
+
if (widget === null || widget === void 0 ? void 0 : widget.id) {
|
|
1050
|
+
this._terminals.remember(folder, widget, conversation);
|
|
1051
|
+
}
|
|
1052
|
+
return widget;
|
|
959
1053
|
}
|
|
960
1054
|
catch (err) {
|
|
961
1055
|
this._notifyLaunchError('Could not open a terminal for this folder', err);
|
|
@@ -977,13 +1071,17 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
977
1071
|
// store key lets the server clear that pin so the new conversation is not
|
|
978
1072
|
// shadowed by a stale one (DEF-9).
|
|
979
1073
|
const existing = ((_a = this._sessions) !== null && _a !== void 0 ? _a : []).find(s => s.project_path === projectPath);
|
|
1074
|
+
// Raised on the click, not by the launch: the launch waits for a tile
|
|
1075
|
+
// launch clicked before it (DEF-PANE-345).
|
|
1076
|
+
const busy = beginSpinner(this._trans.__('Opening %1', this._descriptor.label), this._trans);
|
|
1077
|
+
const request = {
|
|
1078
|
+
project_path: projectPath,
|
|
1079
|
+
encoded_path: existing === null || existing === void 0 ? void 0 : existing.encoded_path,
|
|
1080
|
+
new_session_id: newId,
|
|
1081
|
+
mode: this._launchMode(force)
|
|
1082
|
+
};
|
|
980
1083
|
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);
|
|
1084
|
+
await this._terminals.inClickOrder(() => this._terminals.launch(request, newId, busy));
|
|
987
1085
|
}
|
|
988
1086
|
catch (err) {
|
|
989
1087
|
// Same shape as `_openSession` above - the `finally` refetches, so an
|
|
@@ -991,6 +1089,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
991
1089
|
this._notifyLaunchError('Could not start a new session', err);
|
|
992
1090
|
}
|
|
993
1091
|
finally {
|
|
1092
|
+
endSpinner(busy);
|
|
994
1093
|
void this._fetch().catch(() => undefined);
|
|
995
1094
|
}
|
|
996
1095
|
}
|
|
@@ -1006,8 +1105,9 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1006
1105
|
// (DEF-PANE-318). The menu that ran this command closed before running it,
|
|
1007
1106
|
// so the dialog taking the focus closes nothing.
|
|
1008
1107
|
const busy = beginSpinner(this._trans.__('Branching %1 session', this._descriptor.label), this._trans);
|
|
1108
|
+
const click = ++openClicks;
|
|
1009
1109
|
try {
|
|
1010
|
-
await this._branchByServerCopy(session, undefined, force, busy);
|
|
1110
|
+
await this._terminals.inClickOrder(() => this._branchByServerCopy(session, undefined, force, busy, click));
|
|
1011
1111
|
}
|
|
1012
1112
|
finally {
|
|
1013
1113
|
endSpinner(busy);
|
|
@@ -1017,7 +1117,8 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1017
1117
|
* opens - `server-copy` copies the store entry, `server-native` drives the
|
|
1018
1118
|
* assistant's own non-interactive fork path - so one refresh surfaces the
|
|
1019
1119
|
* branch. `spinner` is the click's token, which the launch joins. */
|
|
1020
|
-
async _branchByServerCopy(session, name, force, spinner) {
|
|
1120
|
+
async _branchByServerCopy(session, name, force, spinner, click = ++openClicks) {
|
|
1121
|
+
var _a;
|
|
1021
1122
|
// Let the parent's colour settle before the server reads it (DEF-74). The
|
|
1022
1123
|
// server copies the store entry while minting the fork, so a colour picked
|
|
1023
1124
|
// on the parent's tab moments earlier - still queued in this store's
|
|
@@ -1045,6 +1146,32 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1045
1146
|
})
|
|
1046
1147
|
});
|
|
1047
1148
|
minted = true;
|
|
1149
|
+
// The fork route has already written the child's pin, so a row click
|
|
1150
|
+
// still opening from before the Branch must not put the parent back
|
|
1151
|
+
// (DEF-PANE-349). The number is the Branch click's: a row clicked after
|
|
1152
|
+
// it outranks it and a row clicked before it stays suppressed. A newer
|
|
1153
|
+
// click has already switched the project, and the fork's own writes
|
|
1154
|
+
// landed after the user chose that conversation, so the correction is
|
|
1155
|
+
// queued on the project's chain - forced, because that decision may
|
|
1156
|
+
// already be marked posted - and it re-posts the newest decision after
|
|
1157
|
+
// the fork's writes. Otherwise the guard stops a fork that answers
|
|
1158
|
+
// after that later row's switch from lowering the recorded number.
|
|
1159
|
+
const recorded = this._currentTokens.get(session.encoded_path);
|
|
1160
|
+
if (recorded && recorded.click > click) {
|
|
1161
|
+
void this._enqueueSwitch(session, true);
|
|
1162
|
+
}
|
|
1163
|
+
else if (click > ((_a = recorded === null || recorded === void 0 ? void 0 : recorded.click) !== null && _a !== void 0 ? _a : 0)) {
|
|
1164
|
+
this._currentTokens.set(session.encoded_path, {
|
|
1165
|
+
click,
|
|
1166
|
+
sessionId: fork.session_id
|
|
1167
|
+
});
|
|
1168
|
+
// A switch post dispatched before this fork can still land after the
|
|
1169
|
+
// fork route's pin write, so the fork is queued on the project's chain
|
|
1170
|
+
// behind it and written again once that post settles. The chain reads
|
|
1171
|
+
// the newest recorded decision when it runs, so a row clicked later
|
|
1172
|
+
// still wins.
|
|
1173
|
+
void this._enqueueSwitch(session, true);
|
|
1174
|
+
}
|
|
1048
1175
|
// No colour write here. `BranchHandler` already inherited it while
|
|
1049
1176
|
// minting this fork, from a read taken server-side at request time -
|
|
1050
1177
|
// strictly fresher than the click-time read this would repeat, and the
|
|
@@ -1057,10 +1184,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1057
1184
|
}, fork.session_id, spinner);
|
|
1058
1185
|
}
|
|
1059
1186
|
catch (err) {
|
|
1060
|
-
if (
|
|
1061
|
-
Notification.error(`${this._descriptor.label} cannot branch this conversation.`, { autoClose: 4000 });
|
|
1062
|
-
}
|
|
1063
|
-
else if (minted) {
|
|
1187
|
+
if (minted) {
|
|
1064
1188
|
// The branch was created and only its terminal was refused - the
|
|
1065
1189
|
// launch preflight's 409 carries words for the user. "Could not
|
|
1066
1190
|
// branch this session" would be false, and the early return would
|
|
@@ -1073,6 +1197,9 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1073
1197
|
this._notifyLaunchError('Branch created; its terminal did not open', err);
|
|
1074
1198
|
await this._fetch().catch(() => undefined);
|
|
1075
1199
|
}
|
|
1200
|
+
else if (isResponseStatus(err, 400)) {
|
|
1201
|
+
Notification.error(`${this._descriptor.label} cannot branch this conversation.`, { autoClose: 4000 });
|
|
1202
|
+
}
|
|
1076
1203
|
else {
|
|
1077
1204
|
this._showActionError('Could not branch this session - try again.', err);
|
|
1078
1205
|
}
|
|
@@ -1172,8 +1299,56 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1172
1299
|
}
|
|
1173
1300
|
return `Rename failed: ${err}`;
|
|
1174
1301
|
}
|
|
1175
|
-
/** Make another conversation the row's current one.
|
|
1176
|
-
|
|
1302
|
+
/** Make another conversation the row's current one. `click` is the number
|
|
1303
|
+
* of the action that asked: a row click carries the number it took when it
|
|
1304
|
+
* was pressed, so when its open resolves after a newer row click, an
|
|
1305
|
+
* explicit switch or a fork, it leaves that newer decision in place and
|
|
1306
|
+
* posts nothing (DEF-PANE-349). The decision is posted through the project's
|
|
1307
|
+
* chain, so its write lands after every switch enqueued before it. */
|
|
1308
|
+
async _switchBranch(session, sessionId, click = ++openClicks) {
|
|
1309
|
+
var _a, _b;
|
|
1310
|
+
if (click <= ((_b = (_a = this._currentTokens.get(session.encoded_path)) === null || _a === void 0 ? void 0 : _a.click) !== null && _b !== void 0 ? _b : 0)) {
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
this._currentTokens.set(session.encoded_path, { click, sessionId });
|
|
1314
|
+
await this._enqueueSwitch(session);
|
|
1315
|
+
}
|
|
1316
|
+
/** Queue a switch post for `session`'s project behind every post already
|
|
1317
|
+
* queued for it, so the server receives the writes in the order the user
|
|
1318
|
+
* acted and the last one to land is the newest decision (DEF-PANE-349).
|
|
1319
|
+
*
|
|
1320
|
+
* A task reads the newest recorded decision when it runs: a row click's
|
|
1321
|
+
* task posts it only if no task has posted that click yet, so clicks that
|
|
1322
|
+
* queue behind one another collapse to the newest. A fork correction is
|
|
1323
|
+
* forced - the fork's own pin write landed after the last post, so the
|
|
1324
|
+
* newest decision has to be written again even when it is already marked
|
|
1325
|
+
* posted. A chain entry is removed when its last task settles. */
|
|
1326
|
+
_enqueueSwitch(session, force = false) {
|
|
1327
|
+
var _a;
|
|
1328
|
+
const key = session.encoded_path;
|
|
1329
|
+
const run = ((_a = this._switchChains.get(key)) !== null && _a !== void 0 ? _a : Promise.resolve()).then(async () => {
|
|
1330
|
+
const newest = this._currentTokens.get(key);
|
|
1331
|
+
if (!newest) {
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1334
|
+
if (!force && this._postedClicks.get(key) === newest.click) {
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
this._postedClicks.set(key, newest.click);
|
|
1338
|
+
await this._postSwitch(session, newest.sessionId);
|
|
1339
|
+
});
|
|
1340
|
+
this._switchChains.set(key, run);
|
|
1341
|
+
void run.then(() => {
|
|
1342
|
+
if (this._switchChains.get(key) === run) {
|
|
1343
|
+
this._switchChains.delete(key);
|
|
1344
|
+
}
|
|
1345
|
+
});
|
|
1346
|
+
return run;
|
|
1347
|
+
}
|
|
1348
|
+
/** Post `sessionId` as the project's current conversation and report the
|
|
1349
|
+
* outcome. Neither reads nor writes the project's token: the project's
|
|
1350
|
+
* chain chooses the id from the recorded decision - see `_enqueueSwitch`. */
|
|
1351
|
+
async _postSwitch(session, sessionId) {
|
|
1177
1352
|
try {
|
|
1178
1353
|
const result = await requestProvider(this._descriptor.id, 'switch', this._serverSettings, {
|
|
1179
1354
|
method: 'POST',
|
|
@@ -1192,8 +1367,8 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1192
1367
|
: `Switch failed: ${err}`, { autoClose: 4000 });
|
|
1193
1368
|
}
|
|
1194
1369
|
finally {
|
|
1195
|
-
// Best-effort refresh - never let a transient failure reject
|
|
1196
|
-
//
|
|
1370
|
+
// Best-effort refresh - never let a transient failure reject the switch:
|
|
1371
|
+
// the user action that asked for it is fire-and-forget.
|
|
1197
1372
|
await this._fetch().catch(() => undefined);
|
|
1198
1373
|
}
|
|
1199
1374
|
}
|
|
@@ -1299,6 +1474,11 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1299
1474
|
}
|
|
1300
1475
|
_render() {
|
|
1301
1476
|
var _a, _b, _c, _d, _e;
|
|
1477
|
+
if (this._pressed) {
|
|
1478
|
+
this._redrawWaits = true;
|
|
1479
|
+
return;
|
|
1480
|
+
}
|
|
1481
|
+
this._redrawWaits = false;
|
|
1302
1482
|
const sessions = (_a = this._sessions) !== null && _a !== void 0 ? _a : [];
|
|
1303
1483
|
// Capture scrollTop per section so a polling refresh does not reset the
|
|
1304
1484
|
// user's place. The body scrolls too in short windows - clearing innerHTML
|
|
@@ -1542,7 +1722,9 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1542
1722
|
if (removing) {
|
|
1543
1723
|
return;
|
|
1544
1724
|
}
|
|
1545
|
-
|
|
1725
|
+
// The row's own controls also make the conversation current, rather
|
|
1726
|
+
// than only opening it (DEF-PANE-349).
|
|
1727
|
+
void this._openSession(session, undefined, undefined, true);
|
|
1546
1728
|
});
|
|
1547
1729
|
row.addEventListener('contextmenu', e => {
|
|
1548
1730
|
e.preventDefault();
|
|
@@ -1591,7 +1773,9 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1591
1773
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
1592
1774
|
// Space would scroll the list otherwise.
|
|
1593
1775
|
e.preventDefault();
|
|
1594
|
-
|
|
1776
|
+
// The keyboard's row click, so it makes the conversation current
|
|
1777
|
+
// exactly as the pointer's does (DEF-PANE-349).
|
|
1778
|
+
void this._openSession(session, undefined, undefined, true);
|
|
1595
1779
|
return;
|
|
1596
1780
|
}
|
|
1597
1781
|
// The two keyboard routes to a context menu. Anchored on the row's own
|
package/lib/core/popup.js
CHANGED
|
@@ -50,6 +50,9 @@ export function showManageSessionsPopup(options) {
|
|
|
50
50
|
// removal is permanent.
|
|
51
51
|
let armed = false;
|
|
52
52
|
let armTimer = 0;
|
|
53
|
+
// The arming click's event.timeStamp, which the confirm click is measured
|
|
54
|
+
// against.
|
|
55
|
+
let armedAt = 0;
|
|
53
56
|
const body = document.createElement('div');
|
|
54
57
|
body.className = 'jp-AiAssistantsPanel-branchPopup';
|
|
55
58
|
const search = document.createElement('input');
|
|
@@ -348,10 +351,17 @@ export function showManageSessionsPopup(options) {
|
|
|
348
351
|
list.removeAttribute('aria-busy');
|
|
349
352
|
}
|
|
350
353
|
};
|
|
351
|
-
deleteBtn.addEventListener('click',
|
|
354
|
+
deleteBtn.addEventListener('click', event => {
|
|
352
355
|
if (deleting || selected.size === 0) {
|
|
353
356
|
return;
|
|
354
357
|
}
|
|
358
|
+
// The second click of a double-click is not a confirmation (DEF-PANE-336):
|
|
359
|
+
// a multi-click within 600 ms of arming is ignored. A later click confirms
|
|
360
|
+
// whatever its detail - the OS double-click interval can be longer than
|
|
361
|
+
// the user's pause, and the click would be dropped (DEF-PANE-338).
|
|
362
|
+
if (armed && event.detail > 1 && event.timeStamp - armedAt < 600) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
355
365
|
// No confirmation DIALOG here: a second Lumino dialog stacked on this popup
|
|
356
366
|
// renders detached. The deletion confirms in the footer itself - the first
|
|
357
367
|
// click arms the button, the second executes, and any selection change or
|
|
@@ -360,6 +370,7 @@ export function showManageSessionsPopup(options) {
|
|
|
360
370
|
// refused move may remove the conversation permanently or leave it.
|
|
361
371
|
if (!armed) {
|
|
362
372
|
armed = true;
|
|
373
|
+
armedAt = event.timeStamp;
|
|
363
374
|
window.clearTimeout(armTimer);
|
|
364
375
|
armTimer = window.setTimeout(() => {
|
|
365
376
|
disarm();
|