jupyterlab_ai_code_assistants_extension 1.2.18 → 1.2.25
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 +9 -6
- package/lib/core/labels.d.ts +18 -0
- package/lib/core/labels.js +68 -0
- package/lib/core/panel.d.ts +5 -2
- package/lib/core/panel.js +55 -29
- package/lib/core/popup.d.ts +3 -2
- package/lib/core/popup.js +3 -15
- package/lib/core/terminals.d.ts +24 -2
- package/lib/core/terminals.js +87 -3
- package/lib/core/types.d.ts +16 -4
- package/lib/providers/claude.js +1 -0
- package/lib/providers/codex.js +1 -0
- package/lib/providers/deepseek.d.ts +3 -0
- package/lib/providers/deepseek.js +63 -0
- package/lib/providers/gemini.js +1 -0
- package/lib/providers/index.js +8 -1
- package/lib/providers/kimi.d.ts +1 -22
- package/lib/providers/kimi.js +5 -80
- package/package.json +1 -1
- package/schema/plugin.json +6 -0
- package/src/__tests__/core-neutrality.spec.ts +42 -1
- package/src/__tests__/labels.spec.ts +10 -4
- package/src/__tests__/launcher.spec.ts +1 -0
- package/src/__tests__/panel.spec.ts +172 -0
- package/src/__tests__/popup.spec.ts +90 -0
- package/src/__tests__/registry.spec.ts +2 -1
- package/src/__tests__/tab-colour-capture.spec.ts +1 -0
- package/src/__tests__/terminals.spec.ts +266 -0
- package/src/core/labels.ts +74 -0
- package/src/core/panel.ts +68 -27
- package/src/core/popup.ts +6 -19
- package/src/core/terminals.ts +100 -4
- package/src/core/types.ts +17 -4
- package/src/providers/claude.ts +1 -0
- package/src/providers/codex.ts +1 -0
- package/src/providers/deepseek.ts +68 -0
- package/src/providers/gemini.ts +1 -0
- package/src/providers/index.ts +8 -1
- package/src/providers/kimi.ts +6 -91
- package/style/base.css +11 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
[](https://kolomolo.com)
|
|
9
9
|
[](https://www.paypal.com/donate/?hosted_button_id=B4KPBJDLLXTSA)
|
|
10
10
|
|
|
11
|
-
A full launcher and manager for every AI code assistant in JupyterLab - Claude Code, Codex, Kimi and
|
|
11
|
+
A full launcher and manager for every AI code assistant in JupyterLab - Claude Code, Codex, Kimi, Gemini and DeepSeek. Start, resume, fork, switch, and clean up CLI sessions from a side panel per assistant - one click lands you in the right terminal with the assistant already running, no duplicate tabs, no UUID hunting. One install replaces the separate `jupyterlab_claude_code_extension`, `jupyterlab_codex_extension` and `jupyterlab_kimi_code_extension` packages and migrates their settings and favourites automatically.
|
|
12
12
|
|
|
13
13
|

|
|
14
14
|
|
|
@@ -25,17 +25,17 @@ Chat-panel extensions re-implement the agent loop and trail the real tool. This
|
|
|
25
25
|
|
|
26
26
|
## Features
|
|
27
27
|
|
|
28
|
-
- **One install, every assistant** - Claude Code, Codex, Kimi and
|
|
28
|
+
- **One install, every assistant** - Claude Code, Codex, Kimi, Gemini and DeepSeek from a single package, each with its own right-side panel wearing its official mark
|
|
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
|
|
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
|
|
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 Codex or Kimi conversation started with `+`, and a Codex fork, run without an id until the CLI writes one, so a click on that row meanwhile opens a second terminal
|
|
33
33
|
- **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
|
|
34
34
|
- **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
|
|
35
35
|
- **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
|
|
36
|
-
- **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 `codex fork`, server-side copies for Kimi and
|
|
36
|
+
- **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 `codex fork`, server-side copies for Kimi, Gemini and DeepSeek) behind the same menu item
|
|
37
37
|
- **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
|
|
38
|
-
- **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 and
|
|
38
|
+
- **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
|
|
39
39
|
- **Your own tab colour wins** - set a colour on a terminal tab and the extension remembers it for that conversation, overriding whatever the assistant chose. `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 Codex or Kimi conversation that has never been resumed cannot be tracked yet, so a colour set on its tab is not kept
|
|
40
40
|
- **Favorites** - star projects you keep coming back to via the right-click menu; favourites from the standalone extensions are migrated on first run
|
|
41
41
|
- **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,13 +44,14 @@ Chat-panel extensions re-implement the agent loop and trail the real tool. This
|
|
|
44
44
|
- **Search** - fuzzy filter per panel, toggled by the funnel button
|
|
45
45
|
- **Presentation modes** - label rows by session name or by path relative to the JupyterLab root
|
|
46
46
|
- **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
|
|
47
|
+
- **DeepSeek in the browser** - the DeepSeek Harness has no terminal chat; its panel lists the projects under `~/.dsh/sessions` (or `$DSH_HOME`) and `Open Web UI` starts `dsh --profile web --no-open --port 0` in the project's terminal, which prints the URL to open. The conversation is then chosen in the harness's own browser UI; branching and deleting work from the panel as for every other assistant. The URL is on the machine running JupyterLab, so a remote server needs a port forward to reach it
|
|
47
48
|
- **Auto-disabled when absent** - an assistant whose CLI is not on `PATH` does not show a panel
|
|
48
49
|
|
|
49
50
|
## Requirements
|
|
50
51
|
|
|
51
52
|
- JupyterLab >= 4.0.0
|
|
52
53
|
- Python >= 3.10
|
|
53
|
-
- At least one assistant CLI on `PATH`: `claude`, `codex`, `kimi`, or `
|
|
54
|
+
- At least one assistant CLI on `PATH`: `claude`, `codex`, `kimi`, `gemini`, or `dsh` (the DeepSeek Harness)
|
|
54
55
|
|
|
55
56
|
## Install
|
|
56
57
|
|
|
@@ -60,6 +61,8 @@ Developers must install via the project `Makefile` (which orchestrates clean, bu
|
|
|
60
61
|
make install
|
|
61
62
|
```
|
|
62
63
|
|
|
64
|
+
The companion packages are version floors, so `make install` here pulls their published releases and replaces a locally built `jupyterlab_colourful_tab_extension` or `jupyterlab_basic_terminal_extension`. When verifying a change that spans this extension and a companion, install the companion last.
|
|
65
|
+
|
|
63
66
|
End-users can install the published package from PyPI:
|
|
64
67
|
|
|
65
68
|
```bash
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** The distinguishing part of a conversation id: its first eight characters
|
|
2
|
+
* past `prefix`, the constant every id of the assistant carries
|
|
3
|
+
* (`IProviderDescriptor.sessionIdPrefix`). Front-sliced when there is no
|
|
4
|
+
* prefix, or when the id does not carry it. */
|
|
5
|
+
export declare function shortSessionId(sessionId: string, prefix?: string): string;
|
|
6
|
+
export declare const MENU_TITLE_COLUMNS = 60;
|
|
7
|
+
/** Trim `text` to `maxColumns` display columns, appending an ellipsis when
|
|
8
|
+
* anything was cut. Iterates by CODE POINT, so an astral character is never
|
|
9
|
+
* split into a lone surrogate (which chromium paints as the replacement
|
|
10
|
+
* glyph). Returns `text` unchanged when it already fits. */
|
|
11
|
+
export declare function truncateToColumns(text: string, maxColumns: number): string;
|
|
12
|
+
/** Menu-item label for one branch: the conversation title trimmed to the
|
|
13
|
+
* column budget, followed by the short session id. Only the TITLE is trimmed -
|
|
14
|
+
* branches of one project share a path, so the id is what tells them apart and
|
|
15
|
+
* it must survive. The bracketed id is dropped when the title already IS the
|
|
16
|
+
* id (the server's last-resort fallback), which can never be truncated since a
|
|
17
|
+
* short id is at most 8 characters. */
|
|
18
|
+
export declare function branchMenuLabel(label: string, shortId: string): string;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// Label derivations of the panel: pure string functions, kept free of
|
|
2
|
+
// JupyterLab imports so the unit tests load them without the panel's DOM.
|
|
3
|
+
/** The distinguishing part of a conversation id: its first eight characters
|
|
4
|
+
* past `prefix`, the constant every id of the assistant carries
|
|
5
|
+
* (`IProviderDescriptor.sessionIdPrefix`). Front-sliced when there is no
|
|
6
|
+
* prefix, or when the id does not carry it. */
|
|
7
|
+
export function shortSessionId(sessionId, prefix) {
|
|
8
|
+
const start = prefix && sessionId.startsWith(prefix) ? prefix.length : 0;
|
|
9
|
+
return sessionId.slice(start, start + 8);
|
|
10
|
+
}
|
|
11
|
+
// Lumino sets no `max-width` on `.lm-Menu-itemLabel`, so an uncapped
|
|
12
|
+
// auto-generated conversation title stretches a submenu across the window.
|
|
13
|
+
// Budget is in COLUMNS, not characters. Han, Kana, Hangul and emoji render at
|
|
14
|
+
// roughly one em against Latin's half, so counting characters bounds nothing:
|
|
15
|
+
// 60 Han glyphs measured 851-862px in a real Lumino submenu, at or above the
|
|
16
|
+
// 850px that filed the defect in the first place. An assistant whose titles
|
|
17
|
+
// are model-generated makes a wide-script title the expected case.
|
|
18
|
+
export const MENU_TITLE_COLUMNS = 60;
|
|
19
|
+
// East Asian Wide and Fullwidth ranges, plus emoji. Deliberately coarse: this
|
|
20
|
+
// bounds a menu width, so a handful of mis-classified glyphs cost pixels, not
|
|
21
|
+
// correctness. Anything outside these ranges counts as one column.
|
|
22
|
+
const WIDE_RANGES = [
|
|
23
|
+
[0x1100, 0x115f], // Hangul Jamo
|
|
24
|
+
[0x2e80, 0x303e], // CJK radicals, Kangxi, CJK symbols and punctuation
|
|
25
|
+
[0x3041, 0x33ff], // Hiragana, Katakana, Bopomofo, Hangul Compat, CJK Compat
|
|
26
|
+
[0x3400, 0x4dbf], // CJK Unified Extension A
|
|
27
|
+
[0x4e00, 0x9fff], // CJK Unified Ideographs
|
|
28
|
+
[0xa000, 0xa4cf], // Yi
|
|
29
|
+
[0xac00, 0xd7a3], // Hangul syllables
|
|
30
|
+
[0xf900, 0xfaff], // CJK Compatibility Ideographs
|
|
31
|
+
[0xfe30, 0xfe6f], // CJK Compatibility Forms
|
|
32
|
+
[0xff00, 0xff60], // Fullwidth forms
|
|
33
|
+
[0xffe0, 0xffe6], // Fullwidth signs
|
|
34
|
+
[0x1f300, 0x1faff], // Emoji and pictographs
|
|
35
|
+
[0x20000, 0x3fffd] // CJK Unified Extensions B and beyond
|
|
36
|
+
];
|
|
37
|
+
function isWide(codePoint) {
|
|
38
|
+
return WIDE_RANGES.some(([lo, hi]) => codePoint >= lo && codePoint <= hi);
|
|
39
|
+
}
|
|
40
|
+
/** Trim `text` to `maxColumns` display columns, appending an ellipsis when
|
|
41
|
+
* anything was cut. Iterates by CODE POINT, so an astral character is never
|
|
42
|
+
* split into a lone surrogate (which chromium paints as the replacement
|
|
43
|
+
* glyph). Returns `text` unchanged when it already fits. */
|
|
44
|
+
export function truncateToColumns(text, maxColumns) {
|
|
45
|
+
let columns = 0;
|
|
46
|
+
let kept = '';
|
|
47
|
+
for (const ch of text) {
|
|
48
|
+
const width = isWide(ch.codePointAt(0)) ? 2 : 1;
|
|
49
|
+
if (columns + width > maxColumns) {
|
|
50
|
+
// The cut can land just after a space; drop it so the ellipsis hugs the
|
|
51
|
+
// last word. `trimEnd` is ES2019 and this project targets ES2018.
|
|
52
|
+
return `${kept.replace(/\s+$/, '')}…`;
|
|
53
|
+
}
|
|
54
|
+
columns += width;
|
|
55
|
+
kept += ch;
|
|
56
|
+
}
|
|
57
|
+
return text;
|
|
58
|
+
}
|
|
59
|
+
/** Menu-item label for one branch: the conversation title trimmed to the
|
|
60
|
+
* column budget, followed by the short session id. Only the TITLE is trimmed -
|
|
61
|
+
* branches of one project share a path, so the id is what tells them apart and
|
|
62
|
+
* it must survive. The bracketed id is dropped when the title already IS the
|
|
63
|
+
* id (the server's last-resort fallback), which can never be truncated since a
|
|
64
|
+
* short id is at most 8 characters. */
|
|
65
|
+
export function branchMenuLabel(label, shortId) {
|
|
66
|
+
const title = truncateToColumns(label, MENU_TITLE_COLUMNS);
|
|
67
|
+
return label === shortId ? title : `${title} (${shortId})`;
|
|
68
|
+
}
|
package/lib/core/panel.d.ts
CHANGED
|
@@ -79,6 +79,10 @@ export declare class AssistantSessionsPanel extends Widget {
|
|
|
79
79
|
private _logError;
|
|
80
80
|
private _fetch;
|
|
81
81
|
private _toggleFavourite;
|
|
82
|
+
/** The warning a permanent deletion earns and a trashed one does not - a
|
|
83
|
+
* trashed conversation is recoverable, and the popup one click away says
|
|
84
|
+
* so; the same sentence on both would be false half the time. */
|
|
85
|
+
private get _irreversibleNote();
|
|
82
86
|
private get _disposalVerb();
|
|
83
87
|
private _removeProject;
|
|
84
88
|
private _cleanupParallel;
|
|
@@ -212,8 +216,7 @@ export declare class AssistantSessionsPanel extends Widget {
|
|
|
212
216
|
* cannot give. */
|
|
213
217
|
private _formatAbsoluteTime;
|
|
214
218
|
/** Branch entry display: conversation name plus short session id. Branches
|
|
215
|
-
* share a project path, so the name and id are all that tell them apart.
|
|
216
|
-
* suffix is dropped when the label already IS the short id. */
|
|
219
|
+
* share a project path, so the name and id are all that tell them apart. */
|
|
217
220
|
private _branchDisplayName;
|
|
218
221
|
/** Menu-item label for a branch: display name, relative time, and a trailing
|
|
219
222
|
* marker when a live worker owns the conversation. The marker lives in the
|
package/lib/core/panel.js
CHANGED
|
@@ -5,6 +5,7 @@ import { CommandRegistry } from '@lumino/commands';
|
|
|
5
5
|
import { UUID } from '@lumino/coreutils';
|
|
6
6
|
import { Widget } from '@lumino/widgets';
|
|
7
7
|
import { ColourStore } from './colour';
|
|
8
|
+
import { branchMenuLabel, shortSessionId } from './labels';
|
|
8
9
|
import { addIcon, branchIcon, cleanupIcon, filterIcon, providerIcon, refreshIcon, removeIcon, shieldIcon, starFilledIcon, statusDotIcon, switchIcon } from './icons';
|
|
9
10
|
import { DEFAULT_RECENT_LIMIT, MAX_RECENT_LIMIT, MIN_RECENT_LIMIT } from './limits';
|
|
10
11
|
import { resolveLaunchMode, resolvedLaunchModeEntry } from './modes';
|
|
@@ -586,6 +587,12 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
586
587
|
this._showActionError('Could not toggle favorite - try again.', err);
|
|
587
588
|
}
|
|
588
589
|
}
|
|
590
|
+
/** The warning a permanent deletion earns and a trashed one does not - a
|
|
591
|
+
* trashed conversation is recoverable, and the popup one click away says
|
|
592
|
+
* so; the same sentence on both would be false half the time. */
|
|
593
|
+
get _irreversibleNote() {
|
|
594
|
+
return this._deleteToTrash ? '' : ' This cannot be undone.';
|
|
595
|
+
}
|
|
589
596
|
get _disposalVerb() {
|
|
590
597
|
return this._deleteToTrash ? 'moved to trash' : 'deleted permanently';
|
|
591
598
|
}
|
|
@@ -596,7 +603,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
596
603
|
title: `Remove from ${this._descriptor.label}`,
|
|
597
604
|
body: `Remove "${name}" from ${this._descriptor.label}? This drops the ` +
|
|
598
605
|
`entire project history and every conversation it holds - ` +
|
|
599
|
-
`${this._disposalVerb}.
|
|
606
|
+
`${this._disposalVerb}.${this._irreversibleNote}`,
|
|
600
607
|
buttons: [
|
|
601
608
|
Dialog.cancelButton(),
|
|
602
609
|
Dialog.warnButton({ label: this._trans.__('Remove') })
|
|
@@ -637,7 +644,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
637
644
|
title: this._trans.__('Clean Up Parallel Sessions'),
|
|
638
645
|
body: `Remove ${extra} parallel session${extra === 1 ? '' : 's'} from ` +
|
|
639
646
|
`"${name}"? The current conversation is kept; the rest are ` +
|
|
640
|
-
`${this._disposalVerb}.
|
|
647
|
+
`${this._disposalVerb}.${this._irreversibleNote}`,
|
|
641
648
|
buttons: [
|
|
642
649
|
Dialog.cancelButton(),
|
|
643
650
|
Dialog.warnButton({ label: this._trans.__('Remove') })
|
|
@@ -839,7 +846,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
839
846
|
if (row) {
|
|
840
847
|
// A terminal already holding that conversation IS the answer: a second
|
|
841
848
|
// process on one history is never what the click meant (ACC-LNCH-152).
|
|
842
|
-
const found = await this._terminals.findForSession(row.session_id);
|
|
849
|
+
const found = await this._terminals.findForSession(row.session_id, folder);
|
|
843
850
|
if (found) {
|
|
844
851
|
this._terminals.focus(found.widget);
|
|
845
852
|
return found.widget;
|
|
@@ -1194,6 +1201,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1194
1201
|
}
|
|
1195
1202
|
/** Make another conversation the row's current one. */
|
|
1196
1203
|
async _switchBranch(session, sessionId) {
|
|
1204
|
+
var _a;
|
|
1197
1205
|
try {
|
|
1198
1206
|
const result = await requestProvider(this._descriptor.id, 'switch', this._serverSettings, {
|
|
1199
1207
|
method: 'POST',
|
|
@@ -1205,6 +1213,15 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1205
1213
|
if (result.current !== result.requested) {
|
|
1206
1214
|
Notification.warning('That conversation could not be made the current one.', { autoClose: 4000 });
|
|
1207
1215
|
}
|
|
1216
|
+
else {
|
|
1217
|
+
// The re-render is the only other signal, and it can read as the
|
|
1218
|
+
// opposite of success: the row now reports the conversation's true
|
|
1219
|
+
// last activity, so switching to an old one dims the row and drops
|
|
1220
|
+
// it out of Recent (DEF-PANE-181).
|
|
1221
|
+
const target = this._lastBranches.find(b => b.session_id === sessionId);
|
|
1222
|
+
const label = (_a = target === null || target === void 0 ? void 0 : target.label) !== null && _a !== void 0 ? _a : shortSessionId(sessionId, this._descriptor.sessionIdPrefix);
|
|
1223
|
+
Notification.success(`${this._lookupName(session)}: switched to ${label}`, { autoClose: 3000 });
|
|
1224
|
+
}
|
|
1208
1225
|
}
|
|
1209
1226
|
catch (err) {
|
|
1210
1227
|
Notification.error(isResponseStatus(err, 404)
|
|
@@ -1486,17 +1503,6 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1486
1503
|
row.classList.add('jp-mod-stale');
|
|
1487
1504
|
}
|
|
1488
1505
|
}
|
|
1489
|
-
const removing = this._removingPaths.has(session.encoded_path);
|
|
1490
|
-
if (removing) {
|
|
1491
|
-
row.classList.add('jp-mod-busy');
|
|
1492
|
-
const spinner = document.createElement('span');
|
|
1493
|
-
spinner.className = 'jp-AiAssistantsPanel-spinner';
|
|
1494
|
-
spinner.title = this._trans.__('Removing...');
|
|
1495
|
-
row.appendChild(spinner);
|
|
1496
|
-
}
|
|
1497
|
-
else {
|
|
1498
|
-
row.appendChild(this._renderIndicator(session));
|
|
1499
|
-
}
|
|
1500
1506
|
const name = document.createElement('span');
|
|
1501
1507
|
name.className = 'jp-AiAssistantsPanel-name';
|
|
1502
1508
|
// The text ellipsises in a span of its own so the badges after it survive
|
|
@@ -1531,6 +1537,21 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1531
1537
|
name.appendChild(bg);
|
|
1532
1538
|
}
|
|
1533
1539
|
row.appendChild(name);
|
|
1540
|
+
// The status dot (or the removal spinner in its place) leads the row
|
|
1541
|
+
// visually - CSS `order` puts it first - but follows the name in the DOM,
|
|
1542
|
+
// so a screen reader announces the project before its status sentence
|
|
1543
|
+
// rather than the same preamble ahead of every active row (DEF-PANE-183).
|
|
1544
|
+
const removing = this._removingPaths.has(session.encoded_path);
|
|
1545
|
+
if (removing) {
|
|
1546
|
+
row.classList.add('jp-mod-busy');
|
|
1547
|
+
const spinner = document.createElement('span');
|
|
1548
|
+
spinner.className = 'jp-AiAssistantsPanel-spinner';
|
|
1549
|
+
spinner.title = this._trans.__('Removing...');
|
|
1550
|
+
row.appendChild(spinner);
|
|
1551
|
+
}
|
|
1552
|
+
else {
|
|
1553
|
+
row.appendChild(this._renderIndicator(session));
|
|
1554
|
+
}
|
|
1534
1555
|
// No star in the Favorites section - every row there is one by definition.
|
|
1535
1556
|
// The star sits before the time so the fixed-width time column stays the
|
|
1536
1557
|
// rightmost alignment anchor across all rows.
|
|
@@ -1759,14 +1780,9 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1759
1780
|
`${pad(d.getHours())}:${pad(d.getMinutes())}`);
|
|
1760
1781
|
}
|
|
1761
1782
|
/** Branch entry display: conversation name plus short session id. Branches
|
|
1762
|
-
* share a project path, so the name and id are all that tell them apart.
|
|
1763
|
-
* suffix is dropped when the label already IS the short id. */
|
|
1783
|
+
* share a project path, so the name and id are all that tell them apart. */
|
|
1764
1784
|
_branchDisplayName(b) {
|
|
1765
|
-
|
|
1766
|
-
if (this._hooks.branchLabel) {
|
|
1767
|
-
return this._hooks.branchLabel(b, shortId);
|
|
1768
|
-
}
|
|
1769
|
-
return b.label === shortId ? b.label : `${b.label} (${shortId})`;
|
|
1785
|
+
return branchMenuLabel(b.label, shortSessionId(b.session_id, this._descriptor.sessionIdPrefix));
|
|
1770
1786
|
}
|
|
1771
1787
|
/** Menu-item label for a branch: display name, relative time, and a trailing
|
|
1772
1788
|
* marker when a live worker owns the conversation. The marker lives in the
|
|
@@ -2186,10 +2202,14 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2186
2202
|
add('reset-colour');
|
|
2187
2203
|
this._contextMenu.addItem({ type: 'separator' });
|
|
2188
2204
|
if (withBranches) {
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2205
|
+
// A project-scoped assistant has one terminal for every conversation,
|
|
2206
|
+
// so opening a branch in its own terminal is not an action it has.
|
|
2207
|
+
if (this._descriptor.terminalScope === 'conversation') {
|
|
2208
|
+
this._contextMenu.addItem({
|
|
2209
|
+
type: 'submenu',
|
|
2210
|
+
submenu: this._openBranchSubmenu
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2193
2213
|
this._contextMenu.addItem({
|
|
2194
2214
|
type: 'submenu',
|
|
2195
2215
|
submenu: this._switchSubmenu
|
|
@@ -2351,11 +2371,12 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2351
2371
|
if (!session) {
|
|
2352
2372
|
return;
|
|
2353
2373
|
}
|
|
2374
|
+
const current = this._lastBranchesCurrent || session.session_id;
|
|
2354
2375
|
showManageSessionsPopup({
|
|
2355
2376
|
trans: this._trans,
|
|
2356
2377
|
branches: this._lastBranches,
|
|
2357
|
-
current
|
|
2358
|
-
|
|
2378
|
+
current,
|
|
2379
|
+
currentName: `${this._lookupName(session)} (${shortSessionId(current, this._descriptor.sessionIdPrefix)})`,
|
|
2359
2380
|
deleteToTrash: this._deleteToTrash,
|
|
2360
2381
|
branchName: b => this._branchDisplayName(b),
|
|
2361
2382
|
formatTime: ms => this._formatRelativeTime(ms),
|
|
@@ -2376,11 +2397,16 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2376
2397
|
* is attached to and carries none. The row's own conversation is not in
|
|
2377
2398
|
* `_lastBranches`, so it answers from the session. */
|
|
2378
2399
|
_openBranchTitle(session, sessionId) {
|
|
2379
|
-
var _a;
|
|
2400
|
+
var _a, _b, _c, _d;
|
|
2401
|
+
// One terminal serves a project-scoped assistant, so the button focuses
|
|
2402
|
+
// or starts it, under the provider's own verb for that.
|
|
2403
|
+
if (this._descriptor.terminalScope === 'project') {
|
|
2404
|
+
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");
|
|
2405
|
+
}
|
|
2380
2406
|
const base = 'Open this conversation in its own terminal';
|
|
2381
2407
|
const target = sessionId === session.session_id
|
|
2382
2408
|
? session
|
|
2383
|
-
: ((
|
|
2409
|
+
: ((_d = this._lastBranches.find(b => b.session_id === sessionId)) !== null && _d !== void 0 ? _d : null);
|
|
2384
2410
|
const variant = this._resumeVariant(target);
|
|
2385
2411
|
return variant ? `${base} (${variant.label})` : base;
|
|
2386
2412
|
}
|
package/lib/core/popup.d.ts
CHANGED
|
@@ -18,8 +18,9 @@ export declare namespace ManageSessionsPopup {
|
|
|
18
18
|
branches: IBranch[];
|
|
19
19
|
/** The project's current conversation id, pinned at the top. */
|
|
20
20
|
current: string;
|
|
21
|
-
/** Display name of the row
|
|
22
|
-
|
|
21
|
+
/** Display name of the current row: the project's name and the short id
|
|
22
|
+
* of `current`, derived by the panel with the same rule as `branchName`. */
|
|
23
|
+
currentName: string;
|
|
23
24
|
/** Whether deletions move to trash - the popup says which it means. */
|
|
24
25
|
deleteToTrash: boolean;
|
|
25
26
|
/** Display name for one branch, `<label> (<short id>)` by default. */
|
package/lib/core/popup.js
CHANGED
|
@@ -158,7 +158,7 @@ export function showManageSessionsPopup(options) {
|
|
|
158
158
|
// same structure the branch rows below use.
|
|
159
159
|
const currentText = document.createElement('span');
|
|
160
160
|
currentText.className = 'jp-AiAssistantsPanel-branchLabelText';
|
|
161
|
-
currentText.textContent =
|
|
161
|
+
currentText.textContent = options.currentName;
|
|
162
162
|
currentLabel.appendChild(currentText);
|
|
163
163
|
currentRow.appendChild(currentLabel);
|
|
164
164
|
const badge = document.createElement('span');
|
|
@@ -245,21 +245,9 @@ export function showManageSessionsPopup(options) {
|
|
|
245
245
|
row.appendChild(time);
|
|
246
246
|
row.appendChild(openButton(b.session_id));
|
|
247
247
|
row.appendChild(copyButton(b.session_id, trans));
|
|
248
|
+
// A row click always switches, whatever is selected: the checkbox cell
|
|
249
|
+
// is the one select target, so a click has one meaning (DEF-PANE-209).
|
|
248
250
|
const activate = () => {
|
|
249
|
-
// Selection mode: while anything is ticked, row clicks toggle
|
|
250
|
-
// selection - no accidental switch mid-selection.
|
|
251
|
-
if (selected.size > 0) {
|
|
252
|
-
if (selected.has(b.session_id)) {
|
|
253
|
-
selected.delete(b.session_id);
|
|
254
|
-
}
|
|
255
|
-
else {
|
|
256
|
-
selected.add(b.session_id);
|
|
257
|
-
}
|
|
258
|
-
check.checked = selected.has(b.session_id);
|
|
259
|
-
disarm();
|
|
260
|
-
updateControls();
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
251
|
dialog.dispose();
|
|
264
252
|
options.onSwitch(b.session_id);
|
|
265
253
|
};
|
package/lib/core/terminals.d.ts
CHANGED
|
@@ -46,6 +46,11 @@ export declare class TerminalManager {
|
|
|
46
46
|
* Launch a terminal for one request and focus it. The returned widget is
|
|
47
47
|
* tagged with `tagSessionId` in the microcache, so a later click on the row
|
|
48
48
|
* that conversation now belongs to reuses this very terminal.
|
|
49
|
+
*
|
|
50
|
+
* For a project-scoped assistant the project's running terminal is the
|
|
51
|
+
* answer instead, whatever the request asked for: every caller - row click,
|
|
52
|
+
* `+`, fork, Launcher tile - lands here, so this one gate is what keeps a
|
|
53
|
+
* second server off the project.
|
|
49
54
|
*/
|
|
50
55
|
launch(request: ILaunchRequest, tagSessionId?: string): Promise<any>;
|
|
51
56
|
/**
|
|
@@ -63,11 +68,28 @@ export declare class TerminalManager {
|
|
|
63
68
|
* too, not only launches that carry an argv id. An id is globally unique to
|
|
64
69
|
* one conversation, so a terminal holding it IS the one to focus, whatever
|
|
65
70
|
* cwd it reports - a process that changed directory, or whose project dir
|
|
66
|
-
* was recreated, must still be reused rather than duplicated.
|
|
67
|
-
|
|
71
|
+
* was recreated, must still be reused rather than duplicated.
|
|
72
|
+
*
|
|
73
|
+
* For a project-scoped assistant the id is not what a terminal holds; the
|
|
74
|
+
* project's running terminal (see `_projectTerminal`) is the answer, so the
|
|
75
|
+
* caller passes the project path. */
|
|
76
|
+
findForSession(wantedSessionId: string | undefined, projectPath?: string): Promise<{
|
|
68
77
|
widget: any;
|
|
69
78
|
runningId: string | null;
|
|
70
79
|
} | null>;
|
|
80
|
+
/** The one terminal serving a project, for a project-scoped assistant, or
|
|
81
|
+
* null. The project's microcache entry whatever conversation it was tagged
|
|
82
|
+
* with, else the first live terminal the server confirms is running this
|
|
83
|
+
* assistant with its working directory inside the project - the same
|
|
84
|
+
* cwd-to-project rule the colour pass applies. The server's `running`
|
|
85
|
+
* answer is what keeps a plain shell opened at the project from matching.
|
|
86
|
+
*
|
|
87
|
+
* The cached terminal is asked too, so a server the user stopped in it does
|
|
88
|
+
* not hold every later open on a dead shell. Only a terminal the server has
|
|
89
|
+
* once confirmed is evicted on a `running: false` answer: a fresh launch
|
|
90
|
+
* answers that until its process is up, and evicting it then would start
|
|
91
|
+
* the second server this method exists to prevent. */
|
|
92
|
+
private _projectTerminal;
|
|
71
93
|
/** Ask the server what a terminal is running. Null when the widget has no
|
|
72
94
|
* session name yet, or the probe fails - a terminal can vanish between
|
|
73
95
|
* enumeration and probe, which is a retry, not an error. */
|
package/lib/core/terminals.js
CHANGED
|
@@ -99,7 +99,8 @@ export class TerminalManager {
|
|
|
99
99
|
// terminal is not painted, because painting is what destroys the pick.
|
|
100
100
|
this._pendingChoices = new Map();
|
|
101
101
|
// Most recent terminal per project, tagged with the conversation it runs so
|
|
102
|
-
// reuse can tell a project's branches apart.
|
|
102
|
+
// reuse can tell a project's branches apart. `confirmed` records that the
|
|
103
|
+
// server once answered `running: true` for it (project scope only).
|
|
103
104
|
this._byProject = new Map();
|
|
104
105
|
// In-flight launches keyed per CONVERSATION, so two branches of one project
|
|
105
106
|
// open independently and concurrently.
|
|
@@ -177,6 +178,17 @@ export class TerminalManager {
|
|
|
177
178
|
}
|
|
178
179
|
async _doOpenSession(session, mode, wanted) {
|
|
179
180
|
var _a;
|
|
181
|
+
// A project-scoped assistant has no per-conversation terminal to find:
|
|
182
|
+
// `launch` answers with the project's running one or starts it.
|
|
183
|
+
if (this._descriptor.terminalScope === 'project') {
|
|
184
|
+
await this.launch({
|
|
185
|
+
project_path: session.project_path,
|
|
186
|
+
encoded_path: session.encoded_path,
|
|
187
|
+
session_id: wanted,
|
|
188
|
+
mode
|
|
189
|
+
}, wanted);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
180
192
|
// 1. Microcache - the most recent terminal of this project, reused only
|
|
181
193
|
// when it carries the wanted conversation.
|
|
182
194
|
const cached = this._byProject.get(session.project_path);
|
|
@@ -211,8 +223,26 @@ export class TerminalManager {
|
|
|
211
223
|
* Launch a terminal for one request and focus it. The returned widget is
|
|
212
224
|
* tagged with `tagSessionId` in the microcache, so a later click on the row
|
|
213
225
|
* that conversation now belongs to reuses this very terminal.
|
|
226
|
+
*
|
|
227
|
+
* For a project-scoped assistant the project's running terminal is the
|
|
228
|
+
* answer instead, whatever the request asked for: every caller - row click,
|
|
229
|
+
* `+`, fork, Launcher tile - lands here, so this one gate is what keeps a
|
|
230
|
+
* second server off the project.
|
|
214
231
|
*/
|
|
215
232
|
async launch(request, tagSessionId) {
|
|
233
|
+
if (this._descriptor.terminalScope === 'project') {
|
|
234
|
+
const running = await this._projectTerminal(request.project_path);
|
|
235
|
+
if (running) {
|
|
236
|
+
// A new conversation still settles the project's pin (DEF-9): the
|
|
237
|
+
// argv route runs the launch's validation and pin bookkeeping without
|
|
238
|
+
// spawning, and the argv it answers with is not needed here.
|
|
239
|
+
if (request.session_id === undefined && request.encoded_path) {
|
|
240
|
+
await requestProvider(this._descriptor.id, 'launch-argv', this._serverSettings, { method: 'POST', body: JSON.stringify(request) });
|
|
241
|
+
}
|
|
242
|
+
this.focus(running);
|
|
243
|
+
return running;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
216
246
|
const spinner = showLaunchSpinner(`Opening ${this._descriptor.label}`, this._trans);
|
|
217
247
|
try {
|
|
218
248
|
const launched = await requestProvider(this._descriptor.id, 'launch', this._serverSettings, { method: 'POST', body: JSON.stringify(request) });
|
|
@@ -262,8 +292,18 @@ export class TerminalManager {
|
|
|
262
292
|
* too, not only launches that carry an argv id. An id is globally unique to
|
|
263
293
|
* one conversation, so a terminal holding it IS the one to focus, whatever
|
|
264
294
|
* cwd it reports - a process that changed directory, or whose project dir
|
|
265
|
-
* was recreated, must still be reused rather than duplicated.
|
|
266
|
-
|
|
295
|
+
* was recreated, must still be reused rather than duplicated.
|
|
296
|
+
*
|
|
297
|
+
* For a project-scoped assistant the id is not what a terminal holds; the
|
|
298
|
+
* project's running terminal (see `_projectTerminal`) is the answer, so the
|
|
299
|
+
* caller passes the project path. */
|
|
300
|
+
async findForSession(wantedSessionId, projectPath) {
|
|
301
|
+
if (this._descriptor.terminalScope === 'project') {
|
|
302
|
+
const widget = projectPath
|
|
303
|
+
? await this._projectTerminal(projectPath)
|
|
304
|
+
: null;
|
|
305
|
+
return widget ? { widget, runningId: null } : null;
|
|
306
|
+
}
|
|
267
307
|
if (!this._tracker || !wantedSessionId) {
|
|
268
308
|
return null;
|
|
269
309
|
}
|
|
@@ -275,6 +315,50 @@ export class TerminalManager {
|
|
|
275
315
|
}
|
|
276
316
|
return null;
|
|
277
317
|
}
|
|
318
|
+
/** The one terminal serving a project, for a project-scoped assistant, or
|
|
319
|
+
* null. The project's microcache entry whatever conversation it was tagged
|
|
320
|
+
* with, else the first live terminal the server confirms is running this
|
|
321
|
+
* assistant with its working directory inside the project - the same
|
|
322
|
+
* cwd-to-project rule the colour pass applies. The server's `running`
|
|
323
|
+
* answer is what keeps a plain shell opened at the project from matching.
|
|
324
|
+
*
|
|
325
|
+
* The cached terminal is asked too, so a server the user stopped in it does
|
|
326
|
+
* not hold every later open on a dead shell. Only a terminal the server has
|
|
327
|
+
* once confirmed is evicted on a `running: false` answer: a fresh launch
|
|
328
|
+
* answers that until its process is up, and evicting it then would start
|
|
329
|
+
* the second server this method exists to prevent. */
|
|
330
|
+
async _projectTerminal(projectPath) {
|
|
331
|
+
var _a, _b;
|
|
332
|
+
const cached = this._byProject.get(projectPath);
|
|
333
|
+
if (cached && !cached.widget.isDisposed) {
|
|
334
|
+
const info = await this.probe(cached.widget);
|
|
335
|
+
if (info === null || info === void 0 ? void 0 : info.running) {
|
|
336
|
+
cached.confirmed = true;
|
|
337
|
+
}
|
|
338
|
+
if ((info === null || info === void 0 ? void 0 : info.running) !== false || !cached.confirmed) {
|
|
339
|
+
return cached.widget;
|
|
340
|
+
}
|
|
341
|
+
this._byProject.delete(projectPath);
|
|
342
|
+
}
|
|
343
|
+
if (!this._tracker) {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
for (const widget of this._liveTerminals()) {
|
|
347
|
+
const info = await this.probe(widget);
|
|
348
|
+
if ((info === null || info === void 0 ? void 0 : info.running) &&
|
|
349
|
+
((_b = sessionForCwds((_a = info.cwds) !== null && _a !== void 0 ? _a : [], this._sessions)) === null || _b === void 0 ? void 0 : _b.project_path) ===
|
|
350
|
+
projectPath) {
|
|
351
|
+
this._byProject.set(projectPath, {
|
|
352
|
+
widget,
|
|
353
|
+
sessionId: undefined,
|
|
354
|
+
confirmed: true
|
|
355
|
+
});
|
|
356
|
+
this._wireDisposal(projectPath, widget);
|
|
357
|
+
return widget;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return null;
|
|
361
|
+
}
|
|
278
362
|
/** Ask the server what a terminal is running. Null when the widget has no
|
|
279
363
|
* session name yet, or the probe fails - a terminal can vanish between
|
|
280
364
|
* enumeration and probe, which is a retry, not an error. */
|
package/lib/core/types.d.ts
CHANGED
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* or read that value.
|
|
16
16
|
*/
|
|
17
17
|
export type ForkStrategy = 'native-flag' | 'native-command' | 'server-copy';
|
|
18
|
+
/** See `IProviderDescriptor.terminalScope`. */
|
|
19
|
+
export type TerminalScope = 'conversation' | 'project';
|
|
18
20
|
/** Where a conversation's DEFAULT tab tint comes from. A user-set colour in
|
|
19
21
|
* the extension's own write-back store always wins over all three. */
|
|
20
22
|
export type ColourSource = 'native' | 'derived' | 'none';
|
|
@@ -56,6 +58,14 @@ export interface IProviderDescriptor {
|
|
|
56
58
|
cliBinary: string;
|
|
57
59
|
forkStrategy: ForkStrategy;
|
|
58
60
|
colourSource: ColourSource;
|
|
61
|
+
/** What one terminal of this assistant holds. `conversation`: the process
|
|
62
|
+
* runs exactly one conversation, so a terminal is reused only when the
|
|
63
|
+
* server reads the wanted id off it. `project`: the process serves every
|
|
64
|
+
* conversation of the project (a web UI chosen in the browser), so the
|
|
65
|
+
* project's one running terminal is the answer to every open - a row click,
|
|
66
|
+
* `+`, a fork, a Launcher tile - and a second process on the same project
|
|
67
|
+
* is always a duplicate. */
|
|
68
|
+
terminalScope: TerminalScope;
|
|
59
69
|
/** Whether the branch flow asks the user to name the new conversation.
|
|
60
70
|
* Where that name then GOES is `forkStrategy`'s job. */
|
|
61
71
|
promptsForBranchName: boolean;
|
|
@@ -65,6 +75,12 @@ export interface IProviderDescriptor {
|
|
|
65
75
|
* `forkStrategy` - an assistant can accept an id for a new session and still
|
|
66
76
|
* have no fork verb at all. */
|
|
67
77
|
mintsNewSessionId: boolean;
|
|
78
|
+
/** A constant prefix every conversation id of this assistant carries, e.g.
|
|
79
|
+
* `session_`. The short id shown beside a branch is sliced past it, since a
|
|
80
|
+
* front-slice of such an id would render the same eight characters for
|
|
81
|
+
* every conversation of a project - the exact thing the short id exists to
|
|
82
|
+
* tell apart. Absent for an assistant whose ids are bare uuids. */
|
|
83
|
+
sessionIdPrefix?: string;
|
|
68
84
|
launchModes: ILaunchMode[];
|
|
69
85
|
/** Sessions can be held under remote control - renders the indicator dot. */
|
|
70
86
|
hasRemoteControl: boolean;
|
|
@@ -286,10 +302,6 @@ export interface IProviderHooks {
|
|
|
286
302
|
resumeLabel?: (session: ISession | null) => string;
|
|
287
303
|
/** Extra lines for the row tooltip. */
|
|
288
304
|
tooltipLines?: (session: ISession) => string[];
|
|
289
|
-
/** Menu label for one branch. Defaults to `<label> (<short id>) - <time>`;
|
|
290
|
-
* a provider whose titles are written in wide scripts overrides it to bound
|
|
291
|
-
* the menu by display columns rather than by code units. */
|
|
292
|
-
branchLabel?: (branch: IBranch, shortId: string) => string;
|
|
293
305
|
}
|
|
294
306
|
/** What `providers/index.ts` registers: one descriptor, optionally with hooks. */
|
|
295
307
|
export interface IProviderModule {
|
package/lib/providers/claude.js
CHANGED
|
@@ -42,6 +42,7 @@ export const descriptor = {
|
|
|
42
42
|
// its transcript, so the assistant supplies the DEFAULT tint - a colour the
|
|
43
43
|
// user then sets on the tab diverges from it deliberately and wins.
|
|
44
44
|
colourSource: 'native',
|
|
45
|
+
terminalScope: 'conversation',
|
|
45
46
|
// `-n <name>` at launch: Claude stamps the name itself and re-stamps it on
|
|
46
47
|
// every turn, which is the only way a fork's name survives - a title written
|
|
47
48
|
// after the fact loses to the parent title the fork inherits.
|
package/lib/providers/codex.js
CHANGED
|
@@ -38,6 +38,7 @@ export const descriptor = {
|
|
|
38
38
|
forkStrategy: 'native-command',
|
|
39
39
|
// No `/color` equivalent, and nothing in the thread store records one.
|
|
40
40
|
colourSource: 'none',
|
|
41
|
+
terminalScope: 'conversation',
|
|
41
42
|
// Forks are unnamed - Codex has no naming flag and no writable title field,
|
|
42
43
|
// so the panel never asks for a name it could not stamp anywhere.
|
|
43
44
|
promptsForBranchName: false,
|