jupyterlab_claude_code_extension 1.2.5 → 1.2.7
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 +2 -1
- package/lib/types.d.ts +7 -0
- package/lib/widget.d.ts +10 -2
- package/lib/widget.js +183 -21
- package/package.json +1 -1
- package/src/__tests__/jupyterlab_claude_code_extension.spec.ts +77 -2
- package/src/types.ts +9 -0
- package/src/widget.ts +200 -21
- package/style/base.css +76 -0
package/README.md
CHANGED
|
@@ -20,7 +20,8 @@ Browse, resume, and manage your Claude Code sessions from a JupyterLab side pane
|
|
|
20
20
|
- **Favorites** - star projects you keep coming back to via the right-click menu
|
|
21
21
|
- **Remove** - drop a project's Claude history from the panel via the right-click menu; the history folder is moved to the trash (it honours JupyterLab's "move files to trash" setting), not deleted permanently
|
|
22
22
|
- **Clean up parallel sessions** - when a project has accumulated extra sessions beyond the main one, a right-click menu item (showing the count in brackets) removes them all, keeping only the main session; removed files honour the same trash setting
|
|
23
|
-
- **
|
|
23
|
+
- **Conversation switcher** - a right-click "Switch and Manage Sessions" submenu lists a project's other conversations by name and short session id, e.g. `home (3f2a1b9c)`, with last-activity time; pick one and it becomes the row's current conversation - the next click resumes exactly that one. The submenu shows the 5 most recent; "Manage Sessions..." opens a searchable popup over the full list where conversations can also be deleted - select one, many, or all via checkboxes, then confirm with a two-step Delete button (removed files honour the trash setting). Rows with multiple conversations show the count in brackets after the name, e.g. `workspace (2)`
|
|
24
|
+
- **Activity at a glance** - each row shows its last activity (`now`, `5m ago`, `2h ago`, `3d ago`); rows active within the last minute light up in the theme's brand colour, rows idle for over a week dim slightly
|
|
24
25
|
- **Search** - fuzzy filter toggled by the funnel button next to refresh
|
|
25
26
|
- **Presentation modes** - label rows by session name (so a `/rename` shows through), folder name, or path relative to the JupyterLab root
|
|
26
27
|
- **Hover tooltip** with project path, last activity, message count, branch, and session id
|
package/lib/types.d.ts
CHANGED
|
@@ -60,6 +60,13 @@ export interface ISwitchResponse {
|
|
|
60
60
|
requested: string;
|
|
61
61
|
current: string | null;
|
|
62
62
|
}
|
|
63
|
+
export interface IDeleteBranchesRequest {
|
|
64
|
+
encoded_path: string;
|
|
65
|
+
session_ids: string[];
|
|
66
|
+
}
|
|
67
|
+
export interface IDeleteBranchesResponse {
|
|
68
|
+
removed_count: number;
|
|
69
|
+
}
|
|
63
70
|
export interface ILaunchTerminalRequest {
|
|
64
71
|
project_path: string;
|
|
65
72
|
/** Omit to start a brand-new claude session instead of resuming one. */
|
package/lib/widget.d.ts
CHANGED
|
@@ -108,9 +108,16 @@ export declare class ClaudeCodeSessionsWidget extends Widget {
|
|
|
108
108
|
* the project has more than one conversation. On a fetch failure the
|
|
109
109
|
* menu opens without the submenu. */
|
|
110
110
|
private _openContextMenu;
|
|
111
|
-
/** Popup with the project's full branch list - browse
|
|
112
|
-
*
|
|
111
|
+
/** Popup with the project's full branch list - browse, filter, switch
|
|
112
|
+
* and manage. Clicking an entry switches while nothing is selected;
|
|
113
|
+
* checkbox selection (one, many, or select-all) arms a two-step Delete
|
|
114
|
+
* button that removes the chosen sessions. The current conversation is
|
|
115
|
+
* shown first, badged and untouchable. */
|
|
113
116
|
private _showBranchPopup;
|
|
117
|
+
/** Delete the given branch sessions of the active row's project.
|
|
118
|
+
* Returns the removed count, or null on failure (after notifying).
|
|
119
|
+
* Always resyncs the panel so the row's conversation count drops. */
|
|
120
|
+
private _deleteBranches;
|
|
114
121
|
/** Switch the active row's project to another conversation branch.
|
|
115
122
|
* The backend touches the branch JSONL's mtime; a refresh then shows
|
|
116
123
|
* the selected conversation as the row's current one. */
|
|
@@ -129,6 +136,7 @@ export declare class ClaudeCodeSessionsWidget extends Widget {
|
|
|
129
136
|
private _contextMenu;
|
|
130
137
|
private _branchSubmenu;
|
|
131
138
|
private _lastBranches;
|
|
139
|
+
private _lastBranchesCurrent;
|
|
132
140
|
private _newSessionMenu;
|
|
133
141
|
private _activeSession;
|
|
134
142
|
private _activeRowEl;
|
package/lib/widget.js
CHANGED
|
@@ -65,6 +65,7 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
65
65
|
this._sessions = null;
|
|
66
66
|
this._expanded = loadExpanded();
|
|
67
67
|
this._lastBranches = [];
|
|
68
|
+
this._lastBranchesCurrent = '';
|
|
68
69
|
this._activeSession = null;
|
|
69
70
|
this._activeRowEl = null;
|
|
70
71
|
this._pollHandle = null;
|
|
@@ -783,6 +784,17 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
783
784
|
const row = document.createElement('div');
|
|
784
785
|
row.className = 'jp-ClaudeSessionsPanel-row';
|
|
785
786
|
row.title = this._buildRowTooltip(session);
|
|
787
|
+
// Age emphasis: active within the last minute reads bright, idle for
|
|
788
|
+
// over a week dims; the state decays/promotes on the next refresh.
|
|
789
|
+
if (session.file_mtime) {
|
|
790
|
+
const age = Date.now() - session.file_mtime;
|
|
791
|
+
if (age < 60000) {
|
|
792
|
+
row.classList.add('jp-mod-recentlyActive');
|
|
793
|
+
}
|
|
794
|
+
else if (age > 7 * 86400000) {
|
|
795
|
+
row.classList.add('jp-mod-stale');
|
|
796
|
+
}
|
|
797
|
+
}
|
|
786
798
|
const removing = this._removingPaths.has(session.encoded_path);
|
|
787
799
|
if (removing) {
|
|
788
800
|
row.classList.add('jp-mod-busy');
|
|
@@ -1043,9 +1055,9 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
1043
1055
|
}
|
|
1044
1056
|
});
|
|
1045
1057
|
this._commands.addCommand('claude-code-sessions:switch-branch-more', {
|
|
1046
|
-
label: () => `
|
|
1058
|
+
label: () => `Manage Sessions... (${this._lastBranches.length})`,
|
|
1047
1059
|
execute: () => {
|
|
1048
|
-
void this._showBranchPopup(this._lastBranches);
|
|
1060
|
+
void this._showBranchPopup(this._lastBranches, this._lastBranchesCurrent);
|
|
1049
1061
|
}
|
|
1050
1062
|
});
|
|
1051
1063
|
this._commands.addCommand('claude-code-sessions:remove', {
|
|
@@ -1081,7 +1093,7 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
1081
1093
|
// sessions/branches fetch.
|
|
1082
1094
|
this._branchSubmenu = new Menu({ commands: this._commands });
|
|
1083
1095
|
this._branchSubmenu.addClass('jp-ClaudeSessionsContextMenu');
|
|
1084
|
-
this._branchSubmenu.title.label = 'Switch
|
|
1096
|
+
this._branchSubmenu.title.label = 'Switch and Manage Sessions';
|
|
1085
1097
|
this._contextMenu = new Menu({ commands: this._commands });
|
|
1086
1098
|
this._contextMenu.addClass('jp-ClaudeSessionsContextMenu');
|
|
1087
1099
|
this._rebuildContextMenu(false);
|
|
@@ -1133,9 +1145,12 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
1133
1145
|
try {
|
|
1134
1146
|
const data = await requestAPI(`sessions/branches?encoded_path=${encodeURIComponent(session.encoded_path)}`, this._serverSettings, { cache: 'no-store' });
|
|
1135
1147
|
this._lastBranches = data.branches;
|
|
1148
|
+
this._lastBranchesCurrent = data.current;
|
|
1136
1149
|
this._branchSubmenu.clearItems();
|
|
1137
|
-
|
|
1138
|
-
//
|
|
1150
|
+
this._branchSubmenu.title.label = `Switch and Manage Sessions (${data.branches.length})`;
|
|
1151
|
+
// The submenu shows only the 5 most recent inline (fewest clicks
|
|
1152
|
+
// for often-used sessions); the full list plus management lives
|
|
1153
|
+
// behind the always-present "Manage Sessions..." popup.
|
|
1139
1154
|
for (const b of data.branches.slice(0, 5)) {
|
|
1140
1155
|
this._branchSubmenu.addItem({
|
|
1141
1156
|
command: 'claude-code-sessions:switch-branch',
|
|
@@ -1145,12 +1160,10 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
1145
1160
|
}
|
|
1146
1161
|
});
|
|
1147
1162
|
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
});
|
|
1153
|
-
}
|
|
1163
|
+
this._branchSubmenu.addItem({ type: 'separator' });
|
|
1164
|
+
this._branchSubmenu.addItem({
|
|
1165
|
+
command: 'claude-code-sessions:switch-branch-more'
|
|
1166
|
+
});
|
|
1154
1167
|
hasBranches = data.branches.length > 0;
|
|
1155
1168
|
}
|
|
1156
1169
|
catch (_a) {
|
|
@@ -1160,35 +1173,90 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
1160
1173
|
this._rebuildContextMenu(hasBranches);
|
|
1161
1174
|
this._contextMenu.open(x, y);
|
|
1162
1175
|
}
|
|
1163
|
-
/** Popup with the project's full branch list - browse
|
|
1164
|
-
*
|
|
1165
|
-
|
|
1176
|
+
/** Popup with the project's full branch list - browse, filter, switch
|
|
1177
|
+
* and manage. Clicking an entry switches while nothing is selected;
|
|
1178
|
+
* checkbox selection (one, many, or select-all) arms a two-step Delete
|
|
1179
|
+
* button that removes the chosen sessions. The current conversation is
|
|
1180
|
+
* shown first, badged and untouchable. */
|
|
1181
|
+
_showBranchPopup(branches, current) {
|
|
1182
|
+
// Local working copy so deletions can refresh the list in place.
|
|
1183
|
+
let items = [...branches];
|
|
1184
|
+
const selected = new Set();
|
|
1185
|
+
let confirmArmed = false;
|
|
1166
1186
|
const body = document.createElement('div');
|
|
1167
1187
|
body.className = 'jp-ClaudeSessionsPanel-branchPopup';
|
|
1168
1188
|
const search = document.createElement('input');
|
|
1169
1189
|
search.type = 'search';
|
|
1170
|
-
search.placeholder = 'Filter
|
|
1190
|
+
search.placeholder = 'Filter sessions...';
|
|
1171
1191
|
search.className = 'jp-ClaudeSessionsPanel-branchSearch';
|
|
1172
1192
|
body.appendChild(search);
|
|
1193
|
+
const selectAllBar = document.createElement('label');
|
|
1194
|
+
selectAllBar.className = 'jp-ClaudeSessionsPanel-branchSelectAll';
|
|
1195
|
+
const selectAll = document.createElement('input');
|
|
1196
|
+
selectAll.type = 'checkbox';
|
|
1197
|
+
selectAllBar.appendChild(selectAll);
|
|
1198
|
+
selectAllBar.appendChild(document.createTextNode('Select all'));
|
|
1199
|
+
body.appendChild(selectAllBar);
|
|
1173
1200
|
const list = document.createElement('div');
|
|
1174
1201
|
list.className = 'jp-ClaudeSessionsPanel-branchList';
|
|
1175
1202
|
body.appendChild(list);
|
|
1203
|
+
const footer = document.createElement('div');
|
|
1204
|
+
footer.className = 'jp-ClaudeSessionsPanel-branchFooter';
|
|
1205
|
+
const deleteBtn = document.createElement('button');
|
|
1206
|
+
deleteBtn.className = 'jp-ClaudeSessionsPanel-branchDelete';
|
|
1207
|
+
footer.appendChild(deleteBtn);
|
|
1208
|
+
body.appendChild(footer);
|
|
1176
1209
|
const bodyWidget = new Widget({ node: body });
|
|
1177
1210
|
const dialog = new Dialog({
|
|
1178
|
-
title: 'Switch
|
|
1211
|
+
title: 'Switch and Manage Sessions',
|
|
1179
1212
|
body: bodyWidget,
|
|
1180
1213
|
buttons: [Dialog.cancelButton()]
|
|
1181
1214
|
});
|
|
1182
|
-
const
|
|
1215
|
+
const visibleMatches = () => {
|
|
1183
1216
|
const needle = search.value.trim().toLowerCase();
|
|
1184
|
-
|
|
1185
|
-
const matches = branches.filter(b => !needle ||
|
|
1217
|
+
return items.filter(b => !needle ||
|
|
1186
1218
|
b.label.toLowerCase().includes(needle) ||
|
|
1187
1219
|
b.session_id.toLowerCase().includes(needle));
|
|
1220
|
+
};
|
|
1221
|
+
// Any selection change disarms a pending confirm.
|
|
1222
|
+
const updateControls = () => {
|
|
1223
|
+
confirmArmed = false;
|
|
1224
|
+
deleteBtn.disabled = selected.size === 0;
|
|
1225
|
+
deleteBtn.textContent = `Delete (${selected.size})`;
|
|
1226
|
+
deleteBtn.classList.remove('jp-mod-confirm');
|
|
1227
|
+
const visible = visibleMatches();
|
|
1228
|
+
const visibleSelected = visible.filter(b => selected.has(b.session_id)).length;
|
|
1229
|
+
selectAll.checked =
|
|
1230
|
+
visible.length > 0 && visibleSelected === visible.length;
|
|
1231
|
+
selectAll.indeterminate =
|
|
1232
|
+
visibleSelected > 0 && visibleSelected < visible.length;
|
|
1233
|
+
};
|
|
1234
|
+
const render = () => {
|
|
1235
|
+
list.replaceChildren();
|
|
1236
|
+
// The current conversation leads the list - badged, unselectable,
|
|
1237
|
+
// undeletable; only the extras below it are manageable.
|
|
1238
|
+
const currentRow = document.createElement('div');
|
|
1239
|
+
currentRow.className = 'jp-ClaudeSessionsPanel-branchRow jp-mod-current';
|
|
1240
|
+
currentRow.title = `Session id: ${current}`;
|
|
1241
|
+
const currentLabel = document.createElement('span');
|
|
1242
|
+
currentLabel.className = 'jp-ClaudeSessionsPanel-branchLabel';
|
|
1243
|
+
const currentName = this._activeSession
|
|
1244
|
+
? this._lookupName(this._activeSession)
|
|
1245
|
+
: current.slice(0, 8);
|
|
1246
|
+
currentLabel.textContent = `${currentName} (${current.slice(0, 8)})`;
|
|
1247
|
+
currentRow.appendChild(currentLabel);
|
|
1248
|
+
const badge = document.createElement('span');
|
|
1249
|
+
badge.className = 'jp-ClaudeSessionsPanel-branchCurrentBadge';
|
|
1250
|
+
badge.textContent = 'current';
|
|
1251
|
+
currentRow.appendChild(badge);
|
|
1252
|
+
list.appendChild(currentRow);
|
|
1253
|
+
const matches = visibleMatches();
|
|
1188
1254
|
if (matches.length === 0) {
|
|
1189
1255
|
const empty = document.createElement('div');
|
|
1190
1256
|
empty.className = 'jp-ClaudeSessionsPanel-emptySection';
|
|
1191
|
-
empty.textContent =
|
|
1257
|
+
empty.textContent = items.length
|
|
1258
|
+
? 'No matching sessions.'
|
|
1259
|
+
: 'No other conversations.';
|
|
1192
1260
|
list.appendChild(empty);
|
|
1193
1261
|
return;
|
|
1194
1262
|
}
|
|
@@ -1196,6 +1264,21 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
1196
1264
|
const row = document.createElement('div');
|
|
1197
1265
|
row.className = 'jp-ClaudeSessionsPanel-branchRow';
|
|
1198
1266
|
row.title = `Session id: ${b.session_id}`;
|
|
1267
|
+
const check = document.createElement('input');
|
|
1268
|
+
check.type = 'checkbox';
|
|
1269
|
+
check.checked = selected.has(b.session_id);
|
|
1270
|
+
// The checkbox is its own click zone - ticking must not switch.
|
|
1271
|
+
check.addEventListener('click', e => {
|
|
1272
|
+
e.stopPropagation();
|
|
1273
|
+
if (check.checked) {
|
|
1274
|
+
selected.add(b.session_id);
|
|
1275
|
+
}
|
|
1276
|
+
else {
|
|
1277
|
+
selected.delete(b.session_id);
|
|
1278
|
+
}
|
|
1279
|
+
updateControls();
|
|
1280
|
+
});
|
|
1281
|
+
row.appendChild(check);
|
|
1199
1282
|
const label = document.createElement('span');
|
|
1200
1283
|
label.className = 'jp-ClaudeSessionsPanel-branchLabel';
|
|
1201
1284
|
label.textContent = this._branchDisplayName(b);
|
|
@@ -1205,17 +1288,96 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
1205
1288
|
time.textContent = this._formatRelativeTime(b.file_mtime);
|
|
1206
1289
|
row.appendChild(time);
|
|
1207
1290
|
row.addEventListener('click', () => {
|
|
1291
|
+
// Selection mode: while anything is ticked, row clicks toggle
|
|
1292
|
+
// selection - no accidental switch mid-selection.
|
|
1293
|
+
if (selected.size > 0) {
|
|
1294
|
+
if (selected.has(b.session_id)) {
|
|
1295
|
+
selected.delete(b.session_id);
|
|
1296
|
+
}
|
|
1297
|
+
else {
|
|
1298
|
+
selected.add(b.session_id);
|
|
1299
|
+
}
|
|
1300
|
+
check.checked = selected.has(b.session_id);
|
|
1301
|
+
updateControls();
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1208
1304
|
dialog.dispose();
|
|
1209
1305
|
void this._switchBranch(b.session_id);
|
|
1210
1306
|
});
|
|
1211
1307
|
list.appendChild(row);
|
|
1212
1308
|
}
|
|
1213
1309
|
};
|
|
1214
|
-
|
|
1310
|
+
selectAll.addEventListener('change', () => {
|
|
1311
|
+
// Select-all acts on the visible (filtered) rows only.
|
|
1312
|
+
const visible = visibleMatches();
|
|
1313
|
+
if (selectAll.checked) {
|
|
1314
|
+
visible.forEach(b => selected.add(b.session_id));
|
|
1315
|
+
}
|
|
1316
|
+
else {
|
|
1317
|
+
visible.forEach(b => selected.delete(b.session_id));
|
|
1318
|
+
}
|
|
1319
|
+
render();
|
|
1320
|
+
updateControls();
|
|
1321
|
+
});
|
|
1322
|
+
deleteBtn.addEventListener('click', () => {
|
|
1323
|
+
if (selected.size === 0) {
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
if (!confirmArmed) {
|
|
1327
|
+
// Two-step delete: first click arms, second click executes.
|
|
1328
|
+
confirmArmed = true;
|
|
1329
|
+
deleteBtn.textContent = `Confirm delete (${selected.size})`;
|
|
1330
|
+
deleteBtn.classList.add('jp-mod-confirm');
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
void this._deleteBranches([...selected]).then(deleted => {
|
|
1334
|
+
if (deleted === null) {
|
|
1335
|
+
return;
|
|
1336
|
+
}
|
|
1337
|
+
items = items.filter(b => !selected.has(b.session_id));
|
|
1338
|
+
selected.clear();
|
|
1339
|
+
this._lastBranches = items;
|
|
1340
|
+
render();
|
|
1341
|
+
updateControls();
|
|
1342
|
+
});
|
|
1343
|
+
});
|
|
1344
|
+
search.addEventListener('input', () => {
|
|
1345
|
+
render();
|
|
1346
|
+
updateControls();
|
|
1347
|
+
});
|
|
1215
1348
|
render();
|
|
1349
|
+
updateControls();
|
|
1216
1350
|
void dialog.launch();
|
|
1217
1351
|
search.focus();
|
|
1218
1352
|
}
|
|
1353
|
+
/** Delete the given branch sessions of the active row's project.
|
|
1354
|
+
* Returns the removed count, or null on failure (after notifying).
|
|
1355
|
+
* Always resyncs the panel so the row's conversation count drops. */
|
|
1356
|
+
async _deleteBranches(sessionIds) {
|
|
1357
|
+
const session = this._activeSession;
|
|
1358
|
+
if (!session) {
|
|
1359
|
+
return null;
|
|
1360
|
+
}
|
|
1361
|
+
try {
|
|
1362
|
+
const result = await requestAPI('sessions/delete-branches', this._serverSettings, {
|
|
1363
|
+
method: 'POST',
|
|
1364
|
+
body: JSON.stringify({
|
|
1365
|
+
encoded_path: session.encoded_path,
|
|
1366
|
+
session_ids: sessionIds
|
|
1367
|
+
})
|
|
1368
|
+
});
|
|
1369
|
+
return result.removed_count;
|
|
1370
|
+
}
|
|
1371
|
+
catch (err) {
|
|
1372
|
+
Notification.error(`Delete failed: ${String(err)}`, {
|
|
1373
|
+
autoClose: 4000
|
|
1374
|
+
});
|
|
1375
|
+
return null;
|
|
1376
|
+
}
|
|
1377
|
+
finally {
|
|
1378
|
+
await this._fetch();
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1219
1381
|
/** Switch the active row's project to another conversation branch.
|
|
1220
1382
|
* The backend touches the branch JSONL's mtime; a refresh then shows
|
|
1221
1383
|
* the selected conversation as the row's current one. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jupyterlab_claude_code_extension",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.7",
|
|
4
4
|
"description": "Browse, resume, and manage your Claude Code CLI sessions from a JupyterLab side panel. One click reactivates the right terminal - no duplicate tabs, live remote-control indicator, and favourites for the projects you keep coming back to.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jupyter",
|
|
@@ -150,10 +150,21 @@ describe('launch spinner dismiss contract', () => {
|
|
|
150
150
|
expect(openMenu).toMatch(/_rebuildContextMenu\(hasBranches\)/);
|
|
151
151
|
});
|
|
152
152
|
|
|
153
|
-
it('caps the submenu at 5
|
|
153
|
+
it('caps the inline submenu at 5 most recent', () => {
|
|
154
154
|
expect(openMenu).toMatch(/\.slice\(0, 5\)/);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it('always adds the Manage Sessions entry, no >5 gate', () => {
|
|
158
|
+
// The popup is the management hub - it must be reachable even for
|
|
159
|
+
// projects with 2-5 conversations, so the entry is unconditional.
|
|
160
|
+
expect(openMenu).toMatch(/switch-branch-more/);
|
|
161
|
+
expect(openMenu).not.toMatch(/branches\.length > 5/);
|
|
162
|
+
expect(widgetSrc).toMatch(/`Manage Sessions\.\.\. \(\$\{/);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('titles the submenu Switch and Manage Sessions with the count', () => {
|
|
155
166
|
expect(openMenu).toMatch(
|
|
156
|
-
/
|
|
167
|
+
/title\.label = `Switch and Manage Sessions \(\$\{data\.branches\.length\}\)`/
|
|
157
168
|
);
|
|
158
169
|
});
|
|
159
170
|
|
|
@@ -183,6 +194,70 @@ describe('launch spinner dismiss contract', () => {
|
|
|
183
194
|
);
|
|
184
195
|
});
|
|
185
196
|
|
|
197
|
+
it('popup leads with the current row, badged and without checkbox', () => {
|
|
198
|
+
const popup = (widgetSrc.match(
|
|
199
|
+
/private _showBranchPopup[\s\S]*?\n \}/
|
|
200
|
+
) ?? [''])[0];
|
|
201
|
+
expect(popup).toMatch(/jp-mod-current/);
|
|
202
|
+
expect(popup).toMatch(/branchCurrentBadge/);
|
|
203
|
+
// The current row is appended before any checkbox is created.
|
|
204
|
+
const currentIdx = popup.indexOf('jp-mod-current');
|
|
205
|
+
const checkboxIdx = popup.indexOf("check.type = 'checkbox'");
|
|
206
|
+
expect(currentIdx).toBeGreaterThan(-1);
|
|
207
|
+
expect(checkboxIdx).toBeGreaterThan(currentIdx);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('checkbox is its own click zone and selection gates row switch', () => {
|
|
211
|
+
const popup = (widgetSrc.match(
|
|
212
|
+
/private _showBranchPopup[\s\S]*?\n \}/
|
|
213
|
+
) ?? [''])[0];
|
|
214
|
+
expect(popup).toMatch(/stopPropagation\(\)/);
|
|
215
|
+
// Selection mode: row click toggles while anything is selected.
|
|
216
|
+
expect(popup).toMatch(
|
|
217
|
+
/if \(selected\.size > 0\) \{[\s\S]*?return;[\s\S]*?dialog\.dispose\(\)/
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it('select-all toggles the visible (filtered) rows only', () => {
|
|
222
|
+
const popup = (widgetSrc.match(
|
|
223
|
+
/private _showBranchPopup[\s\S]*?\n \}/
|
|
224
|
+
) ?? [''])[0];
|
|
225
|
+
expect(popup).toMatch(
|
|
226
|
+
/selectAll\.addEventListener\('change'[\s\S]*?visibleMatches\(\)/
|
|
227
|
+
);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('delete is two-step and any selection change disarms it', () => {
|
|
231
|
+
const popup = (widgetSrc.match(
|
|
232
|
+
/private _showBranchPopup[\s\S]*?\n \}/
|
|
233
|
+
) ?? [''])[0];
|
|
234
|
+
expect(popup).toMatch(/`Confirm delete \(\$\{selected\.size\}\)`/);
|
|
235
|
+
// updateControls (run on every selection change) resets the armed
|
|
236
|
+
// state and the button label.
|
|
237
|
+
expect(popup).toMatch(
|
|
238
|
+
/const updateControls = \(\) => \{\s*confirmArmed = false/
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('delete posts to delete-branches and resyncs the panel', () => {
|
|
243
|
+
const del = (widgetSrc.match(
|
|
244
|
+
/private async _deleteBranches[\s\S]*?\n \}/
|
|
245
|
+
) ?? [''])[0];
|
|
246
|
+
expect(del).toMatch(/sessions\/delete-branches/);
|
|
247
|
+
expect(del).toMatch(/finally[\s\S]*?await this\._fetch\(\)/);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('rows carry age emphasis classes at the 60s and 7d thresholds', () => {
|
|
251
|
+
expect(widgetSrc).toMatch(/age < 60_000[\s\S]*?jp-mod-recentlyActive/);
|
|
252
|
+
expect(widgetSrc).toMatch(/age > 7 \* 86_400_000[\s\S]*?jp-mod-stale/);
|
|
253
|
+
const css: string = fs.readFileSync(
|
|
254
|
+
path.join(__dirname, '..', '..', 'style', 'base.css'),
|
|
255
|
+
'utf-8'
|
|
256
|
+
);
|
|
257
|
+
expect(css).toMatch(/jp-mod-recentlyActive[\s\S]*?--jp-brand-color1/);
|
|
258
|
+
expect(css).toMatch(/jp-mod-stale \{\s*opacity/);
|
|
259
|
+
});
|
|
260
|
+
|
|
186
261
|
it('opens the menu without the submenu when the fetch fails', () => {
|
|
187
262
|
expect(openMenu).toMatch(/catch[\s\S]*?hasBranches = false/);
|
|
188
263
|
});
|
package/src/types.ts
CHANGED
|
@@ -73,6 +73,15 @@ export interface ISwitchResponse {
|
|
|
73
73
|
current: string | null;
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
export interface IDeleteBranchesRequest {
|
|
77
|
+
encoded_path: string;
|
|
78
|
+
session_ids: string[];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface IDeleteBranchesResponse {
|
|
82
|
+
removed_count: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
76
85
|
export interface ILaunchTerminalRequest {
|
|
77
86
|
project_path: string;
|
|
78
87
|
/** Omit to start a brand-new claude session instead of resuming one. */
|
package/src/widget.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
import {
|
|
27
27
|
IBranch,
|
|
28
28
|
IBranchesResponse,
|
|
29
|
+
IDeleteBranchesResponse,
|
|
29
30
|
IFavouriteResponse,
|
|
30
31
|
ILaunchTerminalResponse,
|
|
31
32
|
ICleanupResponse,
|
|
@@ -928,6 +929,17 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
928
929
|
row.className = 'jp-ClaudeSessionsPanel-row';
|
|
929
930
|
row.title = this._buildRowTooltip(session);
|
|
930
931
|
|
|
932
|
+
// Age emphasis: active within the last minute reads bright, idle for
|
|
933
|
+
// over a week dims; the state decays/promotes on the next refresh.
|
|
934
|
+
if (session.file_mtime) {
|
|
935
|
+
const age = Date.now() - session.file_mtime;
|
|
936
|
+
if (age < 60_000) {
|
|
937
|
+
row.classList.add('jp-mod-recentlyActive');
|
|
938
|
+
} else if (age > 7 * 86_400_000) {
|
|
939
|
+
row.classList.add('jp-mod-stale');
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
|
|
931
943
|
const removing = this._removingPaths.has(session.encoded_path);
|
|
932
944
|
if (removing) {
|
|
933
945
|
row.classList.add('jp-mod-busy');
|
|
@@ -1214,9 +1226,12 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
1214
1226
|
});
|
|
1215
1227
|
|
|
1216
1228
|
this._commands.addCommand('claude-code-sessions:switch-branch-more', {
|
|
1217
|
-
label: () => `
|
|
1229
|
+
label: () => `Manage Sessions... (${this._lastBranches.length})`,
|
|
1218
1230
|
execute: () => {
|
|
1219
|
-
void this._showBranchPopup(
|
|
1231
|
+
void this._showBranchPopup(
|
|
1232
|
+
this._lastBranches,
|
|
1233
|
+
this._lastBranchesCurrent
|
|
1234
|
+
);
|
|
1220
1235
|
}
|
|
1221
1236
|
});
|
|
1222
1237
|
|
|
@@ -1257,7 +1272,7 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
1257
1272
|
// sessions/branches fetch.
|
|
1258
1273
|
this._branchSubmenu = new Menu({ commands: this._commands });
|
|
1259
1274
|
this._branchSubmenu.addClass('jp-ClaudeSessionsContextMenu');
|
|
1260
|
-
this._branchSubmenu.title.label = 'Switch
|
|
1275
|
+
this._branchSubmenu.title.label = 'Switch and Manage Sessions';
|
|
1261
1276
|
|
|
1262
1277
|
this._contextMenu = new Menu({ commands: this._commands });
|
|
1263
1278
|
this._contextMenu.addClass('jp-ClaudeSessionsContextMenu');
|
|
@@ -1321,9 +1336,12 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
1321
1336
|
{ cache: 'no-store' }
|
|
1322
1337
|
);
|
|
1323
1338
|
this._lastBranches = data.branches;
|
|
1339
|
+
this._lastBranchesCurrent = data.current;
|
|
1324
1340
|
this._branchSubmenu.clearItems();
|
|
1325
|
-
|
|
1326
|
-
//
|
|
1341
|
+
this._branchSubmenu.title.label = `Switch and Manage Sessions (${data.branches.length})`;
|
|
1342
|
+
// The submenu shows only the 5 most recent inline (fewest clicks
|
|
1343
|
+
// for often-used sessions); the full list plus management lives
|
|
1344
|
+
// behind the always-present "Manage Sessions..." popup.
|
|
1327
1345
|
for (const b of data.branches.slice(0, 5)) {
|
|
1328
1346
|
this._branchSubmenu.addItem({
|
|
1329
1347
|
command: 'claude-code-sessions:switch-branch',
|
|
@@ -1333,12 +1351,10 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
1333
1351
|
}
|
|
1334
1352
|
});
|
|
1335
1353
|
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
});
|
|
1341
|
-
}
|
|
1354
|
+
this._branchSubmenu.addItem({ type: 'separator' });
|
|
1355
|
+
this._branchSubmenu.addItem({
|
|
1356
|
+
command: 'claude-code-sessions:switch-branch-more'
|
|
1357
|
+
});
|
|
1342
1358
|
hasBranches = data.branches.length > 0;
|
|
1343
1359
|
} catch {
|
|
1344
1360
|
hasBranches = false;
|
|
@@ -1348,42 +1364,106 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
1348
1364
|
this._contextMenu.open(x, y);
|
|
1349
1365
|
}
|
|
1350
1366
|
|
|
1351
|
-
/** Popup with the project's full branch list - browse
|
|
1352
|
-
*
|
|
1353
|
-
|
|
1367
|
+
/** Popup with the project's full branch list - browse, filter, switch
|
|
1368
|
+
* and manage. Clicking an entry switches while nothing is selected;
|
|
1369
|
+
* checkbox selection (one, many, or select-all) arms a two-step Delete
|
|
1370
|
+
* button that removes the chosen sessions. The current conversation is
|
|
1371
|
+
* shown first, badged and untouchable. */
|
|
1372
|
+
private _showBranchPopup(branches: IBranch[], current: string): void {
|
|
1373
|
+
// Local working copy so deletions can refresh the list in place.
|
|
1374
|
+
let items = [...branches];
|
|
1375
|
+
const selected = new Set<string>();
|
|
1376
|
+
let confirmArmed = false;
|
|
1377
|
+
|
|
1354
1378
|
const body = document.createElement('div');
|
|
1355
1379
|
body.className = 'jp-ClaudeSessionsPanel-branchPopup';
|
|
1356
1380
|
|
|
1357
1381
|
const search = document.createElement('input');
|
|
1358
1382
|
search.type = 'search';
|
|
1359
|
-
search.placeholder = 'Filter
|
|
1383
|
+
search.placeholder = 'Filter sessions...';
|
|
1360
1384
|
search.className = 'jp-ClaudeSessionsPanel-branchSearch';
|
|
1361
1385
|
body.appendChild(search);
|
|
1362
1386
|
|
|
1387
|
+
const selectAllBar = document.createElement('label');
|
|
1388
|
+
selectAllBar.className = 'jp-ClaudeSessionsPanel-branchSelectAll';
|
|
1389
|
+
const selectAll = document.createElement('input');
|
|
1390
|
+
selectAll.type = 'checkbox';
|
|
1391
|
+
selectAllBar.appendChild(selectAll);
|
|
1392
|
+
selectAllBar.appendChild(document.createTextNode('Select all'));
|
|
1393
|
+
body.appendChild(selectAllBar);
|
|
1394
|
+
|
|
1363
1395
|
const list = document.createElement('div');
|
|
1364
1396
|
list.className = 'jp-ClaudeSessionsPanel-branchList';
|
|
1365
1397
|
body.appendChild(list);
|
|
1366
1398
|
|
|
1399
|
+
const footer = document.createElement('div');
|
|
1400
|
+
footer.className = 'jp-ClaudeSessionsPanel-branchFooter';
|
|
1401
|
+
const deleteBtn = document.createElement('button');
|
|
1402
|
+
deleteBtn.className = 'jp-ClaudeSessionsPanel-branchDelete';
|
|
1403
|
+
footer.appendChild(deleteBtn);
|
|
1404
|
+
body.appendChild(footer);
|
|
1405
|
+
|
|
1367
1406
|
const bodyWidget = new Widget({ node: body });
|
|
1368
1407
|
const dialog = new Dialog({
|
|
1369
|
-
title: 'Switch
|
|
1408
|
+
title: 'Switch and Manage Sessions',
|
|
1370
1409
|
body: bodyWidget,
|
|
1371
1410
|
buttons: [Dialog.cancelButton()]
|
|
1372
1411
|
});
|
|
1373
1412
|
|
|
1374
|
-
const
|
|
1413
|
+
const visibleMatches = (): IBranch[] => {
|
|
1375
1414
|
const needle = search.value.trim().toLowerCase();
|
|
1376
|
-
|
|
1377
|
-
const matches = branches.filter(
|
|
1415
|
+
return items.filter(
|
|
1378
1416
|
b =>
|
|
1379
1417
|
!needle ||
|
|
1380
1418
|
b.label.toLowerCase().includes(needle) ||
|
|
1381
1419
|
b.session_id.toLowerCase().includes(needle)
|
|
1382
1420
|
);
|
|
1421
|
+
};
|
|
1422
|
+
|
|
1423
|
+
// Any selection change disarms a pending confirm.
|
|
1424
|
+
const updateControls = () => {
|
|
1425
|
+
confirmArmed = false;
|
|
1426
|
+
deleteBtn.disabled = selected.size === 0;
|
|
1427
|
+
deleteBtn.textContent = `Delete (${selected.size})`;
|
|
1428
|
+
deleteBtn.classList.remove('jp-mod-confirm');
|
|
1429
|
+
const visible = visibleMatches();
|
|
1430
|
+
const visibleSelected = visible.filter(b =>
|
|
1431
|
+
selected.has(b.session_id)
|
|
1432
|
+
).length;
|
|
1433
|
+
selectAll.checked =
|
|
1434
|
+
visible.length > 0 && visibleSelected === visible.length;
|
|
1435
|
+
selectAll.indeterminate =
|
|
1436
|
+
visibleSelected > 0 && visibleSelected < visible.length;
|
|
1437
|
+
};
|
|
1438
|
+
|
|
1439
|
+
const render = () => {
|
|
1440
|
+
list.replaceChildren();
|
|
1441
|
+
|
|
1442
|
+
// The current conversation leads the list - badged, unselectable,
|
|
1443
|
+
// undeletable; only the extras below it are manageable.
|
|
1444
|
+
const currentRow = document.createElement('div');
|
|
1445
|
+
currentRow.className = 'jp-ClaudeSessionsPanel-branchRow jp-mod-current';
|
|
1446
|
+
currentRow.title = `Session id: ${current}`;
|
|
1447
|
+
const currentLabel = document.createElement('span');
|
|
1448
|
+
currentLabel.className = 'jp-ClaudeSessionsPanel-branchLabel';
|
|
1449
|
+
const currentName = this._activeSession
|
|
1450
|
+
? this._lookupName(this._activeSession)
|
|
1451
|
+
: current.slice(0, 8);
|
|
1452
|
+
currentLabel.textContent = `${currentName} (${current.slice(0, 8)})`;
|
|
1453
|
+
currentRow.appendChild(currentLabel);
|
|
1454
|
+
const badge = document.createElement('span');
|
|
1455
|
+
badge.className = 'jp-ClaudeSessionsPanel-branchCurrentBadge';
|
|
1456
|
+
badge.textContent = 'current';
|
|
1457
|
+
currentRow.appendChild(badge);
|
|
1458
|
+
list.appendChild(currentRow);
|
|
1459
|
+
|
|
1460
|
+
const matches = visibleMatches();
|
|
1383
1461
|
if (matches.length === 0) {
|
|
1384
1462
|
const empty = document.createElement('div');
|
|
1385
1463
|
empty.className = 'jp-ClaudeSessionsPanel-emptySection';
|
|
1386
|
-
empty.textContent =
|
|
1464
|
+
empty.textContent = items.length
|
|
1465
|
+
? 'No matching sessions.'
|
|
1466
|
+
: 'No other conversations.';
|
|
1387
1467
|
list.appendChild(empty);
|
|
1388
1468
|
return;
|
|
1389
1469
|
}
|
|
@@ -1392,6 +1472,21 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
1392
1472
|
row.className = 'jp-ClaudeSessionsPanel-branchRow';
|
|
1393
1473
|
row.title = `Session id: ${b.session_id}`;
|
|
1394
1474
|
|
|
1475
|
+
const check = document.createElement('input');
|
|
1476
|
+
check.type = 'checkbox';
|
|
1477
|
+
check.checked = selected.has(b.session_id);
|
|
1478
|
+
// The checkbox is its own click zone - ticking must not switch.
|
|
1479
|
+
check.addEventListener('click', e => {
|
|
1480
|
+
e.stopPropagation();
|
|
1481
|
+
if (check.checked) {
|
|
1482
|
+
selected.add(b.session_id);
|
|
1483
|
+
} else {
|
|
1484
|
+
selected.delete(b.session_id);
|
|
1485
|
+
}
|
|
1486
|
+
updateControls();
|
|
1487
|
+
});
|
|
1488
|
+
row.appendChild(check);
|
|
1489
|
+
|
|
1395
1490
|
const label = document.createElement('span');
|
|
1396
1491
|
label.className = 'jp-ClaudeSessionsPanel-branchLabel';
|
|
1397
1492
|
label.textContent = this._branchDisplayName(b);
|
|
@@ -1403,19 +1498,102 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
1403
1498
|
row.appendChild(time);
|
|
1404
1499
|
|
|
1405
1500
|
row.addEventListener('click', () => {
|
|
1501
|
+
// Selection mode: while anything is ticked, row clicks toggle
|
|
1502
|
+
// selection - no accidental switch mid-selection.
|
|
1503
|
+
if (selected.size > 0) {
|
|
1504
|
+
if (selected.has(b.session_id)) {
|
|
1505
|
+
selected.delete(b.session_id);
|
|
1506
|
+
} else {
|
|
1507
|
+
selected.add(b.session_id);
|
|
1508
|
+
}
|
|
1509
|
+
check.checked = selected.has(b.session_id);
|
|
1510
|
+
updateControls();
|
|
1511
|
+
return;
|
|
1512
|
+
}
|
|
1406
1513
|
dialog.dispose();
|
|
1407
1514
|
void this._switchBranch(b.session_id);
|
|
1408
1515
|
});
|
|
1409
1516
|
list.appendChild(row);
|
|
1410
1517
|
}
|
|
1411
1518
|
};
|
|
1412
|
-
|
|
1519
|
+
|
|
1520
|
+
selectAll.addEventListener('change', () => {
|
|
1521
|
+
// Select-all acts on the visible (filtered) rows only.
|
|
1522
|
+
const visible = visibleMatches();
|
|
1523
|
+
if (selectAll.checked) {
|
|
1524
|
+
visible.forEach(b => selected.add(b.session_id));
|
|
1525
|
+
} else {
|
|
1526
|
+
visible.forEach(b => selected.delete(b.session_id));
|
|
1527
|
+
}
|
|
1528
|
+
render();
|
|
1529
|
+
updateControls();
|
|
1530
|
+
});
|
|
1531
|
+
|
|
1532
|
+
deleteBtn.addEventListener('click', () => {
|
|
1533
|
+
if (selected.size === 0) {
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
if (!confirmArmed) {
|
|
1537
|
+
// Two-step delete: first click arms, second click executes.
|
|
1538
|
+
confirmArmed = true;
|
|
1539
|
+
deleteBtn.textContent = `Confirm delete (${selected.size})`;
|
|
1540
|
+
deleteBtn.classList.add('jp-mod-confirm');
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
void this._deleteBranches([...selected]).then(deleted => {
|
|
1544
|
+
if (deleted === null) {
|
|
1545
|
+
return;
|
|
1546
|
+
}
|
|
1547
|
+
items = items.filter(b => !selected.has(b.session_id));
|
|
1548
|
+
selected.clear();
|
|
1549
|
+
this._lastBranches = items;
|
|
1550
|
+
render();
|
|
1551
|
+
updateControls();
|
|
1552
|
+
});
|
|
1553
|
+
});
|
|
1554
|
+
|
|
1555
|
+
search.addEventListener('input', () => {
|
|
1556
|
+
render();
|
|
1557
|
+
updateControls();
|
|
1558
|
+
});
|
|
1413
1559
|
render();
|
|
1560
|
+
updateControls();
|
|
1414
1561
|
|
|
1415
1562
|
void dialog.launch();
|
|
1416
1563
|
search.focus();
|
|
1417
1564
|
}
|
|
1418
1565
|
|
|
1566
|
+
/** Delete the given branch sessions of the active row's project.
|
|
1567
|
+
* Returns the removed count, or null on failure (after notifying).
|
|
1568
|
+
* Always resyncs the panel so the row's conversation count drops. */
|
|
1569
|
+
private async _deleteBranches(sessionIds: string[]): Promise<number | null> {
|
|
1570
|
+
const session = this._activeSession;
|
|
1571
|
+
if (!session) {
|
|
1572
|
+
return null;
|
|
1573
|
+
}
|
|
1574
|
+
try {
|
|
1575
|
+
const result = await requestAPI<IDeleteBranchesResponse>(
|
|
1576
|
+
'sessions/delete-branches',
|
|
1577
|
+
this._serverSettings,
|
|
1578
|
+
{
|
|
1579
|
+
method: 'POST',
|
|
1580
|
+
body: JSON.stringify({
|
|
1581
|
+
encoded_path: session.encoded_path,
|
|
1582
|
+
session_ids: sessionIds
|
|
1583
|
+
})
|
|
1584
|
+
}
|
|
1585
|
+
);
|
|
1586
|
+
return result.removed_count;
|
|
1587
|
+
} catch (err) {
|
|
1588
|
+
Notification.error(`Delete failed: ${String(err)}`, {
|
|
1589
|
+
autoClose: 4000
|
|
1590
|
+
});
|
|
1591
|
+
return null;
|
|
1592
|
+
} finally {
|
|
1593
|
+
await this._fetch();
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1419
1597
|
/** Switch the active row's project to another conversation branch.
|
|
1420
1598
|
* The backend touches the branch JSONL's mtime; a refresh then shows
|
|
1421
1599
|
* the selected conversation as the row's current one. */
|
|
@@ -1493,6 +1671,7 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
1493
1671
|
private _contextMenu!: Menu;
|
|
1494
1672
|
private _branchSubmenu!: Menu;
|
|
1495
1673
|
private _lastBranches: IBranch[] = [];
|
|
1674
|
+
private _lastBranchesCurrent = '';
|
|
1496
1675
|
private _newSessionMenu!: Menu;
|
|
1497
1676
|
private _activeSession: ISession | null = null;
|
|
1498
1677
|
private _activeRowEl: HTMLElement | null = null;
|
package/style/base.css
CHANGED
|
@@ -347,6 +347,72 @@
|
|
|
347
347
|
font-size: var(--jp-ui-font-size0);
|
|
348
348
|
}
|
|
349
349
|
|
|
350
|
+
.jp-ClaudeSessionsPanel-branchRow input[type='checkbox'] {
|
|
351
|
+
flex: none;
|
|
352
|
+
margin: 0;
|
|
353
|
+
cursor: pointer;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/* The current conversation leads the popup list - badged, untouchable. */
|
|
357
|
+
.jp-ClaudeSessionsPanel-branchRow.jp-mod-current {
|
|
358
|
+
cursor: default;
|
|
359
|
+
color: var(--jp-ui-font-color2);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.jp-ClaudeSessionsPanel-branchRow.jp-mod-current:hover {
|
|
363
|
+
background: transparent;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
.jp-ClaudeSessionsPanel-branchCurrentBadge {
|
|
367
|
+
flex: none;
|
|
368
|
+
padding: 0 6px;
|
|
369
|
+
border: 1px solid var(--jp-border-color2);
|
|
370
|
+
border-radius: 8px;
|
|
371
|
+
color: var(--jp-ui-font-color2);
|
|
372
|
+
font-size: var(--jp-ui-font-size0);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.jp-ClaudeSessionsPanel-branchSelectAll {
|
|
376
|
+
display: flex;
|
|
377
|
+
align-items: center;
|
|
378
|
+
gap: 8px;
|
|
379
|
+
padding: 0 6px;
|
|
380
|
+
cursor: pointer;
|
|
381
|
+
color: var(--jp-ui-font-color2);
|
|
382
|
+
font-size: var(--jp-ui-font-size0);
|
|
383
|
+
user-select: none;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.jp-ClaudeSessionsPanel-branchSelectAll input[type='checkbox'] {
|
|
387
|
+
margin: 0;
|
|
388
|
+
cursor: pointer;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
.jp-ClaudeSessionsPanel-branchFooter {
|
|
392
|
+
display: flex;
|
|
393
|
+
justify-content: flex-end;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.jp-ClaudeSessionsPanel-branchDelete {
|
|
397
|
+
padding: 4px 12px;
|
|
398
|
+
border: 1px solid var(--jp-border-color1);
|
|
399
|
+
border-radius: 2px;
|
|
400
|
+
background: var(--jp-layout-color1);
|
|
401
|
+
color: var(--jp-ui-font-color1);
|
|
402
|
+
cursor: pointer;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
.jp-ClaudeSessionsPanel-branchDelete:disabled {
|
|
406
|
+
opacity: 0.5;
|
|
407
|
+
cursor: default;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/* Armed state of the two-step delete - second click executes. */
|
|
411
|
+
.jp-ClaudeSessionsPanel-branchDelete.jp-mod-confirm {
|
|
412
|
+
border-color: var(--jp-error-color1);
|
|
413
|
+
color: var(--jp-error-color1);
|
|
414
|
+
}
|
|
415
|
+
|
|
350
416
|
/* Last-activity time on session rows - dim, right of the name. */
|
|
351
417
|
.jp-ClaudeSessionsPanel-rowTime {
|
|
352
418
|
flex: 0 0 auto;
|
|
@@ -354,3 +420,13 @@
|
|
|
354
420
|
color: var(--jp-ui-font-color2);
|
|
355
421
|
font-size: var(--jp-ui-font-size0);
|
|
356
422
|
}
|
|
423
|
+
|
|
424
|
+
/* Age emphasis: a row active within the last minute reads bright, a row
|
|
425
|
+
idle for over a week dims; both decay/promote on the next refresh. */
|
|
426
|
+
.jp-ClaudeSessionsPanel-row.jp-mod-recentlyActive .jp-ClaudeSessionsPanel-name {
|
|
427
|
+
color: var(--jp-brand-color1);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
.jp-ClaudeSessionsPanel-row.jp-mod-stale {
|
|
431
|
+
opacity: 0.65;
|
|
432
|
+
}
|