jupyterlab_ai_code_assistants_extension 1.2.7 → 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 +10 -7
- package/lib/core/icons.js +6 -3
- package/lib/core/labels.d.ts +18 -0
- package/lib/core/labels.js +68 -0
- package/lib/core/panel.d.ts +11 -2
- package/lib/core/panel.js +96 -52
- package/lib/core/popup.d.ts +8 -2
- package/lib/core/popup.js +14 -23
- package/lib/core/terminals.d.ts +31 -3
- package/lib/core/terminals.js +94 -6
- package/lib/core/types.d.ts +16 -4
- package/lib/index.js +23 -3
- 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 +2 -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/icons.ts +6 -3
- package/src/core/labels.ts +74 -0
- package/src/core/panel.ts +115 -46
- package/src/core/popup.ts +25 -27
- package/src/core/terminals.ts +118 -7
- package/src/core/types.ts +17 -4
- package/src/index.ts +28 -3
- 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 +52 -8
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
|
-
- **Your terminal's `claude -c` follows the panel** (Claude) - switching to
|
|
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
|
package/lib/core/icons.js
CHANGED
|
@@ -105,10 +105,13 @@ export const filterIcon = new LabIcon({
|
|
|
105
105
|
});
|
|
106
106
|
// Row status mark - one filled circle, drawn for a conversation that is live
|
|
107
107
|
// or under remote control. An SVG rather than a rounded span so its colour is
|
|
108
|
-
// a CSS `fill` on the circle itself
|
|
109
|
-
//
|
|
108
|
+
// a CSS `fill` on the circle itself, the way every other glyph here is
|
|
109
|
+
// recoloured through its class (DEF-PANE-174). The circle carries no `fill`
|
|
110
|
+
// attribute: the stylesheet is the single source of that colour, and a
|
|
111
|
+
// presentation attribute here would be a second one to keep in step
|
|
112
|
+
// (DEF-PANE-179).
|
|
110
113
|
const statusDotSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
|
|
111
|
-
<circle class="jp-AiAssistantsPanel-dotGlyph" cx="8" cy="8" r="8"
|
|
114
|
+
<circle class="jp-AiAssistantsPanel-dotGlyph" cx="8" cy="8" r="8"/>
|
|
112
115
|
</svg>`;
|
|
113
116
|
export const statusDotIcon = new LabIcon({
|
|
114
117
|
name: `${ICON_PREFIX}:status-dot`,
|
|
@@ -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
|
@@ -2,6 +2,7 @@ import { JupyterFrontEnd } from '@jupyterlab/application';
|
|
|
2
2
|
import { IDefaultFileBrowser } from '@jupyterlab/filebrowser';
|
|
3
3
|
import { ITerminalTracker } from '@jupyterlab/terminal';
|
|
4
4
|
import { IColourfulTabs } from 'jupyterlab_colourful_tab_extension';
|
|
5
|
+
import { TranslationBundle } from '@jupyterlab/translation';
|
|
5
6
|
import { Message } from '@lumino/messaging';
|
|
6
7
|
import { Widget } from '@lumino/widgets';
|
|
7
8
|
import { IProviderDescriptor, IProviderHooks } from './types';
|
|
@@ -78,6 +79,10 @@ export declare class AssistantSessionsPanel extends Widget {
|
|
|
78
79
|
private _logError;
|
|
79
80
|
private _fetch;
|
|
80
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();
|
|
81
86
|
private get _disposalVerb();
|
|
82
87
|
private _removeProject;
|
|
83
88
|
private _cleanupParallel;
|
|
@@ -211,8 +216,7 @@ export declare class AssistantSessionsPanel extends Widget {
|
|
|
211
216
|
* cannot give. */
|
|
212
217
|
private _formatAbsoluteTime;
|
|
213
218
|
/** Branch entry display: conversation name plus short session id. Branches
|
|
214
|
-
* share a project path, so the name and id are all that tell them apart.
|
|
215
|
-
* 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. */
|
|
216
220
|
private _branchDisplayName;
|
|
217
221
|
/** Menu-item label for a branch: display name, relative time, and a trailing
|
|
218
222
|
* marker when a live worker owns the conversation. The marker lives in the
|
|
@@ -303,6 +307,7 @@ export declare class AssistantSessionsPanel extends Widget {
|
|
|
303
307
|
private _stopPolling;
|
|
304
308
|
private readonly _app;
|
|
305
309
|
private readonly _descriptor;
|
|
310
|
+
private readonly _trans;
|
|
306
311
|
private readonly _hooks;
|
|
307
312
|
private readonly _serverSettings;
|
|
308
313
|
private _rootDir;
|
|
@@ -366,5 +371,9 @@ export declare namespace AssistantSessionsPanel {
|
|
|
366
371
|
terminalTracker?: ITerminalTracker | null;
|
|
367
372
|
fileBrowser?: IDefaultFileBrowser | null;
|
|
368
373
|
colourfulTabs?: IColourfulTabs | null;
|
|
374
|
+
/** Bundle every user-visible string passes through. Optional so a
|
|
375
|
+
* caller that does not translate keeps working - the null bundle
|
|
376
|
+
* returns each string unchanged. */
|
|
377
|
+
trans?: TranslationBundle;
|
|
369
378
|
}
|
|
370
379
|
}
|
package/lib/core/panel.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { Clipboard, Dialog, InputDialog, Notification, showDialog } from '@jupyterlab/apputils';
|
|
2
2
|
import { MenuSvg, closeIcon, folderIcon, terminalIcon } from '@jupyterlab/ui-components';
|
|
3
|
+
import { nullTranslator } from '@jupyterlab/translation';
|
|
3
4
|
import { CommandRegistry } from '@lumino/commands';
|
|
4
5
|
import { UUID } from '@lumino/coreutils';
|
|
5
6
|
import { Widget } from '@lumino/widgets';
|
|
6
7
|
import { ColourStore } from './colour';
|
|
8
|
+
import { branchMenuLabel, shortSessionId } from './labels';
|
|
7
9
|
import { addIcon, branchIcon, cleanupIcon, filterIcon, providerIcon, refreshIcon, removeIcon, shieldIcon, starFilledIcon, statusDotIcon, switchIcon } from './icons';
|
|
8
10
|
import { DEFAULT_RECENT_LIMIT, MAX_RECENT_LIMIT, MIN_RECENT_LIMIT } from './limits';
|
|
9
11
|
import { resolveLaunchMode, resolvedLaunchModeEntry } from './modes';
|
|
@@ -97,7 +99,7 @@ function saveExpanded(storageKey, state) {
|
|
|
97
99
|
*/
|
|
98
100
|
export class AssistantSessionsPanel extends Widget {
|
|
99
101
|
constructor(options) {
|
|
100
|
-
var _a, _b, _c, _d;
|
|
102
|
+
var _a, _b, _c, _d, _e;
|
|
101
103
|
super();
|
|
102
104
|
this._rootDir = '';
|
|
103
105
|
this._deleteToTrash = true;
|
|
@@ -138,9 +140,11 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
138
140
|
this._app = options.app;
|
|
139
141
|
this._descriptor = options.descriptor;
|
|
140
142
|
this._hooks = (_a = options.hooks) !== null && _a !== void 0 ? _a : {};
|
|
143
|
+
this._trans =
|
|
144
|
+
(_b = options.trans) !== null && _b !== void 0 ? _b : nullTranslator.load('jupyterlab_ai_code_assistants_extension');
|
|
141
145
|
this._serverSettings = options.app.serviceManager.serverSettings;
|
|
142
146
|
this.setRoot(options.rootDir, options.deleteToTrash !== false);
|
|
143
|
-
this._fileBrowser = (
|
|
147
|
+
this._fileBrowser = (_c = options.fileBrowser) !== null && _c !== void 0 ? _c : null;
|
|
144
148
|
this._expandedKey = `jupyterlab_ai_code_assistants_extension:${this._descriptor.id}:expanded`;
|
|
145
149
|
this._expanded = loadExpanded(this._expandedKey);
|
|
146
150
|
this._colours = new ColourStore(this._descriptor.id, this._serverSettings);
|
|
@@ -148,9 +152,10 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
148
152
|
app: options.app,
|
|
149
153
|
descriptor: this._descriptor,
|
|
150
154
|
colourStore: this._colours,
|
|
151
|
-
terminalTracker: (
|
|
152
|
-
colourfulTabs: (
|
|
153
|
-
serverSettings: this._serverSettings
|
|
155
|
+
terminalTracker: (_d = options.terminalTracker) !== null && _d !== void 0 ? _d : null,
|
|
156
|
+
colourfulTabs: (_e = options.colourfulTabs) !== null && _e !== void 0 ? _e : null,
|
|
157
|
+
serverSettings: this._serverSettings,
|
|
158
|
+
trans: this._trans
|
|
154
159
|
});
|
|
155
160
|
this.id = panelWidgetId(this._descriptor.id);
|
|
156
161
|
this.title.icon = providerIcon(this._descriptor.iconName, this._descriptor.iconSvg);
|
|
@@ -299,7 +304,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
299
304
|
header.appendChild(newBtn);
|
|
300
305
|
const filterBtn = document.createElement('button');
|
|
301
306
|
filterBtn.className = 'jp-AiAssistantsPanel-iconButton';
|
|
302
|
-
filterBtn.title = 'Filter sessions';
|
|
307
|
+
filterBtn.title = this._trans.__('Filter sessions');
|
|
303
308
|
// The pressed state is drawn with a class, which says nothing to a screen
|
|
304
309
|
// reader; the filter bar starts hidden, so this starts false.
|
|
305
310
|
filterBtn.setAttribute('aria-pressed', 'false');
|
|
@@ -309,7 +314,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
309
314
|
this._filterBtn = filterBtn;
|
|
310
315
|
const refreshBtn = document.createElement('button');
|
|
311
316
|
refreshBtn.className = 'jp-AiAssistantsPanel-iconButton';
|
|
312
|
-
refreshBtn.title = 'Refresh';
|
|
317
|
+
refreshBtn.title = this._trans.__('Refresh');
|
|
313
318
|
refreshIcon.element({ container: refreshBtn });
|
|
314
319
|
refreshBtn.addEventListener('click', () => this.refresh());
|
|
315
320
|
header.appendChild(refreshBtn);
|
|
@@ -327,7 +332,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
327
332
|
search.type = 'search';
|
|
328
333
|
search.className = 'jp-AiAssistantsPanel-search';
|
|
329
334
|
search.placeholder = 'Filter sessions...';
|
|
330
|
-
search.setAttribute('aria-label', 'Filter sessions');
|
|
335
|
+
search.setAttribute('aria-label', this._trans.__('Filter sessions'));
|
|
331
336
|
search.spellcheck = false;
|
|
332
337
|
search.addEventListener('input', () => {
|
|
333
338
|
this._filter = search.value;
|
|
@@ -337,7 +342,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
337
342
|
this._searchEl = search;
|
|
338
343
|
const searchClear = document.createElement('button');
|
|
339
344
|
searchClear.className = 'jp-AiAssistantsPanel-searchClear';
|
|
340
|
-
searchClear.title = 'Clear filter';
|
|
345
|
+
searchClear.title = this._trans.__('Clear filter');
|
|
341
346
|
searchClear.hidden = true;
|
|
342
347
|
closeIcon.element({ container: searchClear });
|
|
343
348
|
searchClear.addEventListener('click', () => {
|
|
@@ -582,6 +587,12 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
582
587
|
this._showActionError('Could not toggle favorite - try again.', err);
|
|
583
588
|
}
|
|
584
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
|
+
}
|
|
585
596
|
get _disposalVerb() {
|
|
586
597
|
return this._deleteToTrash ? 'moved to trash' : 'deleted permanently';
|
|
587
598
|
}
|
|
@@ -592,8 +603,11 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
592
603
|
title: `Remove from ${this._descriptor.label}`,
|
|
593
604
|
body: `Remove "${name}" from ${this._descriptor.label}? This drops the ` +
|
|
594
605
|
`entire project history and every conversation it holds - ` +
|
|
595
|
-
`${this._disposalVerb}.
|
|
596
|
-
buttons: [
|
|
606
|
+
`${this._disposalVerb}.${this._irreversibleNote}`,
|
|
607
|
+
buttons: [
|
|
608
|
+
Dialog.cancelButton(),
|
|
609
|
+
Dialog.warnButton({ label: this._trans.__('Remove') })
|
|
610
|
+
],
|
|
597
611
|
// Cancel, explicitly. JupyterLab defaults `defaultButton` to the LAST
|
|
598
612
|
// button and focuses it on attach, so omitting this handed the keyboard
|
|
599
613
|
// to Remove - one Enter on a dialog that has just appeared and the
|
|
@@ -627,11 +641,14 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
627
641
|
const extra = session.extra_sessions;
|
|
628
642
|
const name = this._lookupName(session);
|
|
629
643
|
const confirm = await showDialog({
|
|
630
|
-
title: 'Clean Up Parallel Sessions',
|
|
644
|
+
title: this._trans.__('Clean Up Parallel Sessions'),
|
|
631
645
|
body: `Remove ${extra} parallel session${extra === 1 ? '' : 's'} from ` +
|
|
632
646
|
`"${name}"? The current conversation is kept; the rest are ` +
|
|
633
|
-
`${this._disposalVerb}.
|
|
634
|
-
buttons: [
|
|
647
|
+
`${this._disposalVerb}.${this._irreversibleNote}`,
|
|
648
|
+
buttons: [
|
|
649
|
+
Dialog.cancelButton(),
|
|
650
|
+
Dialog.warnButton({ label: this._trans.__('Remove') })
|
|
651
|
+
],
|
|
635
652
|
// Cancel, as above (DEF-51).
|
|
636
653
|
defaultButton: 0
|
|
637
654
|
});
|
|
@@ -651,9 +668,9 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
651
668
|
bar.max = 1;
|
|
652
669
|
body.node.appendChild(bar);
|
|
653
670
|
const dialog = new Dialog({
|
|
654
|
-
title: 'Clean Up Parallel Sessions',
|
|
671
|
+
title: this._trans.__('Clean Up Parallel Sessions'),
|
|
655
672
|
body,
|
|
656
|
-
buttons: [Dialog.okButton({ label: 'Close' })]
|
|
673
|
+
buttons: [Dialog.okButton({ label: this._trans.__('Close') })]
|
|
657
674
|
});
|
|
658
675
|
// Close stays live while the work runs. Hiding it took away the only exit
|
|
659
676
|
// a keyboard user has: the dialog listens for Escape on its own node, it
|
|
@@ -829,7 +846,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
829
846
|
if (row) {
|
|
830
847
|
// A terminal already holding that conversation IS the answer: a second
|
|
831
848
|
// process on one history is never what the click meant (ACC-LNCH-152).
|
|
832
|
-
const found = await this._terminals.findForSession(row.session_id);
|
|
849
|
+
const found = await this._terminals.findForSession(row.session_id, folder);
|
|
833
850
|
if (found) {
|
|
834
851
|
this._terminals.focus(found.widget);
|
|
835
852
|
return found.widget;
|
|
@@ -1184,6 +1201,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1184
1201
|
}
|
|
1185
1202
|
/** Make another conversation the row's current one. */
|
|
1186
1203
|
async _switchBranch(session, sessionId) {
|
|
1204
|
+
var _a;
|
|
1187
1205
|
try {
|
|
1188
1206
|
const result = await requestProvider(this._descriptor.id, 'switch', this._serverSettings, {
|
|
1189
1207
|
method: 'POST',
|
|
@@ -1195,6 +1213,15 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1195
1213
|
if (result.current !== result.requested) {
|
|
1196
1214
|
Notification.warning('That conversation could not be made the current one.', { autoClose: 4000 });
|
|
1197
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
|
+
}
|
|
1198
1225
|
}
|
|
1199
1226
|
catch (err) {
|
|
1200
1227
|
Notification.error(isResponseStatus(err, 404)
|
|
@@ -1476,17 +1503,6 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1476
1503
|
row.classList.add('jp-mod-stale');
|
|
1477
1504
|
}
|
|
1478
1505
|
}
|
|
1479
|
-
const removing = this._removingPaths.has(session.encoded_path);
|
|
1480
|
-
if (removing) {
|
|
1481
|
-
row.classList.add('jp-mod-busy');
|
|
1482
|
-
const spinner = document.createElement('span');
|
|
1483
|
-
spinner.className = 'jp-AiAssistantsPanel-spinner';
|
|
1484
|
-
spinner.title = 'Removing...';
|
|
1485
|
-
row.appendChild(spinner);
|
|
1486
|
-
}
|
|
1487
|
-
else {
|
|
1488
|
-
row.appendChild(this._renderIndicator(session));
|
|
1489
|
-
}
|
|
1490
1506
|
const name = document.createElement('span');
|
|
1491
1507
|
name.className = 'jp-AiAssistantsPanel-name';
|
|
1492
1508
|
// The text ellipsises in a span of its own so the badges after it survive
|
|
@@ -1521,6 +1537,21 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1521
1537
|
name.appendChild(bg);
|
|
1522
1538
|
}
|
|
1523
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
|
+
}
|
|
1524
1555
|
// No star in the Favorites section - every row there is one by definition.
|
|
1525
1556
|
// The star sits before the time so the fixed-width time column stays the
|
|
1526
1557
|
// rightmost alignment anchor across all rows.
|
|
@@ -1531,7 +1562,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1531
1562
|
// menu, rather than becoming a second hit target inside the row.
|
|
1532
1563
|
const star = document.createElement('span');
|
|
1533
1564
|
star.className = 'jp-AiAssistantsPanel-favStar';
|
|
1534
|
-
star.setAttribute('aria-label', 'Favorite');
|
|
1565
|
+
star.setAttribute('aria-label', this._trans.__('Favorite'));
|
|
1535
1566
|
star.setAttribute('role', 'img');
|
|
1536
1567
|
starFilledIcon.element({ container: star });
|
|
1537
1568
|
row.appendChild(star);
|
|
@@ -1626,7 +1657,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1626
1657
|
if (this._descriptor.hasRemoteControl && session.remote_control) {
|
|
1627
1658
|
dot.className = 'jp-AiAssistantsPanel-dot';
|
|
1628
1659
|
statusDotIcon.element({ container: dot });
|
|
1629
|
-
dot.title = 'Remote control session is active';
|
|
1660
|
+
dot.title = this._trans.__('Remote control session is active');
|
|
1630
1661
|
dot.setAttribute('aria-label', dot.title);
|
|
1631
1662
|
return dot;
|
|
1632
1663
|
}
|
|
@@ -1749,14 +1780,9 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1749
1780
|
`${pad(d.getHours())}:${pad(d.getMinutes())}`);
|
|
1750
1781
|
}
|
|
1751
1782
|
/** Branch entry display: conversation name plus short session id. Branches
|
|
1752
|
-
* share a project path, so the name and id are all that tell them apart.
|
|
1753
|
-
* 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. */
|
|
1754
1784
|
_branchDisplayName(b) {
|
|
1755
|
-
|
|
1756
|
-
if (this._hooks.branchLabel) {
|
|
1757
|
-
return this._hooks.branchLabel(b, shortId);
|
|
1758
|
-
}
|
|
1759
|
-
return b.label === shortId ? b.label : `${b.label} (${shortId})`;
|
|
1785
|
+
return branchMenuLabel(b.label, shortSessionId(b.session_id, this._descriptor.sessionIdPrefix));
|
|
1760
1786
|
}
|
|
1761
1787
|
/** Menu-item label for a branch: display name, relative time, and a trailing
|
|
1762
1788
|
* marker when a live worker owns the conversation. The marker lives in the
|
|
@@ -1838,6 +1864,12 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1838
1864
|
var _a;
|
|
1839
1865
|
return ((_a = this._resolvedVariant()) === null || _a === void 0 ? void 0 : _a.unsafe) ? shieldIcon : undefined;
|
|
1840
1866
|
}
|
|
1867
|
+
/* eslint-disable jupyter/command-described-by --
|
|
1868
|
+
These commands go on the private CommandRegistry built below, which only
|
|
1869
|
+
this panel's own row context menu reads. `describedBy` is metadata for
|
|
1870
|
+
the application registry's consumers - the palette, settings, other
|
|
1871
|
+
extensions - and none of them can see or invoke a command registered
|
|
1872
|
+
here. */
|
|
1841
1873
|
_setupCommands() {
|
|
1842
1874
|
this._commands = new CommandRegistry();
|
|
1843
1875
|
const active = () => this._activeSession;
|
|
@@ -1901,7 +1933,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1901
1933
|
});
|
|
1902
1934
|
}
|
|
1903
1935
|
this._commands.addCommand(this._cmd('open-terminal'), {
|
|
1904
|
-
label: 'Open Terminal',
|
|
1936
|
+
label: this._trans.__('Open Terminal'),
|
|
1905
1937
|
icon: terminalIcon,
|
|
1906
1938
|
execute: () => {
|
|
1907
1939
|
const s = active();
|
|
@@ -1926,7 +1958,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1926
1958
|
}
|
|
1927
1959
|
});
|
|
1928
1960
|
this._commands.addCommand(this._cmd('show-in-filebrowser'), {
|
|
1929
|
-
label: 'Show in File Browser',
|
|
1961
|
+
label: this._trans.__('Show in File Browser'),
|
|
1930
1962
|
icon: folderIcon,
|
|
1931
1963
|
execute: () => {
|
|
1932
1964
|
const s = active();
|
|
@@ -1948,7 +1980,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1948
1980
|
}
|
|
1949
1981
|
});
|
|
1950
1982
|
this._commands.addCommand(this._cmd('copy-path'), {
|
|
1951
|
-
label: 'Copy Path',
|
|
1983
|
+
label: this._trans.__('Copy Path'),
|
|
1952
1984
|
execute: () => {
|
|
1953
1985
|
const s = active();
|
|
1954
1986
|
if (s) {
|
|
@@ -1957,7 +1989,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
1957
1989
|
}
|
|
1958
1990
|
});
|
|
1959
1991
|
this._commands.addCommand(this._cmd('copy-session-id'), {
|
|
1960
|
-
label: 'Copy Session ID',
|
|
1992
|
+
label: this._trans.__('Copy Session ID'),
|
|
1961
1993
|
execute: () => {
|
|
1962
1994
|
var _a;
|
|
1963
1995
|
const id = (_a = active()) === null || _a === void 0 ? void 0 : _a.session_id;
|
|
@@ -2089,6 +2121,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2089
2121
|
});
|
|
2090
2122
|
this._buildMenus();
|
|
2091
2123
|
}
|
|
2124
|
+
/* eslint-enable jupyter/command-described-by */
|
|
2092
2125
|
/** How many forced launch variants would actually render right now. Zero
|
|
2093
2126
|
* means every variant builds the launch the plain item already builds, so a
|
|
2094
2127
|
* menu offering both is a menu with one entry. */
|
|
@@ -2108,17 +2141,17 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2108
2141
|
// Lists the project's other conversations. Items are rebuilt on every
|
|
2109
2142
|
// context-menu open from a fresh branches fetch.
|
|
2110
2143
|
this._switchSubmenu = this._menu();
|
|
2111
|
-
this._switchSubmenu.title.label = 'Switch and Manage Sessions';
|
|
2144
|
+
this._switchSubmenu.title.label = this._trans.__('Switch and Manage Sessions');
|
|
2112
2145
|
this._switchSubmenu.title.icon = switchIcon;
|
|
2113
2146
|
// Opens a conversation directly in its own terminal, as opposed to the
|
|
2114
2147
|
// switch submenu which only changes which one the row points at. Several
|
|
2115
2148
|
// conversations can be open at once, independently.
|
|
2116
2149
|
this._openBranchSubmenu = this._menu();
|
|
2117
|
-
this._openBranchSubmenu.title.label = 'Open Branched Conversation';
|
|
2150
|
+
this._openBranchSubmenu.title.label = this._trans.__('Open Branched Conversation');
|
|
2118
2151
|
this._openBranchSubmenu.title.icon = terminalIcon;
|
|
2119
2152
|
// Groups the branch launch modes.
|
|
2120
2153
|
this._branchSessionMenu = this._menu();
|
|
2121
|
-
this._branchSessionMenu.title.label = 'Branch Session';
|
|
2154
|
+
this._branchSessionMenu.title.label = this._trans.__('Branch Session');
|
|
2122
2155
|
this._branchSessionMenu.title.icon = branchIcon;
|
|
2123
2156
|
this._branchSessionMenu.addItem({ command: this._cmd('branch-session') });
|
|
2124
2157
|
for (const mode of this._variantModes) {
|
|
@@ -2169,10 +2202,14 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2169
2202
|
add('reset-colour');
|
|
2170
2203
|
this._contextMenu.addItem({ type: 'separator' });
|
|
2171
2204
|
if (withBranches) {
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
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
|
+
}
|
|
2176
2213
|
this._contextMenu.addItem({
|
|
2177
2214
|
type: 'submenu',
|
|
2178
2215
|
submenu: this._switchSubmenu
|
|
@@ -2334,10 +2371,12 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2334
2371
|
if (!session) {
|
|
2335
2372
|
return;
|
|
2336
2373
|
}
|
|
2374
|
+
const current = this._lastBranchesCurrent || session.session_id;
|
|
2337
2375
|
showManageSessionsPopup({
|
|
2376
|
+
trans: this._trans,
|
|
2338
2377
|
branches: this._lastBranches,
|
|
2339
|
-
current
|
|
2340
|
-
|
|
2378
|
+
current,
|
|
2379
|
+
currentName: `${this._lookupName(session)} (${shortSessionId(current, this._descriptor.sessionIdPrefix)})`,
|
|
2341
2380
|
deleteToTrash: this._deleteToTrash,
|
|
2342
2381
|
branchName: b => this._branchDisplayName(b),
|
|
2343
2382
|
formatTime: ms => this._formatRelativeTime(ms),
|
|
@@ -2358,11 +2397,16 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2358
2397
|
* is attached to and carries none. The row's own conversation is not in
|
|
2359
2398
|
* `_lastBranches`, so it answers from the session. */
|
|
2360
2399
|
_openBranchTitle(session, sessionId) {
|
|
2361
|
-
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
|
+
}
|
|
2362
2406
|
const base = 'Open this conversation in its own terminal';
|
|
2363
2407
|
const target = sessionId === session.session_id
|
|
2364
2408
|
? session
|
|
2365
|
-
: ((
|
|
2409
|
+
: ((_d = this._lastBranches.find(b => b.session_id === sessionId)) !== null && _d !== void 0 ? _d : null);
|
|
2366
2410
|
const variant = this._resumeVariant(target);
|
|
2367
2411
|
return variant ? `${base} (${variant.label})` : base;
|
|
2368
2412
|
}
|
package/lib/core/popup.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TranslationBundle } from '@jupyterlab/translation';
|
|
1
2
|
import { IBranch } from './types';
|
|
2
3
|
/**
|
|
3
4
|
* The Manage Sessions popup: a searchable, scrollable table over every
|
|
@@ -9,12 +10,17 @@ import { IBranch } from './types';
|
|
|
9
10
|
*/
|
|
10
11
|
export declare namespace ManageSessionsPopup {
|
|
11
12
|
interface IOptions {
|
|
13
|
+
/** Bundle every user-visible string passes through. Optional so a
|
|
14
|
+
* caller that does not translate keeps working - the null bundle
|
|
15
|
+
* returns each string unchanged. */
|
|
16
|
+
trans?: TranslationBundle;
|
|
12
17
|
/** Conversations other than the current one. */
|
|
13
18
|
branches: IBranch[];
|
|
14
19
|
/** The project's current conversation id, pinned at the top. */
|
|
15
20
|
current: string;
|
|
16
|
-
/** Display name of the row
|
|
17
|
-
|
|
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;
|
|
18
24
|
/** Whether deletions move to trash - the popup says which it means. */
|
|
19
25
|
deleteToTrash: boolean;
|
|
20
26
|
/** Display name for one branch, `<label> (<short id>)` by default. */
|