jupyterlab_ai_code_assistants_extension 1.2.73 → 1.2.83
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 +6 -5
- package/lib/core/colour.d.ts +2 -10
- package/lib/core/colour.js +8 -18
- package/lib/core/modes.d.ts +2 -2
- package/lib/core/panel.d.ts +8 -43
- package/lib/core/panel.js +44 -229
- package/lib/core/popup.d.ts +1 -1
- package/lib/core/popup.js +52 -11
- package/lib/core/request.js +13 -6
- package/lib/core/terminals.d.ts +2 -33
- package/lib/core/terminals.js +24 -100
- package/lib/core/types.d.ts +13 -33
- package/lib/providers/claude.js +4 -11
- package/lib/providers/codex.js +9 -12
- package/lib/providers/deepseek.d.ts +1 -2
- package/lib/providers/deepseek.js +14 -35
- package/lib/providers/gemini.js +1 -9
- package/lib/providers/kimi.js +0 -6
- package/package.json +1 -1
- package/src/__tests__/colour.spec.ts +11 -19
- package/src/__tests__/launcher.spec.ts +1 -3
- package/src/__tests__/panel.spec.ts +105 -248
- package/src/__tests__/popup.spec.ts +105 -2
- package/src/__tests__/registry.spec.ts +1 -4
- package/src/__tests__/tab-colour-capture.spec.ts +48 -3
- package/src/__tests__/terminals.spec.ts +29 -143
- package/src/core/colour.ts +8 -23
- package/src/core/modes.ts +2 -2
- package/src/core/panel.ts +48 -272
- package/src/core/popup.ts +58 -12
- package/src/core/request.ts +13 -6
- package/src/core/terminals.ts +30 -118
- package/src/core/types.ts +13 -34
- package/src/index.ts +2 -2
- package/src/providers/claude.ts +4 -11
- package/src/providers/codex.ts +9 -12
- package/src/providers/deepseek.ts +15 -37
- package/src/providers/gemini.ts +1 -9
- package/src/providers/kimi.ts +0 -6
- package/style/base.css +48 -36
package/README.md
CHANGED
|
@@ -29,16 +29,16 @@ Chat-panel extensions re-implement the agent loop and trail the real tool. This
|
|
|
29
29
|
- **Provider registry** - assistant-specific behaviour lives in one module per assistant behind capability flags; no core file names an assistant, and adding one touches no core file
|
|
30
30
|
- **Joint settings page** - one settings section covering all assistants, with a per-assistant toggle (all on by default); toggling takes effect live, no JupyterLab reload, and a change reaches the other JupyterLab windows of the same browser
|
|
31
31
|
- **Three-section side panel** - Favorites, Recent, and All projects, each scrolling independently
|
|
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
|
|
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
35
|
- **Your terminal's `claude -c` follows the panel** (Claude) - switching to a conversation makes plain `claude -c` in that project resume that same conversation, including one that has been compacted, which Claude Code otherwise refuses to continue. Launching repairs a compacted conversation the same way, but only switching moves 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 and per-row open and copy-id buttons
|
|
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 `
|
|
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
|
|
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
|
+
- **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
|
|
39
39
|
- **Launch modes under each assistant's own name** - Claude's skip-permissions, Codex's approval bypass, Kimi's `--yolo`, Gemini's YOLO; unsafe variants carry a shield glyph in the launch menus
|
|
40
40
|
- **Coloured terminal tabs** - each session's colour tints its terminal tab via the companion `jupyterlab_colourful_tab_extension` (installed automatically). Claude's own `/color` supplies its default, Kimi derives a stable colour from the conversation id; Codex, Gemini and DeepSeek have no colour of their own until you set one, and a branched session inherits its parent's colour, which it keeps even if the parent's colour later changes or is reset. Tinting needs the companion release that reports the colours you pick and lets another extension own a tab - against an older one this extension tints no tabs at all and says so once, and the companion's own right-click colours keep working as they always did
|
|
41
|
-
- **Your
|
|
41
|
+
- **Your newest colour choice wins** - set a colour on a terminal tab and the extension remembers it for that conversation, so the tab shows it again when you reopen the conversation. In Claude Code, `/color` is the second way to choose, and whichever you used last wins: a colour picked on the tab after `/color` stays, and running `/color` after a pick takes over again, including `/color` with the same colour or a `/color` reset. `Reset Tab Colour (n)` in the session's right-click menu hands back every colour you set by hand on that project's conversations, and appears once there is one to hand back. Clearing the colour on the tab itself releases it too, for the conversation that terminal is running; `Reset Tab Colour` is the way back for a conversation with no tab open. A Kimi or DeepSeek conversation that has never been resumed cannot be tracked yet, so a colour set on its tab is not kept
|
|
42
42
|
- **Favorites** - star projects you keep coming back to via the right-click menu; favourites from the standalone extensions are migrated on first run
|
|
43
43
|
- **Remove and clean up** - drop a project's history or a project's extra parallel sessions from the right-click menu, confirmation dialog first; removed files honour JupyterLab's "move files to trash" setting
|
|
44
44
|
- **Activity at a glance** - each row shows its last activity in an aligned column; rows active within the last minute light up, rows idle for over a week dim, and rows with parallel conversations show a branch icon with the count
|
|
@@ -46,7 +46,7 @@ Chat-panel extensions re-implement the agent loop and trail the real tool. This
|
|
|
46
46
|
- **Search** - 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
48
|
- **Conflict-safe upgrade** - if a retired standalone extension is still installed, its panel wins and this extension stands down for that assistant instead of showing a duplicate
|
|
49
|
-
- **DeepSeek in the
|
|
49
|
+
- **DeepSeek in the terminal** - the DeepSeek Harness ships no terminal chat of its own, so its panel runs the community TUI bundle (`@deepseek-harness-tui/dsh-tui`, mounted as the `tui` profile with `dsh plugin --profile tui add @deepseek-harness-tui/dsh-tui`). Without that profile, a launch is refused before a terminal opens, naming the bundle and the mount command - `dsh tui` itself would instead die in a stack trace that prints that command with a literal `<package>` where the package name goes, and never the bundle's name. The panel lists the projects under `~/.dsh/sessions` (or `$DSH_HOME`), and a click resumes that row's conversation with `dsh tui --resume <id>` in a terminal; a launch with no conversation named starts a new one, whose id the TUI mints itself. Branching and deleting work from the panel as for every other assistant
|
|
50
50
|
- **Auto-disabled when absent** - an assistant whose CLI is not on `PATH` does not show a panel
|
|
51
51
|
- **Open Callback** - when an assistant's login sends your browser to an `http://127.0.0.1:...` link that fails to load, because the login is waiting on the machine running JupyterLab, run `Open Callback` from the command palette and paste the link; the Jupyter server requests it for you, and the popup says whether the login accepted it, then closes by itself 3 seconds later. Only `http` links to that machine's own loopback are accepted
|
|
52
52
|
|
|
@@ -55,6 +55,7 @@ Chat-panel extensions re-implement the agent loop and trail the real tool. This
|
|
|
55
55
|
- JupyterLab >= 4.6.0
|
|
56
56
|
- Python >= 3.10
|
|
57
57
|
- At least one assistant CLI on `PATH`: `claude`, `codex`, `kimi`, `gemini`, or `dsh` (the DeepSeek Harness)
|
|
58
|
+
- For DeepSeek: the community TUI bundle mounted as the `tui` profile, `dsh plugin --profile tui add @deepseek-harness-tui/dsh-tui` - a DeepSeek launch without it is refused with these words before any terminal opens
|
|
58
59
|
|
|
59
60
|
## Install
|
|
60
61
|
|
package/lib/core/colour.d.ts
CHANGED
|
@@ -95,8 +95,7 @@ export declare class ColourStore {
|
|
|
95
95
|
* times it, not it.
|
|
96
96
|
*
|
|
97
97
|
* An op must never call `load`, `set` or `forget` - it would wait on a chain
|
|
98
|
-
* that is waiting on it.
|
|
99
|
-
* which is why it is not itself queued. */
|
|
98
|
+
* that is waiting on it. */
|
|
100
99
|
private _run;
|
|
101
100
|
/** Open a write against one or more ids. Counted, not flagged: two writes to
|
|
102
101
|
* one id can be queued at once, and a flag would let the first to settle
|
|
@@ -106,8 +105,6 @@ export declare class ColourStore {
|
|
|
106
105
|
private _closeWrite;
|
|
107
106
|
/** Record a colour as this conversation's own. Passing null drops the entry,
|
|
108
107
|
* which is also how a deleted conversation leaves no orphan key behind.
|
|
109
|
-
* `handSet` false marks it inherited - written on a fork's behalf rather
|
|
110
|
-
* than chosen on its tab.
|
|
111
108
|
*
|
|
112
109
|
* Answers whether the store now HOLDS the colour, which is what the capture
|
|
113
110
|
* of a colour picked on a tab gates its repaint on - see `_captureColour`.
|
|
@@ -121,17 +118,12 @@ export declare class ColourStore {
|
|
|
121
118
|
* "Did not throw" is not the test - the answer carries the whole store, so
|
|
122
119
|
* it is checked for the value that was just written. A server that accepted
|
|
123
120
|
* the request and then failed to persist it would otherwise be believed. */
|
|
124
|
-
set(sessionId: string, colour: string | null
|
|
121
|
+
set(sessionId: string, colour: string | null): Promise<boolean>;
|
|
125
122
|
/** Drop stored colours - conversations that no longer exist, or overrides
|
|
126
123
|
* the user released. One request for the whole list, so a release is never
|
|
127
124
|
* left half applied. Same contract as `set`: the answer is checked for the
|
|
128
125
|
* absence of every id, and the cache is touched only once it agrees. */
|
|
129
126
|
forget(sessionIds: string[]): Promise<boolean>;
|
|
130
|
-
/** Copy the parent's EFFECTIVE colour onto a fresh branch, so a branch of a
|
|
131
|
-
* branch inherits whatever its immediate parent actually shows, user-set
|
|
132
|
-
* override included. Called at the fork sites, once the new id is known.
|
|
133
|
-
*/
|
|
134
|
-
inherit(parentColour: string | null, childId: string): Promise<void>;
|
|
135
127
|
private readonly _providerId;
|
|
136
128
|
private readonly _serverSettings;
|
|
137
129
|
private _colours;
|
package/lib/core/colour.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// Terminal tab tint per conversation.
|
|
2
2
|
//
|
|
3
3
|
// Effective colour, highest wins:
|
|
4
|
-
// 1. user-set - the write-back store this module keeps, per provider
|
|
4
|
+
// 1. user-set - the write-back store this module keeps, per provider.
|
|
5
|
+
// Only entries the server still holds: it releases a pick
|
|
6
|
+
// that the assistant's own later colour command overtook,
|
|
7
|
+
// because the newer of the two channels wins (DEF-COLO-306)
|
|
5
8
|
// 2. native - the conversation's own colour, when the assistant has one
|
|
6
9
|
// 3. derived - a stable hash of the conversation id
|
|
7
10
|
// 4. none - no tint
|
|
@@ -186,8 +189,7 @@ export class ColourStore {
|
|
|
186
189
|
* times it, not it.
|
|
187
190
|
*
|
|
188
191
|
* An op must never call `load`, `set` or `forget` - it would wait on a chain
|
|
189
|
-
* that is waiting on it.
|
|
190
|
-
* which is why it is not itself queued. */
|
|
192
|
+
* that is waiting on it. */
|
|
191
193
|
_run(op) {
|
|
192
194
|
const result = this._chain.then(op);
|
|
193
195
|
const settled = () => undefined;
|
|
@@ -218,8 +220,6 @@ export class ColourStore {
|
|
|
218
220
|
}
|
|
219
221
|
/** Record a colour as this conversation's own. Passing null drops the entry,
|
|
220
222
|
* which is also how a deleted conversation leaves no orphan key behind.
|
|
221
|
-
* `handSet` false marks it inherited - written on a fork's behalf rather
|
|
222
|
-
* than chosen on its tab.
|
|
223
223
|
*
|
|
224
224
|
* Answers whether the store now HOLDS the colour, which is what the capture
|
|
225
225
|
* of a colour picked on a tab gates its repaint on - see `_captureColour`.
|
|
@@ -233,7 +233,7 @@ export class ColourStore {
|
|
|
233
233
|
* "Did not throw" is not the test - the answer carries the whole store, so
|
|
234
234
|
* it is checked for the value that was just written. A server that accepted
|
|
235
235
|
* the request and then failed to persist it would otherwise be believed. */
|
|
236
|
-
async set(sessionId, colour
|
|
236
|
+
async set(sessionId, colour) {
|
|
237
237
|
if (!sessionId) {
|
|
238
238
|
return false;
|
|
239
239
|
}
|
|
@@ -249,7 +249,7 @@ export class ColourStore {
|
|
|
249
249
|
body: JSON.stringify({
|
|
250
250
|
session_id: sessionId,
|
|
251
251
|
colour,
|
|
252
|
-
hand_set:
|
|
252
|
+
hand_set: true
|
|
253
253
|
})
|
|
254
254
|
});
|
|
255
255
|
if (((_a = data.colours) !== null && _a !== void 0 ? _a : {})[sessionId] !== (colour !== null && colour !== void 0 ? colour : undefined)) {
|
|
@@ -265,7 +265,7 @@ export class ColourStore {
|
|
|
265
265
|
this._colours.delete(sessionId);
|
|
266
266
|
}
|
|
267
267
|
this._overrides.delete(sessionId);
|
|
268
|
-
if (colour
|
|
268
|
+
if (colour) {
|
|
269
269
|
this._overrides.add(sessionId);
|
|
270
270
|
}
|
|
271
271
|
return true;
|
|
@@ -316,14 +316,4 @@ export class ColourStore {
|
|
|
316
316
|
}
|
|
317
317
|
});
|
|
318
318
|
}
|
|
319
|
-
/** Copy the parent's EFFECTIVE colour onto a fresh branch, so a branch of a
|
|
320
|
-
* branch inherits whatever its immediate parent actually shows, user-set
|
|
321
|
-
* override included. Called at the fork sites, once the new id is known.
|
|
322
|
-
*/
|
|
323
|
-
async inherit(parentColour, childId) {
|
|
324
|
-
if (!parentColour || !childId) {
|
|
325
|
-
return;
|
|
326
|
-
}
|
|
327
|
-
await this.set(childId, parentColour, false);
|
|
328
|
-
}
|
|
329
319
|
}
|
package/lib/core/modes.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export interface IResolvedLaunchMode {
|
|
|
15
15
|
* do. Otherwise the first switch that is on fires. Every mode is a boolean
|
|
16
16
|
* skip-approval switch, so "on" always means the same thing.
|
|
17
17
|
*/
|
|
18
|
-
export declare function resolveLaunchMode(launchModes: ILaunchMode[], values: Record<string, boolean
|
|
18
|
+
export declare function resolveLaunchMode(launchModes: ILaunchMode[], values: Record<string, boolean>, force?: string): string | undefined;
|
|
19
19
|
/**
|
|
20
20
|
* The mode a PLAIN launch would carry right now, as a descriptor entry.
|
|
21
21
|
*
|
|
@@ -26,4 +26,4 @@ export declare function resolveLaunchMode(launchModes: ILaunchMode[], values: Re
|
|
|
26
26
|
* unmarked. Answers the entry rather than the token so the caller can use the
|
|
27
27
|
* assistant's own wording.
|
|
28
28
|
*/
|
|
29
|
-
export declare function resolvedLaunchModeEntry(launchModes: ILaunchMode[], values: Record<string, boolean
|
|
29
|
+
export declare function resolvedLaunchModeEntry(launchModes: ILaunchMode[], values: Record<string, boolean>): IResolvedLaunchMode | null;
|
package/lib/core/panel.d.ts
CHANGED
|
@@ -37,7 +37,7 @@ export declare class AssistantSessionsPanel extends Widget {
|
|
|
37
37
|
setRecentLimit(n: number): void;
|
|
38
38
|
/** Current values of this provider's launch modes, read from settings. Each
|
|
39
39
|
* key is an `ILaunchMode.id` in the assistant's own terminology. */
|
|
40
|
-
setModes(modes: Record<string, boolean
|
|
40
|
+
setModes(modes: Record<string, boolean>): void;
|
|
41
41
|
setColouredTabs(on: boolean): void;
|
|
42
42
|
/** Whether opening a session moves the file browser to its project folder. */
|
|
43
43
|
setSyncFileBrowser(on: boolean): void;
|
|
@@ -143,26 +143,13 @@ export declare class AssistantSessionsPanel extends Widget {
|
|
|
143
143
|
launchHere(cwd?: string): Promise<any>;
|
|
144
144
|
/** Start a brand-new conversation in the file browser's current folder. */
|
|
145
145
|
private _newSession;
|
|
146
|
-
/**
|
|
147
|
-
*
|
|
148
|
-
* `forkStrategy`.
|
|
149
|
-
*
|
|
150
|
-
* The three strategies differ only in who mints the new id and when it
|
|
151
|
-
* becomes knowable - so the shared part (ask for a name, launch a terminal
|
|
152
|
-
* on the fork, inherit the parent's tint, surface the branch) lives here
|
|
153
|
-
* once, and the strategy decides the payload and the discovery.
|
|
154
|
-
*/
|
|
146
|
+
/** Branch the active row's current conversation. Every `forkStrategy` has
|
|
147
|
+
* the server mint the fork, so this is the one path. */
|
|
155
148
|
private _branchSession;
|
|
156
|
-
/** The
|
|
157
|
-
*
|
|
158
|
-
*
|
|
159
|
-
|
|
160
|
-
/** The CLI owns both the fork verb and the id, so the id has to be
|
|
161
|
-
* discovered: snapshot the branch ids first, then diff after launch. */
|
|
162
|
-
private _branchByNativeCommand;
|
|
163
|
-
/** The assistant has no fork at all: the server copies the store entry and
|
|
164
|
-
* answers with the new id, so the branch exists before the terminal opens
|
|
165
|
-
* and one refresh surfaces it - no watcher needed. */
|
|
149
|
+
/** The server mints the new id and answers with it before the terminal
|
|
150
|
+
* opens - `server-copy` copies the store entry, `server-native` drives the
|
|
151
|
+
* assistant's own non-interactive fork path - so one refresh surfaces the
|
|
152
|
+
* branch. */
|
|
166
153
|
private _branchByServerCopy;
|
|
167
154
|
/** Drop the hand-set colours of this project's conversations and re-tint
|
|
168
155
|
* from the defaults at once, so nothing keeps showing a released colour
|
|
@@ -178,26 +165,6 @@ export declare class AssistantSessionsPanel extends Widget {
|
|
|
178
165
|
* the conversation - but only for a conversation whose terminal is open and
|
|
179
166
|
* probes as this assistant's, which is why this path exists as well. */
|
|
180
167
|
private _resetColours;
|
|
181
|
-
/** Copy the parent's tint onto a fresh branch.
|
|
182
|
-
*
|
|
183
|
-
* A native-colour assistant hands every conversation its own colour, so a
|
|
184
|
-
* fork already arrives correctly tinted: only what this extension's own
|
|
185
|
-
* store holds for the parent is inherited there - an entry the parent itself
|
|
186
|
-
* inherited counts, which is what makes a branch of a branch take what its
|
|
187
|
-
* parent actually shows. Copying the parent's native tint instead would pin
|
|
188
|
-
* the fork to it and shadow the fork's own `/color` for good.
|
|
189
|
-
* Every other assistant inherits the parent's effective colour, which is the
|
|
190
|
-
* only tint it has. */
|
|
191
|
-
private _inheritColour;
|
|
192
|
-
/** Poll fast for a fork whose id we already know, then refresh once. The
|
|
193
|
-
* panel is never updated before the branch genuinely exists - it cannot list
|
|
194
|
-
* a store entry that has not been written. */
|
|
195
|
-
private _watchForBranch;
|
|
196
|
-
/** Poll fast for a fork whose id the CLI minted: whichever conversation the
|
|
197
|
-
* snapshot did not hold is the fork. It is then pinned as current (recency
|
|
198
|
-
* alone would not - the actively written parent overtakes it) and inherits
|
|
199
|
-
* the parent's tint. */
|
|
200
|
-
private _watchForNewBranch;
|
|
201
168
|
/** Name the row's current conversation, in the assistant's own store.
|
|
202
169
|
*
|
|
203
170
|
* Only ever offered where the descriptor says the assistant keeps a name
|
|
@@ -273,9 +240,7 @@ export declare class AssistantSessionsPanel extends Widget {
|
|
|
273
240
|
private _variantSuffix;
|
|
274
241
|
/** Whether a forced variant would build the launch the plain item already
|
|
275
242
|
* builds - a mode left on in settings makes the plain item carry it, so the
|
|
276
|
-
* variant beside it would be a second entry for one outcome.
|
|
277
|
-
* variant can match: a forced token is the bare mode id, so an enum sitting
|
|
278
|
-
* on a widening value is a launch no variant duplicates. */
|
|
243
|
+
* variant beside it would be a second entry for one outcome. */
|
|
279
244
|
private _buildsSameLaunch;
|
|
280
245
|
/** The glyph, only where the mode WIDENS what runs without asking. */
|
|
281
246
|
private _variantIcon;
|
package/lib/core/panel.js
CHANGED
|
@@ -32,10 +32,6 @@ const MENU_STATE_BUDGET_MS = 800;
|
|
|
32
32
|
* without a floor the spinner shows for a frame and the click reads as
|
|
33
33
|
* "nothing happened". */
|
|
34
34
|
const REFRESH_SPIN_FLOOR_MS = 500;
|
|
35
|
-
// After a fork is requested, watch for its lazily-written store entry at this
|
|
36
|
-
// faster cadence (bounded) so a new branch surfaces in seconds.
|
|
37
|
-
const BRANCH_WATCH_INTERVAL_MS = 2000;
|
|
38
|
-
const BRANCH_WATCH_MAX_ATTEMPTS = 90; // ~3 minutes
|
|
39
35
|
/** Conversations listed inline in a branch submenu before the popup takes over. */
|
|
40
36
|
const INLINE_BRANCH_LIMIT = 5;
|
|
41
37
|
/** Command id of the sibling extension that spawns a shell-less terminal on a
|
|
@@ -898,7 +894,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
898
894
|
if (row) {
|
|
899
895
|
// A terminal already holding that conversation IS the answer: a second
|
|
900
896
|
// process on one history is never what the click meant (ACC-LNCH-152).
|
|
901
|
-
const found = await this._terminals.findForSession(row.session_id
|
|
897
|
+
const found = await this._terminals.findForSession(row.session_id);
|
|
902
898
|
if (found) {
|
|
903
899
|
this._terminals.focus(found.widget);
|
|
904
900
|
return found.widget;
|
|
@@ -982,126 +978,47 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
982
978
|
void this._fetch().catch(() => undefined);
|
|
983
979
|
}
|
|
984
980
|
}
|
|
985
|
-
/**
|
|
986
|
-
*
|
|
987
|
-
* `forkStrategy`.
|
|
988
|
-
*
|
|
989
|
-
* The three strategies differ only in who mints the new id and when it
|
|
990
|
-
* becomes knowable - so the shared part (ask for a name, launch a terminal
|
|
991
|
-
* on the fork, inherit the parent's tint, surface the branch) lives here
|
|
992
|
-
* once, and the strategy decides the payload and the discovery.
|
|
993
|
-
*/
|
|
981
|
+
/** Branch the active row's current conversation. Every `forkStrategy` has
|
|
982
|
+
* the server mint the fork, so this is the one path. */
|
|
994
983
|
async _branchSession(force) {
|
|
995
|
-
var _a;
|
|
996
984
|
const session = this._activeSession;
|
|
997
985
|
if (!session) {
|
|
998
986
|
return;
|
|
999
987
|
}
|
|
1000
|
-
|
|
1001
|
-
let name;
|
|
1002
|
-
if (this._descriptor.promptsForBranchName) {
|
|
1003
|
-
const named = await InputDialog.getText({
|
|
1004
|
-
title: 'Branch Session',
|
|
1005
|
-
label: 'Name for the new session',
|
|
1006
|
-
placeholder: this._lookupName(session)
|
|
1007
|
-
});
|
|
1008
|
-
if (!named.button.accept) {
|
|
1009
|
-
return;
|
|
1010
|
-
}
|
|
1011
|
-
name = ((_a = named.value) !== null && _a !== void 0 ? _a : '').trim();
|
|
1012
|
-
if (!name) {
|
|
1013
|
-
// Reported, not swallowed. The placeholder shows the parent's name,
|
|
1014
|
-
// which reads as the "press Ok for the default" idiom, so Ok on an
|
|
1015
|
-
// empty field was a closed dialog with no POST and no word (DEF-43).
|
|
1016
|
-
// A notification rather than a disabled Ok because that is how this
|
|
1017
|
-
// panel refuses everywhere else - Open Terminal and Show in File
|
|
1018
|
-
// Browser on a folder outside the root - and `InputDialog.getText`
|
|
1019
|
-
// offers no hook to disable Ok while the field is empty; getting one
|
|
1020
|
-
// means replacing the dialog with a hand-built body widget.
|
|
1021
|
-
Notification.warning('Enter a name for the new session - nothing was branched.', { autoClose: 4000 });
|
|
1022
|
-
return;
|
|
1023
|
-
}
|
|
1024
|
-
}
|
|
1025
|
-
if (this._descriptor.forkStrategy === 'server-copy') {
|
|
1026
|
-
await this._branchByServerCopy(session, name, force);
|
|
1027
|
-
return;
|
|
1028
|
-
}
|
|
1029
|
-
if (this._descriptor.forkStrategy === 'native-command') {
|
|
1030
|
-
await this._branchByNativeCommand(session, force, parentColour);
|
|
1031
|
-
return;
|
|
1032
|
-
}
|
|
1033
|
-
await this._branchByNativeFlag(session, name, force, parentColour);
|
|
1034
|
-
}
|
|
1035
|
-
/** The CLI forks from a flag pair and takes the new id from us, so the fork
|
|
1036
|
-
* is known before it exists. Its store entry is written lazily, on the first
|
|
1037
|
-
* turn, so the branch is watched for rather than expected at once. */
|
|
1038
|
-
async _branchByNativeFlag(session, name, force, parentColour) {
|
|
1039
|
-
const forkId = UUID.uuid4();
|
|
1040
|
-
const request = {
|
|
1041
|
-
project_path: session.project_path,
|
|
1042
|
-
// Arms the server's fork pin - without it the fork never becomes the
|
|
1043
|
-
// row's current conversation and recency drags the row back (DEF-9).
|
|
1044
|
-
encoded_path: session.encoded_path,
|
|
1045
|
-
session_id: session.session_id,
|
|
1046
|
-
fork_session_id: forkId,
|
|
1047
|
-
name,
|
|
1048
|
-
mode: this._launchMode(force)
|
|
1049
|
-
};
|
|
1050
|
-
try {
|
|
1051
|
-
await this._terminals.launch(request, forkId);
|
|
1052
|
-
}
|
|
1053
|
-
catch (err) {
|
|
1054
|
-
this._showActionError('Could not branch this session - try again.', err);
|
|
1055
|
-
return;
|
|
1056
|
-
}
|
|
1057
|
-
await this._inheritColour(parentColour, forkId, session.session_id);
|
|
1058
|
-
this._watchForBranch(session.encoded_path, forkId);
|
|
1059
|
-
}
|
|
1060
|
-
/** The CLI owns both the fork verb and the id, so the id has to be
|
|
1061
|
-
* discovered: snapshot the branch ids first, then diff after launch. */
|
|
1062
|
-
async _branchByNativeCommand(session, force, parentColour) {
|
|
1063
|
-
var _a, _b;
|
|
1064
|
-
const known = new Set((_b = (_a = (await this._fetchBranches(session).catch(() => null))) === null || _a === void 0 ? void 0 : _a.branches.map(b => b.session_id)) !== null && _b !== void 0 ? _b : []);
|
|
1065
|
-
known.add(session.session_id);
|
|
1066
|
-
try {
|
|
1067
|
-
await this._terminals.launch({
|
|
1068
|
-
project_path: session.project_path,
|
|
1069
|
-
fork_from: session.session_id,
|
|
1070
|
-
mode: this._launchMode(force)
|
|
1071
|
-
});
|
|
1072
|
-
}
|
|
1073
|
-
catch (err) {
|
|
1074
|
-
this._showActionError('Could not branch this session - try again.', err);
|
|
1075
|
-
return;
|
|
1076
|
-
}
|
|
1077
|
-
this._watchForNewBranch(session, known, parentColour);
|
|
1078
|
-
void this._fetch().catch(() => undefined);
|
|
988
|
+
await this._branchByServerCopy(session, undefined, force);
|
|
1079
989
|
}
|
|
1080
|
-
/** The
|
|
1081
|
-
*
|
|
1082
|
-
*
|
|
990
|
+
/** The server mints the new id and answers with it before the terminal
|
|
991
|
+
* opens - `server-copy` copies the store entry, `server-native` drives the
|
|
992
|
+
* assistant's own non-interactive fork path - so one refresh surfaces the
|
|
993
|
+
* branch. */
|
|
1083
994
|
async _branchByServerCopy(session, name, force) {
|
|
1084
995
|
// Let the parent's colour settle before the server reads it (DEF-74). The
|
|
1085
996
|
// server copies the store entry while minting the fork, so a colour picked
|
|
1086
997
|
// on the parent's tab moments earlier - still queued in this store's
|
|
1087
998
|
// request chain, unsent - is not in the server's store yet and the branch
|
|
1088
|
-
// is born wearing the PREVIOUS tint.
|
|
1089
|
-
// (DEF-
|
|
1090
|
-
//
|
|
1091
|
-
//
|
|
1092
|
-
//
|
|
999
|
+
// is born wearing the PREVIOUS tint. A reload is a queued request like
|
|
1000
|
+
// every other of this store's (DEF-30), so it is not sent until the
|
|
1001
|
+
// capture ahead of it has been answered, and awaiting it is what makes the
|
|
1002
|
+
// server's read fresh. Only inside the capture window, so an ordinary fork
|
|
1003
|
+
// still costs no request.
|
|
1093
1004
|
if (this._colours.isPending(session.session_id)) {
|
|
1094
1005
|
await this._colours.load();
|
|
1095
1006
|
}
|
|
1007
|
+
// Whether the POST below returned the minted fork. Past that point the
|
|
1008
|
+
// branch exists server-side - row, pin and colour - and only the launch
|
|
1009
|
+
// of its terminal can still fail, which is a different thing to report.
|
|
1010
|
+
let minted = false;
|
|
1096
1011
|
try {
|
|
1097
1012
|
const fork = await requestProvider(this._descriptor.id, 'branch', this._serverSettings, {
|
|
1098
1013
|
method: 'POST',
|
|
1099
1014
|
body: JSON.stringify({
|
|
1100
1015
|
encoded_path: session.encoded_path,
|
|
1101
1016
|
session_id: session.session_id,
|
|
1102
|
-
name
|
|
1017
|
+
name,
|
|
1018
|
+
base_name: session.name
|
|
1103
1019
|
})
|
|
1104
1020
|
});
|
|
1021
|
+
minted = true;
|
|
1105
1022
|
// No colour write here. `BranchHandler` already inherited it while
|
|
1106
1023
|
// minting this fork, from a read taken server-side at request time -
|
|
1107
1024
|
// strictly fresher than the click-time read this would repeat, and the
|
|
@@ -1117,6 +1034,19 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1117
1034
|
if (isResponseStatus(err, 400)) {
|
|
1118
1035
|
Notification.error(`${this._descriptor.label} cannot branch this conversation.`, { autoClose: 4000 });
|
|
1119
1036
|
}
|
|
1037
|
+
else if (minted) {
|
|
1038
|
+
// The branch was created and only its terminal was refused - the
|
|
1039
|
+
// launch preflight's 409 carries words for the user. "Could not
|
|
1040
|
+
// branch this session" would be false, and the early return would
|
|
1041
|
+
// skip the refresh that surfaces the new row, so the refusal keeps
|
|
1042
|
+
// its own message and the list is re-read all the same.
|
|
1043
|
+
// Kept to 41 characters for the TOAST, not for taste: ": " plus the
|
|
1044
|
+
// longest message a store may raise (97, the DeepSeek refusal's
|
|
1045
|
+
// bound in tests/test_provider_stores.py) is 41 + 2 + 97 = 140 -
|
|
1046
|
+
// exactly the window before JupyterLab clips the rest.
|
|
1047
|
+
this._notifyLaunchError('Branch created; its terminal did not open', err);
|
|
1048
|
+
await this._fetch().catch(() => undefined);
|
|
1049
|
+
}
|
|
1120
1050
|
else {
|
|
1121
1051
|
this._showActionError('Could not branch this session - try again.', err);
|
|
1122
1052
|
}
|
|
@@ -1149,108 +1079,6 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1149
1079
|
await this._terminals.reconcileColours();
|
|
1150
1080
|
await this._fetch().catch(() => undefined);
|
|
1151
1081
|
}
|
|
1152
|
-
/** Copy the parent's tint onto a fresh branch.
|
|
1153
|
-
*
|
|
1154
|
-
* A native-colour assistant hands every conversation its own colour, so a
|
|
1155
|
-
* fork already arrives correctly tinted: only what this extension's own
|
|
1156
|
-
* store holds for the parent is inherited there - an entry the parent itself
|
|
1157
|
-
* inherited counts, which is what makes a branch of a branch take what its
|
|
1158
|
-
* parent actually shows. Copying the parent's native tint instead would pin
|
|
1159
|
-
* the fork to it and shadow the fork's own `/color` for good.
|
|
1160
|
-
* Every other assistant inherits the parent's effective colour, which is the
|
|
1161
|
-
* only tint it has. */
|
|
1162
|
-
async _inheritColour(parentColour, childId, parentId) {
|
|
1163
|
-
// The project gained a conversation, so a listing kept from before it is
|
|
1164
|
-
// short by one.
|
|
1165
|
-
this._branchCache = null;
|
|
1166
|
-
// Let the parent's colour settle before copying it (DEF-31). The store
|
|
1167
|
-
// holds only server-confirmed values, so a colour picked on the parent's
|
|
1168
|
-
// tab moments earlier is not in it yet, and a fork started in that window
|
|
1169
|
-
// was born wearing the PREVIOUS tint - stored as inherited, so never
|
|
1170
|
-
// offered for release and corrected by no later pass.
|
|
1171
|
-
//
|
|
1172
|
-
// A reload is a queued request like every other of this store's (DEF-30):
|
|
1173
|
-
// it is not sent until the capture ahead of it has been answered, which is
|
|
1174
|
-
// what makes the re-read below current. It runs only inside the capture
|
|
1175
|
-
// window, so an ordinary fork still costs no request, and the wait is
|
|
1176
|
-
// bounded - every request is capped at REQUEST_TIMEOUT_MS (DEF-72). A
|
|
1177
|
-
// reload that fails keeps the cache, which by then holds whatever the
|
|
1178
|
-
// capture confirmed, so the re-read is current either way.
|
|
1179
|
-
if (this._colours.isPending(parentId)) {
|
|
1180
|
-
await this._colours.load();
|
|
1181
|
-
}
|
|
1182
|
-
// Re-read rather than trust `parentColour`, which was taken at click time -
|
|
1183
|
-
// before the name dialog. A capture in flight at click time may well have
|
|
1184
|
-
// landed while that dialog was open, which leaves nothing pending and the
|
|
1185
|
-
// snapshot stale all the same. It stays as the fallback everywhere but a
|
|
1186
|
-
// `native` provider - it is the only value here in which the derived hash
|
|
1187
|
-
// is resolved, so a parent this store holds no colour for still hands its
|
|
1188
|
-
// fork the tint it actually shows. `native` takes no fallback at all, for
|
|
1189
|
-
// the reason above.
|
|
1190
|
-
const stored = this._colours.get(parentId);
|
|
1191
|
-
const inherited = this._descriptor.colourSource === 'native'
|
|
1192
|
-
? stored
|
|
1193
|
-
: (stored !== null && stored !== void 0 ? stored : parentColour);
|
|
1194
|
-
await this._colours.inherit(inherited, childId);
|
|
1195
|
-
}
|
|
1196
|
-
/** Poll fast for a fork whose id we already know, then refresh once. The
|
|
1197
|
-
* panel is never updated before the branch genuinely exists - it cannot list
|
|
1198
|
-
* a store entry that has not been written. */
|
|
1199
|
-
_watchForBranch(encodedPath, forkId) {
|
|
1200
|
-
let attempts = 0;
|
|
1201
|
-
const tick = async () => {
|
|
1202
|
-
// dispose() can land between an await and this callback; the widget must
|
|
1203
|
-
// not keep polling off a dead object.
|
|
1204
|
-
if (this.isDisposed) {
|
|
1205
|
-
return;
|
|
1206
|
-
}
|
|
1207
|
-
attempts += 1;
|
|
1208
|
-
try {
|
|
1209
|
-
const data = await requestProvider(this._descriptor.id, withQuery('branches', { encoded_path: encodedPath }), this._serverSettings, { cache: 'no-store' });
|
|
1210
|
-
if (data.current === forkId ||
|
|
1211
|
-
data.branches.some(b => b.session_id === forkId)) {
|
|
1212
|
-
await this._fetch();
|
|
1213
|
-
return;
|
|
1214
|
-
}
|
|
1215
|
-
}
|
|
1216
|
-
catch (_a) {
|
|
1217
|
-
// Transient - keep watching until the attempt budget runs out.
|
|
1218
|
-
}
|
|
1219
|
-
if (attempts < BRANCH_WATCH_MAX_ATTEMPTS) {
|
|
1220
|
-
window.setTimeout(() => void tick(), BRANCH_WATCH_INTERVAL_MS);
|
|
1221
|
-
}
|
|
1222
|
-
};
|
|
1223
|
-
window.setTimeout(() => void tick(), BRANCH_WATCH_INTERVAL_MS);
|
|
1224
|
-
}
|
|
1225
|
-
/** Poll fast for a fork whose id the CLI minted: whichever conversation the
|
|
1226
|
-
* snapshot did not hold is the fork. It is then pinned as current (recency
|
|
1227
|
-
* alone would not - the actively written parent overtakes it) and inherits
|
|
1228
|
-
* the parent's tint. */
|
|
1229
|
-
_watchForNewBranch(session, known, parentColour) {
|
|
1230
|
-
let attempts = 0;
|
|
1231
|
-
const tick = async () => {
|
|
1232
|
-
if (this.isDisposed) {
|
|
1233
|
-
return;
|
|
1234
|
-
}
|
|
1235
|
-
attempts += 1;
|
|
1236
|
-
try {
|
|
1237
|
-
const data = await this._fetchBranches(session);
|
|
1238
|
-
const fresh = data.branches.find(b => !known.has(b.session_id));
|
|
1239
|
-
if (fresh) {
|
|
1240
|
-
await this._inheritColour(parentColour, fresh.session_id, session.session_id);
|
|
1241
|
-
await this._switchBranch(session, fresh.session_id);
|
|
1242
|
-
return;
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
catch (_a) {
|
|
1246
|
-
// Transient - keep watching.
|
|
1247
|
-
}
|
|
1248
|
-
if (attempts < BRANCH_WATCH_MAX_ATTEMPTS) {
|
|
1249
|
-
window.setTimeout(() => void tick(), BRANCH_WATCH_INTERVAL_MS);
|
|
1250
|
-
}
|
|
1251
|
-
};
|
|
1252
|
-
window.setTimeout(() => void tick(), BRANCH_WATCH_INTERVAL_MS);
|
|
1253
|
-
}
|
|
1254
1082
|
/** Name the row's current conversation, in the assistant's own store.
|
|
1255
1083
|
*
|
|
1256
1084
|
* Only ever offered where the descriptor says the assistant keeps a name
|
|
@@ -1273,9 +1101,8 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1273
1101
|
}
|
|
1274
1102
|
const name = ((_a = named.value) !== null && _a !== void 0 ? _a : '').trim();
|
|
1275
1103
|
if (!name) {
|
|
1276
|
-
//
|
|
1277
|
-
//
|
|
1278
|
-
// default", so an empty Ok has to say that nothing happened.
|
|
1104
|
+
// The placeholder shows the current name, which reads as "press Ok for
|
|
1105
|
+
// the default", so an empty Ok has to say that nothing happened.
|
|
1279
1106
|
Notification.warning('Enter a name - nothing was renamed.', {
|
|
1280
1107
|
autoClose: 4000
|
|
1281
1108
|
});
|
|
@@ -1945,9 +1772,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1945
1772
|
}
|
|
1946
1773
|
/** Whether a forced variant would build the launch the plain item already
|
|
1947
1774
|
* builds - a mode left on in settings makes the plain item carry it, so the
|
|
1948
|
-
* variant beside it would be a second entry for one outcome.
|
|
1949
|
-
* variant can match: a forced token is the bare mode id, so an enum sitting
|
|
1950
|
-
* on a widening value is a launch no variant duplicates. */
|
|
1775
|
+
* variant beside it would be a second entry for one outcome. */
|
|
1951
1776
|
_buildsSameLaunch(mode) {
|
|
1952
1777
|
const forced = resolveLaunchMode(this._descriptor.launchModes, this._modes, mode.id);
|
|
1953
1778
|
return !!forced && forced === this._launchMode();
|
|
@@ -2093,8 +1918,8 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2093
1918
|
// colourful-tab extension as a choice with no colour and does drop the
|
|
2094
1919
|
// override, but only for the conversation that terminal is running - so
|
|
2095
1920
|
// without this item an override on a branch with no tab open would be
|
|
2096
|
-
// permanent, and on a native
|
|
2097
|
-
//
|
|
1921
|
+
// permanent on an assistant with no colour command, and on a native one
|
|
1922
|
+
// would hold until the next `/color` run in that conversation.
|
|
2098
1923
|
//
|
|
2099
1924
|
// It covers every conversation of the row, not just the current one: a
|
|
2100
1925
|
// terminal opened on a branch files its colour under THAT branch, and this
|
|
@@ -2325,14 +2150,10 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2325
2150
|
add('reset-colour');
|
|
2326
2151
|
this._contextMenu.addItem({ type: 'separator' });
|
|
2327
2152
|
if (withBranches) {
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
type: 'submenu',
|
|
2333
|
-
submenu: this._openBranchSubmenu
|
|
2334
|
-
});
|
|
2335
|
-
}
|
|
2153
|
+
this._contextMenu.addItem({
|
|
2154
|
+
type: 'submenu',
|
|
2155
|
+
submenu: this._openBranchSubmenu
|
|
2156
|
+
});
|
|
2336
2157
|
this._contextMenu.addItem({
|
|
2337
2158
|
type: 'submenu',
|
|
2338
2159
|
submenu: this._switchSubmenu
|
|
@@ -2501,7 +2322,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2501
2322
|
branchName: b => this._branchDisplayName(b),
|
|
2502
2323
|
formatTime: ms => this._formatRelativeTime(ms),
|
|
2503
2324
|
branchBadge: b => this._bgBadgeFor(b),
|
|
2504
|
-
openTitle: () => this._openBranchTitle(
|
|
2325
|
+
openTitle: () => this._openBranchTitle(),
|
|
2505
2326
|
onSwitch: id => void this._switchBranch(session, id),
|
|
2506
2327
|
onOpen: id => void this._openSession(session, undefined, id),
|
|
2507
2328
|
onDelete: ids => this._deleteBranches(session, ids),
|
|
@@ -2514,13 +2335,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2514
2335
|
/** Tooltip for one Open button in the Manage Sessions popup. Same rule as the
|
|
2515
2336
|
* Open Branched Conversation submenu: the button launches a terminal, so it
|
|
2516
2337
|
* names the mode that launch carries. */
|
|
2517
|
-
_openBranchTitle(
|
|
2518
|
-
var _a, _b, _c;
|
|
2519
|
-
// One terminal serves a project-scoped assistant, so the button focuses
|
|
2520
|
-
// or starts it, under the provider's own verb for that.
|
|
2521
|
-
if (this._descriptor.terminalScope === 'project') {
|
|
2522
|
-
return ((_c = (_b = (_a = this._hooks).resumeLabel) === null || _b === void 0 ? void 0 : _b.call(_a, session)) !== null && _c !== void 0 ? _c : "Open the project's terminal");
|
|
2523
|
-
}
|
|
2338
|
+
_openBranchTitle() {
|
|
2524
2339
|
const base = 'Open this conversation in its own terminal';
|
|
2525
2340
|
const variant = this._resolvedVariant();
|
|
2526
2341
|
return variant ? `${base} (${variant.label})` : base;
|
package/lib/core/popup.d.ts
CHANGED
|
@@ -32,7 +32,7 @@ export declare namespace ManageSessionsPopup {
|
|
|
32
32
|
/** Tooltip for one row's Open button. The panel owns it because the launch
|
|
33
33
|
* mode in force is panel state, and every other surface that opens a
|
|
34
34
|
* conversation names it. */
|
|
35
|
-
openTitle: (
|
|
35
|
+
openTitle: () => string;
|
|
36
36
|
/** Make a conversation the row's current one. */
|
|
37
37
|
onSwitch: (sessionId: string) => void;
|
|
38
38
|
/** Open a conversation in its own terminal. */
|