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 +4 -0
- package/lib/types.d.ts +7 -0
- package/lib/widget.d.ts +19 -1
- package/lib/widget.js +124 -41
- package/package.json +1 -2
- package/src/index.ts +4 -0
- package/src/types.ts +9 -0
- package/src/widget.ts +136 -47
- package/style/base.css +23 -0
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
|
|
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
|
|
225
|
-
|
|
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
|
|
384
|
-
const
|
|
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,
|
|
387
|
-
const all = [...
|
|
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.
|
|
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
|
|
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
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
|
454
|
-
const
|
|
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,
|
|
457
|
-
const all = [...
|
|
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;
|