tabminal 1.1.17 → 1.1.18
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 +5 -4
- package/src/terminal-session.mjs +58 -129
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tabminal",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.18",
|
|
4
4
|
"description": "A modern, persistent web terminal with multi-tab support and real-time system monitoring.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"dev": "node --watch src/server.mjs",
|
|
13
13
|
"build": "node build.mjs",
|
|
14
14
|
"test": "node --test",
|
|
15
|
+
"updep": "npx npm-check-updates -u && npm install",
|
|
15
16
|
"test:watch": "node --test --watch",
|
|
16
17
|
"lint": "eslint ."
|
|
17
18
|
},
|
|
@@ -32,7 +33,7 @@
|
|
|
32
33
|
},
|
|
33
34
|
"dependencies": {
|
|
34
35
|
"@fontsource/monaspace-neon": "^5.2.5",
|
|
35
|
-
"@koa/router": "^
|
|
36
|
+
"@koa/router": "^15.0.0",
|
|
36
37
|
"@mozilla/readability": "^0.6.0",
|
|
37
38
|
"js-tiktoken": "^1.0.21",
|
|
38
39
|
"jsdom": "^27.2.0",
|
|
@@ -41,8 +42,8 @@
|
|
|
41
42
|
"koa-static": "^5.0.0",
|
|
42
43
|
"node-ansiparser": "^2.2.1",
|
|
43
44
|
"node-pty": "^1.0.0",
|
|
44
|
-
"openai": "^6.
|
|
45
|
-
"utilitas": "^2000.3.
|
|
45
|
+
"openai": "^6.10.0",
|
|
46
|
+
"utilitas": "^2000.3.26",
|
|
46
47
|
"ws": "^8.18.3"
|
|
47
48
|
},
|
|
48
49
|
"repository": {
|
package/src/terminal-session.mjs
CHANGED
|
@@ -8,7 +8,6 @@ import { config } from './config.mjs';
|
|
|
8
8
|
const execAsync = promisify(exec);
|
|
9
9
|
const WS_STATE_OPEN = 1;
|
|
10
10
|
const DEFAULT_HISTORY_LIMIT = 512 * 1024; // chars
|
|
11
|
-
const PROMPT_MARKER = '\u001b]1337;TabminalPrompt\u0007';
|
|
12
11
|
const OSC_SEQUENCE_REGEX =
|
|
13
12
|
/\u001b\]1337;(ExitCode=(\d+);CommandB64=([a-zA-Z0-9+/=]+)|TabminalPrompt)\u0007/g;
|
|
14
13
|
const CSI_SEQUENCE_REGEX = /\u001b\[[0-9;?]*[ -\/]*[@-~]/g;
|
|
@@ -31,16 +30,16 @@ export class TerminalSession {
|
|
|
31
30
|
this.createdAt = options.createdAt ?? new Date();
|
|
32
31
|
this.shell = options.shell;
|
|
33
32
|
this.initialCwd = options.initialCwd;
|
|
34
|
-
|
|
33
|
+
|
|
35
34
|
this.title = this.shell ? this.shell.split('/').pop() : 'Terminal';
|
|
36
35
|
this.cwd = this.initialCwd;
|
|
37
36
|
this.inputBuffer = '';
|
|
38
|
-
|
|
37
|
+
|
|
39
38
|
// Format the initial environment object into a static string
|
|
40
39
|
this.env = Object.entries(options.env || {})
|
|
41
40
|
.map(([key, value]) => `${key}=${value}`)
|
|
42
41
|
.join('\n');
|
|
43
|
-
|
|
42
|
+
|
|
44
43
|
this.editorState = options.editorState || {};
|
|
45
44
|
this.executions = options.executions || [];
|
|
46
45
|
|
|
@@ -136,7 +135,6 @@ export class TerminalSession {
|
|
|
136
135
|
|
|
137
136
|
this.dataSubscription = this.pty.onData(this._handleData);
|
|
138
137
|
this.exitSubscription = this.pty.onExit(this._handleExit);
|
|
139
|
-
|
|
140
138
|
this.startTitlePolling();
|
|
141
139
|
}
|
|
142
140
|
|
|
@@ -177,14 +175,14 @@ export class TerminalSession {
|
|
|
177
175
|
const rawLine = lines.slice(1).join(' ');
|
|
178
176
|
const cmdAndArgs = (await execAsync(`ps -o args= -p ${currentPid}`)).stdout.trim();
|
|
179
177
|
const envBlock = rawLine.substring(rawLine.indexOf(cmdAndArgs) + cmdAndArgs.length).trim();
|
|
180
|
-
|
|
178
|
+
|
|
181
179
|
const regex = /([A-Z_][A-Z0-9_]*=)/g;
|
|
182
180
|
const indices = [];
|
|
183
181
|
let match;
|
|
184
182
|
while ((match = regex.exec(envBlock)) !== null) {
|
|
185
183
|
indices.push(match.index);
|
|
186
184
|
}
|
|
187
|
-
|
|
185
|
+
|
|
188
186
|
if (indices.length > 0) {
|
|
189
187
|
const envs = [];
|
|
190
188
|
for (let i = 0; i < indices.length; i++) {
|
|
@@ -311,7 +309,7 @@ export class TerminalSession {
|
|
|
311
309
|
// Ignore prefix if it only contains whitespace or terminal control artifacts (CPR)
|
|
312
310
|
const idx = this.inputBuffer.indexOf('#');
|
|
313
311
|
let line = null;
|
|
314
|
-
|
|
312
|
+
|
|
315
313
|
if (idx !== -1) {
|
|
316
314
|
const prefix = this.inputBuffer.substring(0, idx);
|
|
317
315
|
// Allow whitespace, ESC, [, digits, ;, R (typical CPR response)
|
|
@@ -319,10 +317,10 @@ export class TerminalSession {
|
|
|
319
317
|
line = this.inputBuffer.substring(idx);
|
|
320
318
|
}
|
|
321
319
|
}
|
|
322
|
-
|
|
320
|
+
|
|
323
321
|
if (line) {
|
|
324
322
|
// --- HIJACK DETECTED ---
|
|
325
|
-
|
|
323
|
+
|
|
326
324
|
// 1. Write pending data BEFORE this char to pty
|
|
327
325
|
if (i > startIndex) {
|
|
328
326
|
this.pty.write(data.substring(startIndex, i));
|
|
@@ -331,13 +329,13 @@ export class TerminalSession {
|
|
|
331
329
|
// 2. Execute Hijack Logic
|
|
332
330
|
const prompt = line.substring(1).trim();
|
|
333
331
|
this.inputBuffer = ''; // Reset buffer
|
|
334
|
-
|
|
332
|
+
|
|
335
333
|
// Send Ctrl+U to pty to clear the visual line (since user typed it)
|
|
336
334
|
this.pty.write('\x15');
|
|
337
|
-
|
|
335
|
+
|
|
338
336
|
// Suppress PTY echo (like the Ctrl+U echo) to prevent race conditions with AI stream
|
|
339
337
|
this.suppressPtyOutput = true;
|
|
340
|
-
|
|
338
|
+
|
|
341
339
|
// Send newline and reset cursor to User (visual only)
|
|
342
340
|
this._writeToLogAndBroadcast('\r\n\r\x1b[K');
|
|
343
341
|
|
|
@@ -360,7 +358,7 @@ export class TerminalSession {
|
|
|
360
358
|
}
|
|
361
359
|
// Handle Control Chars (Reset buffer to be safe, except Tab)
|
|
362
360
|
else if (char < ' ' && char !== '\t') {
|
|
363
|
-
|
|
361
|
+
this.inputBuffer = '';
|
|
364
362
|
}
|
|
365
363
|
// Normal Text
|
|
366
364
|
else {
|
|
@@ -382,9 +380,9 @@ export class TerminalSession {
|
|
|
382
380
|
if (entry.command === 'ai' && entry.exitCode === 0 && entry.output) {
|
|
383
381
|
// Case A: Successful AI Interaction -> Flush pending history into this turn
|
|
384
382
|
const userContent = (pendingShellHistory ? pendingShellHistory.trim() + '\n\n' : '') + entry.input;
|
|
385
|
-
|
|
383
|
+
|
|
386
384
|
conversationHistory.push({ request: userContent, response: entry.output });
|
|
387
|
-
|
|
385
|
+
|
|
388
386
|
pendingShellHistory = ''; // Reset buffer
|
|
389
387
|
} else {
|
|
390
388
|
// Case B: Shell Command or Failed AI -> Accumulate history
|
|
@@ -393,127 +391,58 @@ export class TerminalSession {
|
|
|
393
391
|
pendingShellHistory += record + '\n';
|
|
394
392
|
}
|
|
395
393
|
}
|
|
396
|
-
|
|
394
|
+
|
|
397
395
|
return { conversationHistory, pendingShellHistory };
|
|
398
396
|
}
|
|
399
397
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
//
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
const finalPrompt = `${currentContext}\n\nQuestion: ${prompt}`;
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
if (config.debug) {
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
console.log('[AI Context Build]');
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
console.log('History:', JSON.stringify(conversationHistory, null, 2));
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
console.log('Current Prompt Preview:', JSON.stringify(finalPrompt, null, 2));
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
const startTime = new Date();
|
|
467
|
-
|
|
468
|
-
let fullResponse = '';
|
|
469
|
-
|
|
470
|
-
let isFirstChunk = true;
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
try {
|
|
475
|
-
|
|
476
|
-
const streamCallback = (chunk) => {
|
|
477
|
-
|
|
478
|
-
if (chunk && chunk.text) {
|
|
479
|
-
|
|
480
|
-
let text = chunk.text;
|
|
481
|
-
|
|
482
|
-
// Normalize newlines for terminal
|
|
483
|
-
|
|
484
|
-
text = text.replace(/\n/g, '\r\n');
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
if (isFirstChunk) {
|
|
489
|
-
|
|
490
|
-
const prefix = '\n\nTabminal:\n\n';
|
|
491
|
-
|
|
492
|
-
text = prefix + text;
|
|
493
|
-
|
|
494
|
-
isFirstChunk = false;
|
|
495
|
-
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
this._writeToLogAndBroadcast(text);
|
|
501
|
-
|
|
398
|
+
async _handleAiCommand(prompt, options = {}) {
|
|
399
|
+
// Prevent duplicate logging from shell integration
|
|
400
|
+
this.skipNextShellLog = true;
|
|
401
|
+
// Ensure clean line start and set Cyan color (No prefix yet)
|
|
402
|
+
this._writeToLogAndBroadcast('\r\x1b[K\x1b[36m');
|
|
403
|
+
// Gather Context (Current Session Only)
|
|
404
|
+
const cleanHistory = (this.executions && this.executions.length > 0) ? this.executions : [];
|
|
405
|
+
// Build Context
|
|
406
|
+
const { conversationHistory, pendingShellHistory } = this._buildAiContext(cleanHistory);
|
|
407
|
+
// Construct Current Prompt
|
|
408
|
+
const currentContext = `Recent Shell History:\n${pendingShellHistory}\nEnvironment:\n${this.env}\nCurrent Path: ${this.cwd}`;
|
|
409
|
+
const finalPrompt = `${currentContext}\n\nQuestion: ${prompt}`;
|
|
410
|
+
if (config.debug) {
|
|
411
|
+
console.log('[AI Context Build]');
|
|
412
|
+
console.log('History:', JSON.stringify(conversationHistory, null, 2));
|
|
413
|
+
console.log('Current Prompt Preview:', JSON.stringify(finalPrompt, null, 2));
|
|
414
|
+
}
|
|
415
|
+
const startTime = new Date();
|
|
416
|
+
let fullResponse = '';
|
|
417
|
+
let isFirstChunk = true;
|
|
418
|
+
try {
|
|
419
|
+
const streamCallback = (chunk) => {
|
|
420
|
+
// console.log('Chunk Received:');
|
|
421
|
+
// console.log(chunk);
|
|
422
|
+
if (chunk && chunk.text) {
|
|
423
|
+
let text = chunk.text;
|
|
424
|
+
// Normalize newlines for terminal
|
|
425
|
+
text = text.replace(/\n/g, '\r\n');
|
|
426
|
+
if (isFirstChunk) {
|
|
427
|
+
const prefix = '\n\nTabminal:\n\n';
|
|
428
|
+
text = prefix + text;
|
|
429
|
+
isFirstChunk = false;
|
|
502
430
|
}
|
|
503
|
-
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
|
|
431
|
+
this._writeToLogAndBroadcast(text);
|
|
432
|
+
}
|
|
433
|
+
};
|
|
434
|
+
// console.log('Start AI Prompt...');
|
|
435
|
+
const result = await alan.prompt(finalPrompt, {
|
|
507
436
|
stream: streamCallback,
|
|
508
437
|
delta: true,
|
|
509
438
|
messages: conversationHistory,
|
|
510
439
|
trimBeginning: true
|
|
511
440
|
});
|
|
512
|
-
|
|
441
|
+
|
|
513
442
|
if (result && result.text) {
|
|
514
443
|
fullResponse = result.text;
|
|
515
444
|
}
|
|
516
|
-
|
|
445
|
+
|
|
517
446
|
// End color and new line
|
|
518
447
|
this._writeToLogAndBroadcast('\x1b[0m\r\n');
|
|
519
448
|
|
|
@@ -529,7 +458,7 @@ export class TerminalSession {
|
|
|
529
458
|
|
|
530
459
|
} catch (e) {
|
|
531
460
|
this._writeToLogAndBroadcast(`\x1b[31mAI Error: ${e.message}\x1b[0m\r\n`);
|
|
532
|
-
|
|
461
|
+
|
|
533
462
|
this._logCommandExecution({
|
|
534
463
|
command: 'ai',
|
|
535
464
|
exitCode: 1,
|
|
@@ -539,10 +468,10 @@ export class TerminalSession {
|
|
|
539
468
|
completedAt: new Date()
|
|
540
469
|
});
|
|
541
470
|
}
|
|
542
|
-
|
|
471
|
+
|
|
543
472
|
// Resume PTY output
|
|
544
473
|
this.suppressPtyOutput = false;
|
|
545
|
-
|
|
474
|
+
|
|
546
475
|
// Restore prompt by sending \r to pty (empty command)
|
|
547
476
|
this.pty.write('\r');
|
|
548
477
|
}
|
|
@@ -930,7 +859,7 @@ export class TerminalSession {
|
|
|
930
859
|
entry.startedAt && entry.completedAt
|
|
931
860
|
? entry.completedAt.getTime() - entry.startedAt.getTime()
|
|
932
861
|
: null;
|
|
933
|
-
|
|
862
|
+
|
|
934
863
|
const record = {
|
|
935
864
|
command: entry.command ?? null,
|
|
936
865
|
exitCode: entry.exitCode ?? null,
|