jupyterlab_claude_code_extension 1.2.5 → 1.2.9
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 +15 -2
- package/lib/icons.d.ts +1 -0
- package/lib/icons.js +9 -0
- package/lib/types.d.ts +7 -0
- package/lib/widget.d.ts +24 -2
- package/lib/widget.js +307 -34
- package/package.json +1 -1
- package/src/__tests__/jupyterlab_claude_code_extension.spec.ts +166 -5
- package/src/icons.ts +11 -0
- package/src/types.ts +9 -0
- package/src/widget.ts +345 -33
- package/style/base.css +127 -5
|
@@ -138,10 +138,15 @@ describe('launch spinner dismiss contract', () => {
|
|
|
138
138
|
/private async _switchBranch[\s\S]*?\n \}/
|
|
139
139
|
) ?? [''])[0];
|
|
140
140
|
|
|
141
|
-
it('shows
|
|
142
|
-
|
|
143
|
-
/
|
|
141
|
+
it('shows a branch icon + count badge only when the project has branches', () => {
|
|
142
|
+
const renderRow = (widgetSrc.match(
|
|
143
|
+
/private _renderRow[\s\S]*?\n \}/
|
|
144
|
+
) ?? [''])[0];
|
|
145
|
+
expect(renderRow).toMatch(
|
|
146
|
+
/session\.extra_sessions > 0[\s\S]*?jp-ClaudeSessionsPanel-branchBadge/
|
|
144
147
|
);
|
|
148
|
+
expect(renderRow).toMatch(/branchIcon\.element/);
|
|
149
|
+
expect(renderRow).toMatch(/String\(session\.extra_sessions \+ 1\)/);
|
|
145
150
|
});
|
|
146
151
|
|
|
147
152
|
it('rebuilds submenu items from a fresh branches fetch on open', () => {
|
|
@@ -150,10 +155,21 @@ describe('launch spinner dismiss contract', () => {
|
|
|
150
155
|
expect(openMenu).toMatch(/_rebuildContextMenu\(hasBranches\)/);
|
|
151
156
|
});
|
|
152
157
|
|
|
153
|
-
it('caps the submenu at 5
|
|
158
|
+
it('caps the inline submenu at 5 most recent', () => {
|
|
154
159
|
expect(openMenu).toMatch(/\.slice\(0, 5\)/);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('always adds the Manage Sessions entry, no >5 gate', () => {
|
|
163
|
+
// The popup is the management hub - it must be reachable even for
|
|
164
|
+
// projects with 2-5 conversations, so the entry is unconditional.
|
|
165
|
+
expect(openMenu).toMatch(/switch-branch-more/);
|
|
166
|
+
expect(openMenu).not.toMatch(/branches\.length > 5/);
|
|
167
|
+
expect(widgetSrc).toMatch(/`Manage Sessions\.\.\. \(\$\{/);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('titles the submenu Switch and Manage Sessions with the count', () => {
|
|
155
171
|
expect(openMenu).toMatch(
|
|
156
|
-
/
|
|
172
|
+
/title\.label = `Switch and Manage Sessions \(\$\{data\.branches\.length\}\)`/
|
|
157
173
|
);
|
|
158
174
|
});
|
|
159
175
|
|
|
@@ -183,6 +199,70 @@ describe('launch spinner dismiss contract', () => {
|
|
|
183
199
|
);
|
|
184
200
|
});
|
|
185
201
|
|
|
202
|
+
it('popup leads with the current row, badged and without checkbox', () => {
|
|
203
|
+
const popup = (widgetSrc.match(
|
|
204
|
+
/private _showBranchPopup[\s\S]*?\n \}/
|
|
205
|
+
) ?? [''])[0];
|
|
206
|
+
expect(popup).toMatch(/jp-mod-current/);
|
|
207
|
+
expect(popup).toMatch(/branchCurrentBadge/);
|
|
208
|
+
// The current row is appended before any checkbox is created.
|
|
209
|
+
const currentIdx = popup.indexOf('jp-mod-current');
|
|
210
|
+
const checkboxIdx = popup.indexOf("check.type = 'checkbox'");
|
|
211
|
+
expect(currentIdx).toBeGreaterThan(-1);
|
|
212
|
+
expect(checkboxIdx).toBeGreaterThan(currentIdx);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('checkbox is its own click zone and selection gates row switch', () => {
|
|
216
|
+
const popup = (widgetSrc.match(
|
|
217
|
+
/private _showBranchPopup[\s\S]*?\n \}/
|
|
218
|
+
) ?? [''])[0];
|
|
219
|
+
expect(popup).toMatch(/stopPropagation\(\)/);
|
|
220
|
+
// Selection mode: row click toggles while anything is selected.
|
|
221
|
+
expect(popup).toMatch(
|
|
222
|
+
/if \(selected\.size > 0\) \{[\s\S]*?return;[\s\S]*?dialog\.dispose\(\)/
|
|
223
|
+
);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('select-all toggles the visible (filtered) rows only', () => {
|
|
227
|
+
const popup = (widgetSrc.match(
|
|
228
|
+
/private _showBranchPopup[\s\S]*?\n \}/
|
|
229
|
+
) ?? [''])[0];
|
|
230
|
+
expect(popup).toMatch(
|
|
231
|
+
/selectAll\.addEventListener\('change'[\s\S]*?visibleMatches\(\)/
|
|
232
|
+
);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('delete is two-step and any selection change disarms it', () => {
|
|
236
|
+
const popup = (widgetSrc.match(
|
|
237
|
+
/private _showBranchPopup[\s\S]*?\n \}/
|
|
238
|
+
) ?? [''])[0];
|
|
239
|
+
expect(popup).toMatch(/`Confirm delete \(\$\{selected\.size\}\)`/);
|
|
240
|
+
// updateControls (run on every selection change) resets the armed
|
|
241
|
+
// state and the button label.
|
|
242
|
+
expect(popup).toMatch(
|
|
243
|
+
/const updateControls = \(\) => \{\s*confirmArmed = false/
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('delete posts to delete-branches and resyncs the panel', () => {
|
|
248
|
+
const del = (widgetSrc.match(
|
|
249
|
+
/private async _deleteBranches[\s\S]*?\n \}/
|
|
250
|
+
) ?? [''])[0];
|
|
251
|
+
expect(del).toMatch(/sessions\/delete-branches/);
|
|
252
|
+
expect(del).toMatch(/finally[\s\S]*?await this\._fetch\(\)/);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('rows carry age emphasis classes at the 60s and 7d thresholds', () => {
|
|
256
|
+
expect(widgetSrc).toMatch(/age < 60_000[\s\S]*?jp-mod-recentlyActive/);
|
|
257
|
+
expect(widgetSrc).toMatch(/age > 7 \* 86_400_000[\s\S]*?jp-mod-stale/);
|
|
258
|
+
const css: string = fs.readFileSync(
|
|
259
|
+
path.join(__dirname, '..', '..', 'style', 'base.css'),
|
|
260
|
+
'utf-8'
|
|
261
|
+
);
|
|
262
|
+
expect(css).toMatch(/jp-mod-recentlyActive[\s\S]*?--jp-brand-color1/);
|
|
263
|
+
expect(css).toMatch(/jp-mod-stale \{\s*opacity/);
|
|
264
|
+
});
|
|
265
|
+
|
|
186
266
|
it('opens the menu without the submenu when the fetch fails', () => {
|
|
187
267
|
expect(openMenu).toMatch(/catch[\s\S]*?hasBranches = false/);
|
|
188
268
|
});
|
|
@@ -218,6 +298,87 @@ describe('launch spinner dismiss contract', () => {
|
|
|
218
298
|
/result\.current !== result\.requested[\s\S]*?Notification\.warning/
|
|
219
299
|
);
|
|
220
300
|
});
|
|
301
|
+
|
|
302
|
+
it('renders the favourite star before the time column', () => {
|
|
303
|
+
const renderRow = (widgetSrc.match(
|
|
304
|
+
/private _renderRow[\s\S]*?\n \}/
|
|
305
|
+
) ?? [''])[0];
|
|
306
|
+
const starAt = renderRow.indexOf('jp-ClaudeSessionsPanel-favStar');
|
|
307
|
+
const timeAt = renderRow.indexOf('jp-ClaudeSessionsPanel-rowTime');
|
|
308
|
+
expect(starAt).toBeGreaterThan(-1);
|
|
309
|
+
expect(timeAt).toBeGreaterThan(-1);
|
|
310
|
+
expect(starAt).toBeLessThan(timeAt);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('time labels form fixed-width right-aligned columns', () => {
|
|
314
|
+
const css: string = fs.readFileSync(
|
|
315
|
+
path.join(__dirname, '..', '..', 'style', 'base.css'),
|
|
316
|
+
'utf-8'
|
|
317
|
+
);
|
|
318
|
+
const rowTime = (css.match(
|
|
319
|
+
/\.jp-ClaudeSessionsPanel-rowTime \{[\s\S]*?\}/
|
|
320
|
+
) ?? [''])[0];
|
|
321
|
+
expect(rowTime).toMatch(/width: 52px/);
|
|
322
|
+
expect(rowTime).toMatch(/text-align: right/);
|
|
323
|
+
const branchTime = (css.match(
|
|
324
|
+
/\.jp-ClaudeSessionsPanel-branchTime \{[\s\S]*?\}/
|
|
325
|
+
) ?? [''])[0];
|
|
326
|
+
expect(branchTime).toMatch(/width: 52px/);
|
|
327
|
+
expect(branchTime).toMatch(/text-align: right/);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('now label shares the recently-active emphasis colour', () => {
|
|
331
|
+
const css: string = fs.readFileSync(
|
|
332
|
+
path.join(__dirname, '..', '..', 'style', 'base.css'),
|
|
333
|
+
'utf-8'
|
|
334
|
+
);
|
|
335
|
+
expect(css).toMatch(
|
|
336
|
+
/jp-mod-recentlyActive[\s\S]{0,200}?jp-ClaudeSessionsPanel-rowTime[\s\S]{0,80}?--jp-brand-color1/
|
|
337
|
+
);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('branch session commands exist in normal and skip-permissions modes', () => {
|
|
341
|
+
expect(widgetSrc).toMatch(/claude-code-sessions:branch-session'/);
|
|
342
|
+
expect(widgetSrc).toMatch(
|
|
343
|
+
/claude-code-sessions:branch-session-dangerous/
|
|
344
|
+
);
|
|
345
|
+
expect(widgetSrc).toMatch(
|
|
346
|
+
/Branch Session \(Skip Permissions\)\.\.\.[\s\S]{0,80}?icon: shieldIcon/
|
|
347
|
+
);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('branch session asks for a name and launches a known fork id', () => {
|
|
351
|
+
const branch = (widgetSrc.match(
|
|
352
|
+
/private async _branchSession[\s\S]*?\n \}/
|
|
353
|
+
) ?? [''])[0];
|
|
354
|
+
expect(branch).toMatch(/InputDialog\.getText/);
|
|
355
|
+
expect(branch).toMatch(/UUID\.uuid4\(\)/);
|
|
356
|
+
expect(branch).toMatch(/fork_session_id: forkId/);
|
|
357
|
+
expect(branch).toMatch(/session_id: session\.session_id/);
|
|
358
|
+
expect(branch).toMatch(/_stampForkTitle/);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('fork title stamping retries on 404 until the JSONL appears', () => {
|
|
362
|
+
const stamp = (widgetSrc.match(
|
|
363
|
+
/private async _stampForkTitle[\s\S]*?\n \}/
|
|
364
|
+
) ?? [''])[0];
|
|
365
|
+
expect(stamp).toMatch(/sessions\/set-title/);
|
|
366
|
+
expect(stamp).toMatch(/status === 404/);
|
|
367
|
+
expect(stamp).toMatch(/await this\._fetch\(\)/);
|
|
368
|
+
expect(stamp).toMatch(/Notification\.warning/);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('live dot is softened with reduced opacity', () => {
|
|
372
|
+
const css: string = fs.readFileSync(
|
|
373
|
+
path.join(__dirname, '..', '..', 'style', 'base.css'),
|
|
374
|
+
'utf-8'
|
|
375
|
+
);
|
|
376
|
+
const dot = (css.match(/\.jp-ClaudeSessionsPanel-dot \{[\s\S]*?\}/) ?? [
|
|
377
|
+
''
|
|
378
|
+
])[0];
|
|
379
|
+
expect(dot).toMatch(/--jp-success-color1/);
|
|
380
|
+
expect(dot).toMatch(/opacity: 0\.75/);
|
|
381
|
+
});
|
|
221
382
|
});
|
|
222
383
|
|
|
223
384
|
it('_doResumeInTerminal dismisses spinner via dispose(), not resolve()', () => {
|
package/src/icons.ts
CHANGED
|
@@ -71,6 +71,17 @@ export const addIcon = new LabIcon({
|
|
|
71
71
|
svgstr: addSvgStr
|
|
72
72
|
});
|
|
73
73
|
|
|
74
|
+
// Git-branch glyph (Octicons git-branch-16, MIT) - marks rows that carry
|
|
75
|
+
// parallel conversations and the Branch Session menu entries.
|
|
76
|
+
const branchSvgStr = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="16" height="16">
|
|
77
|
+
<path class="jp-icon3" fill="#616161" d="M9.5 3.25a2.25 2.25 0 1 1 3 2.122V6A2.5 2.5 0 0 1 10 8.5H6a1 1 0 0 0-1 1v1.128a2.251 2.251 0 1 1-1.5 0V5.372a2.25 2.25 0 1 1 1.5 0v1.836A2.493 2.493 0 0 1 6 7h4a1 1 0 0 0 1-1v-.628a2.25 2.25 0 0 1-1.5-2.122Zm-6 0a.75.75 0 1 0 1.5 0 .75.75 0 0 0-1.5 0Zm8.25-.75a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5ZM4.25 12a.75.75 0 1 0 0 1.5.75.75 0 0 0 0-1.5Z"/>
|
|
78
|
+
</svg>`;
|
|
79
|
+
|
|
80
|
+
export const branchIcon = new LabIcon({
|
|
81
|
+
name: 'jupyterlab_claude_code_extension:branch',
|
|
82
|
+
svgstr: branchSvgStr
|
|
83
|
+
});
|
|
84
|
+
|
|
74
85
|
// Funnel copied verbatim from @jupyterlab/ui-components'
|
|
75
86
|
// `search/filter.svg` - the same image the file browser's filter
|
|
76
87
|
// toggle uses. The `class="jp-icon3"` lets JupyterLab's theme drive
|
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. */
|