tabminal 2.0.13 → 2.0.14
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/package.json +4 -4
- package/src/server.mjs +2 -2
- package/src/terminal-manager.mjs +46 -11
- package/src/terminal-session.mjs +102 -7
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tabminal",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.14",
|
|
4
4
|
"description": "A modern, persistent web terminal with multi-tab support and real-time system monitoring.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"@fontsource/monaspace-neon": "^5.2.5",
|
|
36
36
|
"@koa/router": "^15.4.0",
|
|
37
37
|
"@mozilla/readability": "^0.6.0",
|
|
38
|
-
"jsdom": "^29.0.
|
|
38
|
+
"jsdom": "^29.0.1",
|
|
39
39
|
"koa": "^3.1.2",
|
|
40
40
|
"koa-bodyparser": "^4.4.1",
|
|
41
41
|
"koa-static": "^5.0.0",
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
"node-pty": "^1.1.0",
|
|
44
44
|
"openai": "^6.32.0",
|
|
45
45
|
"utilitas": "^2001.1.135",
|
|
46
|
-
"ws": "^8.
|
|
46
|
+
"ws": "^8.20.0",
|
|
47
47
|
"xterm-addon-serialize": "^0.11.0",
|
|
48
48
|
"xterm-headless": "^5.3.0"
|
|
49
49
|
},
|
|
@@ -56,6 +56,6 @@
|
|
|
56
56
|
},
|
|
57
57
|
"homepage": "https://github.com/leask/tabminal#readme",
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"eslint": "^10.0
|
|
59
|
+
"eslint": "^10.1.0"
|
|
60
60
|
}
|
|
61
61
|
}
|
package/src/server.mjs
CHANGED
|
@@ -199,9 +199,9 @@ router.post('/api/sessions', (ctx) => {
|
|
|
199
199
|
};
|
|
200
200
|
});
|
|
201
201
|
|
|
202
|
-
router.delete('/api/sessions/:id', (ctx) => {
|
|
202
|
+
router.delete('/api/sessions/:id', async (ctx) => {
|
|
203
203
|
const { id } = ctx.params;
|
|
204
|
-
terminalManager.removeSession(id);
|
|
204
|
+
await terminalManager.removeSession(id);
|
|
205
205
|
ctx.status = 204;
|
|
206
206
|
});
|
|
207
207
|
|
package/src/terminal-manager.mjs
CHANGED
|
@@ -60,11 +60,29 @@ export class TerminalManager {
|
|
|
60
60
|
constructor() {
|
|
61
61
|
this.sessions = new Map();
|
|
62
62
|
this.snapshotPersistTimers = new Map();
|
|
63
|
+
this.sessionPersistenceChains = new Map();
|
|
63
64
|
this.lastCols = initialCols;
|
|
64
65
|
this.lastRows = initialRows;
|
|
65
66
|
this.disposing = false;
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
queueSessionPersistence(id, operation) {
|
|
70
|
+
const previous = this.sessionPersistenceChains.get(id)
|
|
71
|
+
|| Promise.resolve();
|
|
72
|
+
const next = previous
|
|
73
|
+
.catch(() => {})
|
|
74
|
+
.then(operation);
|
|
75
|
+
|
|
76
|
+
this.sessionPersistenceChains.set(id, next);
|
|
77
|
+
next.finally(() => {
|
|
78
|
+
if (this.sessionPersistenceChains.get(id) === next) {
|
|
79
|
+
this.sessionPersistenceChains.delete(id);
|
|
80
|
+
}
|
|
81
|
+
}).catch(() => {});
|
|
82
|
+
|
|
83
|
+
return next;
|
|
84
|
+
}
|
|
85
|
+
|
|
68
86
|
createSession(restoredData = null) {
|
|
69
87
|
// Use ID from options if present, otherwise generate new
|
|
70
88
|
const id = (restoredData && restoredData.id) ? restoredData.id : crypto.randomUUID();
|
|
@@ -200,24 +218,28 @@ precmd_functions+=(_tabminal_zsh_apply_prompt_marker)
|
|
|
200
218
|
});
|
|
201
219
|
}
|
|
202
220
|
|
|
221
|
+
this.sessions.set(id, session);
|
|
222
|
+
|
|
203
223
|
// Initial save
|
|
204
|
-
this.saveSessionState(session);
|
|
224
|
+
void this.saveSessionState(session);
|
|
205
225
|
|
|
206
226
|
ptyProcess.onExit(() => {
|
|
207
|
-
this.removeSession(id);
|
|
227
|
+
void this.removeSession(id);
|
|
208
228
|
// Cleanup temp files
|
|
209
229
|
try {
|
|
210
230
|
if (initDirPath && fs.existsSync(initDirPath)) fs.rmSync(initDirPath, { recursive: true, force: true });
|
|
211
231
|
} catch { /* ignore cleanup errors */ }
|
|
212
232
|
});
|
|
213
|
-
|
|
214
|
-
this.sessions.set(id, session);
|
|
215
233
|
debugLog(`[Manager] Created session ${id}`);
|
|
216
234
|
return session;
|
|
217
235
|
}
|
|
218
236
|
|
|
219
237
|
saveSessionState(session) {
|
|
220
|
-
|
|
238
|
+
if (this.sessions.get(session.id) !== session) {
|
|
239
|
+
return Promise.resolve();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return this.queueSessionPersistence(session.id, () => persistence.saveSession(session.id, {
|
|
221
243
|
id: session.id,
|
|
222
244
|
title: session.title,
|
|
223
245
|
cwd: session.cwd,
|
|
@@ -227,7 +249,7 @@ precmd_functions+=(_tabminal_zsh_apply_prompt_marker)
|
|
|
227
249
|
createdAt: session.createdAt,
|
|
228
250
|
editorState: session.editorState,
|
|
229
251
|
executions: session.executions
|
|
230
|
-
});
|
|
252
|
+
}));
|
|
231
253
|
}
|
|
232
254
|
|
|
233
255
|
updateSessionState(id, data) {
|
|
@@ -250,13 +272,17 @@ precmd_functions+=(_tabminal_zsh_apply_prompt_marker)
|
|
|
250
272
|
clearTimeout(existing);
|
|
251
273
|
}
|
|
252
274
|
|
|
253
|
-
const timer = setTimeout(
|
|
275
|
+
const timer = setTimeout(() => {
|
|
254
276
|
this.snapshotPersistTimers.delete(id);
|
|
255
277
|
const currentSession = this.sessions.get(id);
|
|
256
278
|
if (!currentSession) return;
|
|
257
279
|
|
|
258
|
-
|
|
259
|
-
|
|
280
|
+
void this.queueSessionPersistence(id, async () => {
|
|
281
|
+
if (this.sessions.get(id) !== currentSession) return;
|
|
282
|
+
const snapshot = await currentSession.serializeSnapshot();
|
|
283
|
+
if (this.sessions.get(id) !== currentSession) return;
|
|
284
|
+
await persistence.saveSessionSnapshot(id, snapshot);
|
|
285
|
+
});
|
|
260
286
|
}, 250);
|
|
261
287
|
|
|
262
288
|
this.snapshotPersistTimers.set(id, timer);
|
|
@@ -280,7 +306,7 @@ precmd_functions+=(_tabminal_zsh_apply_prompt_marker)
|
|
|
280
306
|
this.lastRows = rows;
|
|
281
307
|
}
|
|
282
308
|
|
|
283
|
-
removeSession(id) {
|
|
309
|
+
async removeSession(id) {
|
|
284
310
|
const session = this.sessions.get(id);
|
|
285
311
|
if (session) {
|
|
286
312
|
const timer = this.snapshotPersistTimers.get(id);
|
|
@@ -288,9 +314,18 @@ precmd_functions+=(_tabminal_zsh_apply_prompt_marker)
|
|
|
288
314
|
clearTimeout(timer);
|
|
289
315
|
this.snapshotPersistTimers.delete(id);
|
|
290
316
|
}
|
|
317
|
+
try {
|
|
318
|
+
if (process.platform === 'win32') {
|
|
319
|
+
session.pty.kill();
|
|
320
|
+
} else {
|
|
321
|
+
session.pty.kill('SIGHUP');
|
|
322
|
+
}
|
|
323
|
+
} catch {
|
|
324
|
+
// ignore
|
|
325
|
+
}
|
|
291
326
|
session.dispose();
|
|
292
327
|
this.sessions.delete(id);
|
|
293
|
-
persistence.deleteSession(id);
|
|
328
|
+
await this.queueSessionPersistence(id, () => persistence.deleteSession(id));
|
|
294
329
|
debugLog(`[Manager] Removed session ${id}`);
|
|
295
330
|
}
|
|
296
331
|
}
|
package/src/terminal-session.mjs
CHANGED
|
@@ -121,7 +121,9 @@ export class TerminalSession {
|
|
|
121
121
|
this.captureStartedAt = null;
|
|
122
122
|
this.lastExecution = null;
|
|
123
123
|
this.skipNextShellLog = false;
|
|
124
|
+
this.skipNextShellLogResetTimer = null;
|
|
124
125
|
this.partialSequenceBuffer = '';
|
|
126
|
+
this.activeAiRun = null;
|
|
125
127
|
this.snapshotScrollback = estimateSnapshotScrollback(
|
|
126
128
|
this.pty.cols,
|
|
127
129
|
this.pty.rows,
|
|
@@ -352,6 +354,7 @@ export class TerminalSession {
|
|
|
352
354
|
|
|
353
355
|
dispose() {
|
|
354
356
|
this.stopTitlePolling();
|
|
357
|
+
this._clearSkipNextShellLogResetTimer();
|
|
355
358
|
this.clients.clear();
|
|
356
359
|
this.pendingClients.clear();
|
|
357
360
|
this.dataSubscription?.dispose?.();
|
|
@@ -416,6 +419,11 @@ export class TerminalSession {
|
|
|
416
419
|
}
|
|
417
420
|
|
|
418
421
|
write(data) {
|
|
422
|
+
if (this.activeAiRun && typeof data === 'string') {
|
|
423
|
+
this._handleInputDuringAi(data);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
419
427
|
if (typeof data === 'string' && data.startsWith('\x1b')) {
|
|
420
428
|
this.pty.write(data);
|
|
421
429
|
this.inputBuffer = '';
|
|
@@ -503,6 +511,19 @@ export class TerminalSession {
|
|
|
503
511
|
}
|
|
504
512
|
}
|
|
505
513
|
|
|
514
|
+
_handleInputDuringAi(data) {
|
|
515
|
+
for (const char of data) {
|
|
516
|
+
if (char === '\x03') {
|
|
517
|
+
this._cancelActiveAiRun('ctrl_c');
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (char === '\x04') {
|
|
521
|
+
this._cancelActiveAiRun('ctrl_d');
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
506
527
|
_buildAiContext(history) {
|
|
507
528
|
let pendingShellHistory = '';
|
|
508
529
|
const conversationHistory = [];
|
|
@@ -526,9 +547,67 @@ export class TerminalSession {
|
|
|
526
547
|
return { conversationHistory, pendingShellHistory };
|
|
527
548
|
}
|
|
528
549
|
|
|
550
|
+
_promptAi(prompt, options) {
|
|
551
|
+
return alan.prompt(prompt, options);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
_clearSkipNextShellLogResetTimer() {
|
|
555
|
+
if (this.skipNextShellLogResetTimer) {
|
|
556
|
+
clearTimeout(this.skipNextShellLogResetTimer);
|
|
557
|
+
this.skipNextShellLogResetTimer = null;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
_scheduleSkipNextShellLogReset() {
|
|
562
|
+
this._clearSkipNextShellLogResetTimer();
|
|
563
|
+
this.skipNextShellLogResetTimer = setTimeout(() => {
|
|
564
|
+
this.skipNextShellLog = false;
|
|
565
|
+
this.skipNextShellLogResetTimer = null;
|
|
566
|
+
}, 500);
|
|
567
|
+
this.skipNextShellLogResetTimer.unref?.();
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
_finishAiInteraction(mode = 'normal') {
|
|
571
|
+
this.suppressPtyOutput = false;
|
|
572
|
+
this._scheduleSkipNextShellLogReset();
|
|
573
|
+
|
|
574
|
+
if (mode === 'ctrl_c') {
|
|
575
|
+
this._writeToLogAndBroadcast('\x1b[0m');
|
|
576
|
+
this.pty.write('\x03');
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
this._writeToLogAndBroadcast('\x1b[0m\r\n');
|
|
581
|
+
if (mode === 'ctrl_d') {
|
|
582
|
+
this.pty.write('\x15');
|
|
583
|
+
}
|
|
584
|
+
this.pty.write('\r');
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
_cancelActiveAiRun(reason) {
|
|
588
|
+
const run = this.activeAiRun;
|
|
589
|
+
if (!run || run.cancelled) {
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
run.cancelled = true;
|
|
594
|
+
this.activeAiRun = null;
|
|
595
|
+
this._logCommandExecution({
|
|
596
|
+
command: 'ai',
|
|
597
|
+
exitCode: 130,
|
|
598
|
+
input: run.prompt,
|
|
599
|
+
output: 'AI generation cancelled.',
|
|
600
|
+
startedAt: run.startedAt,
|
|
601
|
+
completedAt: new Date()
|
|
602
|
+
});
|
|
603
|
+
this._finishAiInteraction(reason);
|
|
604
|
+
return true;
|
|
605
|
+
}
|
|
606
|
+
|
|
529
607
|
async _handleAiCommand(prompt) {
|
|
530
608
|
// Prevent duplicate logging from shell integration
|
|
531
609
|
this.skipNextShellLog = true;
|
|
610
|
+
this._clearSkipNextShellLogResetTimer();
|
|
532
611
|
// Ensure clean line start and set Cyan color (No prefix yet)
|
|
533
612
|
this._writeToLogAndBroadcast('\r\x1b[K\x1b[36m');
|
|
534
613
|
// Gather Context (Current Session Only)
|
|
@@ -546,8 +625,17 @@ export class TerminalSession {
|
|
|
546
625
|
const startTime = new Date();
|
|
547
626
|
let fullResponse = '';
|
|
548
627
|
let isFirstChunk = true;
|
|
628
|
+
const run = {
|
|
629
|
+
prompt,
|
|
630
|
+
startedAt: startTime,
|
|
631
|
+
cancelled: false
|
|
632
|
+
};
|
|
633
|
+
this.activeAiRun = run;
|
|
549
634
|
try {
|
|
550
635
|
const streamCallback = (chunk) => {
|
|
636
|
+
if (this.activeAiRun !== run || run.cancelled) {
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
551
639
|
// console.log('Chunk Received:');
|
|
552
640
|
// console.log(chunk);
|
|
553
641
|
if (chunk && chunk.text) {
|
|
@@ -563,13 +651,17 @@ export class TerminalSession {
|
|
|
563
651
|
}
|
|
564
652
|
};
|
|
565
653
|
// console.log('Start AI Prompt...');
|
|
566
|
-
const result = await
|
|
654
|
+
const result = await this._promptAi(finalPrompt, {
|
|
567
655
|
stream: streamCallback,
|
|
568
656
|
delta: true,
|
|
569
657
|
messages: conversationHistory,
|
|
570
658
|
trimBeginning: true
|
|
571
659
|
});
|
|
572
660
|
|
|
661
|
+
if (this.activeAiRun !== run || run.cancelled) {
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
|
|
573
665
|
if (result && result.text) {
|
|
574
666
|
fullResponse = result.text;
|
|
575
667
|
}
|
|
@@ -588,6 +680,9 @@ export class TerminalSession {
|
|
|
588
680
|
});
|
|
589
681
|
|
|
590
682
|
} catch (e) {
|
|
683
|
+
if (this.activeAiRun !== run || run.cancelled) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
591
686
|
this._writeToLogAndBroadcast(`\x1b[31mAI Error: ${e.message}\x1b[0m\r\n`);
|
|
592
687
|
|
|
593
688
|
this._logCommandExecution({
|
|
@@ -598,13 +693,12 @@ export class TerminalSession {
|
|
|
598
693
|
startedAt: startTime,
|
|
599
694
|
completedAt: new Date()
|
|
600
695
|
});
|
|
696
|
+
} finally {
|
|
697
|
+
if (this.activeAiRun === run) {
|
|
698
|
+
this.activeAiRun = null;
|
|
699
|
+
this._finishAiInteraction('normal');
|
|
700
|
+
}
|
|
601
701
|
}
|
|
602
|
-
|
|
603
|
-
// Resume PTY output
|
|
604
|
-
this.suppressPtyOutput = false;
|
|
605
|
-
|
|
606
|
-
// Restore prompt by sending \r to pty (empty command)
|
|
607
|
-
this.pty.write('\r');
|
|
608
702
|
}
|
|
609
703
|
|
|
610
704
|
_handleResize(cols, rows) {
|
|
@@ -639,6 +733,7 @@ export class TerminalSession {
|
|
|
639
733
|
_handleExitCodeSequence(exitCodeStr, cmdB64) {
|
|
640
734
|
if (this.skipNextShellLog) {
|
|
641
735
|
this.skipNextShellLog = false;
|
|
736
|
+
this._clearSkipNextShellLogResetTimer();
|
|
642
737
|
this.captureBuffer = '';
|
|
643
738
|
this.captureStartedAt = null;
|
|
644
739
|
return;
|