jupyterlab_claude_code_extension 1.0.16 → 1.0.21

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,27 @@ 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
+ /** Normalise strings for filter comparison: NFD-decompose, strip combining
17
+ * diacritic marks, lowercase, and collapse separators (`-`, `_`, `.`, `/`,
18
+ * whitespace) entirely. So "foo-bar", "foo_bar", "foo bar", "Foo Bar" all
19
+ * compare equal as "foobar".
20
+ */
21
+ private _normalize;
22
+ /** Strict fuzzy match: substring on normalised strings, with up to 2%
23
+ * Levenshtein tolerance for queries of 6+ chars (so the 98% threshold
24
+ * only relaxes substring strictness when the query is long enough that
25
+ * 2% rounds to a nonzero edit budget - effectively substring-only for
26
+ * short queries).
27
+ */
28
+ private _fuzzyMatch;
29
+ private _levenshtein;
30
+ private _matchesFilter;
14
31
  private _showLoading;
15
32
  private _showError;
16
33
  private _fetch;
@@ -18,7 +35,6 @@ export declare class ClaudeCodeSessionsWidget extends Widget {
18
35
  private _remove;
19
36
  private _resumeInTerminal;
20
37
  private _doResumeInTerminal;
21
- private _shellQuote;
22
38
  private _findTerminalForCwd;
23
39
  private _wireTerminalDisposal;
24
40
  /** Apply the resolve-names setting + path-segment disambiguation. */
@@ -56,5 +72,7 @@ export declare class ClaudeCodeSessionsWidget extends Widget {
56
72
  private readonly _pendingByPath;
57
73
  private readonly _rootDir;
58
74
  private _resolveNames;
75
+ private _recentLimit;
59
76
  private _displayNames;
77
+ private _filter;
60
78
  }
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,106 @@ 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
+ /** Normalise strings for filter comparison: NFD-decompose, strip combining
155
+ * diacritic marks, lowercase, and collapse separators (`-`, `_`, `.`, `/`,
156
+ * whitespace) entirely. So "foo-bar", "foo_bar", "foo bar", "Foo Bar" all
157
+ * compare equal as "foobar".
158
+ */
159
+ _normalize(s) {
160
+ return s
161
+ .normalize('NFD')
162
+ .replace(/[̀-ͯ]/g, '')
163
+ .toLowerCase()
164
+ .replace(/[\s\-_./]+/g, '');
165
+ }
166
+ /** Strict fuzzy match: substring on normalised strings, with up to 2%
167
+ * Levenshtein tolerance for queries of 6+ chars (so the 98% threshold
168
+ * only relaxes substring strictness when the query is long enough that
169
+ * 2% rounds to a nonzero edit budget - effectively substring-only for
170
+ * short queries).
171
+ */
172
+ _fuzzyMatch(haystack, needle) {
173
+ if (!needle) {
174
+ return true;
175
+ }
176
+ const h = this._normalize(haystack);
177
+ const n = this._normalize(needle);
178
+ if (!n) {
179
+ return true;
180
+ }
181
+ if (h.includes(n)) {
182
+ return true;
183
+ }
184
+ const tol = Math.floor(n.length * 0.02);
185
+ if (tol === 0) {
186
+ return false;
187
+ }
188
+ for (let len = n.length - tol; len <= n.length + tol; len += 1) {
189
+ if (len <= 0) {
190
+ continue;
191
+ }
192
+ for (let i = 0; i + len <= h.length; i += 1) {
193
+ if (this._levenshtein(h.slice(i, i + len), n) <= tol) {
194
+ return true;
195
+ }
196
+ }
197
+ }
198
+ return false;
199
+ }
200
+ _levenshtein(a, b) {
201
+ const m = a.length;
202
+ const n = b.length;
203
+ if (m === 0) {
204
+ return n;
205
+ }
206
+ if (n === 0) {
207
+ return m;
208
+ }
209
+ const dp = new Array(n + 1);
210
+ for (let j = 0; j <= n; j += 1) {
211
+ dp[j] = j;
212
+ }
213
+ for (let i = 1; i <= m; i += 1) {
214
+ let prev = dp[0];
215
+ dp[0] = i;
216
+ for (let j = 1; j <= n; j += 1) {
217
+ const tmp = dp[j];
218
+ dp[j] =
219
+ a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[j], dp[j - 1]);
220
+ prev = tmp;
221
+ }
222
+ }
223
+ return dp[n];
224
+ }
225
+ _matchesFilter(s) {
226
+ const q = this._filter.trim();
227
+ if (!q) {
228
+ return true;
229
+ }
230
+ return (this._fuzzyMatch(s.name, q) ||
231
+ this._fuzzyMatch(s.project_path, q) ||
232
+ this._fuzzyMatch(this._lookupName(s), q));
233
+ }
133
234
  _showLoading() {
134
235
  this._statusEl.textContent = this._sessions === null ? 'Loading...' : '';
135
236
  }
@@ -201,7 +302,6 @@ export class ClaudeCodeSessionsWidget extends Widget {
201
302
  return promise;
202
303
  }
203
304
  async _doResumeInTerminal(session) {
204
- var _a, _b, _c;
205
305
  try {
206
306
  // 1. In-memory microcache - covers rapid-click and post-creation reuse
207
307
  // before the new widget propagates fully through the tracker. Cleared
@@ -221,49 +321,30 @@ export class ClaudeCodeSessionsWidget extends Widget {
221
321
  this._app.shell.activateById(found.id);
222
322
  return;
223
323
  }
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 });
324
+ // 3. No matching terminal - spawn a new one with `claude --resume <id>`
325
+ // as the pty's only process (no shell). Server-side endpoint calls
326
+ // terminal_manager.create(shell_command=[claude, --resume, sid], cwd=...)
327
+ // and returns the terminal name; we then attach JL's standard widget
328
+ // via terminal:open. When claude exits, the tab closes.
329
+ const launched = await requestAPI('launch-terminal', this._serverSettings, {
330
+ method: 'POST',
331
+ body: JSON.stringify({
332
+ project_path: session.project_path,
333
+ session_id: session.session_id
334
+ })
335
+ });
336
+ const widget = await this._app.commands.execute('terminal:open', {
337
+ name: launched.terminal_name
338
+ });
226
339
  if (widget === null || widget === void 0 ? void 0 : widget.id) {
227
340
  this._terminalsByPath.set(session.project_path, widget);
228
341
  this._wireTerminalDisposal(session.project_path, widget);
229
342
  }
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
343
  }
260
344
  catch (err) {
261
345
  this._showError(err);
262
346
  }
263
347
  }
264
- _shellQuote(s) {
265
- return `'${s.replace(/'/g, "'\\''")}'`;
266
- }
267
348
  async _findTerminalForCwd(projectPath) {
268
349
  var _a, _b;
269
350
  if (!this._terminalTracker) {
@@ -378,13 +459,15 @@ export class ClaudeCodeSessionsWidget extends Widget {
378
459
  this._bodyEl.appendChild(empty);
379
460
  return;
380
461
  }
381
- // Compute disambiguated display names once per render.
462
+ // Compute disambiguated display names once per render (against the
463
+ // full set so suffixes stay stable when filtering narrows the view).
382
464
  this._displayNames = this._disambiguate(sessions);
383
- const favourites = sessions.filter(s => s.favourite);
384
- const recent = [...sessions]
465
+ const filtered = sessions.filter(s => this._matchesFilter(s));
466
+ const favourites = filtered.filter(s => s.favourite);
467
+ const recent = [...filtered]
385
468
  .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)));
469
+ .slice(0, this._recentLimit);
470
+ const all = [...filtered].sort((a, b) => this._lookupName(a).localeCompare(this._lookupName(b)));
388
471
  if (favourites.length > 0) {
389
472
  this._renderSection('favourites', favourites);
390
473
  }
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.21",
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,92 @@ export class ClaudeCodeSessionsWidget extends Widget {
164
186
  this._statusEl = status;
165
187
  }
166
188
 
189
+ /** Normalise strings for filter comparison: NFD-decompose, strip combining
190
+ * diacritic marks, lowercase, and collapse separators (`-`, `_`, `.`, `/`,
191
+ * whitespace) entirely. So "foo-bar", "foo_bar", "foo bar", "Foo Bar" all
192
+ * compare equal as "foobar".
193
+ */
194
+ private _normalize(s: string): string {
195
+ return s
196
+ .normalize('NFD')
197
+ .replace(/[̀-ͯ]/g, '')
198
+ .toLowerCase()
199
+ .replace(/[\s\-_./]+/g, '');
200
+ }
201
+
202
+ /** Strict fuzzy match: substring on normalised strings, with up to 2%
203
+ * Levenshtein tolerance for queries of 6+ chars (so the 98% threshold
204
+ * only relaxes substring strictness when the query is long enough that
205
+ * 2% rounds to a nonzero edit budget - effectively substring-only for
206
+ * short queries).
207
+ */
208
+ private _fuzzyMatch(haystack: string, needle: string): boolean {
209
+ if (!needle) {
210
+ return true;
211
+ }
212
+ const h = this._normalize(haystack);
213
+ const n = this._normalize(needle);
214
+ if (!n) {
215
+ return true;
216
+ }
217
+ if (h.includes(n)) {
218
+ return true;
219
+ }
220
+ const tol = Math.floor(n.length * 0.02);
221
+ if (tol === 0) {
222
+ return false;
223
+ }
224
+ for (let len = n.length - tol; len <= n.length + tol; len += 1) {
225
+ if (len <= 0) {
226
+ continue;
227
+ }
228
+ for (let i = 0; i + len <= h.length; i += 1) {
229
+ if (this._levenshtein(h.slice(i, i + len), n) <= tol) {
230
+ return true;
231
+ }
232
+ }
233
+ }
234
+ return false;
235
+ }
236
+
237
+ private _levenshtein(a: string, b: string): number {
238
+ const m = a.length;
239
+ const n = b.length;
240
+ if (m === 0) {
241
+ return n;
242
+ }
243
+ if (n === 0) {
244
+ return m;
245
+ }
246
+ const dp: number[] = new Array(n + 1);
247
+ for (let j = 0; j <= n; j += 1) {
248
+ dp[j] = j;
249
+ }
250
+ for (let i = 1; i <= m; i += 1) {
251
+ let prev = dp[0];
252
+ dp[0] = i;
253
+ for (let j = 1; j <= n; j += 1) {
254
+ const tmp = dp[j];
255
+ dp[j] =
256
+ a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[j], dp[j - 1]);
257
+ prev = tmp;
258
+ }
259
+ }
260
+ return dp[n];
261
+ }
262
+
263
+ private _matchesFilter(s: ISession): boolean {
264
+ const q = this._filter.trim();
265
+ if (!q) {
266
+ return true;
267
+ }
268
+ return (
269
+ this._fuzzyMatch(s.name, q) ||
270
+ this._fuzzyMatch(s.project_path, q) ||
271
+ this._fuzzyMatch(this._lookupName(s), q)
272
+ );
273
+ }
274
+
167
275
  private _showLoading(): void {
168
276
  this._statusEl.textContent = this._sessions === null ? 'Loading...' : '';
169
277
  }
@@ -272,57 +380,34 @@ export class ClaudeCodeSessionsWidget extends Widget {
272
380
  return;
273
381
  }
274
382
 
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 }
383
+ // 3. No matching terminal - spawn a new one with `claude --resume <id>`
384
+ // as the pty's only process (no shell). Server-side endpoint calls
385
+ // terminal_manager.create(shell_command=[claude, --resume, sid], cwd=...)
386
+ // and returns the terminal name; we then attach JL's standard widget
387
+ // via terminal:open. When claude exits, the tab closes.
388
+ const launched = await requestAPI<ILaunchTerminalResponse>(
389
+ 'launch-terminal',
390
+ this._serverSettings,
391
+ {
392
+ method: 'POST',
393
+ body: JSON.stringify({
394
+ project_path: session.project_path,
395
+ session_id: session.session_id
396
+ })
397
+ }
279
398
  );
280
-
399
+ const widget: any = await this._app.commands.execute('terminal:open', {
400
+ name: launched.terminal_name
401
+ });
281
402
  if (widget?.id) {
282
403
  this._terminalsByPath.set(session.project_path, widget);
283
404
  this._wireTerminalDisposal(session.project_path, widget);
284
405
  }
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
406
  } catch (err) {
318
407
  this._showError(err);
319
408
  }
320
409
  }
321
410
 
322
- private _shellQuote(s: string): string {
323
- return `'${s.replace(/'/g, "'\\''")}'`;
324
- }
325
-
326
411
  private async _findTerminalForCwd(projectPath: string): Promise<any | null> {
327
412
  if (!this._terminalTracker) {
328
413
  return null;
@@ -447,14 +532,16 @@ export class ClaudeCodeSessionsWidget extends Widget {
447
532
  return;
448
533
  }
449
534
 
450
- // Compute disambiguated display names once per render.
535
+ // Compute disambiguated display names once per render (against the
536
+ // full set so suffixes stay stable when filtering narrows the view).
451
537
  this._displayNames = this._disambiguate(sessions);
452
538
 
453
- const favourites = sessions.filter(s => s.favourite);
454
- const recent = [...sessions]
539
+ const filtered = sessions.filter(s => this._matchesFilter(s));
540
+ const favourites = filtered.filter(s => s.favourite);
541
+ const recent = [...filtered]
455
542
  .sort((a, b) => b.file_mtime - a.file_mtime)
456
- .slice(0, RECENT_LIMIT);
457
- const all = [...sessions].sort((a, b) =>
543
+ .slice(0, this._recentLimit);
544
+ const all = [...filtered].sort((a, b) =>
458
545
  this._lookupName(a).localeCompare(this._lookupName(b))
459
546
  );
460
547
 
@@ -760,5 +847,7 @@ export class ClaudeCodeSessionsWidget extends Widget {
760
847
  private readonly _pendingByPath: Map<string, Promise<void>> = new Map();
761
848
  private readonly _rootDir: string;
762
849
  private _resolveNames: boolean = true;
850
+ private _recentLimit: number = DEFAULT_RECENT_LIMIT;
763
851
  private _displayNames: Map<string, string> = new Map();
852
+ private _filter: string = '';
764
853
  }
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;