tabminal 2.0.13 → 2.0.15

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.
@@ -13,7 +13,7 @@ const {
13
13
  const WS_STATE_OPEN = 1;
14
14
  const DEFAULT_HISTORY_LIMIT = 512 * 1024; // chars
15
15
  const OSC_SEQUENCE_REGEX =
16
- /\u001b\]1337;(ExitCode=(\d+);CommandB64=([a-zA-Z0-9+/=]+)|TabminalPrompt)\u0007/g;
16
+ /\u001b\]1337;(ExitCode=(\d+);CommandB64=([a-zA-Z0-9+/=]+)|CommandStartB64=([a-zA-Z0-9+/=]+)|TabminalPrompt)\u0007/g;
17
17
  const EXTRA_PRIVATE_MODE_REGEX = /\u001b\[\?(1005|1006|1015)([hl])/g;
18
18
  const CSI_SEQUENCE_REGEX = /\u001b\[[0-9;?]*[ -\/]*[@-~]/g;
19
19
  const OSC_STRIP_REGEX = /\u001b\][\s\S]*?(?:\u0007|\u001b\\)/g;
@@ -29,6 +29,13 @@ const IGNORED_COMMANDS = [
29
29
  'TABMINAL_SHELL_READY=1'
30
30
  ];
31
31
 
32
+ function isIgnoredExecutionCommand(command) {
33
+ return !!(
34
+ command
35
+ && IGNORED_COMMANDS.some((ignored) => command.includes(ignored))
36
+ );
37
+ }
38
+
32
39
  const PROMPT_PREFIX = "You are now operating as an AI terminal assistant. Your name is `Tabminal`. You will assist users in resolving terminal or coding issues and answering other inquiries. When troubleshooting terminal errors, you will be provided with the execution history to understand the context. However, please focus primarily on the most recent runtime errors and the user's latest questions. Keep your answers concise and accurate. Resolve the issue clearly and provide the reasoning while avoiding lengthy elaborations. Most user terminal variable keys are normal under typical circumstances and do not need to be treated as security risks.\n\n";
33
40
 
34
41
  async function loadHeadlessXtermPackages() {
@@ -96,10 +103,17 @@ export class TerminalSession {
96
103
  this.id = options.id;
97
104
  this.manager = options.manager;
98
105
  this.createdAt = options.createdAt ?? new Date();
106
+ this.updatedAt = this.createdAt;
99
107
  this.shell = options.shell;
100
108
  this.initialCwd = options.initialCwd;
101
-
102
- this.title = this.shell ? this.shell.split('/').pop() : 'Terminal';
109
+ this.managed = options.managed || null;
110
+ this.persistent = options.persistent !== false;
111
+ this.removeOnExit = options.removeOnExit !== false;
112
+ this.enableAiHijack = options.enableAiHijack !== false;
113
+ this.enableTitlePolling = options.enableTitlePolling !== false;
114
+
115
+ this.title = options.title
116
+ || (this.shell ? this.shell.split('/').pop() : 'Terminal');
103
117
  this.cwd = this.initialCwd;
104
118
  this.inputBuffer = '';
105
119
 
@@ -116,12 +130,20 @@ export class TerminalSession {
116
130
  this.clients = new Set();
117
131
  this.pendingClients = new Map();
118
132
  this.closed = false;
133
+ this.exitStatus = null;
134
+ this.exitWaiters = [];
135
+ this.stateListeners = new Set();
119
136
  this.pollingInterval = null;
120
137
  this.captureBuffer = '';
121
138
  this.captureStartedAt = null;
122
139
  this.lastExecution = null;
140
+ this.executionCounter = 0;
141
+ this.currentExecutionId = '';
142
+ this.ignoreCurrentExecution = false;
123
143
  this.skipNextShellLog = false;
144
+ this.skipNextShellLogResetTimer = null;
124
145
  this.partialSequenceBuffer = '';
146
+ this.activeAiRun = null;
125
147
  this.snapshotScrollback = estimateSnapshotScrollback(
126
148
  this.pty.cols,
127
149
  this.pty.rows,
@@ -150,7 +172,9 @@ export class TerminalSession {
150
172
  const newTitle = s.substring(2);
151
173
  if (newTitle && newTitle !== this.title) {
152
174
  this.title = newTitle;
175
+ this.updatedAt = new Date();
153
176
  this._broadcast({ type: 'meta', title: this.title, cwd: this.cwd, env: this.env, cols: this.pty.cols, rows: this.pty.rows });
177
+ this._emitStateChange();
154
178
  }
155
179
  } else if (s.startsWith('7;')) {
156
180
  try {
@@ -159,7 +183,9 @@ export class TerminalSession {
159
183
  const newCwd = decodeURIComponent(url.pathname);
160
184
  if (newCwd !== this.cwd) {
161
185
  this.cwd = newCwd;
186
+ this.updatedAt = new Date();
162
187
  this._broadcast({ type: 'meta', title: this.title, cwd: this.cwd, env: this.env, cols: this.pty.cols, rows: this.pty.rows });
188
+ this._emitStateChange();
163
189
  }
164
190
  }
165
191
  } catch { /* ignore */ }
@@ -196,6 +222,8 @@ export class TerminalSession {
196
222
  const exitCodeStr = match[2];
197
223
  const cmdB64 = match[3];
198
224
  this._handleExitCodeSequence(exitCodeStr, cmdB64);
225
+ } else if (sequence.startsWith('CommandStartB64=')) {
226
+ this._handleCommandStartSequence(match[4]);
199
227
  } else {
200
228
  this._handlePromptMarker();
201
229
  }
@@ -213,27 +241,43 @@ export class TerminalSession {
213
241
 
214
242
  this._appendSnapshotData(cleaned);
215
243
  this._appendHistory(cleaned);
244
+ this.updatedAt = new Date();
216
245
  this.ansiParser.parse(cleaned);
217
246
  if (this.manager?.scheduleSnapshotPersist) {
218
247
  this.manager.scheduleSnapshotPersist(this.id);
219
248
  }
220
249
  this._broadcast({ type: 'output', data: cleaned });
250
+ this._emitStateChange();
221
251
  };
222
252
 
223
253
  this._handleExit = (details) => {
224
254
  this.closed = true;
255
+ this.exitStatus = {
256
+ exitCode: Number.isFinite(details?.exitCode)
257
+ ? details.exitCode
258
+ : null,
259
+ signal: details?.signal ?? null
260
+ };
261
+ this.updatedAt = new Date();
225
262
  this.stopTitlePolling();
226
263
  this._broadcast({
227
264
  type: 'status',
228
265
  status: 'terminated',
229
- code: details?.exitCode ?? 0,
230
- signal: details?.signal ?? null
266
+ code: this.exitStatus.exitCode ?? 0,
267
+ signal: this.exitStatus.signal
231
268
  });
269
+ this._emitStateChange();
270
+ for (const waiter of this.exitWaiters) {
271
+ waiter(this.exitStatus);
272
+ }
273
+ this.exitWaiters.length = 0;
232
274
  };
233
275
 
234
276
  this.dataSubscription = this.pty.onData(this._handleData);
235
277
  this.exitSubscription = this.pty.onExit(this._handleExit);
236
- this.startTitlePolling();
278
+ if (this.enableTitlePolling) {
279
+ this.startTitlePolling();
280
+ }
237
281
  }
238
282
 
239
283
  startTitlePolling() {
@@ -321,7 +365,9 @@ export class TerminalSession {
321
365
  if (titleChanged) this.title = newTitle;
322
366
  if (envChanged) this.env = newEnv;
323
367
  if (cwdChanged) this.cwd = newCwd;
368
+ this.updatedAt = new Date();
324
369
  this._broadcast({ type: 'meta', title: this.title, cwd: this.cwd, env: this.env, cols: this.pty.cols, rows: this.pty.rows });
370
+ this._emitStateChange();
325
371
  }
326
372
  } catch { /* ignore */ }
327
373
  };
@@ -352,6 +398,7 @@ export class TerminalSession {
352
398
 
353
399
  dispose() {
354
400
  this.stopTitlePolling();
401
+ this._clearSkipNextShellLogResetTimer();
355
402
  this.clients.clear();
356
403
  this.pendingClients.clear();
357
404
  this.dataSubscription?.dispose?.();
@@ -369,11 +416,40 @@ export class TerminalSession {
369
416
  this.manager.scheduleSnapshotPersist(this.id);
370
417
  }
371
418
  this._broadcast({ type: 'meta', title: this.title, cwd: this.cwd, env: this.env, cols, rows });
419
+ this.updatedAt = new Date();
420
+ this._emitStateChange();
372
421
  if (this.manager && this.manager.saveSessionState) {
373
422
  this.manager.saveSessionState(this);
374
423
  }
375
424
  }
376
425
 
426
+ waitForExit() {
427
+ if (this.exitStatus) {
428
+ return Promise.resolve(this.exitStatus);
429
+ }
430
+ return new Promise((resolve) => {
431
+ this.exitWaiters.push(resolve);
432
+ });
433
+ }
434
+
435
+ onStateChange(listener) {
436
+ if (typeof listener !== 'function') return () => {};
437
+ this.stateListeners.add(listener);
438
+ return () => {
439
+ this.stateListeners.delete(listener);
440
+ };
441
+ }
442
+
443
+ _emitStateChange() {
444
+ for (const listener of this.stateListeners) {
445
+ try {
446
+ listener(this);
447
+ } catch {
448
+ // Ignore state listener failures.
449
+ }
450
+ }
451
+ }
452
+
377
453
  async restoreSnapshot(snapshot) {
378
454
  if (typeof snapshot !== 'string' || !snapshot) return;
379
455
  this.history = snapshot;
@@ -416,6 +492,11 @@ export class TerminalSession {
416
492
  }
417
493
 
418
494
  write(data) {
495
+ if (this.activeAiRun && typeof data === 'string') {
496
+ this._handleInputDuringAi(data);
497
+ return;
498
+ }
499
+
419
500
  if (typeof data === 'string' && data.startsWith('\x1b')) {
420
501
  this.pty.write(data);
421
502
  this.inputBuffer = '';
@@ -428,7 +509,7 @@ export class TerminalSession {
428
509
  }
429
510
 
430
511
  let startIndex = 0;
431
- const aiEnabled = this._isAiEnabled();
512
+ const aiEnabled = this.enableAiHijack && this._isAiEnabled();
432
513
  for (let i = 0; i < data.length; i++) {
433
514
  const char = data[i];
434
515
 
@@ -503,6 +584,19 @@ export class TerminalSession {
503
584
  }
504
585
  }
505
586
 
587
+ _handleInputDuringAi(data) {
588
+ for (const char of data) {
589
+ if (char === '\x03') {
590
+ this._cancelActiveAiRun('ctrl_c');
591
+ return;
592
+ }
593
+ if (char === '\x04') {
594
+ this._cancelActiveAiRun('ctrl_d');
595
+ return;
596
+ }
597
+ }
598
+ }
599
+
506
600
  _buildAiContext(history) {
507
601
  let pendingShellHistory = '';
508
602
  const conversationHistory = [];
@@ -526,9 +620,67 @@ export class TerminalSession {
526
620
  return { conversationHistory, pendingShellHistory };
527
621
  }
528
622
 
623
+ _promptAi(prompt, options) {
624
+ return alan.prompt(prompt, options);
625
+ }
626
+
627
+ _clearSkipNextShellLogResetTimer() {
628
+ if (this.skipNextShellLogResetTimer) {
629
+ clearTimeout(this.skipNextShellLogResetTimer);
630
+ this.skipNextShellLogResetTimer = null;
631
+ }
632
+ }
633
+
634
+ _scheduleSkipNextShellLogReset() {
635
+ this._clearSkipNextShellLogResetTimer();
636
+ this.skipNextShellLogResetTimer = setTimeout(() => {
637
+ this.skipNextShellLog = false;
638
+ this.skipNextShellLogResetTimer = null;
639
+ }, 500);
640
+ this.skipNextShellLogResetTimer.unref?.();
641
+ }
642
+
643
+ _finishAiInteraction(mode = 'normal') {
644
+ this.suppressPtyOutput = false;
645
+ this._scheduleSkipNextShellLogReset();
646
+
647
+ if (mode === 'ctrl_c') {
648
+ this._writeToLogAndBroadcast('\x1b[0m');
649
+ this.pty.write('\x03');
650
+ return;
651
+ }
652
+
653
+ this._writeToLogAndBroadcast('\x1b[0m\r\n');
654
+ if (mode === 'ctrl_d') {
655
+ this.pty.write('\x15');
656
+ }
657
+ this.pty.write('\r');
658
+ }
659
+
660
+ _cancelActiveAiRun(reason) {
661
+ const run = this.activeAiRun;
662
+ if (!run || run.cancelled) {
663
+ return false;
664
+ }
665
+
666
+ run.cancelled = true;
667
+ this.activeAiRun = null;
668
+ this._logCommandExecution({
669
+ command: 'ai',
670
+ exitCode: 130,
671
+ input: run.prompt,
672
+ output: 'AI generation cancelled.',
673
+ startedAt: run.startedAt,
674
+ completedAt: new Date()
675
+ });
676
+ this._finishAiInteraction(reason);
677
+ return true;
678
+ }
679
+
529
680
  async _handleAiCommand(prompt) {
530
681
  // Prevent duplicate logging from shell integration
531
682
  this.skipNextShellLog = true;
683
+ this._clearSkipNextShellLogResetTimer();
532
684
  // Ensure clean line start and set Cyan color (No prefix yet)
533
685
  this._writeToLogAndBroadcast('\r\x1b[K\x1b[36m');
534
686
  // Gather Context (Current Session Only)
@@ -546,8 +698,17 @@ export class TerminalSession {
546
698
  const startTime = new Date();
547
699
  let fullResponse = '';
548
700
  let isFirstChunk = true;
701
+ const run = {
702
+ prompt,
703
+ startedAt: startTime,
704
+ cancelled: false
705
+ };
706
+ this.activeAiRun = run;
549
707
  try {
550
708
  const streamCallback = (chunk) => {
709
+ if (this.activeAiRun !== run || run.cancelled) {
710
+ return;
711
+ }
551
712
  // console.log('Chunk Received:');
552
713
  // console.log(chunk);
553
714
  if (chunk && chunk.text) {
@@ -563,13 +724,17 @@ export class TerminalSession {
563
724
  }
564
725
  };
565
726
  // console.log('Start AI Prompt...');
566
- const result = await alan.prompt(finalPrompt, {
727
+ const result = await this._promptAi(finalPrompt, {
567
728
  stream: streamCallback,
568
729
  delta: true,
569
730
  messages: conversationHistory,
570
731
  trimBeginning: true
571
732
  });
572
733
 
734
+ if (this.activeAiRun !== run || run.cancelled) {
735
+ return;
736
+ }
737
+
573
738
  if (result && result.text) {
574
739
  fullResponse = result.text;
575
740
  }
@@ -588,6 +753,9 @@ export class TerminalSession {
588
753
  });
589
754
 
590
755
  } catch (e) {
756
+ if (this.activeAiRun !== run || run.cancelled) {
757
+ return;
758
+ }
591
759
  this._writeToLogAndBroadcast(`\x1b[31mAI Error: ${e.message}\x1b[0m\r\n`);
592
760
 
593
761
  this._logCommandExecution({
@@ -598,13 +766,12 @@ export class TerminalSession {
598
766
  startedAt: startTime,
599
767
  completedAt: new Date()
600
768
  });
769
+ } finally {
770
+ if (this.activeAiRun === run) {
771
+ this.activeAiRun = null;
772
+ this._finishAiInteraction('normal');
773
+ }
601
774
  }
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
775
  }
609
776
 
610
777
  _handleResize(cols, rows) {
@@ -632,13 +799,43 @@ export class TerminalSession {
632
799
  }
633
800
 
634
801
  _handlePromptMarker() {
802
+ if (this.currentExecutionId && !this.ignoreCurrentExecution) {
803
+ this._broadcast({
804
+ type: 'execution',
805
+ phase: 'idle',
806
+ executionId: this.currentExecutionId
807
+ });
808
+ }
809
+ this.currentExecutionId = '';
810
+ this.ignoreCurrentExecution = false;
635
811
  this.captureBuffer = '';
636
812
  this.captureStartedAt = null;
637
813
  }
638
814
 
815
+ _handleCommandStartSequence(cmdB64) {
816
+ const command = this._decodeCommandSafe(cmdB64);
817
+ const startedAt = new Date();
818
+ this.captureStartedAt = startedAt;
819
+ this.ignoreCurrentExecution = isIgnoredExecutionCommand(command);
820
+ if (this.ignoreCurrentExecution) {
821
+ this.currentExecutionId = '';
822
+ return;
823
+ }
824
+ this.executionCounter += 1;
825
+ this.currentExecutionId = `exec-${this.executionCounter}`;
826
+ this._broadcast({
827
+ type: 'execution',
828
+ phase: 'started',
829
+ executionId: this.currentExecutionId,
830
+ command,
831
+ startedAt
832
+ });
833
+ }
834
+
639
835
  _handleExitCodeSequence(exitCodeStr, cmdB64) {
640
836
  if (this.skipNextShellLog) {
641
837
  this.skipNextShellLog = false;
838
+ this._clearSkipNextShellLogResetTimer();
642
839
  this.captureBuffer = '';
643
840
  this.captureStartedAt = null;
644
841
  return;
@@ -646,6 +843,10 @@ export class TerminalSession {
646
843
 
647
844
  const exitCode = Number.parseInt(exitCodeStr, 10);
648
845
  const command = this._decodeCommandSafe(cmdB64);
846
+ const executionId = this.currentExecutionId
847
+ || `exec-${++this.executionCounter}`;
848
+ const isIgnored = this.ignoreCurrentExecution
849
+ || isIgnoredExecutionCommand(command);
649
850
 
650
851
  const completedAt = new Date();
651
852
  const entry = this._postProcessExecutionEntry({
@@ -659,9 +860,22 @@ export class TerminalSession {
659
860
  });
660
861
 
661
862
  this.lastExecution = entry;
863
+ this.currentExecutionId = '';
864
+ this.ignoreCurrentExecution = false;
865
+ if (isIgnored) {
866
+ this.captureBuffer = '';
867
+ this.captureStartedAt = null;
868
+ return;
869
+ }
662
870
  this._logCommandExecution(entry);
663
871
  this.captureBuffer = '';
664
872
  this.captureStartedAt = null;
873
+ this._broadcast({
874
+ type: 'execution',
875
+ phase: 'completed',
876
+ executionId,
877
+ entry
878
+ });
665
879
 
666
880
  // Auto-Fix: If command failed, ask AI for help
667
881
  if (exitCode !== 0 && entry.command && this._isAiEnabled()) {
@@ -970,7 +1184,7 @@ export class TerminalSession {
970
1184
 
971
1185
  _logCommandExecution(entry) {
972
1186
  // Filter out internal shell integration commands
973
- if (entry.command && IGNORED_COMMANDS.some(ignored => entry.command.includes(ignored))) {
1187
+ if (isIgnoredExecutionCommand(entry.command)) {
974
1188
  return;
975
1189
  }
976
1190
 
@@ -1001,6 +1215,8 @@ export class TerminalSession {
1001
1215
  if (this.manager) {
1002
1216
  this.manager.saveSessionState(this);
1003
1217
  }
1218
+ this.updatedAt = new Date();
1219
+ this._emitStateChange();
1004
1220
  }
1005
1221
 
1006
1222
  _broadcast(message) {
@@ -1022,10 +1238,12 @@ export class TerminalSession {
1022
1238
  if (!text) return;
1023
1239
  this._appendSnapshotData(text);
1024
1240
  this._appendHistory(text);
1241
+ this.updatedAt = new Date();
1025
1242
  if (this.manager?.scheduleSnapshotPersist) {
1026
1243
  this.manager.scheduleSnapshotPersist(this.id);
1027
1244
  }
1028
1245
  this._broadcast({ type: 'output', data: text });
1246
+ this._emitStateChange();
1029
1247
  }
1030
1248
 
1031
1249
  _queueSnapshotMutation(mutate) {