jupyterlab_ai_code_assistants_extension 1.2.118 → 1.2.123
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/lib/core/panel.js +10 -3
- package/lib/core/popup.d.ts +4 -0
- package/lib/core/popup.js +37 -21
- package/package.json +1 -1
- package/src/__tests__/panel.spec.ts +28 -2
- package/src/__tests__/popup.spec.ts +75 -2
- package/src/core/panel.ts +11 -3
- package/src/core/popup.ts +41 -21
- package/style/base.css +0 -20
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ Chat-panel extensions re-implement the agent loop and trail the real tool. This
|
|
|
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
|
-
- **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
|
|
36
|
+
- **Conversation switcher** - a right-click "Switch and Manage Sessions" submenu lists a project's other conversations by name and short id with last-activity time; "Manage Sessions..." opens a searchable popup over the full list with multi-select delete, a per-row action that switches or opens according to the submenu it was opened from, and a copy-id button
|
|
37
37
|
- **Branch session** - fork the current conversation into a new named session via the right-click menu; each assistant forks its own way (Claude's native `--fork-session`, Codex's app-server `thread/fork`, server-side copies for Kimi, Gemini and DeepSeek) behind the same menu item
|
|
38
38
|
- **Rename session** - name a conversation from the right-click menu, and the name is written where the assistant itself reads it back: Claude gets the record its own `/rename` writes, Kimi a custom title, Gemini the chat's summary, DeepSeek its title event. The panel shows the name the assistant actually kept. Codex's name is set through Codex's own app-server, so its CLI lists the same name the panel shows
|
|
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
|
package/lib/core/panel.js
CHANGED
|
@@ -2083,7 +2083,13 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2083
2083
|
});
|
|
2084
2084
|
this._commands.addCommand(this._cmd('manage-sessions'), {
|
|
2085
2085
|
label: () => `Manage Sessions... (${this._lastBranches.length})`,
|
|
2086
|
-
execute: () => this._openManagePopup()
|
|
2086
|
+
execute: () => this._openManagePopup('switch')
|
|
2087
|
+
});
|
|
2088
|
+
// The open submenu's copy of the item: same popup, but a row action
|
|
2089
|
+
// launches the conversation rather than making it current.
|
|
2090
|
+
this._commands.addCommand(this._cmd('manage-sessions-open'), {
|
|
2091
|
+
label: () => `Manage Sessions... (${this._lastBranches.length})`,
|
|
2092
|
+
execute: () => this._openManagePopup('open')
|
|
2087
2093
|
});
|
|
2088
2094
|
this._commands.addCommand(this._cmd('open-branch'), {
|
|
2089
2095
|
// Names the mode the launch carries, like Resume, `+` and Branch Session
|
|
@@ -2417,10 +2423,10 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2417
2423
|
}
|
|
2418
2424
|
this._openBranchSubmenu.addItem({ type: 'separator' });
|
|
2419
2425
|
this._openBranchSubmenu.addItem({
|
|
2420
|
-
command: this._cmd('manage-sessions')
|
|
2426
|
+
command: this._cmd('manage-sessions-open')
|
|
2421
2427
|
});
|
|
2422
2428
|
}
|
|
2423
|
-
_openManagePopup() {
|
|
2429
|
+
_openManagePopup(mode) {
|
|
2424
2430
|
const session = this._activeSession;
|
|
2425
2431
|
if (!session) {
|
|
2426
2432
|
return;
|
|
@@ -2428,6 +2434,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2428
2434
|
const current = this._lastBranchesCurrent || session.session_id;
|
|
2429
2435
|
showManageSessionsPopup({
|
|
2430
2436
|
trans: this._trans,
|
|
2437
|
+
mode,
|
|
2431
2438
|
branches: this._lastBranches,
|
|
2432
2439
|
current,
|
|
2433
2440
|
currentName: `${this._lookupName(session)} (${shortSessionId(current, this._descriptor.sessionIdPrefix)})`,
|
package/lib/core/popup.d.ts
CHANGED
|
@@ -29,6 +29,10 @@ export declare namespace ManageSessionsPopup {
|
|
|
29
29
|
formatTime: (epochMs: number) => string;
|
|
30
30
|
/** Provider chip for a branch row, or null. */
|
|
31
31
|
branchBadge?: (branch: IBranch) => HTMLElement | null;
|
|
32
|
+
/** What a row's action does, inherited from the submenu that opened the
|
|
33
|
+
* popup: `switch` makes the conversation current, `open` launches it in
|
|
34
|
+
* its own terminal. */
|
|
35
|
+
mode: 'switch' | 'open';
|
|
32
36
|
/** Tooltip for one row's Open button. The panel owns it because the launch
|
|
33
37
|
* mode in force is panel state, and every other surface that opens a
|
|
34
38
|
* conversation names it. */
|
package/lib/core/popup.js
CHANGED
|
@@ -115,19 +115,32 @@ export function showManageSessionsPopup(options) {
|
|
|
115
115
|
});
|
|
116
116
|
// Scopes the action-row layout in base.css to this one dialog.
|
|
117
117
|
dialog.addClass('jp-AiAssistantsPanel-branchDialog');
|
|
118
|
-
//
|
|
119
|
-
// the
|
|
120
|
-
//
|
|
121
|
-
const
|
|
118
|
+
// The row action follows the mode the invoking submenu passed: switch makes
|
|
119
|
+
// the conversation current, open launches it in its own terminal. Both close
|
|
120
|
+
// the popup first - the action is complete once it runs.
|
|
121
|
+
const runAction = (sessionId) => {
|
|
122
|
+
dialog.dispose();
|
|
123
|
+
if (options.mode === 'switch') {
|
|
124
|
+
options.onSwitch(sessionId);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
options.onOpen(sessionId);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
// Per-row action button. stopPropagation keeps the click from toggling
|
|
131
|
+
// selection or repeating the action through the row's own click handler.
|
|
132
|
+
const actionButton = (sessionId) => {
|
|
122
133
|
const btn = document.createElement('button');
|
|
123
134
|
btn.type = 'button';
|
|
124
135
|
btn.className = 'jp-AiAssistantsPanel-branchOpen';
|
|
125
|
-
btn.textContent = 'Open';
|
|
126
|
-
btn.title =
|
|
136
|
+
btn.textContent = options.mode === 'switch' ? 'Switch' : 'Open';
|
|
137
|
+
btn.title =
|
|
138
|
+
options.mode === 'switch'
|
|
139
|
+
? "Make this the project's current conversation"
|
|
140
|
+
: options.openTitle();
|
|
127
141
|
btn.addEventListener('click', e => {
|
|
128
142
|
e.stopPropagation();
|
|
129
|
-
|
|
130
|
-
options.onOpen(sessionId);
|
|
143
|
+
runAction(sessionId);
|
|
131
144
|
});
|
|
132
145
|
return btn;
|
|
133
146
|
};
|
|
@@ -192,7 +205,11 @@ export function showManageSessionsPopup(options) {
|
|
|
192
205
|
badge.className = 'jp-AiAssistantsPanel-branchCurrentBadge';
|
|
193
206
|
badge.textContent = 'current';
|
|
194
207
|
currentRow.appendChild(badge);
|
|
195
|
-
|
|
208
|
+
// Switch mode has nothing to switch to on the row that is already current;
|
|
209
|
+
// open mode can still launch it in a terminal.
|
|
210
|
+
if (options.mode === 'open') {
|
|
211
|
+
currentRow.appendChild(actionButton(options.current));
|
|
212
|
+
}
|
|
196
213
|
currentRow.appendChild(copyButton(options.current, trans));
|
|
197
214
|
list.appendChild(currentRow);
|
|
198
215
|
const matches = visibleMatches();
|
|
@@ -270,20 +287,19 @@ export function showManageSessionsPopup(options) {
|
|
|
270
287
|
time.className = 'jp-AiAssistantsPanel-branchTime';
|
|
271
288
|
time.textContent = options.formatTime(b.file_mtime);
|
|
272
289
|
row.appendChild(time);
|
|
273
|
-
row.appendChild(
|
|
290
|
+
row.appendChild(actionButton(b.session_id));
|
|
274
291
|
row.appendChild(copyButton(b.session_id, trans));
|
|
275
|
-
// A row click
|
|
276
|
-
// is the one select target, so a click has one meaning
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
options.onSwitch(b.session_id);
|
|
280
|
-
};
|
|
292
|
+
// A row click follows the popup's mode, whatever is selected: the
|
|
293
|
+
// checkbox cell is the one select target, so a click has one meaning
|
|
294
|
+
// (DEF-PANE-209).
|
|
295
|
+
const activate = () => runAction(b.session_id);
|
|
281
296
|
row.addEventListener('click', activate);
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
// not the row: the row also hosts a checkbox and the
|
|
285
|
-
// and a role="button" wrapping them both swallows
|
|
286
|
-
// and makes them presentational children (ARIA
|
|
297
|
+
// The row action also has a button; the label keeps a role and key route
|
|
298
|
+
// so the row itself stays keyboard-reachable. The affordance sits on the
|
|
299
|
+
// label span, not the row: the row also hosts a checkbox and the
|
|
300
|
+
// action/Copy buttons, and a role="button" wrapping them both swallows
|
|
301
|
+
// their own Enter/Space and makes them presentational children (ARIA
|
|
302
|
+
// nested-interactive).
|
|
287
303
|
label.setAttribute('role', 'button');
|
|
288
304
|
label.tabIndex = 0;
|
|
289
305
|
label.addEventListener('keydown', e => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jupyterlab_ai_code_assistants_extension",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.123",
|
|
4
4
|
"description": "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.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -986,17 +986,43 @@ describe('the Manage Sessions popup is handed the current row name', () => {
|
|
|
986
986
|
name: 'proj',
|
|
987
987
|
session_id: 'session_9f8e7d6c-1111-2222-3333-444455556666'
|
|
988
988
|
});
|
|
989
|
-
(p as any)._openManagePopup();
|
|
989
|
+
(p as any)._openManagePopup('switch');
|
|
990
990
|
const calls = (showManageSessionsPopup as jest.Mock).mock.calls;
|
|
991
991
|
const [options] = calls[calls.length - 1];
|
|
992
992
|
// A front-slice would read `proj (session_)` on every conversation.
|
|
993
993
|
expect(options.currentName).toEqual('proj (9f8e7d6c)');
|
|
994
|
+
expect(options.mode).toEqual('switch');
|
|
994
995
|
} finally {
|
|
995
996
|
p.dispose();
|
|
996
997
|
}
|
|
997
998
|
});
|
|
998
999
|
});
|
|
999
1000
|
|
|
1001
|
+
describe('Manage Sessions popup mode follows the invoking submenu (DEF-PANE-326)', () => {
|
|
1002
|
+
const lastOptions = (): any => {
|
|
1003
|
+
const calls = (showManageSessionsPopup as jest.Mock).mock.calls;
|
|
1004
|
+
return calls[calls.length - 1][0];
|
|
1005
|
+
};
|
|
1006
|
+
|
|
1007
|
+
it('the switch submenu opens the popup in switch mode', () => {
|
|
1008
|
+
(panel as any)._activeSession = session({ name: 'proj' });
|
|
1009
|
+
(panel as any)._fillBranchSubmenus([]);
|
|
1010
|
+
const item = (panel as any)._switchSubmenu.items.at(-1);
|
|
1011
|
+
(panel as any)._commands.execute(item.command, item.args);
|
|
1012
|
+
|
|
1013
|
+
expect(lastOptions().mode).toBe('switch');
|
|
1014
|
+
});
|
|
1015
|
+
|
|
1016
|
+
it('the open submenu opens the popup in open mode', () => {
|
|
1017
|
+
(panel as any)._activeSession = session({ name: 'proj' });
|
|
1018
|
+
(panel as any)._fillBranchSubmenus([]);
|
|
1019
|
+
const item = (panel as any)._openBranchSubmenu.items.at(-1);
|
|
1020
|
+
(panel as any)._commands.execute(item.command, item.args);
|
|
1021
|
+
|
|
1022
|
+
expect(lastOptions().mode).toBe('open');
|
|
1023
|
+
});
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1000
1026
|
describe('per-conversation open surfaces', () => {
|
|
1001
1027
|
function submenuLabels(p: AssistantSessionsPanel): string[] {
|
|
1002
1028
|
(p as any)._rebuildContextMenu(true);
|
|
@@ -1827,7 +1853,7 @@ describe('ACC-SESS-190..192 - opening a session moves the file browser', () => {
|
|
|
1827
1853
|
'the Manage Sessions Open button',
|
|
1828
1854
|
(p, row) => {
|
|
1829
1855
|
(p as any)._activeSession = row;
|
|
1830
|
-
(p as any)._openManagePopup();
|
|
1856
|
+
(p as any)._openManagePopup('open');
|
|
1831
1857
|
const calls = (showManageSessionsPopup as jest.Mock).mock.calls;
|
|
1832
1858
|
calls[calls.length - 1][0].onOpen('sid-branch');
|
|
1833
1859
|
}
|
|
@@ -26,13 +26,15 @@ function branch(sessionId: string, label: string): IBranch {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
let onSwitch: jest.Mock;
|
|
29
|
+
let onOpen: jest.Mock;
|
|
29
30
|
let onDelete: jest.Mock;
|
|
30
31
|
// Each test opens a fresh popup in beforeEach; a describe that needs the
|
|
31
32
|
// permanent mode flips this in its beforeAll, which runs ahead of it.
|
|
32
33
|
let deleteToTrash = true;
|
|
33
34
|
|
|
34
|
-
function open(): void {
|
|
35
|
+
function open(mode: 'switch' | 'open' = 'switch'): void {
|
|
35
36
|
showManageSessionsPopup({
|
|
37
|
+
mode,
|
|
36
38
|
branches: [branch(FIRST, 'first'), branch(SECOND, 'second')],
|
|
37
39
|
current: 'session-00000000-0000-4000-8000-000000000000',
|
|
38
40
|
currentName: 'proj (00000000)',
|
|
@@ -41,12 +43,20 @@ function open(): void {
|
|
|
41
43
|
formatTime: () => 'now',
|
|
42
44
|
openTitle: () => 'Open',
|
|
43
45
|
onSwitch,
|
|
44
|
-
onOpen
|
|
46
|
+
onOpen,
|
|
45
47
|
onDelete,
|
|
46
48
|
onRefetch: jest.fn(async () => [])
|
|
47
49
|
});
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
/** Replace the default switch-mode popup with one opened in `mode`, and let
|
|
53
|
+
* the launch queue attach it before the test queries rows. */
|
|
54
|
+
async function reopen(mode: 'switch' | 'open'): Promise<void> {
|
|
55
|
+
Dialog.flush();
|
|
56
|
+
open(mode);
|
|
57
|
+
await new Promise(resolve => setTimeout(resolve, 0));
|
|
58
|
+
}
|
|
59
|
+
|
|
50
60
|
function rows(): HTMLElement[] {
|
|
51
61
|
return Array.from(
|
|
52
62
|
document.querySelectorAll<HTMLElement>('.jp-AiAssistantsPanel-branchRow')
|
|
@@ -63,8 +73,16 @@ function deleteButton(): HTMLButtonElement {
|
|
|
63
73
|
)!;
|
|
64
74
|
}
|
|
65
75
|
|
|
76
|
+
/** The per-row action button - Switch or Open, by popup mode. */
|
|
77
|
+
function actionButtons(row: HTMLElement): HTMLButtonElement[] {
|
|
78
|
+
return Array.from(
|
|
79
|
+
row.querySelectorAll<HTMLButtonElement>('.jp-AiAssistantsPanel-branchOpen')
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
66
83
|
beforeEach(() => {
|
|
67
84
|
onSwitch = jest.fn();
|
|
85
|
+
onOpen = jest.fn();
|
|
68
86
|
onDelete = jest.fn(async () => 0);
|
|
69
87
|
open();
|
|
70
88
|
});
|
|
@@ -100,6 +118,61 @@ describe('Manage Sessions popup click zones', () => {
|
|
|
100
118
|
});
|
|
101
119
|
});
|
|
102
120
|
|
|
121
|
+
describe('Manage Sessions popup action mode (DEF-PANE-326)', () => {
|
|
122
|
+
it('switch mode labels non-current rows Switch and calls onSwitch', () => {
|
|
123
|
+
const [, first, second] = rows();
|
|
124
|
+
const buttons = [...actionButtons(first), ...actionButtons(second)];
|
|
125
|
+
expect(buttons.map(b => b.textContent)).toEqual(['Switch', 'Switch']);
|
|
126
|
+
expect(buttons[0].title).toBe(
|
|
127
|
+
"Make this the project's current conversation"
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
actionButtons(second)[0].click();
|
|
131
|
+
|
|
132
|
+
expect(onSwitch).toHaveBeenCalledWith(SECOND);
|
|
133
|
+
expect(onOpen).not.toHaveBeenCalled();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('open mode labels non-current rows Open and calls onOpen', async () => {
|
|
137
|
+
await reopen('open');
|
|
138
|
+
const [, first, second] = rows();
|
|
139
|
+
const buttons = [...actionButtons(first), ...actionButtons(second)];
|
|
140
|
+
expect(buttons.map(b => b.textContent)).toEqual(['Open', 'Open']);
|
|
141
|
+
expect(buttons[0].title).toBe('Open');
|
|
142
|
+
|
|
143
|
+
actionButtons(second)[0].click();
|
|
144
|
+
|
|
145
|
+
expect(onOpen).toHaveBeenCalledWith(SECOND);
|
|
146
|
+
expect(onSwitch).not.toHaveBeenCalled();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('a row click switches in switch mode and opens in open mode', async () => {
|
|
150
|
+
const [, first] = rows();
|
|
151
|
+
first.click();
|
|
152
|
+
|
|
153
|
+
expect(onSwitch).toHaveBeenCalledWith(FIRST);
|
|
154
|
+
expect(onOpen).not.toHaveBeenCalled();
|
|
155
|
+
|
|
156
|
+
await reopen('open');
|
|
157
|
+
const [, openFirst] = rows();
|
|
158
|
+
openFirst.click();
|
|
159
|
+
|
|
160
|
+
expect(onOpen).toHaveBeenCalledWith(FIRST);
|
|
161
|
+
expect(onSwitch).toHaveBeenCalledTimes(1);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('the current row has no action in switch mode and Open in open mode', async () => {
|
|
165
|
+
const [current] = rows();
|
|
166
|
+
expect(actionButtons(current)).toHaveLength(0);
|
|
167
|
+
|
|
168
|
+
await reopen('open');
|
|
169
|
+
const [openCurrent] = rows();
|
|
170
|
+
expect(actionButtons(openCurrent).map(b => b.textContent)).toEqual([
|
|
171
|
+
'Open'
|
|
172
|
+
]);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
103
176
|
describe('Manage Sessions popup focus (DEF-PANE-314)', () => {
|
|
104
177
|
// The popup moves the focus to the filter once the dialog is ready.
|
|
105
178
|
const ready = (): Promise<void> => Dialog.tracker.find(() => true)!.ready;
|
package/src/core/panel.ts
CHANGED
|
@@ -2424,7 +2424,14 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2424
2424
|
|
|
2425
2425
|
this._commands.addCommand(this._cmd('manage-sessions'), {
|
|
2426
2426
|
label: () => `Manage Sessions... (${this._lastBranches.length})`,
|
|
2427
|
-
execute: () => this._openManagePopup()
|
|
2427
|
+
execute: () => this._openManagePopup('switch')
|
|
2428
|
+
});
|
|
2429
|
+
|
|
2430
|
+
// The open submenu's copy of the item: same popup, but a row action
|
|
2431
|
+
// launches the conversation rather than making it current.
|
|
2432
|
+
this._commands.addCommand(this._cmd('manage-sessions-open'), {
|
|
2433
|
+
label: () => `Manage Sessions... (${this._lastBranches.length})`,
|
|
2434
|
+
execute: () => this._openManagePopup('open')
|
|
2428
2435
|
});
|
|
2429
2436
|
|
|
2430
2437
|
this._commands.addCommand(this._cmd('open-branch'), {
|
|
@@ -2791,11 +2798,11 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2791
2798
|
}
|
|
2792
2799
|
this._openBranchSubmenu.addItem({ type: 'separator' });
|
|
2793
2800
|
this._openBranchSubmenu.addItem({
|
|
2794
|
-
command: this._cmd('manage-sessions')
|
|
2801
|
+
command: this._cmd('manage-sessions-open')
|
|
2795
2802
|
});
|
|
2796
2803
|
}
|
|
2797
2804
|
|
|
2798
|
-
private _openManagePopup(): void {
|
|
2805
|
+
private _openManagePopup(mode: 'switch' | 'open'): void {
|
|
2799
2806
|
const session = this._activeSession;
|
|
2800
2807
|
if (!session) {
|
|
2801
2808
|
return;
|
|
@@ -2803,6 +2810,7 @@ export class AssistantSessionsPanel extends Widget {
|
|
|
2803
2810
|
const current = this._lastBranchesCurrent || session.session_id;
|
|
2804
2811
|
showManageSessionsPopup({
|
|
2805
2812
|
trans: this._trans,
|
|
2813
|
+
mode,
|
|
2806
2814
|
branches: this._lastBranches,
|
|
2807
2815
|
current,
|
|
2808
2816
|
currentName: `${this._lookupName(session)} (${shortSessionId(
|
package/src/core/popup.ts
CHANGED
|
@@ -35,6 +35,10 @@ export namespace ManageSessionsPopup {
|
|
|
35
35
|
formatTime: (epochMs: number) => string;
|
|
36
36
|
/** Provider chip for a branch row, or null. */
|
|
37
37
|
branchBadge?: (branch: IBranch) => HTMLElement | null;
|
|
38
|
+
/** What a row's action does, inherited from the submenu that opened the
|
|
39
|
+
* popup: `switch` makes the conversation current, `open` launches it in
|
|
40
|
+
* its own terminal. */
|
|
41
|
+
mode: 'switch' | 'open';
|
|
38
42
|
/** Tooltip for one row's Open button. The panel owns it because the launch
|
|
39
43
|
* mode in force is panel state, and every other surface that opens a
|
|
40
44
|
* conversation names it. */
|
|
@@ -185,19 +189,32 @@ export function showManageSessionsPopup(
|
|
|
185
189
|
// Scopes the action-row layout in base.css to this one dialog.
|
|
186
190
|
dialog.addClass('jp-AiAssistantsPanel-branchDialog');
|
|
187
191
|
|
|
188
|
-
//
|
|
189
|
-
// the
|
|
190
|
-
//
|
|
191
|
-
const
|
|
192
|
+
// The row action follows the mode the invoking submenu passed: switch makes
|
|
193
|
+
// the conversation current, open launches it in its own terminal. Both close
|
|
194
|
+
// the popup first - the action is complete once it runs.
|
|
195
|
+
const runAction = (sessionId: string): void => {
|
|
196
|
+
dialog.dispose();
|
|
197
|
+
if (options.mode === 'switch') {
|
|
198
|
+
options.onSwitch(sessionId);
|
|
199
|
+
} else {
|
|
200
|
+
options.onOpen(sessionId);
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// Per-row action button. stopPropagation keeps the click from toggling
|
|
205
|
+
// selection or repeating the action through the row's own click handler.
|
|
206
|
+
const actionButton = (sessionId: string): HTMLButtonElement => {
|
|
192
207
|
const btn = document.createElement('button');
|
|
193
208
|
btn.type = 'button';
|
|
194
209
|
btn.className = 'jp-AiAssistantsPanel-branchOpen';
|
|
195
|
-
btn.textContent = 'Open';
|
|
196
|
-
btn.title =
|
|
210
|
+
btn.textContent = options.mode === 'switch' ? 'Switch' : 'Open';
|
|
211
|
+
btn.title =
|
|
212
|
+
options.mode === 'switch'
|
|
213
|
+
? "Make this the project's current conversation"
|
|
214
|
+
: options.openTitle();
|
|
197
215
|
btn.addEventListener('click', e => {
|
|
198
216
|
e.stopPropagation();
|
|
199
|
-
|
|
200
|
-
options.onOpen(sessionId);
|
|
217
|
+
runAction(sessionId);
|
|
201
218
|
});
|
|
202
219
|
return btn;
|
|
203
220
|
};
|
|
@@ -271,7 +288,11 @@ export function showManageSessionsPopup(
|
|
|
271
288
|
badge.className = 'jp-AiAssistantsPanel-branchCurrentBadge';
|
|
272
289
|
badge.textContent = 'current';
|
|
273
290
|
currentRow.appendChild(badge);
|
|
274
|
-
|
|
291
|
+
// Switch mode has nothing to switch to on the row that is already current;
|
|
292
|
+
// open mode can still launch it in a terminal.
|
|
293
|
+
if (options.mode === 'open') {
|
|
294
|
+
currentRow.appendChild(actionButton(options.current));
|
|
295
|
+
}
|
|
275
296
|
currentRow.appendChild(copyButton(options.current, trans));
|
|
276
297
|
list.appendChild(currentRow);
|
|
277
298
|
|
|
@@ -353,21 +374,20 @@ export function showManageSessionsPopup(
|
|
|
353
374
|
time.textContent = options.formatTime(b.file_mtime);
|
|
354
375
|
row.appendChild(time);
|
|
355
376
|
|
|
356
|
-
row.appendChild(
|
|
377
|
+
row.appendChild(actionButton(b.session_id));
|
|
357
378
|
row.appendChild(copyButton(b.session_id, trans));
|
|
358
379
|
|
|
359
|
-
// A row click
|
|
360
|
-
// is the one select target, so a click has one meaning
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
options.onSwitch(b.session_id);
|
|
364
|
-
};
|
|
380
|
+
// A row click follows the popup's mode, whatever is selected: the
|
|
381
|
+
// checkbox cell is the one select target, so a click has one meaning
|
|
382
|
+
// (DEF-PANE-209).
|
|
383
|
+
const activate = (): void => runAction(b.session_id);
|
|
365
384
|
row.addEventListener('click', activate);
|
|
366
|
-
//
|
|
367
|
-
//
|
|
368
|
-
// not the row: the row also hosts a checkbox and the
|
|
369
|
-
// and a role="button" wrapping them both swallows
|
|
370
|
-
// and makes them presentational children (ARIA
|
|
385
|
+
// The row action also has a button; the label keeps a role and key route
|
|
386
|
+
// so the row itself stays keyboard-reachable. The affordance sits on the
|
|
387
|
+
// label span, not the row: the row also hosts a checkbox and the
|
|
388
|
+
// action/Copy buttons, and a role="button" wrapping them both swallows
|
|
389
|
+
// their own Enter/Space and makes them presentational children (ARIA
|
|
390
|
+
// nested-interactive).
|
|
371
391
|
label.setAttribute('role', 'button');
|
|
372
392
|
label.tabIndex = 0;
|
|
373
393
|
label.addEventListener('keydown', e => {
|
package/style/base.css
CHANGED
|
@@ -550,26 +550,6 @@ body[data-jp-theme-light='true'] .jp-AiAssistantsPanel-dotHalo {
|
|
|
550
550
|
order: -1;
|
|
551
551
|
}
|
|
552
552
|
|
|
553
|
-
/* The spin carries "busy", not information, so under a reduced-motion
|
|
554
|
-
preference it degrades to the same ring held still and dimmed. */
|
|
555
|
-
@media (prefers-reduced-motion: reduce) {
|
|
556
|
-
.jp-AiAssistantsPanel-iconButton.jp-mod-spinning svg,
|
|
557
|
-
.jp-aiAssistants-spinner,
|
|
558
|
-
.jp-AiAssistantsPanel-spinner {
|
|
559
|
-
animation: none;
|
|
560
|
-
opacity: 0.5;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
/* The click-time busy spinner is the only thing on screen while a click is
|
|
564
|
-
being handled, so it keeps full opacity: dimmed, its brand arc measures
|
|
565
|
-
1.74:1 on the light panel, below the 3:1 minimum for a non-text cue. Held
|
|
566
|
-
still it would read as idle, so the overlay names the action in a visible
|
|
567
|
-
label beside it; the label, not an animation, carries the busy state. */
|
|
568
|
-
.jp-AiAssistants-busySpinner {
|
|
569
|
-
opacity: 1;
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
|
|
573
553
|
/* Full-panel veil shown only during an explicit refresh (refresh button or
|
|
574
554
|
panel show) - the background poll never raises it. The rule is gated on
|
|
575
555
|
:not([hidden]) because this author `display` would otherwise beat the UA
|