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.
- package/ACP_PLANING.md +184 -0
- package/README.md +238 -105
- package/package.json +6 -4
- package/public/app.js +8481 -553
- package/public/index.html +150 -2
- package/public/styles.css +1977 -84
- package/shell/tabminal-hooks.bash +10 -0
- package/src/acp-manager.mjs +3469 -0
- package/src/acp-test-agent.mjs +691 -0
- package/src/persistence.mjs +153 -0
- package/src/server.mjs +300 -12
- package/src/terminal-manager.mjs +184 -73
- package/src/terminal-session.mjs +233 -15
package/src/terminal-manager.mjs
CHANGED
|
@@ -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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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 =
|
|
144
|
-
const rows =
|
|
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
|
|
151
|
-
rows
|
|
182
|
+
cols,
|
|
183
|
+
rows,
|
|
152
184
|
cwd: initialCwd,
|
|
153
|
-
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:
|
|
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
|
|
191
|
-
|
|
192
|
-
|
|
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 (
|
|
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
|
-
|
|
204
|
-
|
|
243
|
+
this.sessions.set(id, session);
|
|
244
|
+
|
|
245
|
+
if (session.persistent) {
|
|
246
|
+
void this.saveSessionState(session);
|
|
247
|
+
}
|
|
205
248
|
|
|
206
249
|
ptyProcess.onExit(() => {
|
|
207
|
-
|
|
208
|
-
|
|
250
|
+
if (session.removeOnExit) {
|
|
251
|
+
void this.removeSession(id);
|
|
252
|
+
}
|
|
209
253
|
try {
|
|
210
|
-
if (initDirPath && fs.existsSync(initDirPath))
|
|
211
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
259
|
-
|
|
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
|
}));
|