@seflless/ghosttown 1.6.2 → 2.0.0
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/ghostty-web.js +79 -78
- package/dist/ghostty-web.umd.cjs +3 -3
- package/package.json +9 -4
- package/src/cli.js +410 -367
- package/src/cli.test.ts +279 -5
- package/src/session/history-replay.d.ts +118 -0
- package/src/session/history-replay.js +174 -0
- package/src/session/history-replay.js.map +1 -0
- package/src/session/history-replay.ts +253 -0
- package/src/session/index.d.ts +10 -0
- package/src/session/index.js +11 -0
- package/src/session/index.js.map +1 -0
- package/src/session/index.ts +11 -0
- package/src/session/output-recorder.d.ts +131 -0
- package/src/session/output-recorder.js +247 -0
- package/src/session/output-recorder.js.map +1 -0
- package/src/session/output-recorder.ts +322 -0
- package/src/session/session-manager.d.ts +147 -0
- package/src/session/session-manager.js +489 -0
- package/src/session/session-manager.js.map +1 -0
- package/src/session/session-manager.ts +587 -0
- package/src/session/types.d.ts +221 -0
- package/src/session/types.js +8 -0
- package/src/session/types.js.map +1 -0
- package/src/session/types.ts +236 -0
- package/src/session-utils.js +91 -0
- package/src/session-utils.test.js +229 -0
package/src/cli.js
CHANGED
|
@@ -50,6 +50,24 @@ import pty from '@lydell/node-pty';
|
|
|
50
50
|
import { WebSocketServer } from 'ws';
|
|
51
51
|
// ASCII art generator
|
|
52
52
|
import { asciiArt } from '../bin/ascii.js';
|
|
53
|
+
// Session name utilities
|
|
54
|
+
import { displayNameExists, findSession, validateSessionName } from './session-utils.js';
|
|
55
|
+
// Session management - custom PTY session manager (replaces tmux)
|
|
56
|
+
import { SessionManager } from './session/session-manager.js';
|
|
57
|
+
|
|
58
|
+
// Global SessionManager instance (lazy-initialized)
|
|
59
|
+
let sessionManager = null;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get or create the SessionManager instance
|
|
63
|
+
*/
|
|
64
|
+
async function getSessionManager() {
|
|
65
|
+
if (!sessionManager) {
|
|
66
|
+
sessionManager = new SessionManager();
|
|
67
|
+
await sessionManager.init();
|
|
68
|
+
}
|
|
69
|
+
return sessionManager;
|
|
70
|
+
}
|
|
53
71
|
|
|
54
72
|
const __filename = fileURLToPath(import.meta.url);
|
|
55
73
|
const __dirname = path.dirname(__filename);
|
|
@@ -422,40 +440,43 @@ function isInsideGhosttownSession() {
|
|
|
422
440
|
return sessionName && sessionName.startsWith('gt-');
|
|
423
441
|
}
|
|
424
442
|
|
|
443
|
+
// Note: validateSessionName and displayNameExists are imported from session-utils.js
|
|
444
|
+
// CLI uses getTmuxSessionsForValidation() to adapt tmux sessions to the expected format
|
|
445
|
+
// Web server uses SessionManager directly
|
|
446
|
+
|
|
425
447
|
/**
|
|
426
|
-
*
|
|
427
|
-
*
|
|
448
|
+
* Parse a ghosttown tmux session name into its components.
|
|
449
|
+
* Format: gt-<stable-id>-<display-name>
|
|
450
|
+
* DEPRECATED: Only used by legacy CLI commands that still use tmux
|
|
451
|
+
*
|
|
452
|
+
* @param {string} fullName - The full tmux session name
|
|
453
|
+
* @returns {{ stableId: string|null, displayName: string, fullName: string }|null}
|
|
428
454
|
*/
|
|
429
|
-
function
|
|
430
|
-
if (!
|
|
431
|
-
return
|
|
432
|
-
}
|
|
433
|
-
if (name.length > 50) {
|
|
434
|
-
return { valid: false, error: 'Session name too long (max 50 chars)' };
|
|
435
|
-
}
|
|
436
|
-
if (/^[0-9]+$/.test(name)) {
|
|
437
|
-
return {
|
|
438
|
-
valid: false,
|
|
439
|
-
error: 'Session name cannot be purely numeric (conflicts with auto-generated IDs)',
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
// Reserved name for background server
|
|
443
|
-
if (name === 'ghosttown-host') {
|
|
444
|
-
return { valid: false, error: "'ghosttown-host' is a reserved name" };
|
|
455
|
+
function parseSessionName(fullName) {
|
|
456
|
+
if (!fullName || !fullName.startsWith('gt-')) {
|
|
457
|
+
return null;
|
|
445
458
|
}
|
|
446
|
-
//
|
|
447
|
-
|
|
448
|
-
if (
|
|
459
|
+
// Format: gt-<stable-id>-<display-name>
|
|
460
|
+
const match = fullName.match(/^gt-(\d+)-(.+)$/);
|
|
461
|
+
if (match) {
|
|
449
462
|
return {
|
|
450
|
-
|
|
451
|
-
|
|
463
|
+
stableId: match[1],
|
|
464
|
+
displayName: match[2],
|
|
465
|
+
fullName: fullName,
|
|
452
466
|
};
|
|
453
467
|
}
|
|
454
|
-
|
|
468
|
+
// Legacy format: gt-<name> (no stable ID)
|
|
469
|
+
const legacyName = fullName.slice(3);
|
|
470
|
+
return {
|
|
471
|
+
stableId: null,
|
|
472
|
+
displayName: legacyName,
|
|
473
|
+
fullName: fullName,
|
|
474
|
+
};
|
|
455
475
|
}
|
|
456
476
|
|
|
457
477
|
/**
|
|
458
478
|
* Check if a tmux session with the given name exists
|
|
479
|
+
* DEPRECATED: Only used by legacy CLI commands that still use tmux
|
|
459
480
|
*/
|
|
460
481
|
function sessionExists(sessionName) {
|
|
461
482
|
try {
|
|
@@ -473,10 +494,11 @@ function sessionExists(sessionName) {
|
|
|
473
494
|
}
|
|
474
495
|
|
|
475
496
|
/**
|
|
476
|
-
*
|
|
477
|
-
*
|
|
497
|
+
* Get tmux sessions in a format compatible with displayNameExists().
|
|
498
|
+
* Returns array of objects with displayName property.
|
|
499
|
+
* DEPRECATED: Only used by legacy CLI commands that still use tmux
|
|
478
500
|
*/
|
|
479
|
-
function
|
|
501
|
+
function getTmuxSessionsForValidation() {
|
|
480
502
|
try {
|
|
481
503
|
const output = execSync('tmux list-sessions -F "#{session_name}"', {
|
|
482
504
|
encoding: 'utf8',
|
|
@@ -484,61 +506,54 @@ function displayNameExists(displayName) {
|
|
|
484
506
|
});
|
|
485
507
|
return output
|
|
486
508
|
.split('\n')
|
|
487
|
-
.filter((
|
|
488
|
-
.
|
|
509
|
+
.filter((name) => name.startsWith('gt-'))
|
|
510
|
+
.map((name) => {
|
|
489
511
|
const parsed = parseSessionName(name);
|
|
490
|
-
return parsed
|
|
512
|
+
return { displayName: parsed ? parsed.displayName : name.replace('gt-', '') };
|
|
491
513
|
});
|
|
492
514
|
} catch (err) {
|
|
493
|
-
return
|
|
515
|
+
return [];
|
|
494
516
|
}
|
|
495
517
|
}
|
|
496
518
|
|
|
497
519
|
/**
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
*
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
if (!fullName || !fullName.startsWith('gt-')) {
|
|
504
|
-
return null;
|
|
505
|
-
}
|
|
506
|
-
// Format: gt-<stable-id>-<display-name>
|
|
507
|
-
const match = fullName.match(/^gt-(\d+)-(.+)$/);
|
|
508
|
-
if (match) {
|
|
509
|
-
return {
|
|
510
|
-
stableId: match[1],
|
|
511
|
-
displayName: match[2],
|
|
512
|
-
fullName: fullName,
|
|
513
|
-
};
|
|
514
|
-
}
|
|
515
|
-
// Legacy format: gt-<name> (no stable ID)
|
|
516
|
-
// Treat the whole thing after gt- as the display name, no stable ID
|
|
517
|
-
const legacyName = fullName.slice(3);
|
|
518
|
-
return {
|
|
519
|
-
stableId: null,
|
|
520
|
-
displayName: legacyName,
|
|
521
|
-
fullName: fullName,
|
|
522
|
-
};
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
/**
|
|
526
|
-
* Find a ghosttown session by its stable ID
|
|
527
|
-
* Returns the full session name or null if not found
|
|
520
|
+
* Find a tmux session by display name or stableId.
|
|
521
|
+
* Used by CLI commands (attach, rename, kill) that interact with tmux sessions.
|
|
522
|
+
*
|
|
523
|
+
* @param {string} identifier - Display name or stableId to search for
|
|
524
|
+
* @returns {{ displayName: string, stableId: string|null, fullName: string }|null}
|
|
528
525
|
*/
|
|
529
|
-
function
|
|
526
|
+
function findSessionByDisplayName(identifier) {
|
|
530
527
|
try {
|
|
531
528
|
const output = execSync('tmux list-sessions -F "#{session_name}"', {
|
|
532
529
|
encoding: 'utf8',
|
|
533
530
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
534
531
|
});
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
532
|
+
|
|
533
|
+
const sessions = output
|
|
534
|
+
.split('\n')
|
|
535
|
+
.filter((name) => name.startsWith('gt-') && name !== SERVER_SESSION_NAME)
|
|
536
|
+
.map((fullName) => {
|
|
537
|
+
const parsed = parseSessionName(fullName);
|
|
538
|
+
return {
|
|
539
|
+
displayName: parsed ? parsed.displayName : fullName.replace('gt-', ''),
|
|
540
|
+
stableId: parsed ? parsed.stableId : null,
|
|
541
|
+
fullName,
|
|
542
|
+
};
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
// Try exact display name match first
|
|
546
|
+
const byName = sessions.find((s) => s.displayName === identifier);
|
|
547
|
+
if (byName) {
|
|
548
|
+
return byName;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Try stableId match (for backwards compatibility)
|
|
552
|
+
const byId = sessions.find((s) => s.stableId === identifier);
|
|
553
|
+
if (byId) {
|
|
554
|
+
return byId;
|
|
541
555
|
}
|
|
556
|
+
|
|
542
557
|
return null;
|
|
543
558
|
} catch (err) {
|
|
544
559
|
return null;
|
|
@@ -766,7 +781,7 @@ function createTmuxSession(command, customName = null) {
|
|
|
766
781
|
process.exit(1);
|
|
767
782
|
}
|
|
768
783
|
// Check if display name already exists
|
|
769
|
-
if (displayNameExists(customName)) {
|
|
784
|
+
if (displayNameExists(getTmuxSessionsForValidation(), customName)) {
|
|
770
785
|
console.log('');
|
|
771
786
|
console.log(` ${RED}Error:${RESET} Session '${customName}' already exists.`);
|
|
772
787
|
console.log('');
|
|
@@ -937,32 +952,13 @@ function attachToSession(sessionName) {
|
|
|
937
952
|
const RED = '\x1b[31m';
|
|
938
953
|
const BEIGE = '\x1b[38;2;255;220;150m';
|
|
939
954
|
|
|
940
|
-
//
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
// Check if session exists
|
|
946
|
-
try {
|
|
947
|
-
const output = execSync('tmux list-sessions -F "#{session_name}"', {
|
|
948
|
-
encoding: 'utf8',
|
|
949
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
950
|
-
});
|
|
951
|
-
|
|
952
|
-
const sessions = output.split('\n').filter((s) => s.trim());
|
|
953
|
-
if (!sessions.includes(sessionName)) {
|
|
954
|
-
console.log('');
|
|
955
|
-
console.log(` ${RED}Error:${RESET} Session '${sessionName}' not found.`);
|
|
956
|
-
console.log('');
|
|
957
|
-
console.log(` ${BEIGE}No ghosttown sessions are currently running.${RESET}`);
|
|
958
|
-
console.log('');
|
|
959
|
-
process.exit(1);
|
|
960
|
-
}
|
|
961
|
-
} catch (err) {
|
|
955
|
+
// Look up session by display name
|
|
956
|
+
const found = findSessionByDisplayName(sessionName);
|
|
957
|
+
if (!found) {
|
|
962
958
|
console.log('');
|
|
963
|
-
console.log(` ${RED}Error:${RESET}
|
|
959
|
+
console.log(` ${RED}Error:${RESET} Session '${sessionName}' not found.`);
|
|
964
960
|
console.log('');
|
|
965
|
-
|
|
961
|
+
listSessionsInline();
|
|
966
962
|
console.log('');
|
|
967
963
|
process.exit(1);
|
|
968
964
|
}
|
|
@@ -971,7 +967,7 @@ function attachToSession(sessionName) {
|
|
|
971
967
|
// Use the same pattern as detachFromTmux which works
|
|
972
968
|
const result = spawnSync(
|
|
973
969
|
process.env.SHELL || '/bin/sh',
|
|
974
|
-
['-c', `tmux attach-session -t ${
|
|
970
|
+
['-c', `tmux attach-session -t ${found.fullName}`],
|
|
975
971
|
{
|
|
976
972
|
stdio: 'inherit',
|
|
977
973
|
}
|
|
@@ -1087,57 +1083,57 @@ function killSession(sessionName) {
|
|
|
1087
1083
|
}
|
|
1088
1084
|
|
|
1089
1085
|
sessionName = getCurrentTmuxSessionName();
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
// Add gt- prefix if not present
|
|
1093
|
-
if (!sessionName.startsWith('gt-')) {
|
|
1094
|
-
sessionName = `gt-${sessionName}`;
|
|
1095
|
-
}
|
|
1086
|
+
// sessionName is now the full tmux name (e.g., gt-12-13)
|
|
1087
|
+
const parsed = parseSessionName(sessionName);
|
|
1096
1088
|
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
});
|
|
1089
|
+
// Kill the session
|
|
1090
|
+
try {
|
|
1091
|
+
execSync(`tmux kill-session -t ${sessionName}`, {
|
|
1092
|
+
stdio: 'pipe',
|
|
1093
|
+
});
|
|
1103
1094
|
|
|
1104
|
-
|
|
1105
|
-
if (!sessions.includes(sessionName)) {
|
|
1095
|
+
const displayName = parsed ? parsed.displayName : sessionName.replace('gt-', '');
|
|
1106
1096
|
console.log('');
|
|
1107
|
-
console.log(` ${
|
|
1097
|
+
console.log(` ${BOLD_YELLOW}Session ${displayName} has been killed.${RESET}`);
|
|
1108
1098
|
console.log('');
|
|
1109
|
-
console.log(` ${
|
|
1099
|
+
console.log(` ${CYAN}To list remaining:${RESET} ${BEIGE}gt list${RESET}`);
|
|
1100
|
+
console.log('');
|
|
1101
|
+
process.exit(0);
|
|
1102
|
+
} catch (err) {
|
|
1103
|
+
const displayName = parsed ? parsed.displayName : sessionName.replace('gt-', '');
|
|
1104
|
+
console.log('');
|
|
1105
|
+
console.log(` ${RED}Error:${RESET} Failed to kill session '${displayName}'.`);
|
|
1110
1106
|
console.log('');
|
|
1111
1107
|
process.exit(1);
|
|
1112
1108
|
}
|
|
1113
|
-
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// User provided a session name - look it up by display name
|
|
1112
|
+
const found = findSessionByDisplayName(sessionName);
|
|
1113
|
+
if (!found) {
|
|
1114
1114
|
console.log('');
|
|
1115
|
-
console.log(` ${RED}Error:${RESET}
|
|
1115
|
+
console.log(` ${RED}Error:${RESET} Session '${sessionName}' not found.`);
|
|
1116
1116
|
console.log('');
|
|
1117
|
-
|
|
1117
|
+
listSessionsInline();
|
|
1118
1118
|
console.log('');
|
|
1119
1119
|
process.exit(1);
|
|
1120
1120
|
}
|
|
1121
1121
|
|
|
1122
1122
|
// Kill the session
|
|
1123
1123
|
try {
|
|
1124
|
-
execSync(`tmux kill-session -t ${
|
|
1124
|
+
execSync(`tmux kill-session -t ${found.fullName}`, {
|
|
1125
1125
|
stdio: 'pipe',
|
|
1126
1126
|
});
|
|
1127
1127
|
|
|
1128
|
-
// Extract display name from session name
|
|
1129
|
-
const parsed = parseSessionName(sessionName);
|
|
1130
|
-
const displayName = parsed ? parsed.displayName : sessionName.replace('gt-', '');
|
|
1131
|
-
|
|
1132
1128
|
console.log('');
|
|
1133
|
-
console.log(` ${BOLD_YELLOW}Session ${displayName} has been killed.${RESET}`);
|
|
1129
|
+
console.log(` ${BOLD_YELLOW}Session ${found.displayName} has been killed.${RESET}`);
|
|
1134
1130
|
console.log('');
|
|
1135
1131
|
console.log(` ${CYAN}To list remaining:${RESET} ${BEIGE}gt list${RESET}`);
|
|
1136
1132
|
console.log('');
|
|
1137
1133
|
process.exit(0);
|
|
1138
1134
|
} catch (err) {
|
|
1139
1135
|
console.log('');
|
|
1140
|
-
console.log(` ${RED}Error:${RESET} Failed to kill session '${
|
|
1136
|
+
console.log(` ${RED}Error:${RESET} Failed to kill session '${found.displayName}'.`);
|
|
1141
1137
|
console.log('');
|
|
1142
1138
|
process.exit(1);
|
|
1143
1139
|
}
|
|
@@ -1442,29 +1438,6 @@ function listSessionsInline() {
|
|
|
1442
1438
|
}
|
|
1443
1439
|
}
|
|
1444
1440
|
|
|
1445
|
-
/**
|
|
1446
|
-
* Find a ghosttown session by display name
|
|
1447
|
-
* Returns the parsed session info or null if not found
|
|
1448
|
-
*/
|
|
1449
|
-
function findSessionByDisplayName(displayName) {
|
|
1450
|
-
try {
|
|
1451
|
-
const output = execSync('tmux list-sessions -F "#{session_name}"', {
|
|
1452
|
-
encoding: 'utf8',
|
|
1453
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
1454
|
-
});
|
|
1455
|
-
const sessions = output.split('\n').filter((s) => s.trim() && s.startsWith('gt-'));
|
|
1456
|
-
for (const fullName of sessions) {
|
|
1457
|
-
const parsed = parseSessionName(fullName);
|
|
1458
|
-
if (parsed && parsed.displayName === displayName) {
|
|
1459
|
-
return parsed;
|
|
1460
|
-
}
|
|
1461
|
-
}
|
|
1462
|
-
return null;
|
|
1463
|
-
} catch (err) {
|
|
1464
|
-
return null;
|
|
1465
|
-
}
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
1441
|
/**
|
|
1469
1442
|
* Rename a ghosttown session
|
|
1470
1443
|
* Usage:
|
|
@@ -1540,7 +1513,7 @@ function renameSession(renameArgs) {
|
|
|
1540
1513
|
}
|
|
1541
1514
|
|
|
1542
1515
|
// Check new display name doesn't conflict
|
|
1543
|
-
if (displayNameExists(newDisplayName)) {
|
|
1516
|
+
if (displayNameExists(getTmuxSessionsForValidation(), newDisplayName)) {
|
|
1544
1517
|
console.log('');
|
|
1545
1518
|
console.log(` ${RED}Error:${RESET} Session '${newDisplayName}' already exists.`);
|
|
1546
1519
|
console.log('');
|
|
@@ -1672,8 +1645,7 @@ Aliases:
|
|
|
1672
1645
|
if (!sessionArg) {
|
|
1673
1646
|
console.error('Error: attach requires a session name or ID');
|
|
1674
1647
|
console.error('Usage: gt attach <name|id>');
|
|
1675
|
-
|
|
1676
|
-
break;
|
|
1648
|
+
process.exit(1);
|
|
1677
1649
|
}
|
|
1678
1650
|
handled = true;
|
|
1679
1651
|
attachToSession(sessionArg);
|
|
@@ -1978,7 +1950,15 @@ function startWebServer(cliArgs) {
|
|
|
1978
1950
|
color: #e5e5e5;
|
|
1979
1951
|
font-size: 22px;
|
|
1980
1952
|
font-weight: 600;
|
|
1981
|
-
margin-bottom:
|
|
1953
|
+
margin-bottom: 4px;
|
|
1954
|
+
text-align: center;
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
.session-card .subtitle {
|
|
1958
|
+
color: #888;
|
|
1959
|
+
font-size: 13px;
|
|
1960
|
+
font-weight: 400;
|
|
1961
|
+
margin-bottom: 16px;
|
|
1982
1962
|
text-align: center;
|
|
1983
1963
|
}
|
|
1984
1964
|
|
|
@@ -2382,6 +2362,7 @@ function startWebServer(cliArgs) {
|
|
|
2382
2362
|
<div class="session-list-view" id="session-list-view">
|
|
2383
2363
|
<div class="session-card">
|
|
2384
2364
|
<h1>Ghost Town</h1>
|
|
2365
|
+
<p class="subtitle">Choose a terminal to connect to</p>
|
|
2385
2366
|
<div class="error-message" id="error-message"></div>
|
|
2386
2367
|
<div class="session-table-header" id="session-table-header" style="display: none;">
|
|
2387
2368
|
<span class="header-name">Name</span>
|
|
@@ -2454,13 +2435,15 @@ function startWebServer(cliArgs) {
|
|
|
2454
2435
|
}
|
|
2455
2436
|
|
|
2456
2437
|
// Get session info from URL query parameters
|
|
2457
|
-
// Returns { name, stableId } or null
|
|
2438
|
+
// Returns { sessionId, name, stableId } or null
|
|
2439
|
+
// Supports both new (?sessionId=uuid) and legacy (?session=name&id=stableId) formats
|
|
2458
2440
|
function getSessionFromUrl() {
|
|
2459
2441
|
const params = new URLSearchParams(window.location.search);
|
|
2442
|
+
const sessionId = params.get('sessionId');
|
|
2460
2443
|
const name = params.get('session');
|
|
2461
2444
|
const stableId = params.get('id');
|
|
2462
|
-
if (name || stableId) {
|
|
2463
|
-
return { name, stableId };
|
|
2445
|
+
if (sessionId || name || stableId) {
|
|
2446
|
+
return { sessionId, name, stableId };
|
|
2464
2447
|
}
|
|
2465
2448
|
return null;
|
|
2466
2449
|
}
|
|
@@ -2469,9 +2452,13 @@ function startWebServer(cliArgs) {
|
|
|
2469
2452
|
async function initApp() {
|
|
2470
2453
|
const sessionInfo = getSessionFromUrl();
|
|
2471
2454
|
|
|
2472
|
-
if (sessionInfo && sessionInfo.
|
|
2473
|
-
//
|
|
2474
|
-
requestedSessionName = sessionInfo.name
|
|
2455
|
+
if (sessionInfo && sessionInfo.sessionId) {
|
|
2456
|
+
// New format: use UUID sessionId
|
|
2457
|
+
requestedSessionName = sessionInfo.name || sessionInfo.sessionId;
|
|
2458
|
+
showTerminalView(sessionInfo.sessionId);
|
|
2459
|
+
} else if (sessionInfo && sessionInfo.stableId) {
|
|
2460
|
+
// Legacy format: use stable ID
|
|
2461
|
+
requestedSessionName = sessionInfo.name;
|
|
2475
2462
|
showTerminalView(sessionInfo.stableId);
|
|
2476
2463
|
} else if (sessionInfo && sessionInfo.name) {
|
|
2477
2464
|
// Legacy URL with just name - show terminal view
|
|
@@ -3356,15 +3343,22 @@ function startWebServer(cliArgs) {
|
|
|
3356
3343
|
res.end();
|
|
3357
3344
|
return;
|
|
3358
3345
|
}
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3346
|
+
(async () => {
|
|
3347
|
+
try {
|
|
3348
|
+
const manager = await getSessionManager();
|
|
3349
|
+
// Create session without starting PTY - it will start on first WebSocket connect
|
|
3350
|
+
const newSession = await manager.createSession({ startProcess: false });
|
|
3351
|
+
// Use sessionId for new format, keep session param for backward compat
|
|
3352
|
+
res.writeHead(302, {
|
|
3353
|
+
Location: `/?sessionId=${newSession.id}&session=${encodeURIComponent(newSession.displayName)}`,
|
|
3354
|
+
});
|
|
3355
|
+
res.end();
|
|
3356
|
+
} catch (err) {
|
|
3357
|
+
console.error('Failed to create session:', err);
|
|
3358
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
3359
|
+
res.end('Failed to create session');
|
|
3360
|
+
}
|
|
3361
|
+
})();
|
|
3368
3362
|
return;
|
|
3369
3363
|
}
|
|
3370
3364
|
|
|
@@ -3403,7 +3397,7 @@ function startWebServer(cliArgs) {
|
|
|
3403
3397
|
return;
|
|
3404
3398
|
}
|
|
3405
3399
|
|
|
3406
|
-
// API: List sessions
|
|
3400
|
+
// API: List sessions - requires authentication
|
|
3407
3401
|
if (pathname === '/api/sessions' && req.method === 'GET') {
|
|
3408
3402
|
if (!session) {
|
|
3409
3403
|
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
@@ -3411,40 +3405,31 @@ function startWebServer(cliArgs) {
|
|
|
3411
3405
|
return;
|
|
3412
3406
|
}
|
|
3413
3407
|
try {
|
|
3414
|
-
const
|
|
3415
|
-
|
|
3416
|
-
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
.
|
|
3421
|
-
.
|
|
3422
|
-
.
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
stableId: parsed ? parsed.stableId : null,
|
|
3428
|
-
fullName: fullName,
|
|
3429
|
-
lastActivity: Number.parseInt(activity, 10) * 1000,
|
|
3430
|
-
attached: attached === '1',
|
|
3431
|
-
windows: Number.parseInt(windows, 10),
|
|
3432
|
-
};
|
|
3433
|
-
});
|
|
3408
|
+
const manager = await getSessionManager();
|
|
3409
|
+
const sessionList = await manager.listSessions();
|
|
3410
|
+
|
|
3411
|
+
// Map to API response format with backward-compatible fields
|
|
3412
|
+
const response = sessionList.map((s) => ({
|
|
3413
|
+
name: s.displayName,
|
|
3414
|
+
stableId: s.id.slice(0, 8), // Short ID for backward compat
|
|
3415
|
+
id: s.id, // Full UUID (new)
|
|
3416
|
+
lastActivity: s.lastActivity,
|
|
3417
|
+
attached: s.connectionCount > 0, // Backward compat: attached means has connections
|
|
3418
|
+
isRunning: s.isRunning,
|
|
3419
|
+
connectionCount: s.connectionCount,
|
|
3420
|
+
}));
|
|
3434
3421
|
|
|
3435
|
-
// Sort by recent activity
|
|
3436
|
-
sessionList.sort((a, b) => b.lastActivity - a.lastActivity);
|
|
3437
3422
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
3438
|
-
res.end(JSON.stringify({ sessions:
|
|
3423
|
+
res.end(JSON.stringify({ sessions: response }));
|
|
3439
3424
|
} catch (err) {
|
|
3440
|
-
|
|
3425
|
+
console.error('Error listing sessions:', err);
|
|
3441
3426
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
3442
3427
|
res.end(JSON.stringify({ sessions: [] }));
|
|
3443
3428
|
}
|
|
3444
3429
|
return;
|
|
3445
3430
|
}
|
|
3446
3431
|
|
|
3447
|
-
// API: Create session
|
|
3432
|
+
// API: Create session - requires authentication
|
|
3448
3433
|
if (pathname === '/api/sessions/create' && req.method === 'POST') {
|
|
3449
3434
|
if (!session) {
|
|
3450
3435
|
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
@@ -3452,10 +3437,18 @@ function startWebServer(cliArgs) {
|
|
|
3452
3437
|
return;
|
|
3453
3438
|
}
|
|
3454
3439
|
try {
|
|
3455
|
-
const
|
|
3440
|
+
const manager = await getSessionManager();
|
|
3441
|
+
const newSession = await manager.createSession();
|
|
3456
3442
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
3457
|
-
res.end(
|
|
3443
|
+
res.end(
|
|
3444
|
+
JSON.stringify({
|
|
3445
|
+
name: newSession.displayName,
|
|
3446
|
+
stableId: newSession.id.slice(0, 8), // Short ID for backward compat
|
|
3447
|
+
id: newSession.id, // Full UUID (new)
|
|
3448
|
+
})
|
|
3449
|
+
);
|
|
3458
3450
|
} catch (err) {
|
|
3451
|
+
console.error('Error creating session:', err);
|
|
3459
3452
|
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
3460
3453
|
res.end(JSON.stringify({ error: 'Failed to create session' }));
|
|
3461
3454
|
}
|
|
@@ -3472,85 +3465,123 @@ function startWebServer(cliArgs) {
|
|
|
3472
3465
|
|
|
3473
3466
|
const cols = Number.parseInt(url.searchParams.get('cols') || '80');
|
|
3474
3467
|
const rows = Number.parseInt(url.searchParams.get('rows') || '24');
|
|
3475
|
-
|
|
3476
|
-
const
|
|
3468
|
+
// Support both new (sessionId) and legacy (session) parameters
|
|
3469
|
+
const sessionIdParam = url.searchParams.get('sessionId');
|
|
3470
|
+
const legacySession = url.searchParams.get('session');
|
|
3471
|
+
const requestedName = url.searchParams.get('name');
|
|
3472
|
+
const identifier = sessionIdParam || legacySession;
|
|
3477
3473
|
|
|
3478
|
-
if (!
|
|
3474
|
+
if (!identifier) {
|
|
3479
3475
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
3480
3476
|
res.end(JSON.stringify({ error: 'Session parameter required' }));
|
|
3481
3477
|
return;
|
|
3482
3478
|
}
|
|
3483
3479
|
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3480
|
+
try {
|
|
3481
|
+
const manager = await getSessionManager();
|
|
3482
|
+
|
|
3483
|
+
// Look up session by UUID, short ID, or display name
|
|
3484
|
+
const sessionObj = await findSessionForWeb(identifier);
|
|
3485
|
+
if (!sessionObj) {
|
|
3486
|
+
let errorMsg;
|
|
3487
|
+
if (requestedName && requestedName !== identifier) {
|
|
3488
|
+
errorMsg = `No session found named ${requestedName}, or with id ${identifier}`;
|
|
3489
|
+
} else {
|
|
3490
|
+
errorMsg = `No session found named ${identifier} or with id ${identifier}`;
|
|
3491
|
+
}
|
|
3492
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
3493
|
+
res.end(JSON.stringify({ error: errorMsg }));
|
|
3494
|
+
return;
|
|
3496
3495
|
}
|
|
3497
|
-
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
3498
|
-
res.end(JSON.stringify({ error: errorMsg }));
|
|
3499
|
-
return;
|
|
3500
|
-
}
|
|
3501
3496
|
|
|
3502
|
-
|
|
3503
|
-
|
|
3497
|
+
// Generate unique connection ID
|
|
3498
|
+
const connectionId = `sse-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
3504
3499
|
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3500
|
+
// Set up SSE headers first
|
|
3501
|
+
res.writeHead(200, {
|
|
3502
|
+
'Content-Type': 'text/event-stream',
|
|
3503
|
+
'Cache-Control': 'no-cache',
|
|
3504
|
+
Connection: 'keep-alive',
|
|
3505
|
+
'X-SSE-Connection-Id': connectionId,
|
|
3506
|
+
});
|
|
3512
3507
|
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3508
|
+
// Send session info as first event
|
|
3509
|
+
res.write(
|
|
3510
|
+
`data: ${JSON.stringify({ type: 'session_info', name: sessionObj.displayName, stableId: sessionObj.id.slice(0, 8), id: sessionObj.id, connectionId })}\n\n`
|
|
3511
|
+
);
|
|
3517
3512
|
|
|
3518
|
-
|
|
3519
|
-
|
|
3513
|
+
// Timeout for closing connection after session exit (30 seconds)
|
|
3514
|
+
const SSE_EXIT_TIMEOUT_MS = 30000;
|
|
3515
|
+
let exitTimeout = null;
|
|
3516
|
+
|
|
3517
|
+
// Set up event handlers BEFORE connecting (to catch early output like shell prompt)
|
|
3518
|
+
const outputHandler = (sid, data) => {
|
|
3519
|
+
if (sid === sessionObj.id && !res.writableEnded) {
|
|
3520
|
+
// Clear exit timeout if session produces output (it restarted)
|
|
3521
|
+
if (exitTimeout) {
|
|
3522
|
+
clearTimeout(exitTimeout);
|
|
3523
|
+
exitTimeout = null;
|
|
3524
|
+
}
|
|
3525
|
+
const encoded = Buffer.from(data).toString('base64');
|
|
3526
|
+
res.write(`data: ${JSON.stringify({ type: 'output', data: encoded })}\n\n`);
|
|
3527
|
+
}
|
|
3528
|
+
};
|
|
3520
3529
|
|
|
3521
|
-
|
|
3522
|
-
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3530
|
+
const exitHandler = (sid) => {
|
|
3531
|
+
if (sid === sessionObj.id && !res.writableEnded) {
|
|
3532
|
+
res.write(`data: ${JSON.stringify({ type: 'exit' })}\n\n`);
|
|
3533
|
+
// Set timeout to close connection if session doesn't restart
|
|
3534
|
+
exitTimeout = setTimeout(() => {
|
|
3535
|
+
if (!res.writableEnded) {
|
|
3536
|
+
res.write(
|
|
3537
|
+
`data: ${JSON.stringify({ type: 'timeout', message: 'Session exited and did not restart' })}\n\n`
|
|
3538
|
+
);
|
|
3539
|
+
res.end();
|
|
3540
|
+
const conn = sseConnections.get(connectionId);
|
|
3541
|
+
if (conn) {
|
|
3542
|
+
manager.off('session:output', conn.outputHandler);
|
|
3543
|
+
manager.off('session:exit', conn.exitHandler);
|
|
3544
|
+
sseConnections.delete(connectionId);
|
|
3545
|
+
}
|
|
3546
|
+
}
|
|
3547
|
+
}, SSE_EXIT_TIMEOUT_MS);
|
|
3548
|
+
}
|
|
3549
|
+
};
|
|
3528
3550
|
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
if (!res.writableEnded) {
|
|
3532
|
-
// Base64 encode to handle binary data safely
|
|
3533
|
-
const encoded = Buffer.from(data).toString('base64');
|
|
3534
|
-
res.write(`data: ${JSON.stringify({ type: 'output', data: encoded })}\n\n`);
|
|
3535
|
-
}
|
|
3536
|
-
});
|
|
3551
|
+
manager.on('session:output', outputHandler);
|
|
3552
|
+
manager.on('session:exit', exitHandler);
|
|
3537
3553
|
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
res
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
|
|
3554
|
+
// Store connection
|
|
3555
|
+
sseConnections.set(connectionId, {
|
|
3556
|
+
res,
|
|
3557
|
+
sessionId: sessionObj.id,
|
|
3558
|
+
outputHandler,
|
|
3559
|
+
exitHandler,
|
|
3560
|
+
exitTimeout: () => exitTimeout, // Getter for cleanup
|
|
3561
|
+
});
|
|
3545
3562
|
|
|
3546
|
-
|
|
3547
|
-
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3563
|
+
// Connect to session (respawns PTY if needed) - after handlers are set up
|
|
3564
|
+
await manager.connectToSession(sessionObj.id);
|
|
3565
|
+
await manager.resizeSession(sessionObj.id, cols, rows);
|
|
3566
|
+
|
|
3567
|
+
// Handle client disconnect
|
|
3568
|
+
req.on('close', () => {
|
|
3569
|
+
const conn = sseConnections.get(connectionId);
|
|
3570
|
+
if (conn) {
|
|
3571
|
+
// Clear any pending exit timeout
|
|
3572
|
+
const timeout = conn.exitTimeout?.();
|
|
3573
|
+
if (timeout) clearTimeout(timeout);
|
|
3574
|
+
manager.off('session:output', conn.outputHandler);
|
|
3575
|
+
manager.off('session:exit', conn.exitHandler);
|
|
3576
|
+
sseConnections.delete(connectionId);
|
|
3577
|
+
// Note: We do NOT kill the PTY - session persists for reconnection
|
|
3578
|
+
}
|
|
3579
|
+
});
|
|
3580
|
+
} catch (err) {
|
|
3581
|
+
console.error('SSE connection error:', err);
|
|
3582
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
3583
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
3584
|
+
}
|
|
3554
3585
|
|
|
3555
3586
|
return;
|
|
3556
3587
|
}
|
|
@@ -3567,7 +3598,7 @@ function startWebServer(cliArgs) {
|
|
|
3567
3598
|
req.on('data', (chunk) => {
|
|
3568
3599
|
body += chunk;
|
|
3569
3600
|
});
|
|
3570
|
-
req.on('end', () => {
|
|
3601
|
+
req.on('end', async () => {
|
|
3571
3602
|
try {
|
|
3572
3603
|
const { connectionId, data, type, cols, rows } = JSON.parse(body);
|
|
3573
3604
|
|
|
@@ -3578,12 +3609,14 @@ function startWebServer(cliArgs) {
|
|
|
3578
3609
|
return;
|
|
3579
3610
|
}
|
|
3580
3611
|
|
|
3612
|
+
const manager = await getSessionManager();
|
|
3613
|
+
|
|
3581
3614
|
if (type === 'resize' && cols && rows) {
|
|
3582
|
-
conn.
|
|
3615
|
+
await manager.resizeSession(conn.sessionId, cols, rows);
|
|
3583
3616
|
} else if (data) {
|
|
3584
3617
|
// Decode base64 input
|
|
3585
3618
|
const decoded = Buffer.from(data, 'base64').toString('utf8');
|
|
3586
|
-
|
|
3619
|
+
manager.write(conn.sessionId, decoded);
|
|
3587
3620
|
}
|
|
3588
3621
|
|
|
3589
3622
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
@@ -3620,56 +3653,35 @@ function startWebServer(cliArgs) {
|
|
|
3620
3653
|
// WebSocket Server
|
|
3621
3654
|
// ============================================================================
|
|
3622
3655
|
|
|
3623
|
-
// Track active WebSocket connections to
|
|
3624
|
-
// ws -> {
|
|
3656
|
+
// Track active WebSocket connections to sessions
|
|
3657
|
+
// ws -> { sessionId, outputHandler, exitHandler }
|
|
3625
3658
|
const wsConnections = new Map();
|
|
3626
3659
|
|
|
3627
3660
|
// Track active SSE connections (fallback for mobile)
|
|
3628
|
-
// connectionId -> { res,
|
|
3661
|
+
// connectionId -> { res, sessionId, outputHandler, exitHandler }
|
|
3629
3662
|
const sseConnections = new Map();
|
|
3630
3663
|
|
|
3631
3664
|
/**
|
|
3632
|
-
* Find a
|
|
3633
|
-
* Returns
|
|
3665
|
+
* Find a session by various identifiers (UUID, short ID, or display name)
|
|
3666
|
+
* Returns session object or null if not found
|
|
3634
3667
|
*/
|
|
3635
|
-
function
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
encoding: 'utf8',
|
|
3639
|
-
stdio: ['pipe', 'pipe', 'pipe'],
|
|
3640
|
-
});
|
|
3641
|
-
const sessions = output.split('\n').filter((s) => s.trim() && s.startsWith('gt-'));
|
|
3642
|
-
for (const fullName of sessions) {
|
|
3643
|
-
const parsed = parseSessionName(fullName);
|
|
3644
|
-
if (parsed && parsed.stableId === stableId) {
|
|
3645
|
-
return parsed;
|
|
3646
|
-
}
|
|
3647
|
-
}
|
|
3648
|
-
return null;
|
|
3649
|
-
} catch (err) {
|
|
3650
|
-
return null;
|
|
3651
|
-
}
|
|
3652
|
-
}
|
|
3668
|
+
async function findSessionForWeb(identifier) {
|
|
3669
|
+
const manager = await getSessionManager();
|
|
3670
|
+
const sessions = await manager.listSessions();
|
|
3653
3671
|
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
function createTmuxAttachPty(fullSessionName, cols, rows) {
|
|
3658
|
-
const ptyProcess = pty.spawn('tmux', ['attach-session', '-t', fullSessionName], {
|
|
3659
|
-
name: 'xterm-256color',
|
|
3660
|
-
cols: cols,
|
|
3661
|
-
rows: rows,
|
|
3662
|
-
cwd: homedir(),
|
|
3663
|
-
env: {
|
|
3664
|
-
...process.env,
|
|
3665
|
-
TERM: 'xterm-256color',
|
|
3666
|
-
COLORTERM: 'truecolor',
|
|
3667
|
-
LANG: 'en_US.UTF-8',
|
|
3668
|
-
LC_ALL: 'en_US.UTF-8',
|
|
3669
|
-
},
|
|
3670
|
-
});
|
|
3672
|
+
// Try exact UUID match first
|
|
3673
|
+
let session = sessions.find((s) => s.id === identifier);
|
|
3674
|
+
if (session) return manager.getSession(session.id);
|
|
3671
3675
|
|
|
3672
|
-
|
|
3676
|
+
// Try short ID prefix match
|
|
3677
|
+
session = sessions.find((s) => s.id.startsWith(identifier));
|
|
3678
|
+
if (session) return manager.getSession(session.id);
|
|
3679
|
+
|
|
3680
|
+
// Try display name match
|
|
3681
|
+
session = sessions.find((s) => s.displayName === identifier);
|
|
3682
|
+
if (session) return manager.getSession(session.id);
|
|
3683
|
+
|
|
3684
|
+
return null;
|
|
3673
3685
|
}
|
|
3674
3686
|
|
|
3675
3687
|
const wss = new WebSocketServer({ noServer: true });
|
|
@@ -3700,99 +3712,130 @@ function startWebServer(cliArgs) {
|
|
|
3700
3712
|
}
|
|
3701
3713
|
});
|
|
3702
3714
|
|
|
3703
|
-
wss.on('connection', (ws, req) => {
|
|
3715
|
+
wss.on('connection', async (ws, req) => {
|
|
3704
3716
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
3705
3717
|
const cols = Number.parseInt(url.searchParams.get('cols') || '80');
|
|
3706
3718
|
const rows = Number.parseInt(url.searchParams.get('rows') || '24');
|
|
3707
|
-
|
|
3719
|
+
|
|
3720
|
+
// Support both new (sessionId) and legacy (session) parameters
|
|
3721
|
+
const sessionId = url.searchParams.get('sessionId');
|
|
3722
|
+
const legacySession = url.searchParams.get('session');
|
|
3708
3723
|
const requestedName = url.searchParams.get('name'); // Original name from URL (if different from session)
|
|
3724
|
+
const identifier = sessionId || legacySession;
|
|
3709
3725
|
|
|
3710
|
-
// Session parameter is required
|
|
3711
|
-
if (!
|
|
3726
|
+
// Session parameter is required
|
|
3727
|
+
if (!identifier) {
|
|
3712
3728
|
ws.send(JSON.stringify({ type: 'error', message: 'Session parameter required' }));
|
|
3713
3729
|
ws.close();
|
|
3714
3730
|
return;
|
|
3715
3731
|
}
|
|
3716
3732
|
|
|
3717
|
-
|
|
3718
|
-
|
|
3719
|
-
|
|
3720
|
-
//
|
|
3721
|
-
|
|
3722
|
-
|
|
3723
|
-
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3733
|
+
try {
|
|
3734
|
+
const manager = await getSessionManager();
|
|
3735
|
+
|
|
3736
|
+
// Look up session by UUID, short ID, or display name
|
|
3737
|
+
const session = await findSessionForWeb(identifier);
|
|
3738
|
+
|
|
3739
|
+
if (!session) {
|
|
3740
|
+
// Build error message with both name and id if available
|
|
3741
|
+
let errorMsg;
|
|
3742
|
+
if (requestedName && requestedName !== identifier) {
|
|
3743
|
+
errorMsg = `No session found named ${requestedName}, or with id ${identifier}`;
|
|
3744
|
+
} else {
|
|
3745
|
+
errorMsg = `No session found named ${identifier} or with id ${identifier}`;
|
|
3746
|
+
}
|
|
3747
|
+
ws.send(JSON.stringify({ type: 'error', message: errorMsg }));
|
|
3748
|
+
ws.close();
|
|
3749
|
+
return;
|
|
3730
3750
|
}
|
|
3731
|
-
ws.send(JSON.stringify({ type: 'error', message: errorMsg }));
|
|
3732
|
-
ws.close();
|
|
3733
|
-
return;
|
|
3734
|
-
}
|
|
3735
3751
|
|
|
3736
|
-
|
|
3737
|
-
|
|
3752
|
+
// Set up event handlers BEFORE connecting (to catch early output like shell prompt)
|
|
3753
|
+
const outputHandler = (sid, data) => {
|
|
3754
|
+
if (sid === session.id && ws.readyState === ws.OPEN) {
|
|
3755
|
+
ws.send(data);
|
|
3756
|
+
}
|
|
3757
|
+
};
|
|
3738
3758
|
|
|
3739
|
-
|
|
3759
|
+
const exitHandler = (sid) => {
|
|
3760
|
+
if (sid === session.id && ws.readyState === ws.OPEN) {
|
|
3761
|
+
ws.send(JSON.stringify({ type: 'session_exit' }));
|
|
3762
|
+
// Don't close - allow reconnect
|
|
3763
|
+
}
|
|
3764
|
+
};
|
|
3740
3765
|
|
|
3741
|
-
|
|
3742
|
-
|
|
3743
|
-
JSON.stringify({
|
|
3744
|
-
type: 'session_info',
|
|
3745
|
-
name: sessionInfo.displayName,
|
|
3746
|
-
stableId: sessionInfo.stableId,
|
|
3747
|
-
})
|
|
3748
|
-
);
|
|
3766
|
+
manager.on('session:output', outputHandler);
|
|
3767
|
+
manager.on('session:exit', exitHandler);
|
|
3749
3768
|
|
|
3750
|
-
|
|
3751
|
-
if (ws.readyState === ws.OPEN) {
|
|
3752
|
-
ws.send(data);
|
|
3753
|
-
}
|
|
3754
|
-
});
|
|
3769
|
+
wsConnections.set(ws, { sessionId: session.id, outputHandler, exitHandler });
|
|
3755
3770
|
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3771
|
+
// Connect to session (respawns PTY if needed after server restart)
|
|
3772
|
+
await manager.connectToSession(session.id);
|
|
3773
|
+
|
|
3774
|
+
// Resize to requested dimensions
|
|
3775
|
+
await manager.resizeSession(session.id, cols, rows);
|
|
3776
|
+
|
|
3777
|
+
// Send session info with current display name
|
|
3778
|
+
ws.send(
|
|
3779
|
+
JSON.stringify({
|
|
3780
|
+
type: 'session_info',
|
|
3781
|
+
name: session.displayName,
|
|
3782
|
+
stableId: session.id.slice(0, 8), // Backward compat
|
|
3783
|
+
id: session.id, // Full UUID (new)
|
|
3784
|
+
})
|
|
3785
|
+
);
|
|
3786
|
+
|
|
3787
|
+
// Replay scrollback buffer for reconnecting clients
|
|
3788
|
+
const scrollback = await manager.getScrollback(session.id);
|
|
3789
|
+
if (scrollback) {
|
|
3790
|
+
ws.send(scrollback);
|
|
3759
3791
|
}
|
|
3760
|
-
wsConnections.delete(ws);
|
|
3761
|
-
});
|
|
3762
3792
|
|
|
3763
|
-
|
|
3764
|
-
|
|
3793
|
+
ws.on('message', (data) => {
|
|
3794
|
+
const message = data.toString('utf8');
|
|
3765
3795
|
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3796
|
+
if (message.startsWith('{')) {
|
|
3797
|
+
try {
|
|
3798
|
+
const msg = JSON.parse(message);
|
|
3799
|
+
if (msg.type === 'resize') {
|
|
3800
|
+
manager.resizeSession(session.id, msg.cols, msg.rows);
|
|
3801
|
+
return;
|
|
3802
|
+
}
|
|
3803
|
+
} catch (e) {
|
|
3804
|
+
// Not JSON, treat as input
|
|
3772
3805
|
}
|
|
3773
|
-
} catch (e) {
|
|
3774
|
-
// Not JSON, treat as input
|
|
3775
3806
|
}
|
|
3776
|
-
}
|
|
3777
3807
|
|
|
3778
|
-
|
|
3779
|
-
|
|
3808
|
+
// Write to PTY
|
|
3809
|
+
try {
|
|
3810
|
+
manager.write(session.id, message);
|
|
3811
|
+
} catch {
|
|
3812
|
+
// PTY may have exited, that's ok
|
|
3813
|
+
}
|
|
3814
|
+
});
|
|
3780
3815
|
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3816
|
+
ws.on('close', () => {
|
|
3817
|
+
const conn = wsConnections.get(ws);
|
|
3818
|
+
if (conn) {
|
|
3819
|
+
manager.off('session:output', conn.outputHandler);
|
|
3820
|
+
manager.off('session:exit', conn.exitHandler);
|
|
3821
|
+
wsConnections.delete(ws);
|
|
3822
|
+
// Note: We do NOT kill the PTY - session persists for reconnection
|
|
3823
|
+
}
|
|
3824
|
+
});
|
|
3788
3825
|
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3826
|
+
ws.on('error', () => {
|
|
3827
|
+
const conn = wsConnections.get(ws);
|
|
3828
|
+
if (conn) {
|
|
3829
|
+
manager.off('session:output', conn.outputHandler);
|
|
3830
|
+
manager.off('session:exit', conn.exitHandler);
|
|
3831
|
+
wsConnections.delete(ws);
|
|
3832
|
+
}
|
|
3833
|
+
});
|
|
3834
|
+
} catch (err) {
|
|
3835
|
+
console.error('WebSocket connection error:', err);
|
|
3836
|
+
ws.send(JSON.stringify({ type: 'error', message: err.message }));
|
|
3837
|
+
ws.close();
|
|
3838
|
+
}
|
|
3796
3839
|
});
|
|
3797
3840
|
|
|
3798
3841
|
// ============================================================================
|