tabminal 2.0.14 → 2.0.16
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/AGENTS.md +360 -209
- package/README.md +238 -105
- package/package.json +3 -1
- 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 +298 -10
- package/src/terminal-manager.mjs +140 -64
- package/src/terminal-session.mjs +131 -8
package/src/terminal-manager.mjs
CHANGED
|
@@ -56,6 +56,18 @@ 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();
|
|
@@ -83,48 +95,49 @@ export class TerminalManager {
|
|
|
83
95
|
return next;
|
|
84
96
|
}
|
|
85
97
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// Inject shell tools
|
|
99
|
-
const shellToolsPath = path.join(process.cwd(), 'shell');
|
|
100
|
-
const pathDelimiter = path.delimiter;
|
|
101
|
-
const pathKey = Object.keys(env).find((key) => key.toLowerCase() === 'path') || 'PATH';
|
|
102
|
-
const existingPath = env[pathKey];
|
|
103
|
-
env[pathKey] = existingPath
|
|
104
|
-
? `${shellToolsPath}${pathDelimiter}${existingPath}`
|
|
105
|
-
: shellToolsPath;
|
|
106
|
-
|
|
107
|
-
let spawnShell = shell;
|
|
108
|
-
let args = [];
|
|
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 : [];
|
|
109
110
|
let initDirPath = null;
|
|
110
111
|
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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 = `
|
|
128
141
|
unset ZDOTDIR
|
|
129
142
|
[ -f ~/.zshrc ] && source ~/.zshrc
|
|
130
143
|
export PATH="${shellToolsPath}:$PATH"
|
|
@@ -150,25 +163,26 @@ preexec_functions+=(_tabminal_zsh_preexec)
|
|
|
150
163
|
precmd_functions+=(_tabminal_zsh_postexec)
|
|
151
164
|
precmd_functions+=(_tabminal_zsh_apply_prompt_marker)
|
|
152
165
|
`;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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);
|
|
156
172
|
}
|
|
157
|
-
} catch (err) {
|
|
158
|
-
console.error('[Manager] Failed to create init script:', err);
|
|
159
173
|
}
|
|
160
174
|
|
|
161
|
-
const cols =
|
|
162
|
-
const rows =
|
|
175
|
+
const cols = Number.isFinite(options.cols) ? options.cols : this.lastCols;
|
|
176
|
+
const rows = Number.isFinite(options.rows) ? options.rows : this.lastRows;
|
|
163
177
|
|
|
164
178
|
let ptyProcess;
|
|
165
179
|
try {
|
|
166
180
|
const ptyOptions = {
|
|
167
181
|
name: 'xterm-256color',
|
|
168
|
-
cols
|
|
169
|
-
rows
|
|
182
|
+
cols,
|
|
183
|
+
rows,
|
|
170
184
|
cwd: initialCwd,
|
|
171
|
-
env
|
|
185
|
+
env
|
|
172
186
|
};
|
|
173
187
|
if (process.platform !== 'win32') {
|
|
174
188
|
ptyOptions.encoding = 'utf8';
|
|
@@ -200,17 +214,25 @@ precmd_functions+=(_tabminal_zsh_apply_prompt_marker)
|
|
|
200
214
|
|
|
201
215
|
const session = new TerminalSession(ptyProcess, {
|
|
202
216
|
id,
|
|
203
|
-
historyLimit,
|
|
204
|
-
createdAt:
|
|
217
|
+
historyLimit: options.historyLimit ?? historyLimit,
|
|
218
|
+
createdAt: options.createdAt
|
|
219
|
+
? new Date(options.createdAt)
|
|
220
|
+
: new Date(),
|
|
205
221
|
manager: this,
|
|
206
222
|
shell,
|
|
207
223
|
initialCwd,
|
|
208
|
-
env
|
|
209
|
-
|
|
210
|
-
|
|
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
|
|
211
233
|
});
|
|
212
234
|
|
|
213
|
-
if (
|
|
235
|
+
if (options.restoreSnapshot) {
|
|
214
236
|
persistence.loadSessionSnapshot(id).then(async (snapshot) => {
|
|
215
237
|
if (!snapshot) return;
|
|
216
238
|
await session.restoreSnapshot(snapshot);
|
|
@@ -220,21 +242,70 @@ precmd_functions+=(_tabminal_zsh_apply_prompt_marker)
|
|
|
220
242
|
|
|
221
243
|
this.sessions.set(id, session);
|
|
222
244
|
|
|
223
|
-
|
|
224
|
-
|
|
245
|
+
if (session.persistent) {
|
|
246
|
+
void this.saveSessionState(session);
|
|
247
|
+
}
|
|
225
248
|
|
|
226
249
|
ptyProcess.onExit(() => {
|
|
227
|
-
|
|
228
|
-
|
|
250
|
+
if (session.removeOnExit) {
|
|
251
|
+
void this.removeSession(id);
|
|
252
|
+
}
|
|
229
253
|
try {
|
|
230
|
-
if (initDirPath && fs.existsSync(initDirPath))
|
|
231
|
-
|
|
254
|
+
if (initDirPath && fs.existsSync(initDirPath)) {
|
|
255
|
+
fs.rmSync(initDirPath, { recursive: true, force: true });
|
|
256
|
+
}
|
|
257
|
+
} catch {
|
|
258
|
+
// ignore cleanup errors
|
|
259
|
+
}
|
|
232
260
|
});
|
|
233
261
|
debugLog(`[Manager] Created session ${id}`);
|
|
234
262
|
return session;
|
|
235
263
|
}
|
|
236
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
|
+
|
|
237
305
|
saveSessionState(session) {
|
|
306
|
+
if (!session?.persistent) {
|
|
307
|
+
return Promise.resolve();
|
|
308
|
+
}
|
|
238
309
|
if (this.sessions.get(session.id) !== session) {
|
|
239
310
|
return Promise.resolve();
|
|
240
311
|
}
|
|
@@ -259,13 +330,15 @@ precmd_functions+=(_tabminal_zsh_apply_prompt_marker)
|
|
|
259
330
|
if (data.editorState) {
|
|
260
331
|
session.editorState = { ...session.editorState, ...data.editorState };
|
|
261
332
|
}
|
|
262
|
-
|
|
333
|
+
if (session.persistent) {
|
|
334
|
+
this.saveSessionState(session);
|
|
335
|
+
}
|
|
263
336
|
}
|
|
264
337
|
}
|
|
265
338
|
|
|
266
339
|
scheduleSnapshotPersist(id) {
|
|
267
340
|
const session = this.sessions.get(id);
|
|
268
|
-
if (!session) return;
|
|
341
|
+
if (!session || !session.persistent) return;
|
|
269
342
|
|
|
270
343
|
const existing = this.snapshotPersistTimers.get(id);
|
|
271
344
|
if (existing) {
|
|
@@ -341,6 +414,9 @@ precmd_functions+=(_tabminal_zsh_apply_prompt_marker)
|
|
|
341
414
|
env: s.env,
|
|
342
415
|
cols: s.pty.cols,
|
|
343
416
|
rows: s.pty.rows,
|
|
417
|
+
closed: !!s.closed,
|
|
418
|
+
exitStatus: s.exitStatus || null,
|
|
419
|
+
managed: s.managed || null,
|
|
344
420
|
editorState: s.editorState,
|
|
345
421
|
executions: s.executions
|
|
346
422
|
}));
|
package/src/terminal-session.mjs
CHANGED
|
@@ -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.
|
|
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,10 +130,16 @@ 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;
|
|
124
144
|
this.skipNextShellLogResetTimer = null;
|
|
125
145
|
this.partialSequenceBuffer = '';
|
|
@@ -152,7 +172,9 @@ export class TerminalSession {
|
|
|
152
172
|
const newTitle = s.substring(2);
|
|
153
173
|
if (newTitle && newTitle !== this.title) {
|
|
154
174
|
this.title = newTitle;
|
|
175
|
+
this.updatedAt = new Date();
|
|
155
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();
|
|
156
178
|
}
|
|
157
179
|
} else if (s.startsWith('7;')) {
|
|
158
180
|
try {
|
|
@@ -161,7 +183,9 @@ export class TerminalSession {
|
|
|
161
183
|
const newCwd = decodeURIComponent(url.pathname);
|
|
162
184
|
if (newCwd !== this.cwd) {
|
|
163
185
|
this.cwd = newCwd;
|
|
186
|
+
this.updatedAt = new Date();
|
|
164
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();
|
|
165
189
|
}
|
|
166
190
|
}
|
|
167
191
|
} catch { /* ignore */ }
|
|
@@ -198,6 +222,8 @@ export class TerminalSession {
|
|
|
198
222
|
const exitCodeStr = match[2];
|
|
199
223
|
const cmdB64 = match[3];
|
|
200
224
|
this._handleExitCodeSequence(exitCodeStr, cmdB64);
|
|
225
|
+
} else if (sequence.startsWith('CommandStartB64=')) {
|
|
226
|
+
this._handleCommandStartSequence(match[4]);
|
|
201
227
|
} else {
|
|
202
228
|
this._handlePromptMarker();
|
|
203
229
|
}
|
|
@@ -215,27 +241,43 @@ export class TerminalSession {
|
|
|
215
241
|
|
|
216
242
|
this._appendSnapshotData(cleaned);
|
|
217
243
|
this._appendHistory(cleaned);
|
|
244
|
+
this.updatedAt = new Date();
|
|
218
245
|
this.ansiParser.parse(cleaned);
|
|
219
246
|
if (this.manager?.scheduleSnapshotPersist) {
|
|
220
247
|
this.manager.scheduleSnapshotPersist(this.id);
|
|
221
248
|
}
|
|
222
249
|
this._broadcast({ type: 'output', data: cleaned });
|
|
250
|
+
this._emitStateChange();
|
|
223
251
|
};
|
|
224
252
|
|
|
225
253
|
this._handleExit = (details) => {
|
|
226
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();
|
|
227
262
|
this.stopTitlePolling();
|
|
228
263
|
this._broadcast({
|
|
229
264
|
type: 'status',
|
|
230
265
|
status: 'terminated',
|
|
231
|
-
code:
|
|
232
|
-
signal:
|
|
266
|
+
code: this.exitStatus.exitCode ?? 0,
|
|
267
|
+
signal: this.exitStatus.signal
|
|
233
268
|
});
|
|
269
|
+
this._emitStateChange();
|
|
270
|
+
for (const waiter of this.exitWaiters) {
|
|
271
|
+
waiter(this.exitStatus);
|
|
272
|
+
}
|
|
273
|
+
this.exitWaiters.length = 0;
|
|
234
274
|
};
|
|
235
275
|
|
|
236
276
|
this.dataSubscription = this.pty.onData(this._handleData);
|
|
237
277
|
this.exitSubscription = this.pty.onExit(this._handleExit);
|
|
238
|
-
this.
|
|
278
|
+
if (this.enableTitlePolling) {
|
|
279
|
+
this.startTitlePolling();
|
|
280
|
+
}
|
|
239
281
|
}
|
|
240
282
|
|
|
241
283
|
startTitlePolling() {
|
|
@@ -323,7 +365,9 @@ export class TerminalSession {
|
|
|
323
365
|
if (titleChanged) this.title = newTitle;
|
|
324
366
|
if (envChanged) this.env = newEnv;
|
|
325
367
|
if (cwdChanged) this.cwd = newCwd;
|
|
368
|
+
this.updatedAt = new Date();
|
|
326
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();
|
|
327
371
|
}
|
|
328
372
|
} catch { /* ignore */ }
|
|
329
373
|
};
|
|
@@ -372,11 +416,40 @@ export class TerminalSession {
|
|
|
372
416
|
this.manager.scheduleSnapshotPersist(this.id);
|
|
373
417
|
}
|
|
374
418
|
this._broadcast({ type: 'meta', title: this.title, cwd: this.cwd, env: this.env, cols, rows });
|
|
419
|
+
this.updatedAt = new Date();
|
|
420
|
+
this._emitStateChange();
|
|
375
421
|
if (this.manager && this.manager.saveSessionState) {
|
|
376
422
|
this.manager.saveSessionState(this);
|
|
377
423
|
}
|
|
378
424
|
}
|
|
379
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
|
+
|
|
380
453
|
async restoreSnapshot(snapshot) {
|
|
381
454
|
if (typeof snapshot !== 'string' || !snapshot) return;
|
|
382
455
|
this.history = snapshot;
|
|
@@ -436,7 +509,7 @@ export class TerminalSession {
|
|
|
436
509
|
}
|
|
437
510
|
|
|
438
511
|
let startIndex = 0;
|
|
439
|
-
const aiEnabled = this._isAiEnabled();
|
|
512
|
+
const aiEnabled = this.enableAiHijack && this._isAiEnabled();
|
|
440
513
|
for (let i = 0; i < data.length; i++) {
|
|
441
514
|
const char = data[i];
|
|
442
515
|
|
|
@@ -726,10 +799,39 @@ export class TerminalSession {
|
|
|
726
799
|
}
|
|
727
800
|
|
|
728
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;
|
|
729
811
|
this.captureBuffer = '';
|
|
730
812
|
this.captureStartedAt = null;
|
|
731
813
|
}
|
|
732
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
|
+
|
|
733
835
|
_handleExitCodeSequence(exitCodeStr, cmdB64) {
|
|
734
836
|
if (this.skipNextShellLog) {
|
|
735
837
|
this.skipNextShellLog = false;
|
|
@@ -741,6 +843,10 @@ export class TerminalSession {
|
|
|
741
843
|
|
|
742
844
|
const exitCode = Number.parseInt(exitCodeStr, 10);
|
|
743
845
|
const command = this._decodeCommandSafe(cmdB64);
|
|
846
|
+
const executionId = this.currentExecutionId
|
|
847
|
+
|| `exec-${++this.executionCounter}`;
|
|
848
|
+
const isIgnored = this.ignoreCurrentExecution
|
|
849
|
+
|| isIgnoredExecutionCommand(command);
|
|
744
850
|
|
|
745
851
|
const completedAt = new Date();
|
|
746
852
|
const entry = this._postProcessExecutionEntry({
|
|
@@ -754,9 +860,22 @@ export class TerminalSession {
|
|
|
754
860
|
});
|
|
755
861
|
|
|
756
862
|
this.lastExecution = entry;
|
|
863
|
+
this.currentExecutionId = '';
|
|
864
|
+
this.ignoreCurrentExecution = false;
|
|
865
|
+
if (isIgnored) {
|
|
866
|
+
this.captureBuffer = '';
|
|
867
|
+
this.captureStartedAt = null;
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
757
870
|
this._logCommandExecution(entry);
|
|
758
871
|
this.captureBuffer = '';
|
|
759
872
|
this.captureStartedAt = null;
|
|
873
|
+
this._broadcast({
|
|
874
|
+
type: 'execution',
|
|
875
|
+
phase: 'completed',
|
|
876
|
+
executionId,
|
|
877
|
+
entry
|
|
878
|
+
});
|
|
760
879
|
|
|
761
880
|
// Auto-Fix: If command failed, ask AI for help
|
|
762
881
|
if (exitCode !== 0 && entry.command && this._isAiEnabled()) {
|
|
@@ -1065,7 +1184,7 @@ export class TerminalSession {
|
|
|
1065
1184
|
|
|
1066
1185
|
_logCommandExecution(entry) {
|
|
1067
1186
|
// Filter out internal shell integration commands
|
|
1068
|
-
if (
|
|
1187
|
+
if (isIgnoredExecutionCommand(entry.command)) {
|
|
1069
1188
|
return;
|
|
1070
1189
|
}
|
|
1071
1190
|
|
|
@@ -1096,6 +1215,8 @@ export class TerminalSession {
|
|
|
1096
1215
|
if (this.manager) {
|
|
1097
1216
|
this.manager.saveSessionState(this);
|
|
1098
1217
|
}
|
|
1218
|
+
this.updatedAt = new Date();
|
|
1219
|
+
this._emitStateChange();
|
|
1099
1220
|
}
|
|
1100
1221
|
|
|
1101
1222
|
_broadcast(message) {
|
|
@@ -1117,10 +1238,12 @@ export class TerminalSession {
|
|
|
1117
1238
|
if (!text) return;
|
|
1118
1239
|
this._appendSnapshotData(text);
|
|
1119
1240
|
this._appendHistory(text);
|
|
1241
|
+
this.updatedAt = new Date();
|
|
1120
1242
|
if (this.manager?.scheduleSnapshotPersist) {
|
|
1121
1243
|
this.manager.scheduleSnapshotPersist(this.id);
|
|
1122
1244
|
}
|
|
1123
1245
|
this._broadcast({ type: 'output', data: text });
|
|
1246
|
+
this._emitStateChange();
|
|
1124
1247
|
}
|
|
1125
1248
|
|
|
1126
1249
|
_queueSnapshotMutation(mutate) {
|