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.
@@ -56,57 +56,88 @@ function buildBashBootstrap({
56
56
  };
57
57
  }
58
58
 
59
+ function clearBashPromptEnv(env) {
60
+ for (const key of [
61
+ 'PROMPT_COMMAND',
62
+ 'PS0',
63
+ 'PS1',
64
+ 'PS2',
65
+ 'PS4'
66
+ ]) {
67
+ delete env[key];
68
+ }
69
+ }
70
+
59
71
  export class TerminalManager {
60
72
  constructor() {
61
73
  this.sessions = new Map();
62
74
  this.snapshotPersistTimers = new Map();
75
+ this.sessionPersistenceChains = new Map();
63
76
  this.lastCols = initialCols;
64
77
  this.lastRows = initialRows;
65
78
  this.disposing = false;
66
79
  }
67
80
 
68
- createSession(restoredData = null) {
69
- // Use ID from options if present, otherwise generate new
70
- const id = (restoredData && restoredData.id) ? restoredData.id : crypto.randomUUID();
71
- const shell = resolveShell();
72
-
73
- // Use CWD from options if present, otherwise default
74
- const initialCwd = (restoredData && restoredData.cwd)
75
- ? restoredData.cwd
76
- : (process.env.TABMINAL_CWD || os.homedir());
77
-
78
- const env = { ...process.env };
79
-
80
- // Inject shell tools
81
- const shellToolsPath = path.join(process.cwd(), 'shell');
82
- const pathDelimiter = path.delimiter;
83
- const pathKey = Object.keys(env).find((key) => key.toLowerCase() === 'path') || 'PATH';
84
- const existingPath = env[pathKey];
85
- env[pathKey] = existingPath
86
- ? `${shellToolsPath}${pathDelimiter}${existingPath}`
87
- : shellToolsPath;
88
-
89
- let spawnShell = shell;
90
- let args = [];
81
+ queueSessionPersistence(id, operation) {
82
+ const previous = this.sessionPersistenceChains.get(id)
83
+ || Promise.resolve();
84
+ const next = previous
85
+ .catch(() => {})
86
+ .then(operation);
87
+
88
+ this.sessionPersistenceChains.set(id, next);
89
+ next.finally(() => {
90
+ if (this.sessionPersistenceChains.get(id) === next) {
91
+ this.sessionPersistenceChains.delete(id);
92
+ }
93
+ }).catch(() => {});
94
+
95
+ return next;
96
+ }
97
+
98
+ _createPtySession(options = {}) {
99
+ const id = options.id || crypto.randomUUID();
100
+ const shell = options.shell || resolveShell();
101
+ const initialCwd = options.cwd
102
+ || process.env.TABMINAL_CWD
103
+ || os.homedir();
104
+ const env = {
105
+ ...process.env,
106
+ ...(options.env || {})
107
+ };
108
+ let spawnShell = options.spawnCommand || shell;
109
+ let args = Array.isArray(options.spawnArgs) ? options.spawnArgs : [];
91
110
  let initDirPath = null;
92
111
 
93
- try {
94
- const shellName = path.basename(shell);
95
- if (shellName === 'bash') {
96
- const bootstrap = buildBashBootstrap({
97
- env,
98
- shell,
99
- shellToolsPath,
100
- sessionId: id
101
- });
102
- spawnShell = bootstrap.shell;
103
- args = bootstrap.args;
104
- } else if (shellName === 'zsh') {
105
- initDirPath = path.join(os.tmpdir(), `tabminal-zsh-${id}`);
106
- fs.mkdirSync(initDirPath, { recursive: true });
107
- const initFilePath = path.join(initDirPath, '.zshrc');
108
-
109
- const zshScript = `
112
+ if (!options.directSpawn) {
113
+ const shellToolsPath = path.join(process.cwd(), 'shell');
114
+ const pathDelimiter = path.delimiter;
115
+ const pathKey = Object.keys(env).find(
116
+ (key) => key.toLowerCase() === 'path'
117
+ ) || 'PATH';
118
+ const existingPath = env[pathKey];
119
+ env[pathKey] = existingPath
120
+ ? `${shellToolsPath}${pathDelimiter}${existingPath}`
121
+ : shellToolsPath;
122
+
123
+ try {
124
+ const shellName = path.basename(shell);
125
+ if (shellName === 'bash') {
126
+ clearBashPromptEnv(env);
127
+ const bootstrap = buildBashBootstrap({
128
+ env,
129
+ shell,
130
+ shellToolsPath,
131
+ sessionId: id
132
+ });
133
+ spawnShell = bootstrap.shell;
134
+ args = bootstrap.args;
135
+ } else if (shellName === 'zsh') {
136
+ initDirPath = path.join(os.tmpdir(), `tabminal-zsh-${id}`);
137
+ fs.mkdirSync(initDirPath, { recursive: true });
138
+ const initFilePath = path.join(initDirPath, '.zshrc');
139
+
140
+ const zshScript = `
110
141
  unset ZDOTDIR
111
142
  [ -f ~/.zshrc ] && source ~/.zshrc
112
143
  export PATH="${shellToolsPath}:$PATH"
@@ -132,25 +163,26 @@ preexec_functions+=(_tabminal_zsh_preexec)
132
163
  precmd_functions+=(_tabminal_zsh_postexec)
133
164
  precmd_functions+=(_tabminal_zsh_apply_prompt_marker)
134
165
  `;
135
- fs.writeFileSync(initFilePath, zshScript);
136
- env.ZDOTDIR = initDirPath;
137
- args = ['-i'];
166
+ fs.writeFileSync(initFilePath, zshScript);
167
+ env.ZDOTDIR = initDirPath;
168
+ args = ['-i'];
169
+ }
170
+ } catch (err) {
171
+ console.error('[Manager] Failed to create init script:', err);
138
172
  }
139
- } catch (err) {
140
- console.error('[Manager] Failed to create init script:', err);
141
173
  }
142
174
 
143
- const cols = restoredData ? restoredData.cols : this.lastCols;
144
- const rows = restoredData ? restoredData.rows : this.lastRows;
175
+ const cols = Number.isFinite(options.cols) ? options.cols : this.lastCols;
176
+ const rows = Number.isFinite(options.rows) ? options.rows : this.lastRows;
145
177
 
146
178
  let ptyProcess;
147
179
  try {
148
180
  const ptyOptions = {
149
181
  name: 'xterm-256color',
150
- cols: cols,
151
- rows: rows,
182
+ cols,
183
+ rows,
152
184
  cwd: initialCwd,
153
- env: env
185
+ env
154
186
  };
155
187
  if (process.platform !== 'win32') {
156
188
  ptyOptions.encoding = 'utf8';
@@ -182,17 +214,25 @@ precmd_functions+=(_tabminal_zsh_apply_prompt_marker)
182
214
 
183
215
  const session = new TerminalSession(ptyProcess, {
184
216
  id,
185
- historyLimit,
186
- createdAt: restoredData ? new Date(restoredData.createdAt) : new Date(),
217
+ historyLimit: options.historyLimit ?? historyLimit,
218
+ createdAt: options.createdAt
219
+ ? new Date(options.createdAt)
220
+ : new Date(),
187
221
  manager: this,
188
222
  shell,
189
223
  initialCwd,
190
- env: env,
191
- editorState: restoredData ? restoredData.editorState : undefined,
192
- executions: restoredData ? restoredData.executions : undefined
224
+ env,
225
+ title: options.title || '',
226
+ managed: options.managed || null,
227
+ persistent: options.persistent !== false,
228
+ removeOnExit: options.removeOnExit !== false,
229
+ enableAiHijack: options.enableAiHijack !== false,
230
+ enableTitlePolling: options.enableTitlePolling !== false,
231
+ editorState: options.editorState,
232
+ executions: options.executions
193
233
  });
194
234
 
195
- if (restoredData) {
235
+ if (options.restoreSnapshot) {
196
236
  persistence.loadSessionSnapshot(id).then(async (snapshot) => {
197
237
  if (!snapshot) return;
198
238
  await session.restoreSnapshot(snapshot);
@@ -200,24 +240,77 @@ precmd_functions+=(_tabminal_zsh_apply_prompt_marker)
200
240
  });
201
241
  }
202
242
 
203
- // Initial save
204
- this.saveSessionState(session);
243
+ this.sessions.set(id, session);
244
+
245
+ if (session.persistent) {
246
+ void this.saveSessionState(session);
247
+ }
205
248
 
206
249
  ptyProcess.onExit(() => {
207
- this.removeSession(id);
208
- // Cleanup temp files
250
+ if (session.removeOnExit) {
251
+ void this.removeSession(id);
252
+ }
209
253
  try {
210
- if (initDirPath && fs.existsSync(initDirPath)) fs.rmSync(initDirPath, { recursive: true, force: true });
211
- } catch { /* ignore cleanup errors */ }
254
+ if (initDirPath && fs.existsSync(initDirPath)) {
255
+ fs.rmSync(initDirPath, { recursive: true, force: true });
256
+ }
257
+ } catch {
258
+ // ignore cleanup errors
259
+ }
212
260
  });
213
-
214
- this.sessions.set(id, session);
215
261
  debugLog(`[Manager] Created session ${id}`);
216
262
  return session;
217
263
  }
218
264
 
265
+ createSession(restoredData = null) {
266
+ return this._createPtySession({
267
+ id: restoredData?.id,
268
+ shell: resolveShell(),
269
+ cwd: restoredData?.cwd,
270
+ cols: restoredData?.cols,
271
+ rows: restoredData?.rows,
272
+ createdAt: restoredData?.createdAt,
273
+ title: restoredData?.title,
274
+ editorState: restoredData?.editorState,
275
+ executions: restoredData?.executions,
276
+ restoreSnapshot: Boolean(restoredData),
277
+ persistent: true,
278
+ removeOnExit: true,
279
+ enableAiHijack: true,
280
+ enableTitlePolling: true
281
+ });
282
+ }
283
+
284
+ createManagedSession(options = {}) {
285
+ const spawnRequest = options.spawnRequest || {};
286
+ const shell = spawnRequest.command || resolveShell();
287
+ return this._createPtySession({
288
+ shell,
289
+ cwd: options.cwd,
290
+ env: options.env,
291
+ cols: options.cols,
292
+ rows: options.rows,
293
+ title: options.title || path.basename(shell) || 'Terminal',
294
+ directSpawn: true,
295
+ spawnCommand: spawnRequest.command,
296
+ spawnArgs: spawnRequest.args,
297
+ persistent: false,
298
+ removeOnExit: false,
299
+ enableAiHijack: false,
300
+ enableTitlePolling: false,
301
+ managed: options.managed || null
302
+ });
303
+ }
304
+
219
305
  saveSessionState(session) {
220
- persistence.saveSession(session.id, {
306
+ if (!session?.persistent) {
307
+ return Promise.resolve();
308
+ }
309
+ if (this.sessions.get(session.id) !== session) {
310
+ return Promise.resolve();
311
+ }
312
+
313
+ return this.queueSessionPersistence(session.id, () => persistence.saveSession(session.id, {
221
314
  id: session.id,
222
315
  title: session.title,
223
316
  cwd: session.cwd,
@@ -227,7 +320,7 @@ precmd_functions+=(_tabminal_zsh_apply_prompt_marker)
227
320
  createdAt: session.createdAt,
228
321
  editorState: session.editorState,
229
322
  executions: session.executions
230
- });
323
+ }));
231
324
  }
232
325
 
233
326
  updateSessionState(id, data) {
@@ -237,26 +330,32 @@ precmd_functions+=(_tabminal_zsh_apply_prompt_marker)
237
330
  if (data.editorState) {
238
331
  session.editorState = { ...session.editorState, ...data.editorState };
239
332
  }
240
- this.saveSessionState(session);
333
+ if (session.persistent) {
334
+ this.saveSessionState(session);
335
+ }
241
336
  }
242
337
  }
243
338
 
244
339
  scheduleSnapshotPersist(id) {
245
340
  const session = this.sessions.get(id);
246
- if (!session) return;
341
+ if (!session || !session.persistent) return;
247
342
 
248
343
  const existing = this.snapshotPersistTimers.get(id);
249
344
  if (existing) {
250
345
  clearTimeout(existing);
251
346
  }
252
347
 
253
- const timer = setTimeout(async () => {
348
+ const timer = setTimeout(() => {
254
349
  this.snapshotPersistTimers.delete(id);
255
350
  const currentSession = this.sessions.get(id);
256
351
  if (!currentSession) return;
257
352
 
258
- const snapshot = await currentSession.serializeSnapshot();
259
- await persistence.saveSessionSnapshot(id, snapshot);
353
+ void this.queueSessionPersistence(id, async () => {
354
+ if (this.sessions.get(id) !== currentSession) return;
355
+ const snapshot = await currentSession.serializeSnapshot();
356
+ if (this.sessions.get(id) !== currentSession) return;
357
+ await persistence.saveSessionSnapshot(id, snapshot);
358
+ });
260
359
  }, 250);
261
360
 
262
361
  this.snapshotPersistTimers.set(id, timer);
@@ -280,7 +379,7 @@ precmd_functions+=(_tabminal_zsh_apply_prompt_marker)
280
379
  this.lastRows = rows;
281
380
  }
282
381
 
283
- removeSession(id) {
382
+ async removeSession(id) {
284
383
  const session = this.sessions.get(id);
285
384
  if (session) {
286
385
  const timer = this.snapshotPersistTimers.get(id);
@@ -288,9 +387,18 @@ precmd_functions+=(_tabminal_zsh_apply_prompt_marker)
288
387
  clearTimeout(timer);
289
388
  this.snapshotPersistTimers.delete(id);
290
389
  }
390
+ try {
391
+ if (process.platform === 'win32') {
392
+ session.pty.kill();
393
+ } else {
394
+ session.pty.kill('SIGHUP');
395
+ }
396
+ } catch {
397
+ // ignore
398
+ }
291
399
  session.dispose();
292
400
  this.sessions.delete(id);
293
- persistence.deleteSession(id);
401
+ await this.queueSessionPersistence(id, () => persistence.deleteSession(id));
294
402
  debugLog(`[Manager] Removed session ${id}`);
295
403
  }
296
404
  }
@@ -306,6 +414,9 @@ precmd_functions+=(_tabminal_zsh_apply_prompt_marker)
306
414
  env: s.env,
307
415
  cols: s.pty.cols,
308
416
  rows: s.pty.rows,
417
+ closed: !!s.closed,
418
+ exitStatus: s.exitStatus || null,
419
+ managed: s.managed || null,
309
420
  editorState: s.editorState,
310
421
  executions: s.executions
311
422
  }));