jupyterlab_claude_code_extension 1.2.4 → 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 +15 -2
- package/lib/widget.js +193 -23
- package/package.json +1 -1
- package/src/__tests__/jupyterlab_claude_code_extension.spec.ts +91 -2
- package/src/types.ts +9 -0
- package/src/widget.ts +211 -23
- 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
|
@@ -92,6 +92,11 @@ export declare class ClaudeCodeSessionsWidget extends Widget {
|
|
|
92
92
|
* case the file browser has no way to address it. */
|
|
93
93
|
private _pathUnderRoot;
|
|
94
94
|
private _formatRelativeTime;
|
|
95
|
+
/** Branch entry display: conversation name plus short session id in
|
|
96
|
+
* brackets; branches share the project path so only the name and id
|
|
97
|
+
* distinguish them. Skips the suffix when the label already is the
|
|
98
|
+
* short id (the backend's last-resort fallback). */
|
|
99
|
+
private _branchDisplayName;
|
|
95
100
|
private _setRefreshSpinning;
|
|
96
101
|
private _setActiveRow;
|
|
97
102
|
private _setupContextMenu;
|
|
@@ -103,9 +108,16 @@ export declare class ClaudeCodeSessionsWidget extends Widget {
|
|
|
103
108
|
* the project has more than one conversation. On a fetch failure the
|
|
104
109
|
* menu opens without the submenu. */
|
|
105
110
|
private _openContextMenu;
|
|
106
|
-
/** Popup with the project's full branch list - browse
|
|
107
|
-
*
|
|
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. */
|
|
108
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;
|
|
109
121
|
/** Switch the active row's project to another conversation branch.
|
|
110
122
|
* The backend touches the branch JSONL's mtime; a refresh then shows
|
|
111
123
|
* the selected conversation as the row's current one. */
|
|
@@ -124,6 +136,7 @@ export declare class ClaudeCodeSessionsWidget extends Widget {
|
|
|
124
136
|
private _contextMenu;
|
|
125
137
|
private _branchSubmenu;
|
|
126
138
|
private _lastBranches;
|
|
139
|
+
private _lastBranchesCurrent;
|
|
127
140
|
private _newSessionMenu;
|
|
128
141
|
private _activeSession;
|
|
129
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');
|
|
@@ -914,6 +926,14 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
914
926
|
}
|
|
915
927
|
return `${Math.floor(diff / 86400000)}d ago`;
|
|
916
928
|
}
|
|
929
|
+
/** Branch entry display: conversation name plus short session id in
|
|
930
|
+
* brackets; branches share the project path so only the name and id
|
|
931
|
+
* distinguish them. Skips the suffix when the label already is the
|
|
932
|
+
* short id (the backend's last-resort fallback). */
|
|
933
|
+
_branchDisplayName(b) {
|
|
934
|
+
const shortId = b.session_id.slice(0, 8);
|
|
935
|
+
return b.label === shortId ? b.label : `${b.label} (${shortId})`;
|
|
936
|
+
}
|
|
917
937
|
_setRefreshSpinning(on) {
|
|
918
938
|
if (!this._refreshBtn) {
|
|
919
939
|
return;
|
|
@@ -1035,9 +1055,9 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
1035
1055
|
}
|
|
1036
1056
|
});
|
|
1037
1057
|
this._commands.addCommand('claude-code-sessions:switch-branch-more', {
|
|
1038
|
-
label: () => `
|
|
1058
|
+
label: () => `Manage Sessions... (${this._lastBranches.length})`,
|
|
1039
1059
|
execute: () => {
|
|
1040
|
-
void this._showBranchPopup(this._lastBranches);
|
|
1060
|
+
void this._showBranchPopup(this._lastBranches, this._lastBranchesCurrent);
|
|
1041
1061
|
}
|
|
1042
1062
|
});
|
|
1043
1063
|
this._commands.addCommand('claude-code-sessions:remove', {
|
|
@@ -1073,7 +1093,7 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
1073
1093
|
// sessions/branches fetch.
|
|
1074
1094
|
this._branchSubmenu = new Menu({ commands: this._commands });
|
|
1075
1095
|
this._branchSubmenu.addClass('jp-ClaudeSessionsContextMenu');
|
|
1076
|
-
this._branchSubmenu.title.label = 'Switch
|
|
1096
|
+
this._branchSubmenu.title.label = 'Switch and Manage Sessions';
|
|
1077
1097
|
this._contextMenu = new Menu({ commands: this._commands });
|
|
1078
1098
|
this._contextMenu.addClass('jp-ClaudeSessionsContextMenu');
|
|
1079
1099
|
this._rebuildContextMenu(false);
|
|
@@ -1125,24 +1145,25 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
1125
1145
|
try {
|
|
1126
1146
|
const data = await requestAPI(`sessions/branches?encoded_path=${encodeURIComponent(session.encoded_path)}`, this._serverSettings, { cache: 'no-store' });
|
|
1127
1147
|
this._lastBranches = data.branches;
|
|
1148
|
+
this._lastBranchesCurrent = data.current;
|
|
1128
1149
|
this._branchSubmenu.clearItems();
|
|
1129
|
-
|
|
1130
|
-
//
|
|
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.
|
|
1131
1154
|
for (const b of data.branches.slice(0, 5)) {
|
|
1132
1155
|
this._branchSubmenu.addItem({
|
|
1133
1156
|
command: 'claude-code-sessions:switch-branch',
|
|
1134
1157
|
args: {
|
|
1135
1158
|
session_id: b.session_id,
|
|
1136
|
-
label: `${b
|
|
1159
|
+
label: `${this._branchDisplayName(b)} - ${this._formatRelativeTime(b.file_mtime)}`
|
|
1137
1160
|
}
|
|
1138
1161
|
});
|
|
1139
1162
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
});
|
|
1145
|
-
}
|
|
1163
|
+
this._branchSubmenu.addItem({ type: 'separator' });
|
|
1164
|
+
this._branchSubmenu.addItem({
|
|
1165
|
+
command: 'claude-code-sessions:switch-branch-more'
|
|
1166
|
+
});
|
|
1146
1167
|
hasBranches = data.branches.length > 0;
|
|
1147
1168
|
}
|
|
1148
1169
|
catch (_a) {
|
|
@@ -1152,35 +1173,90 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
1152
1173
|
this._rebuildContextMenu(hasBranches);
|
|
1153
1174
|
this._contextMenu.open(x, y);
|
|
1154
1175
|
}
|
|
1155
|
-
/** Popup with the project's full branch list - browse
|
|
1156
|
-
*
|
|
1157
|
-
|
|
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;
|
|
1158
1186
|
const body = document.createElement('div');
|
|
1159
1187
|
body.className = 'jp-ClaudeSessionsPanel-branchPopup';
|
|
1160
1188
|
const search = document.createElement('input');
|
|
1161
1189
|
search.type = 'search';
|
|
1162
|
-
search.placeholder = 'Filter
|
|
1190
|
+
search.placeholder = 'Filter sessions...';
|
|
1163
1191
|
search.className = 'jp-ClaudeSessionsPanel-branchSearch';
|
|
1164
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);
|
|
1165
1200
|
const list = document.createElement('div');
|
|
1166
1201
|
list.className = 'jp-ClaudeSessionsPanel-branchList';
|
|
1167
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);
|
|
1168
1209
|
const bodyWidget = new Widget({ node: body });
|
|
1169
1210
|
const dialog = new Dialog({
|
|
1170
|
-
title: 'Switch
|
|
1211
|
+
title: 'Switch and Manage Sessions',
|
|
1171
1212
|
body: bodyWidget,
|
|
1172
1213
|
buttons: [Dialog.cancelButton()]
|
|
1173
1214
|
});
|
|
1174
|
-
const
|
|
1215
|
+
const visibleMatches = () => {
|
|
1175
1216
|
const needle = search.value.trim().toLowerCase();
|
|
1176
|
-
|
|
1177
|
-
const matches = branches.filter(b => !needle ||
|
|
1217
|
+
return items.filter(b => !needle ||
|
|
1178
1218
|
b.label.toLowerCase().includes(needle) ||
|
|
1179
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();
|
|
1180
1254
|
if (matches.length === 0) {
|
|
1181
1255
|
const empty = document.createElement('div');
|
|
1182
1256
|
empty.className = 'jp-ClaudeSessionsPanel-emptySection';
|
|
1183
|
-
empty.textContent =
|
|
1257
|
+
empty.textContent = items.length
|
|
1258
|
+
? 'No matching sessions.'
|
|
1259
|
+
: 'No other conversations.';
|
|
1184
1260
|
list.appendChild(empty);
|
|
1185
1261
|
return;
|
|
1186
1262
|
}
|
|
@@ -1188,26 +1264,120 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
1188
1264
|
const row = document.createElement('div');
|
|
1189
1265
|
row.className = 'jp-ClaudeSessionsPanel-branchRow';
|
|
1190
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);
|
|
1191
1282
|
const label = document.createElement('span');
|
|
1192
1283
|
label.className = 'jp-ClaudeSessionsPanel-branchLabel';
|
|
1193
|
-
label.textContent = b
|
|
1284
|
+
label.textContent = this._branchDisplayName(b);
|
|
1194
1285
|
row.appendChild(label);
|
|
1195
1286
|
const time = document.createElement('span');
|
|
1196
1287
|
time.className = 'jp-ClaudeSessionsPanel-branchTime';
|
|
1197
1288
|
time.textContent = this._formatRelativeTime(b.file_mtime);
|
|
1198
1289
|
row.appendChild(time);
|
|
1199
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
|
+
}
|
|
1200
1304
|
dialog.dispose();
|
|
1201
1305
|
void this._switchBranch(b.session_id);
|
|
1202
1306
|
});
|
|
1203
1307
|
list.appendChild(row);
|
|
1204
1308
|
}
|
|
1205
1309
|
};
|
|
1206
|
-
|
|
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
|
+
});
|
|
1207
1348
|
render();
|
|
1349
|
+
updateControls();
|
|
1208
1350
|
void dialog.launch();
|
|
1209
1351
|
search.focus();
|
|
1210
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
|
+
}
|
|
1211
1381
|
/** Switch the active row's project to another conversation branch.
|
|
1212
1382
|
* The backend touches the branch JSONL's mtime; a refresh then shows
|
|
1213
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,35 @@ 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\}\)`/
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('branch entries render name plus short id via _branchDisplayName', () => {
|
|
172
|
+
const display = (widgetSrc.match(
|
|
173
|
+
/private _branchDisplayName[\s\S]*?\n \}/
|
|
174
|
+
) ?? [''])[0];
|
|
175
|
+
expect(display).toMatch(/session_id\.slice\(0, 8\)/);
|
|
176
|
+
expect(display).toMatch(/`\$\{b\.label\} \(\$\{shortId\}\)`/);
|
|
177
|
+
expect(widgetSrc).toMatch(
|
|
178
|
+
/label: `\$\{this\._branchDisplayName\(b\)\} - \$\{this\._formatRelativeTime\(b\.file_mtime\)\}`/
|
|
179
|
+
);
|
|
180
|
+
expect(widgetSrc).toMatch(
|
|
181
|
+
/label\.textContent = this\._branchDisplayName\(b\)/
|
|
157
182
|
);
|
|
158
183
|
});
|
|
159
184
|
|
|
@@ -169,6 +194,70 @@ describe('launch spinner dismiss contract', () => {
|
|
|
169
194
|
);
|
|
170
195
|
});
|
|
171
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
|
+
|
|
172
261
|
it('opens the menu without the submenu when the fetch fails', () => {
|
|
173
262
|
expect(openMenu).toMatch(/catch[\s\S]*?hasBranches = false/);
|
|
174
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');
|
|
@@ -1068,6 +1080,15 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
1068
1080
|
return `${Math.floor(diff / 86_400_000)}d ago`;
|
|
1069
1081
|
}
|
|
1070
1082
|
|
|
1083
|
+
/** Branch entry display: conversation name plus short session id in
|
|
1084
|
+
* brackets; branches share the project path so only the name and id
|
|
1085
|
+
* distinguish them. Skips the suffix when the label already is the
|
|
1086
|
+
* short id (the backend's last-resort fallback). */
|
|
1087
|
+
private _branchDisplayName(b: IBranch): string {
|
|
1088
|
+
const shortId = b.session_id.slice(0, 8);
|
|
1089
|
+
return b.label === shortId ? b.label : `${b.label} (${shortId})`;
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1071
1092
|
private _setRefreshSpinning(on: boolean): void {
|
|
1072
1093
|
if (!this._refreshBtn) {
|
|
1073
1094
|
return;
|
|
@@ -1205,9 +1226,12 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
1205
1226
|
});
|
|
1206
1227
|
|
|
1207
1228
|
this._commands.addCommand('claude-code-sessions:switch-branch-more', {
|
|
1208
|
-
label: () => `
|
|
1229
|
+
label: () => `Manage Sessions... (${this._lastBranches.length})`,
|
|
1209
1230
|
execute: () => {
|
|
1210
|
-
void this._showBranchPopup(
|
|
1231
|
+
void this._showBranchPopup(
|
|
1232
|
+
this._lastBranches,
|
|
1233
|
+
this._lastBranchesCurrent
|
|
1234
|
+
);
|
|
1211
1235
|
}
|
|
1212
1236
|
});
|
|
1213
1237
|
|
|
@@ -1248,7 +1272,7 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
1248
1272
|
// sessions/branches fetch.
|
|
1249
1273
|
this._branchSubmenu = new Menu({ commands: this._commands });
|
|
1250
1274
|
this._branchSubmenu.addClass('jp-ClaudeSessionsContextMenu');
|
|
1251
|
-
this._branchSubmenu.title.label = 'Switch
|
|
1275
|
+
this._branchSubmenu.title.label = 'Switch and Manage Sessions';
|
|
1252
1276
|
|
|
1253
1277
|
this._contextMenu = new Menu({ commands: this._commands });
|
|
1254
1278
|
this._contextMenu.addClass('jp-ClaudeSessionsContextMenu');
|
|
@@ -1312,24 +1336,25 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
1312
1336
|
{ cache: 'no-store' }
|
|
1313
1337
|
);
|
|
1314
1338
|
this._lastBranches = data.branches;
|
|
1339
|
+
this._lastBranchesCurrent = data.current;
|
|
1315
1340
|
this._branchSubmenu.clearItems();
|
|
1316
|
-
|
|
1317
|
-
//
|
|
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.
|
|
1318
1345
|
for (const b of data.branches.slice(0, 5)) {
|
|
1319
1346
|
this._branchSubmenu.addItem({
|
|
1320
1347
|
command: 'claude-code-sessions:switch-branch',
|
|
1321
1348
|
args: {
|
|
1322
1349
|
session_id: b.session_id,
|
|
1323
|
-
label: `${b
|
|
1350
|
+
label: `${this._branchDisplayName(b)} - ${this._formatRelativeTime(b.file_mtime)}`
|
|
1324
1351
|
}
|
|
1325
1352
|
});
|
|
1326
1353
|
}
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
});
|
|
1332
|
-
}
|
|
1354
|
+
this._branchSubmenu.addItem({ type: 'separator' });
|
|
1355
|
+
this._branchSubmenu.addItem({
|
|
1356
|
+
command: 'claude-code-sessions:switch-branch-more'
|
|
1357
|
+
});
|
|
1333
1358
|
hasBranches = data.branches.length > 0;
|
|
1334
1359
|
} catch {
|
|
1335
1360
|
hasBranches = false;
|
|
@@ -1339,42 +1364,106 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
1339
1364
|
this._contextMenu.open(x, y);
|
|
1340
1365
|
}
|
|
1341
1366
|
|
|
1342
|
-
/** Popup with the project's full branch list - browse
|
|
1343
|
-
*
|
|
1344
|
-
|
|
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
|
+
|
|
1345
1378
|
const body = document.createElement('div');
|
|
1346
1379
|
body.className = 'jp-ClaudeSessionsPanel-branchPopup';
|
|
1347
1380
|
|
|
1348
1381
|
const search = document.createElement('input');
|
|
1349
1382
|
search.type = 'search';
|
|
1350
|
-
search.placeholder = 'Filter
|
|
1383
|
+
search.placeholder = 'Filter sessions...';
|
|
1351
1384
|
search.className = 'jp-ClaudeSessionsPanel-branchSearch';
|
|
1352
1385
|
body.appendChild(search);
|
|
1353
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
|
+
|
|
1354
1395
|
const list = document.createElement('div');
|
|
1355
1396
|
list.className = 'jp-ClaudeSessionsPanel-branchList';
|
|
1356
1397
|
body.appendChild(list);
|
|
1357
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
|
+
|
|
1358
1406
|
const bodyWidget = new Widget({ node: body });
|
|
1359
1407
|
const dialog = new Dialog({
|
|
1360
|
-
title: 'Switch
|
|
1408
|
+
title: 'Switch and Manage Sessions',
|
|
1361
1409
|
body: bodyWidget,
|
|
1362
1410
|
buttons: [Dialog.cancelButton()]
|
|
1363
1411
|
});
|
|
1364
1412
|
|
|
1365
|
-
const
|
|
1413
|
+
const visibleMatches = (): IBranch[] => {
|
|
1366
1414
|
const needle = search.value.trim().toLowerCase();
|
|
1367
|
-
|
|
1368
|
-
const matches = branches.filter(
|
|
1415
|
+
return items.filter(
|
|
1369
1416
|
b =>
|
|
1370
1417
|
!needle ||
|
|
1371
1418
|
b.label.toLowerCase().includes(needle) ||
|
|
1372
1419
|
b.session_id.toLowerCase().includes(needle)
|
|
1373
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();
|
|
1374
1461
|
if (matches.length === 0) {
|
|
1375
1462
|
const empty = document.createElement('div');
|
|
1376
1463
|
empty.className = 'jp-ClaudeSessionsPanel-emptySection';
|
|
1377
|
-
empty.textContent =
|
|
1464
|
+
empty.textContent = items.length
|
|
1465
|
+
? 'No matching sessions.'
|
|
1466
|
+
: 'No other conversations.';
|
|
1378
1467
|
list.appendChild(empty);
|
|
1379
1468
|
return;
|
|
1380
1469
|
}
|
|
@@ -1383,9 +1472,24 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
1383
1472
|
row.className = 'jp-ClaudeSessionsPanel-branchRow';
|
|
1384
1473
|
row.title = `Session id: ${b.session_id}`;
|
|
1385
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
|
+
|
|
1386
1490
|
const label = document.createElement('span');
|
|
1387
1491
|
label.className = 'jp-ClaudeSessionsPanel-branchLabel';
|
|
1388
|
-
label.textContent = b
|
|
1492
|
+
label.textContent = this._branchDisplayName(b);
|
|
1389
1493
|
row.appendChild(label);
|
|
1390
1494
|
|
|
1391
1495
|
const time = document.createElement('span');
|
|
@@ -1394,19 +1498,102 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
1394
1498
|
row.appendChild(time);
|
|
1395
1499
|
|
|
1396
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
|
+
}
|
|
1397
1513
|
dialog.dispose();
|
|
1398
1514
|
void this._switchBranch(b.session_id);
|
|
1399
1515
|
});
|
|
1400
1516
|
list.appendChild(row);
|
|
1401
1517
|
}
|
|
1402
1518
|
};
|
|
1403
|
-
|
|
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
|
+
});
|
|
1404
1559
|
render();
|
|
1560
|
+
updateControls();
|
|
1405
1561
|
|
|
1406
1562
|
void dialog.launch();
|
|
1407
1563
|
search.focus();
|
|
1408
1564
|
}
|
|
1409
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
|
+
|
|
1410
1597
|
/** Switch the active row's project to another conversation branch.
|
|
1411
1598
|
* The backend touches the branch JSONL's mtime; a refresh then shows
|
|
1412
1599
|
* the selected conversation as the row's current one. */
|
|
@@ -1484,6 +1671,7 @@ export class ClaudeCodeSessionsWidget extends Widget {
|
|
|
1484
1671
|
private _contextMenu!: Menu;
|
|
1485
1672
|
private _branchSubmenu!: Menu;
|
|
1486
1673
|
private _lastBranches: IBranch[] = [];
|
|
1674
|
+
private _lastBranchesCurrent = '';
|
|
1487
1675
|
private _newSessionMenu!: Menu;
|
|
1488
1676
|
private _activeSession: ISession | null = null;
|
|
1489
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
|
+
}
|