ai-agent-session-center 2.8.2 → 2.9.2
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/dist/client/assets/{AgendaView-DQAv5CVb.js → AgendaView-Dl3FQpEX.js} +1 -1
- package/dist/client/assets/{CyberdromeScene-DBlv7xBi.js → CyberdromeScene-EY1fKXEu.js} +1 -1
- package/dist/client/assets/{HistoryView-DW2ffQlM.js → HistoryView-CleN2H-B.js} +1 -1
- package/dist/client/assets/{ProjectBrowserView-DB5IJ4IC.js → ProjectBrowserView-D8l8UZ3Z.js} +1 -1
- package/dist/client/assets/{QueueView-BOYx9a7Z.js → QueueView-D9KUZrU5.js} +1 -1
- package/dist/client/assets/{index-bOzE64NK.css → index-D3mXxwbI.css} +1 -1
- package/dist/client/assets/{index-CCOZp8gY.js → index-DI0_qhqI.js} +76 -76
- package/dist/client/index.html +2 -2
- package/package.json +1 -1
- package/server/apiRouter.ts +33 -7
- package/server/sessionStore.ts +17 -14
- package/server/sshManager.ts +44 -2
package/dist/client/index.html
CHANGED
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
|
|
8
8
|
<link rel="apple-touch-icon" href="/apple-touch-icon.svg">
|
|
9
9
|
<meta name="theme-color" content="#0a0a1a">
|
|
10
|
-
<script type="module" crossorigin src="/assets/index-
|
|
11
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-DI0_qhqI.js"></script>
|
|
11
|
+
<link rel="stylesheet" crossorigin href="/assets/index-D3mXxwbI.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
14
14
|
<div id="root"></div>
|
package/package.json
CHANGED
package/server/apiRouter.ts
CHANGED
|
@@ -80,6 +80,7 @@ const terminalCreateSchema = z.object({
|
|
|
80
80
|
sessionTitle: z.string().max(500).optional(),
|
|
81
81
|
label: z.string().optional(),
|
|
82
82
|
enableOpsTerminal: z.boolean().optional(),
|
|
83
|
+
forceNew: z.boolean().optional(),
|
|
83
84
|
});
|
|
84
85
|
|
|
85
86
|
const tmuxSessionsSchema = z.object({
|
|
@@ -747,11 +748,14 @@ router.post('/terminals', async (req: Request, res: Response) => {
|
|
|
747
748
|
}
|
|
748
749
|
|
|
749
750
|
// Deduplicate: if an active session with matching config already exists, return it
|
|
750
|
-
|
|
751
|
-
if (
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
751
|
+
// Skip deduplication when forceNew is explicitly requested (e.g. Quick session modal)
|
|
752
|
+
if (!body.forceNew) {
|
|
753
|
+
const existing = findActiveSessionByConfig(config);
|
|
754
|
+
if (existing) {
|
|
755
|
+
log.info('api', `Deduplicated terminal creation — reusing session ${existing.sessionId} for ${resolvedHost}:${config.workingDir}`);
|
|
756
|
+
res.json({ ok: true, terminalId: existing.sessionId, deduplicated: true });
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
755
759
|
}
|
|
756
760
|
|
|
757
761
|
const terminalId = await createTerminal(config, null);
|
|
@@ -1245,11 +1249,33 @@ router.get('/files/stream', (req: Request, res: Response) => {
|
|
|
1245
1249
|
};
|
|
1246
1250
|
const contentType = mimeMap[fileExt] || 'application/octet-stream';
|
|
1247
1251
|
|
|
1248
|
-
res.setHeader('Content-Type', contentType);
|
|
1249
|
-
res.setHeader('Content-Length', stat.size);
|
|
1250
1252
|
const safeName = basename(fullPath).replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
1251
1253
|
res.setHeader('Content-Disposition', `inline; filename="${safeName}"`);
|
|
1252
1254
|
|
|
1255
|
+
// Video/audio: support HTTP Range requests so browsers can seek
|
|
1256
|
+
const isMedia = contentType.startsWith('video/') || contentType.startsWith('audio/');
|
|
1257
|
+
if (isMedia) {
|
|
1258
|
+
res.setHeader('Accept-Ranges', 'bytes');
|
|
1259
|
+
const rangeHeader = req.headers.range;
|
|
1260
|
+
if (rangeHeader) {
|
|
1261
|
+
const [startStr, endStr] = rangeHeader.replace(/bytes=/, '').split('-');
|
|
1262
|
+
const start = parseInt(startStr, 10);
|
|
1263
|
+
const end = endStr ? parseInt(endStr, 10) : stat.size - 1;
|
|
1264
|
+
const chunkSize = end - start + 1;
|
|
1265
|
+
res.status(206);
|
|
1266
|
+
res.setHeader('Content-Range', `bytes ${start}-${end}/${stat.size}`);
|
|
1267
|
+
res.setHeader('Content-Length', chunkSize);
|
|
1268
|
+
res.setHeader('Content-Type', contentType);
|
|
1269
|
+
const rangeStream = createReadStream(fullPath, { start, end });
|
|
1270
|
+
rangeStream.pipe(res);
|
|
1271
|
+
rangeStream.on('error', () => { if (!res.headersSent) res.status(500).json({ error: 'Stream error' }); });
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
res.setHeader('Content-Type', contentType);
|
|
1277
|
+
res.setHeader('Content-Length', stat.size);
|
|
1278
|
+
|
|
1253
1279
|
const stream = createReadStream(fullPath);
|
|
1254
1280
|
stream.pipe(res);
|
|
1255
1281
|
stream.on('error', () => {
|
package/server/sessionStore.ts
CHANGED
|
@@ -928,21 +928,24 @@ export async function createTerminalSession(terminalId: string, config: Terminal
|
|
|
928
928
|
|
|
929
929
|
await broadcastAsync({ type: WS_TYPES.SESSION_UPDATE, session: { ...session } });
|
|
930
930
|
|
|
931
|
-
//
|
|
931
|
+
// Auto-transition from connecting to idle if hooks don't arrive in time.
|
|
932
|
+
// Non-Claude CLIs: 3s (they never send hooks).
|
|
933
|
+
// Claude: 30s fallback — hooks should arrive via SessionStart, but if the
|
|
934
|
+
// session_id or path doesn't match (e.g. SSH remote path mismatch), the card
|
|
935
|
+
// would be stuck in connecting forever without this safety net.
|
|
932
936
|
const command = config.command || 'claude';
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
}
|
|
937
|
+
const connectingTimeout = command.startsWith('claude') ? 30_000 : 3_000;
|
|
938
|
+
setTimeout(async () => {
|
|
939
|
+
const s = sessions.get(terminalId);
|
|
940
|
+
if (s && s.status === (SESSION_STATUS.CONNECTING as string)) {
|
|
941
|
+
s.status = SESSION_STATUS.IDLE;
|
|
942
|
+
s.animationState = ANIMATION_STATE.IDLE;
|
|
943
|
+
s.emote = null;
|
|
944
|
+
if (!command.startsWith('claude')) s.model = command;
|
|
945
|
+
await broadcastAsync({ type: WS_TYPES.SESSION_UPDATE, session: { ...s } });
|
|
946
|
+
log.info('session', `Auto-transitioned session ${terminalId} to idle after connecting timeout (${command})`);
|
|
947
|
+
}
|
|
948
|
+
}, connectingTimeout);
|
|
946
949
|
|
|
947
950
|
return session;
|
|
948
951
|
}
|
package/server/sshManager.ts
CHANGED
|
@@ -313,6 +313,44 @@ export function createTerminal(config: TerminalConfig, wsClient: WebSocket | nul
|
|
|
313
313
|
|
|
314
314
|
log.info('pty', `Spawned ${local ? 'local' : `remote (${config.host})`} terminal ${terminalId} (pid: ${ptyProcess.pid})`);
|
|
315
315
|
|
|
316
|
+
// Auto-type SSH password when password auth is used.
|
|
317
|
+
// Watches PTY output for "password:" prompts and sends the stored password.
|
|
318
|
+
if (!local && config.password) {
|
|
319
|
+
const storedPassword = config.password;
|
|
320
|
+
let passwordBuffer = '';
|
|
321
|
+
let passwordAttempts = 0;
|
|
322
|
+
const maxAttempts = 2;
|
|
323
|
+
const passwordDisp = ptyProcess.onData((data: string) => {
|
|
324
|
+
passwordBuffer += data;
|
|
325
|
+
if (passwordBuffer.length > 4096) passwordBuffer = passwordBuffer.slice(-4096);
|
|
326
|
+
const stripped = passwordBuffer.replace(ANSI_ESC_RE, '').toLowerCase();
|
|
327
|
+
if (stripped.includes('password:') || stripped.includes('password for')) {
|
|
328
|
+
passwordAttempts++;
|
|
329
|
+
if (passwordAttempts > maxAttempts) {
|
|
330
|
+
log.error('pty', `SSH password rejected after ${maxAttempts} attempts for ${terminalId}`);
|
|
331
|
+
passwordDisp.dispose();
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
// Small delay to ensure the SSH process is ready for input
|
|
335
|
+
setTimeout(() => {
|
|
336
|
+
const term = terminals.get(terminalId);
|
|
337
|
+
if (term?.pty) {
|
|
338
|
+
term.pty.write(storedPassword + '\r');
|
|
339
|
+
log.info('pty', `Auto-typed SSH password for ${terminalId} (attempt ${passwordAttempts})`);
|
|
340
|
+
}
|
|
341
|
+
}, 100);
|
|
342
|
+
// Reset buffer so we don't re-trigger on the same prompt
|
|
343
|
+
passwordBuffer = '';
|
|
344
|
+
}
|
|
345
|
+
// Stop watching after successful auth (shell prompt appears)
|
|
346
|
+
if (stripped.includes('last login') || SHELL_PROMPT_RE.test(stripped.split(/[\r\n]+/).filter(l => l.trim()).pop() || '')) {
|
|
347
|
+
passwordDisp.dispose();
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
// Safety cleanup — stop watching after 30s regardless
|
|
351
|
+
setTimeout(() => { passwordDisp.dispose(); }, 30000);
|
|
352
|
+
}
|
|
353
|
+
|
|
316
354
|
// Detect when the shell is ready (prompt visible) before sending commands.
|
|
317
355
|
// Local shells init in ~100-300ms; remote SSH can take seconds for key exchange.
|
|
318
356
|
const shellReady = detectShellReady(ptyProcess, terminalId, local ? 2000 : 10000);
|
|
@@ -327,8 +365,12 @@ export function createTerminal(config: TerminalConfig, wsClient: WebSocket | nul
|
|
|
327
365
|
shellReady,
|
|
328
366
|
});
|
|
329
367
|
|
|
330
|
-
// Register pending link for session matching
|
|
331
|
-
|
|
368
|
+
// Register pending link for session matching.
|
|
369
|
+
// Skip for ops terminals (command='') — they never run Claude so they
|
|
370
|
+
// must not overwrite the main terminal's pending link for the same workDir.
|
|
371
|
+
if (!skipAutoLaunch) {
|
|
372
|
+
pendingLinks.set(workDir, { terminalId, host: config.host || 'localhost', createdAt: Date.now() });
|
|
373
|
+
}
|
|
332
374
|
|
|
333
375
|
// #19: Store disposables for proper cleanup on terminal close
|
|
334
376
|
const disposables: IDisposable[] = [];
|