jupyterlab_claude_code_extension 1.0.16 → 1.0.19

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/lib/index.js CHANGED
@@ -34,6 +34,10 @@ const plugin = {
34
34
  const resolve = settings.get('resolveSessionNames')
35
35
  .composite;
36
36
  widget.setResolveSessionNames(resolve !== false);
37
+ const limit = settings.get('recentLimit').composite;
38
+ if (typeof limit === 'number') {
39
+ widget.setRecentLimit(limit);
40
+ }
37
41
  };
38
42
  apply();
39
43
  settings.changed.connect(apply);
package/lib/types.d.ts CHANGED
@@ -34,3 +34,10 @@ export interface IRemoveRequest {
34
34
  export interface IRemoveResponse {
35
35
  removed: string;
36
36
  }
37
+ export interface ILaunchTerminalRequest {
38
+ project_path: string;
39
+ session_id: string;
40
+ }
41
+ export interface ILaunchTerminalResponse {
42
+ terminal_name: string;
43
+ }
package/lib/widget.d.ts CHANGED
@@ -7,10 +7,15 @@ export declare class ClaudeCodeSessionsWidget extends Widget {
7
7
  refresh(): void;
8
8
  /** Toggle whether explicit ``/rename`` names are honoured. */
9
9
  setResolveSessionNames(on: boolean): void;
10
+ /** Set how many rows the Recent section displays. */
11
+ setRecentLimit(n: number): void;
10
12
  protected onAfterShow(_msg: Message): void;
11
13
  protected onBeforeHide(_msg: Message): void;
12
14
  protected onCloseRequest(msg: Message): void;
13
15
  private _buildShell;
16
+ /** Lowercase substring + subsequence match. */
17
+ private _fuzzyMatch;
18
+ private _matchesFilter;
14
19
  private _showLoading;
15
20
  private _showError;
16
21
  private _fetch;
@@ -18,7 +23,6 @@ export declare class ClaudeCodeSessionsWidget extends Widget {
18
23
  private _remove;
19
24
  private _resumeInTerminal;
20
25
  private _doResumeInTerminal;
21
- private _shellQuote;
22
26
  private _findTerminalForCwd;
23
27
  private _wireTerminalDisposal;
24
28
  /** Apply the resolve-names setting + path-segment disambiguation. */
@@ -56,5 +60,7 @@ export declare class ClaudeCodeSessionsWidget extends Widget {
56
60
  private readonly _pendingByPath;
57
61
  private readonly _rootDir;
58
62
  private _resolveNames;
63
+ private _recentLimit;
59
64
  private _displayNames;
65
+ private _filter;
60
66
  }
package/lib/widget.js CHANGED
@@ -3,7 +3,7 @@ import { Menu, Widget } from '@lumino/widgets';
3
3
  import { requestAPI } from './request';
4
4
  import { claudeIcon, refreshIcon, removeIcon, starFilledIcon } from './icons';
5
5
  const POLL_INTERVAL_MS = 10000;
6
- const RECENT_LIMIT = 10;
6
+ const DEFAULT_RECENT_LIMIT = 10;
7
7
  const EXPANDED_STORAGE_KEY = 'jupyterlab_claude_code_extension:expanded';
8
8
  const SECTION_LABELS = {
9
9
  favourites: 'Favourites',
@@ -65,7 +65,9 @@ export class ClaudeCodeSessionsWidget extends Widget {
65
65
  this._terminalsByPath = new Map();
66
66
  this._pendingByPath = new Map();
67
67
  this._resolveNames = true;
68
+ this._recentLimit = DEFAULT_RECENT_LIMIT;
68
69
  this._displayNames = new Map();
70
+ this._filter = '';
69
71
  this._app = app;
70
72
  this._serverSettings = app.serviceManager.serverSettings;
71
73
  this._rootDir = rootDir.replace(/\/+$/, '');
@@ -92,6 +94,15 @@ export class ClaudeCodeSessionsWidget extends Widget {
92
94
  this._resolveNames = on;
93
95
  this._render();
94
96
  }
97
+ /** Set how many rows the Recent section displays. */
98
+ setRecentLimit(n) {
99
+ const clamped = Math.max(1, Math.min(100, Math.floor(n)));
100
+ if (this._recentLimit === clamped) {
101
+ return;
102
+ }
103
+ this._recentLimit = clamped;
104
+ this._render();
105
+ }
95
106
  onAfterShow(_msg) {
96
107
  this.refresh();
97
108
  this._startPolling();
@@ -120,16 +131,53 @@ export class ClaudeCodeSessionsWidget extends Widget {
120
131
  refreshBtn.addEventListener('click', () => this.refresh());
121
132
  header.appendChild(refreshBtn);
122
133
  this._refreshBtn = refreshBtn;
134
+ const search = document.createElement('input');
135
+ search.type = 'search';
136
+ search.className = 'jp-ClaudeSessionsPanel-search';
137
+ search.placeholder = 'Filter sessions...';
138
+ search.spellcheck = false;
139
+ search.addEventListener('input', () => {
140
+ this._filter = search.value;
141
+ this._render();
142
+ });
123
143
  const body = document.createElement('div');
124
144
  body.className = 'jp-ClaudeSessionsPanel-body';
125
145
  const status = document.createElement('div');
126
146
  status.className = 'jp-ClaudeSessionsPanel-status';
127
147
  root.appendChild(header);
148
+ root.appendChild(search);
128
149
  root.appendChild(body);
129
150
  root.appendChild(status);
130
151
  this._bodyEl = body;
131
152
  this._statusEl = status;
132
153
  }
154
+ /** Lowercase substring + subsequence match. */
155
+ _fuzzyMatch(haystack, needle) {
156
+ if (!needle) {
157
+ return true;
158
+ }
159
+ const h = haystack.toLowerCase();
160
+ const n = needle.toLowerCase();
161
+ if (h.includes(n)) {
162
+ return true;
163
+ }
164
+ let j = 0;
165
+ for (let i = 0; i < h.length && j < n.length; i++) {
166
+ if (h[i] === n[j]) {
167
+ j += 1;
168
+ }
169
+ }
170
+ return j === n.length;
171
+ }
172
+ _matchesFilter(s) {
173
+ const q = this._filter.trim();
174
+ if (!q) {
175
+ return true;
176
+ }
177
+ return (this._fuzzyMatch(s.name, q) ||
178
+ this._fuzzyMatch(s.project_path, q) ||
179
+ this._fuzzyMatch(this._lookupName(s), q));
180
+ }
133
181
  _showLoading() {
134
182
  this._statusEl.textContent = this._sessions === null ? 'Loading...' : '';
135
183
  }
@@ -201,7 +249,6 @@ export class ClaudeCodeSessionsWidget extends Widget {
201
249
  return promise;
202
250
  }
203
251
  async _doResumeInTerminal(session) {
204
- var _a, _b, _c;
205
252
  try {
206
253
  // 1. In-memory microcache - covers rapid-click and post-creation reuse
207
254
  // before the new widget propagates fully through the tracker. Cleared
@@ -221,49 +268,30 @@ export class ClaudeCodeSessionsWidget extends Widget {
221
268
  this._app.shell.activateById(found.id);
222
269
  return;
223
270
  }
224
- // 3. No matching open terminal - create a new one rooted at this folder.
225
- const widget = await this._app.commands.execute('terminal:create-new', { cwd: session.project_path });
271
+ // 3. No matching terminal - spawn a new one with `claude --resume <id>`
272
+ // as the pty's only process (no shell). Server-side endpoint calls
273
+ // terminal_manager.create(shell_command=[claude, --resume, sid], cwd=...)
274
+ // and returns the terminal name; we then attach JL's standard widget
275
+ // via terminal:open. When claude exits, the tab closes.
276
+ const launched = await requestAPI('launch-terminal', this._serverSettings, {
277
+ method: 'POST',
278
+ body: JSON.stringify({
279
+ project_path: session.project_path,
280
+ session_id: session.session_id
281
+ })
282
+ });
283
+ const widget = await this._app.commands.execute('terminal:open', {
284
+ name: launched.terminal_name
285
+ });
226
286
  if (widget === null || widget === void 0 ? void 0 : widget.id) {
227
287
  this._terminalsByPath.set(session.project_path, widget);
228
288
  this._wireTerminalDisposal(session.project_path, widget);
229
289
  }
230
- const term = (_b = (_a = widget === null || widget === void 0 ? void 0 : widget.content) === null || _a === void 0 ? void 0 : _a.session) !== null && _b !== void 0 ? _b : widget === null || widget === void 0 ? void 0 : widget.session;
231
- const command = `cd ${this._shellQuote(session.project_path)} && claude --resume ${session.session_id}\r`;
232
- if (!term || typeof term.send !== 'function') {
233
- this._statusEl.textContent = `Run in a terminal: cd ${session.project_path} && claude --resume ${session.session_id}`;
234
- return;
235
- }
236
- // Wait for the first message from the terminal (shell prompt) before
237
- // injecting input - sending too early arrives before the shell is ready.
238
- let sent = false;
239
- const send = () => {
240
- if (sent) {
241
- return;
242
- }
243
- sent = true;
244
- term.send({ type: 'stdin', content: [command] });
245
- };
246
- if ((_c = term.messageReceived) === null || _c === void 0 ? void 0 : _c.connect) {
247
- const handler = () => {
248
- term.messageReceived.disconnect(handler);
249
- // Tiny additional delay so the prompt is rendered and ready for input
250
- setTimeout(send, 150);
251
- };
252
- term.messageReceived.connect(handler);
253
- // Hard fallback: if no message arrives within 2s, send anyway
254
- setTimeout(send, 2000);
255
- }
256
- else {
257
- setTimeout(send, 600);
258
- }
259
290
  }
260
291
  catch (err) {
261
292
  this._showError(err);
262
293
  }
263
294
  }
264
- _shellQuote(s) {
265
- return `'${s.replace(/'/g, "'\\''")}'`;
266
- }
267
295
  async _findTerminalForCwd(projectPath) {
268
296
  var _a, _b;
269
297
  if (!this._terminalTracker) {
@@ -378,13 +406,15 @@ export class ClaudeCodeSessionsWidget extends Widget {
378
406
  this._bodyEl.appendChild(empty);
379
407
  return;
380
408
  }
381
- // Compute disambiguated display names once per render.
409
+ // Compute disambiguated display names once per render (against the
410
+ // full set so suffixes stay stable when filtering narrows the view).
382
411
  this._displayNames = this._disambiguate(sessions);
383
- const favourites = sessions.filter(s => s.favourite);
384
- const recent = [...sessions]
412
+ const filtered = sessions.filter(s => this._matchesFilter(s));
413
+ const favourites = filtered.filter(s => s.favourite);
414
+ const recent = [...filtered]
385
415
  .sort((a, b) => b.file_mtime - a.file_mtime)
386
- .slice(0, RECENT_LIMIT);
387
- const all = [...sessions].sort((a, b) => this._lookupName(a).localeCompare(this._lookupName(b)));
416
+ .slice(0, this._recentLimit);
417
+ const all = [...filtered].sort((a, b) => this._lookupName(a).localeCompare(this._lookupName(b)));
388
418
  if (favourites.length > 0) {
389
419
  this._renderSection('favourites', favourites);
390
420
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jupyterlab_claude_code_extension",
3
- "version": "1.0.16",
3
+ "version": "1.0.19",
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",
@@ -56,7 +56,6 @@
56
56
  "watch:labextension": "jupyter labextension watch ."
57
57
  },
58
58
  "dependencies": {
59
- "@jupyterlab/application": "^4.0.0",
60
59
  "@jupyterlab/apputils": "^4.0.0",
61
60
  "@jupyterlab/coreutils": "^6.0.0",
62
61
  "@jupyterlab/services": "^7.0.0",
package/src/index.ts CHANGED
@@ -62,6 +62,10 @@ const plugin: JupyterFrontEndPlugin<void> = {
62
62
  const resolve = settings.get('resolveSessionNames')
63
63
  .composite as boolean;
64
64
  widget.setResolveSessionNames(resolve !== false);
65
+ const limit = settings.get('recentLimit').composite as number;
66
+ if (typeof limit === 'number') {
67
+ widget.setRecentLimit(limit);
68
+ }
65
69
  };
66
70
  apply();
67
71
  settings.changed.connect(apply);
package/src/types.ts CHANGED
@@ -40,3 +40,12 @@ export interface IRemoveRequest {
40
40
  export interface IRemoveResponse {
41
41
  removed: string;
42
42
  }
43
+
44
+ export interface ILaunchTerminalRequest {
45
+ project_path: string;
46
+ session_id: string;
47
+ }
48
+
49
+ export interface ILaunchTerminalResponse {
50
+ terminal_name: string;
51
+ }
package/src/widget.ts CHANGED
@@ -9,13 +9,14 @@ import { requestAPI } from './request';
9
9
  import { claudeIcon, refreshIcon, removeIcon, starFilledIcon } from './icons';
10
10
  import {
11
11
  IFavouriteResponse,
12
+ ILaunchTerminalResponse,
12
13
  IRemoveResponse,
13
14
  ISession,
14
15
  ISessionsListResponse
15
16
  } from './types';
16
17
 
17
18
  const POLL_INTERVAL_MS = 10_000;
18
- const RECENT_LIMIT = 10;
19
+ const DEFAULT_RECENT_LIMIT = 10;
19
20
  const EXPANDED_STORAGE_KEY = 'jupyterlab_claude_code_extension:expanded';
20
21
 
21
22
  type SectionKey = 'favourites' | 'recent' | 'all';
@@ -114,6 +115,16 @@ export class ClaudeCodeSessionsWidget extends Widget {
114
115
  this._render();
115
116
  }
116
117
 
118
+ /** Set how many rows the Recent section displays. */
119
+ setRecentLimit(n: number): void {
120
+ const clamped = Math.max(1, Math.min(100, Math.floor(n)));
121
+ if (this._recentLimit === clamped) {
122
+ return;
123
+ }
124
+ this._recentLimit = clamped;
125
+ this._render();
126
+ }
127
+
117
128
  protected onAfterShow(_msg: Message): void {
118
129
  this.refresh();
119
130
  this._startPolling();
@@ -150,6 +161,16 @@ export class ClaudeCodeSessionsWidget extends Widget {
150
161
  header.appendChild(refreshBtn);
151
162
  this._refreshBtn = refreshBtn;
152
163
 
164
+ const search = document.createElement('input');
165
+ search.type = 'search';
166
+ search.className = 'jp-ClaudeSessionsPanel-search';
167
+ search.placeholder = 'Filter sessions...';
168
+ search.spellcheck = false;
169
+ search.addEventListener('input', () => {
170
+ this._filter = search.value;
171
+ this._render();
172
+ });
173
+
153
174
  const body = document.createElement('div');
154
175
  body.className = 'jp-ClaudeSessionsPanel-body';
155
176
 
@@ -157,6 +178,7 @@ export class ClaudeCodeSessionsWidget extends Widget {
157
178
  status.className = 'jp-ClaudeSessionsPanel-status';
158
179
 
159
180
  root.appendChild(header);
181
+ root.appendChild(search);
160
182
  root.appendChild(body);
161
183
  root.appendChild(status);
162
184
 
@@ -164,6 +186,37 @@ export class ClaudeCodeSessionsWidget extends Widget {
164
186
  this._statusEl = status;
165
187
  }
166
188
 
189
+ /** Lowercase substring + subsequence match. */
190
+ private _fuzzyMatch(haystack: string, needle: string): boolean {
191
+ if (!needle) {
192
+ return true;
193
+ }
194
+ const h = haystack.toLowerCase();
195
+ const n = needle.toLowerCase();
196
+ if (h.includes(n)) {
197
+ return true;
198
+ }
199
+ let j = 0;
200
+ for (let i = 0; i < h.length && j < n.length; i++) {
201
+ if (h[i] === n[j]) {
202
+ j += 1;
203
+ }
204
+ }
205
+ return j === n.length;
206
+ }
207
+
208
+ private _matchesFilter(s: ISession): boolean {
209
+ const q = this._filter.trim();
210
+ if (!q) {
211
+ return true;
212
+ }
213
+ return (
214
+ this._fuzzyMatch(s.name, q) ||
215
+ this._fuzzyMatch(s.project_path, q) ||
216
+ this._fuzzyMatch(this._lookupName(s), q)
217
+ );
218
+ }
219
+
167
220
  private _showLoading(): void {
168
221
  this._statusEl.textContent = this._sessions === null ? 'Loading...' : '';
169
222
  }
@@ -272,57 +325,34 @@ export class ClaudeCodeSessionsWidget extends Widget {
272
325
  return;
273
326
  }
274
327
 
275
- // 3. No matching open terminal - create a new one rooted at this folder.
276
- const widget: any = await this._app.commands.execute(
277
- 'terminal:create-new',
278
- { cwd: session.project_path }
328
+ // 3. No matching terminal - spawn a new one with `claude --resume <id>`
329
+ // as the pty's only process (no shell). Server-side endpoint calls
330
+ // terminal_manager.create(shell_command=[claude, --resume, sid], cwd=...)
331
+ // and returns the terminal name; we then attach JL's standard widget
332
+ // via terminal:open. When claude exits, the tab closes.
333
+ const launched = await requestAPI<ILaunchTerminalResponse>(
334
+ 'launch-terminal',
335
+ this._serverSettings,
336
+ {
337
+ method: 'POST',
338
+ body: JSON.stringify({
339
+ project_path: session.project_path,
340
+ session_id: session.session_id
341
+ })
342
+ }
279
343
  );
280
-
344
+ const widget: any = await this._app.commands.execute('terminal:open', {
345
+ name: launched.terminal_name
346
+ });
281
347
  if (widget?.id) {
282
348
  this._terminalsByPath.set(session.project_path, widget);
283
349
  this._wireTerminalDisposal(session.project_path, widget);
284
350
  }
285
-
286
- const term = widget?.content?.session ?? widget?.session;
287
- const command = `cd ${this._shellQuote(session.project_path)} && claude --resume ${session.session_id}\r`;
288
-
289
- if (!term || typeof term.send !== 'function') {
290
- this._statusEl.textContent = `Run in a terminal: cd ${session.project_path} && claude --resume ${session.session_id}`;
291
- return;
292
- }
293
-
294
- // Wait for the first message from the terminal (shell prompt) before
295
- // injecting input - sending too early arrives before the shell is ready.
296
- let sent = false;
297
- const send = () => {
298
- if (sent) {
299
- return;
300
- }
301
- sent = true;
302
- term.send({ type: 'stdin', content: [command] });
303
- };
304
-
305
- if (term.messageReceived?.connect) {
306
- const handler = () => {
307
- term.messageReceived.disconnect(handler);
308
- // Tiny additional delay so the prompt is rendered and ready for input
309
- setTimeout(send, 150);
310
- };
311
- term.messageReceived.connect(handler);
312
- // Hard fallback: if no message arrives within 2s, send anyway
313
- setTimeout(send, 2000);
314
- } else {
315
- setTimeout(send, 600);
316
- }
317
351
  } catch (err) {
318
352
  this._showError(err);
319
353
  }
320
354
  }
321
355
 
322
- private _shellQuote(s: string): string {
323
- return `'${s.replace(/'/g, "'\\''")}'`;
324
- }
325
-
326
356
  private async _findTerminalForCwd(projectPath: string): Promise<any | null> {
327
357
  if (!this._terminalTracker) {
328
358
  return null;
@@ -447,14 +477,16 @@ export class ClaudeCodeSessionsWidget extends Widget {
447
477
  return;
448
478
  }
449
479
 
450
- // Compute disambiguated display names once per render.
480
+ // Compute disambiguated display names once per render (against the
481
+ // full set so suffixes stay stable when filtering narrows the view).
451
482
  this._displayNames = this._disambiguate(sessions);
452
483
 
453
- const favourites = sessions.filter(s => s.favourite);
454
- const recent = [...sessions]
484
+ const filtered = sessions.filter(s => this._matchesFilter(s));
485
+ const favourites = filtered.filter(s => s.favourite);
486
+ const recent = [...filtered]
455
487
  .sort((a, b) => b.file_mtime - a.file_mtime)
456
- .slice(0, RECENT_LIMIT);
457
- const all = [...sessions].sort((a, b) =>
488
+ .slice(0, this._recentLimit);
489
+ const all = [...filtered].sort((a, b) =>
458
490
  this._lookupName(a).localeCompare(this._lookupName(b))
459
491
  );
460
492
 
@@ -760,5 +792,7 @@ export class ClaudeCodeSessionsWidget extends Widget {
760
792
  private readonly _pendingByPath: Map<string, Promise<void>> = new Map();
761
793
  private readonly _rootDir: string;
762
794
  private _resolveNames: boolean = true;
795
+ private _recentLimit: number = DEFAULT_RECENT_LIMIT;
763
796
  private _displayNames: Map<string, string> = new Map();
797
+ private _filter: string = '';
764
798
  }
package/style/base.css CHANGED
@@ -57,6 +57,29 @@
57
57
  height: 14px;
58
58
  }
59
59
 
60
+ .jp-ClaudeSessionsPanel-search {
61
+ flex: 0 0 auto;
62
+ margin: 4px 8px;
63
+ padding: 2px 6px;
64
+ background: var(--jp-layout-color1);
65
+ color: var(--jp-ui-font-color1);
66
+ border: 1px solid var(--jp-border-color2);
67
+ border-radius: 2px;
68
+ font-family: inherit;
69
+ font-size: var(--jp-ui-font-size1);
70
+ outline: none;
71
+ -webkit-appearance: none;
72
+ appearance: none;
73
+ }
74
+
75
+ .jp-ClaudeSessionsPanel-search:focus {
76
+ border-color: var(--jp-brand-color1);
77
+ }
78
+
79
+ .jp-ClaudeSessionsPanel-search::placeholder {
80
+ color: var(--jp-ui-font-color3);
81
+ }
82
+
60
83
  .jp-ClaudeSessionsPanel-body {
61
84
  flex: 1 1 auto;
62
85
  display: flex;