x-shell.js 1.0.0 → 1.1.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.
Files changed (35) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +292 -2
  3. package/dist/client/browser-bundle.js +197 -3
  4. package/dist/client/browser-bundle.js.map +2 -2
  5. package/dist/client/index.d.ts +1 -1
  6. package/dist/client/index.d.ts.map +1 -1
  7. package/dist/client/terminal-client.d.ts +59 -3
  8. package/dist/client/terminal-client.d.ts.map +1 -1
  9. package/dist/client/terminal-client.js +194 -4
  10. package/dist/client/terminal-client.js.map +1 -1
  11. package/dist/server/circular-buffer.d.ts +55 -0
  12. package/dist/server/circular-buffer.d.ts.map +1 -0
  13. package/dist/server/circular-buffer.js +91 -0
  14. package/dist/server/circular-buffer.js.map +1 -0
  15. package/dist/server/index.d.ts +4 -1
  16. package/dist/server/index.d.ts.map +1 -1
  17. package/dist/server/index.js +3 -0
  18. package/dist/server/index.js.map +1 -1
  19. package/dist/server/session-manager.d.ts +195 -0
  20. package/dist/server/session-manager.d.ts.map +1 -0
  21. package/dist/server/session-manager.js +448 -0
  22. package/dist/server/session-manager.js.map +1 -0
  23. package/dist/server/terminal-server.d.ts +54 -6
  24. package/dist/server/terminal-server.d.ts.map +1 -1
  25. package/dist/server/terminal-server.js +313 -80
  26. package/dist/server/terminal-server.js.map +1 -1
  27. package/dist/shared/types.d.ts +122 -2
  28. package/dist/shared/types.d.ts.map +1 -1
  29. package/dist/ui/browser-bundle.js +745 -47
  30. package/dist/ui/browser-bundle.js.map +3 -3
  31. package/dist/ui/x-shell-terminal.d.ts +99 -1
  32. package/dist/ui/x-shell-terminal.d.ts.map +1 -1
  33. package/dist/ui/x-shell-terminal.js +604 -48
  34. package/dist/ui/x-shell-terminal.js.map +1 -1
  35. package/package.json +5 -2
@@ -791,9 +791,19 @@ var TerminalClient = class {
791
791
  this.spawnedHandlers = [];
792
792
  this.serverInfoHandlers = [];
793
793
  this.containerListHandlers = [];
794
- // Promise resolvers for spawn
794
+ // Session multiplexing handlers
795
+ this.sessionListHandlers = [];
796
+ this.joinedHandlers = [];
797
+ this.leftHandlers = [];
798
+ this.clientJoinedHandlers = [];
799
+ this.clientLeftHandlers = [];
800
+ this.sessionClosedHandlers = [];
801
+ // Promise resolvers for spawn/join
795
802
  this.spawnResolve = null;
796
803
  this.spawnReject = null;
804
+ this.joinResolve = null;
805
+ this.joinReject = null;
806
+ this.listSessionsResolve = null;
797
807
  this.config = {
798
808
  url: config.url,
799
809
  reconnect: config.reconnect ?? true,
@@ -927,6 +937,11 @@ var TerminalClient = class {
927
937
  this.spawnResolve = null;
928
938
  this.spawnReject = null;
929
939
  }
940
+ if (this.joinReject) {
941
+ this.joinReject(error);
942
+ this.joinResolve = null;
943
+ this.joinReject = null;
944
+ }
930
945
  break;
931
946
  case "serverInfo":
932
947
  this.serverInfo = message.info;
@@ -935,6 +950,64 @@ var TerminalClient = class {
935
950
  case "containerList":
936
951
  this.containerListHandlers.forEach((handler) => handler(message.containers));
937
952
  break;
953
+ case "sessionList":
954
+ this.sessionListHandlers.forEach(
955
+ (handler) => handler(message.sessions)
956
+ );
957
+ if (this.listSessionsResolve) {
958
+ this.listSessionsResolve(message.sessions);
959
+ this.listSessionsResolve = null;
960
+ }
961
+ break;
962
+ case "joined":
963
+ const joinedSession = message.session;
964
+ const history = message.history;
965
+ this.sessionId = message.sessionId;
966
+ this.sessionInfo = {
967
+ sessionId: joinedSession.sessionId,
968
+ shell: joinedSession.shell,
969
+ cwd: joinedSession.cwd,
970
+ cols: joinedSession.cols,
971
+ rows: joinedSession.rows,
972
+ createdAt: joinedSession.createdAt,
973
+ container: joinedSession.container
974
+ };
975
+ this.joinedHandlers.forEach((handler) => handler(joinedSession, history));
976
+ if (this.joinResolve) {
977
+ this.joinResolve(joinedSession);
978
+ this.joinResolve = null;
979
+ this.joinReject = null;
980
+ }
981
+ break;
982
+ case "left":
983
+ const leftSessionId = message.sessionId;
984
+ if (this.sessionId === leftSessionId) {
985
+ this.sessionId = null;
986
+ this.sessionInfo = null;
987
+ }
988
+ this.leftHandlers.forEach((handler) => handler(leftSessionId));
989
+ break;
990
+ case "clientJoined":
991
+ this.clientJoinedHandlers.forEach(
992
+ (handler) => handler(message.sessionId, message.clientCount)
993
+ );
994
+ break;
995
+ case "clientLeft":
996
+ this.clientLeftHandlers.forEach(
997
+ (handler) => handler(message.sessionId, message.clientCount)
998
+ );
999
+ break;
1000
+ case "sessionClosed":
1001
+ const closedSessionId = message.sessionId;
1002
+ const reason = message.reason;
1003
+ if (this.sessionId === closedSessionId) {
1004
+ this.sessionId = null;
1005
+ this.sessionInfo = null;
1006
+ }
1007
+ this.sessionClosedHandlers.forEach(
1008
+ (handler) => handler(closedSessionId, reason)
1009
+ );
1010
+ break;
938
1011
  }
939
1012
  }
940
1013
  /**
@@ -947,7 +1020,7 @@ var TerminalClient = class {
947
1020
  return;
948
1021
  }
949
1022
  if (this.sessionId) {
950
- reject(new Error("Session already spawned. Call kill() first."));
1023
+ reject(new Error("Session already active. Call kill() or leave() first."));
951
1024
  return;
952
1025
  }
953
1026
  this.spawnResolve = resolve;
@@ -1002,7 +1075,7 @@ var TerminalClient = class {
1002
1075
  );
1003
1076
  }
1004
1077
  /**
1005
- * Kill the terminal session
1078
+ * Kill the terminal session (close and terminate)
1006
1079
  */
1007
1080
  kill() {
1008
1081
  if (!this.ws || this.state !== "connected") {
@@ -1021,6 +1094,91 @@ var TerminalClient = class {
1021
1094
  this.sessionInfo = null;
1022
1095
  }
1023
1096
  // ==========================================
1097
+ // Session Multiplexing Methods
1098
+ // ==========================================
1099
+ /**
1100
+ * List available sessions
1101
+ */
1102
+ listSessions(filter) {
1103
+ return new Promise((resolve, reject) => {
1104
+ if (this.state !== "connected" || !this.ws) {
1105
+ reject(new Error("Not connected to server"));
1106
+ return;
1107
+ }
1108
+ this.listSessionsResolve = resolve;
1109
+ this.ws.send(
1110
+ JSON.stringify({
1111
+ type: "listSessions",
1112
+ filter
1113
+ })
1114
+ );
1115
+ });
1116
+ }
1117
+ /**
1118
+ * Join an existing session
1119
+ */
1120
+ join(options) {
1121
+ return new Promise((resolve, reject) => {
1122
+ if (this.state !== "connected" || !this.ws) {
1123
+ reject(new Error("Not connected to server"));
1124
+ return;
1125
+ }
1126
+ if (this.sessionId) {
1127
+ reject(new Error("Already in a session. Call leave() first."));
1128
+ return;
1129
+ }
1130
+ this.joinResolve = resolve;
1131
+ this.joinReject = reject;
1132
+ this.ws.send(
1133
+ JSON.stringify({
1134
+ type: "join",
1135
+ options
1136
+ })
1137
+ );
1138
+ });
1139
+ }
1140
+ /**
1141
+ * Leave the current session without killing it
1142
+ */
1143
+ leave(sessionId) {
1144
+ if (!this.ws || this.state !== "connected") {
1145
+ console.error("[x-shell] Cannot leave: not connected");
1146
+ return;
1147
+ }
1148
+ const targetSession = sessionId || this.sessionId;
1149
+ if (!targetSession) {
1150
+ console.error("[x-shell] Cannot leave: no active session");
1151
+ return;
1152
+ }
1153
+ this.ws.send(
1154
+ JSON.stringify({
1155
+ type: "leave",
1156
+ sessionId: targetSession
1157
+ })
1158
+ );
1159
+ if (targetSession === this.sessionId) {
1160
+ this.sessionId = null;
1161
+ this.sessionInfo = null;
1162
+ }
1163
+ }
1164
+ /**
1165
+ * Request session list and trigger onSessionList handlers
1166
+ * (Fire-and-forget version of listSessions)
1167
+ */
1168
+ requestSessionList(filter) {
1169
+ this.listSessions(filter).then((sessions) => {
1170
+ this.sessionListHandlers.forEach((handler) => {
1171
+ try {
1172
+ handler(sessions);
1173
+ } catch (e5) {
1174
+ console.error("[x-shell] Error in sessionList handler:", e5);
1175
+ }
1176
+ });
1177
+ }).catch((err) => {
1178
+ console.error("[x-shell] Failed to list sessions:", err);
1179
+ });
1180
+ }
1181
+ // ==========================================
1024
1182
  // Event handlers
1025
1183
  // ==========================================
1026
1184
  /**
@@ -1074,6 +1232,42 @@ var TerminalClient = class {
1074
1232
  onContainerList(handler) {
1075
1233
  this.containerListHandlers.push(handler);
1076
1234
  }
1235
+ /**
1236
+ * Called when session list is received
1237
+ */
1238
+ onSessionList(handler) {
1239
+ this.sessionListHandlers.push(handler);
1240
+ }
1241
+ /**
1242
+ * Called when successfully joined a session
1243
+ */
1244
+ onJoined(handler) {
1245
+ this.joinedHandlers.push(handler);
1246
+ }
1247
+ /**
1248
+ * Called when left a session
1249
+ */
1250
+ onLeft(handler) {
1251
+ this.leftHandlers.push(handler);
1252
+ }
1253
+ /**
1254
+ * Called when another client joins the current session
1255
+ */
1256
+ onClientJoined(handler) {
1257
+ this.clientJoinedHandlers.push(handler);
1258
+ }
1259
+ /**
1260
+ * Called when another client leaves the current session
1261
+ */
1262
+ onClientLeft(handler) {
1263
+ this.clientLeftHandlers.push(handler);
1264
+ }
1265
+ /**
1266
+ * Called when the session is closed by owner or orphan timeout
1267
+ */
1268
+ onSessionClosed(handler) {
1269
+ this.sessionClosedHandlers.push(handler);
1270
+ }
1077
1271
  /**
1078
1272
  * Request list of available containers
1079
1273
  */
@@ -1145,6 +1339,7 @@ var XShellTerminal = class extends i4 {
1145
1339
  this.showConnectionPanel = false;
1146
1340
  this.showSettings = false;
1147
1341
  this.showStatusBar = false;
1342
+ this.showTabs = false;
1148
1343
  this.fontSize = 14;
1149
1344
  this.fontFamily = 'Menlo, Monaco, "Courier New", monospace';
1150
1345
  this.client = null;
@@ -1159,10 +1354,16 @@ var XShellTerminal = class extends i4 {
1159
1354
  this.serverInfo = null;
1160
1355
  this.selectedContainer = "";
1161
1356
  this.selectedShell = "/bin/sh";
1162
- this.connectionMode = "docker";
1357
+ this.connectionMode = "local";
1358
+ this.availableSessions = [];
1359
+ this.selectedSessionId = "";
1360
+ this.clientCount = 1;
1163
1361
  this.settingsMenuOpen = false;
1164
1362
  this.statusMessage = "";
1165
1363
  this.statusType = "info";
1364
+ this.tabs = [];
1365
+ this.activeTabId = "";
1366
+ this.tabCounter = 0;
1166
1367
  // xterm.js module (loaded dynamically)
1167
1368
  this.xtermModule = null;
1168
1369
  this.fitAddonModule = null;
@@ -1170,6 +1371,9 @@ var XShellTerminal = class extends i4 {
1170
1371
  }
1171
1372
  connectedCallback() {
1172
1373
  super.connectedCallback();
1374
+ if (this.showTabs && this.tabs.length === 0) {
1375
+ this.createTab();
1376
+ }
1173
1377
  if (this.autoConnect && this.url) {
1174
1378
  this.connect();
1175
1379
  }
@@ -1248,23 +1452,55 @@ var XShellTerminal = class extends i4 {
1248
1452
  new CustomEvent("error", { detail: { error: err }, bubbles: true, composed: true })
1249
1453
  );
1250
1454
  });
1455
+ const client = this.client;
1251
1456
  this.client.onData((data) => {
1252
- if (this.terminal) {
1457
+ if (this.showTabs) {
1458
+ const tab = this.tabs.find((t4) => t4.client === client);
1459
+ if (tab?.terminal) {
1460
+ tab.terminal.write(data);
1461
+ }
1462
+ } else if (this.terminal) {
1253
1463
  this.terminal.write(data);
1254
1464
  }
1255
1465
  });
1256
1466
  this.client.onExit((code) => {
1257
- this.sessionActive = false;
1258
- this.sessionInfo = null;
1259
- if (this.terminal) {
1260
- this.terminal.writeln("");
1261
- this.terminal.writeln(`\x1B[1;33m[Process exited with code: ${code}]\x1B[0m`);
1467
+ if (this.showTabs) {
1468
+ const tab = this.tabs.find((t4) => t4.client === client);
1469
+ if (tab) {
1470
+ tab.sessionActive = false;
1471
+ tab.sessionInfo = null;
1472
+ if (tab.terminal) {
1473
+ tab.terminal.writeln("");
1474
+ tab.terminal.writeln(`\x1B[1;33m[Process exited with code: ${code}]\x1B[0m`);
1475
+ }
1476
+ if (tab.id === this.activeTabId) {
1477
+ this.sessionActive = false;
1478
+ this.sessionInfo = null;
1479
+ }
1480
+ this.tabs = [...this.tabs];
1481
+ }
1482
+ } else {
1483
+ this.sessionActive = false;
1484
+ this.sessionInfo = null;
1485
+ if (this.terminal) {
1486
+ this.terminal.writeln("");
1487
+ this.terminal.writeln(`\x1B[1;33m[Process exited with code: ${code}]\x1B[0m`);
1488
+ }
1262
1489
  }
1263
1490
  this.dispatchEvent(
1264
1491
  new CustomEvent("exit", { detail: { exitCode: code }, bubbles: true, composed: true })
1265
1492
  );
1266
1493
  });
1267
1494
  this.client.onSpawned((info) => {
1495
+ if (this.showTabs) {
1496
+ const tab = this.tabs.find((t4) => t4.client === client);
1497
+ if (tab) {
1498
+ tab.sessionInfo = info;
1499
+ tab.sessionActive = true;
1500
+ tab.label = info.container || info.shell.split("/").pop() || "Terminal";
1501
+ this.tabs = [...this.tabs];
1502
+ }
1503
+ }
1268
1504
  this.sessionInfo = info;
1269
1505
  this.setStatus(`Session started: ${info.container || info.shell}`, "success");
1270
1506
  this.dispatchEvent(
@@ -1285,7 +1521,55 @@ var XShellTerminal = class extends i4 {
1285
1521
  this.selectedContainer = containers[0].name;
1286
1522
  }
1287
1523
  });
1524
+ this.client.onSessionList((sessions) => {
1525
+ this.availableSessions = sessions;
1526
+ if (sessions.length > 0 && !this.selectedSessionId) {
1527
+ this.selectedSessionId = sessions[0].sessionId;
1528
+ }
1529
+ });
1530
+ this.client.onClientJoined((sessionId, count) => {
1531
+ if (this.showTabs) {
1532
+ const tab = this.tabs.find((t4) => t4.client === client);
1533
+ if (tab) {
1534
+ tab.clientCount = count;
1535
+ this.tabs = [...this.tabs];
1536
+ }
1537
+ }
1538
+ this.clientCount = count;
1539
+ this.setStatus(`Client joined (${count} total)`, "info");
1540
+ });
1541
+ this.client.onClientLeft((sessionId, count) => {
1542
+ if (this.showTabs) {
1543
+ const tab = this.tabs.find((t4) => t4.client === client);
1544
+ if (tab) {
1545
+ tab.clientCount = count;
1546
+ this.tabs = [...this.tabs];
1547
+ }
1548
+ }
1549
+ this.clientCount = count;
1550
+ this.setStatus(`Client left (${count} remaining)`, "info");
1551
+ });
1552
+ this.client.onSessionClosed((sessionId, reason) => {
1553
+ if (this.showTabs) {
1554
+ const tab = this.tabs.find((t4) => t4.client === client && t4.sessionInfo?.sessionId === sessionId);
1555
+ if (tab) {
1556
+ tab.sessionActive = false;
1557
+ tab.sessionInfo = null;
1558
+ this.tabs = [...this.tabs];
1559
+ }
1560
+ }
1561
+ if (this.sessionInfo?.sessionId === sessionId) {
1562
+ this.sessionActive = false;
1563
+ this.sessionInfo = null;
1564
+ this.setStatus(`Session closed: ${reason}`, "info");
1565
+ }
1566
+ client.requestSessionList();
1567
+ });
1288
1568
  await this.client.connect();
1569
+ this.client.requestSessionList();
1570
+ if (this.showTabs) {
1571
+ this.syncStateToActiveTab();
1572
+ }
1289
1573
  } catch (err) {
1290
1574
  this.error = err instanceof Error ? err.message : "Connection failed";
1291
1575
  } finally {
@@ -1329,6 +1613,10 @@ var XShellTerminal = class extends i4 {
1329
1613
  const info = await this.client.spawn(spawnOptions);
1330
1614
  this.sessionActive = true;
1331
1615
  this.sessionInfo = info;
1616
+ if (this.showTabs) {
1617
+ this.syncStateToActiveTab();
1618
+ }
1619
+ this.client.requestSessionList();
1332
1620
  if (this.terminal) {
1333
1621
  this.terminal.focus();
1334
1622
  }
@@ -1348,7 +1636,12 @@ var XShellTerminal = class extends i4 {
1348
1636
  return;
1349
1637
  await this.loadXterm();
1350
1638
  await this.updateComplete;
1351
- const container = this.shadowRoot?.querySelector(".terminal-container");
1639
+ let container = null;
1640
+ if (this.showTabs && this.activeTabId) {
1641
+ container = this.shadowRoot?.querySelector(`.tab-terminal-container[data-tab-id="${this.activeTabId}"]`) ?? null;
1642
+ } else {
1643
+ container = this.shadowRoot?.querySelector(".terminal-container") ?? null;
1644
+ }
1352
1645
  if (!container)
1353
1646
  return;
1354
1647
  const terminalTheme = this.getTerminalTheme();
@@ -1378,12 +1671,22 @@ var XShellTerminal = class extends i4 {
1378
1671
  this.client.resize(cols, rows);
1379
1672
  }
1380
1673
  });
1381
- this.resizeObserver = new ResizeObserver(() => {
1382
- if (this.fitAddon) {
1383
- this.fitAddon.fit();
1384
- }
1385
- });
1674
+ if (!this.resizeObserver) {
1675
+ this.resizeObserver = new ResizeObserver(() => {
1676
+ if (this.showTabs) {
1677
+ const activeTab = this.getActiveTab();
1678
+ if (activeTab?.fitAddon) {
1679
+ activeTab.fitAddon.fit();
1680
+ }
1681
+ } else if (this.fitAddon) {
1682
+ this.fitAddon.fit();
1683
+ }
1684
+ });
1685
+ }
1386
1686
  this.resizeObserver.observe(container);
1687
+ if (this.showTabs) {
1688
+ this.syncStateToActiveTab();
1689
+ }
1387
1690
  }
1388
1691
  /**
1389
1692
  * Get terminal theme based on component theme
@@ -1472,6 +1775,141 @@ var XShellTerminal = class extends i4 {
1472
1775
  }
1473
1776
  this.fitAddon = null;
1474
1777
  }
1778
+ // ==================== Tab Management Methods ====================
1779
+ /**
1780
+ * Get the active tab
1781
+ */
1782
+ getActiveTab() {
1783
+ return this.tabs.find((t4) => t4.id === this.activeTabId);
1784
+ }
1785
+ /**
1786
+ * Create a new tab
1787
+ */
1788
+ createTab(label) {
1789
+ this.tabCounter++;
1790
+ const tab = {
1791
+ id: `tab-${this.tabCounter}`,
1792
+ label: label || `Terminal ${this.tabCounter}`,
1793
+ client: null,
1794
+ terminal: null,
1795
+ fitAddon: null,
1796
+ connected: false,
1797
+ sessionActive: false,
1798
+ sessionInfo: null,
1799
+ clientCount: 1,
1800
+ containerEl: null
1801
+ };
1802
+ this.tabs = [...this.tabs, tab];
1803
+ this.switchTab(tab.id);
1804
+ return tab;
1805
+ }
1806
+ /**
1807
+ * Switch to a tab
1808
+ */
1809
+ switchTab(tabId) {
1810
+ const tab = this.tabs.find((t4) => t4.id === tabId);
1811
+ if (!tab)
1812
+ return;
1813
+ this.activeTabId = tabId;
1814
+ this.client = tab.client;
1815
+ this.terminal = tab.terminal;
1816
+ this.fitAddon = tab.fitAddon;
1817
+ this.connected = tab.connected;
1818
+ this.sessionActive = tab.sessionActive;
1819
+ this.sessionInfo = tab.sessionInfo;
1820
+ this.clientCount = tab.clientCount;
1821
+ this.updateComplete.then(() => {
1822
+ if (tab.terminal) {
1823
+ tab.terminal.focus();
1824
+ }
1825
+ if (tab.fitAddon) {
1826
+ tab.fitAddon.fit();
1827
+ }
1828
+ });
1829
+ }
1830
+ /**
1831
+ * Close a tab
1832
+ */
1833
+ closeTab(tabId) {
1834
+ const tabIndex = this.tabs.findIndex((t4) => t4.id === tabId);
1835
+ if (tabIndex === -1)
1836
+ return;
1837
+ const tab = this.tabs[tabIndex];
1838
+ if (tab.terminal) {
1839
+ tab.terminal.dispose();
1840
+ }
1841
+ if (tab.client) {
1842
+ tab.client.disconnect();
1843
+ }
1844
+ this.tabs = this.tabs.filter((t4) => t4.id !== tabId);
1845
+ if (this.activeTabId === tabId && this.tabs.length > 0) {
1846
+ const newIndex = Math.max(0, tabIndex - 1);
1847
+ this.switchTab(this.tabs[newIndex].id);
1848
+ }
1849
+ if (this.tabs.length === 0) {
1850
+ this.activeTabId = "";
1851
+ this.client = null;
1852
+ this.terminal = null;
1853
+ this.fitAddon = null;
1854
+ this.connected = false;
1855
+ this.sessionActive = false;
1856
+ this.sessionInfo = null;
1857
+ }
1858
+ }
1859
+ /**
1860
+ * Update the active tab's state from component state
1861
+ */
1862
+ syncStateToActiveTab() {
1863
+ const tab = this.getActiveTab();
1864
+ if (tab) {
1865
+ tab.client = this.client;
1866
+ tab.terminal = this.terminal;
1867
+ tab.fitAddon = this.fitAddon;
1868
+ tab.connected = this.connected;
1869
+ tab.sessionActive = this.sessionActive;
1870
+ tab.sessionInfo = this.sessionInfo;
1871
+ tab.clientCount = this.clientCount;
1872
+ this.tabs = [...this.tabs];
1873
+ }
1874
+ }
1875
+ /**
1876
+ * Render the tab bar
1877
+ */
1878
+ renderTabBar() {
1879
+ if (!this.showTabs)
1880
+ return A;
1881
+ return b2`
1882
+ <div class="tab-bar">
1883
+ ${this.tabs.map(
1884
+ (tab) => b2`
1885
+ <button
1886
+ class="tab ${tab.id === this.activeTabId ? "active" : ""}"
1887
+ @click=${() => this.switchTab(tab.id)}
1888
+ >
1889
+ <span class="tab-status ${tab.sessionActive ? "connected" : ""}"></span>
1890
+ <span>${tab.label}</span>
1891
+ ${this.tabs.length > 1 ? b2`
1892
+ <button
1893
+ class="tab-close"
1894
+ @click=${(e5) => {
1895
+ e5.stopPropagation();
1896
+ this.closeTab(tab.id);
1897
+ }}
1898
+ title="Close tab"
1899
+ >
1900
+ ×
1901
+ </button>
1902
+ ` : A}
1903
+ </button>
1904
+ `
1905
+ )}
1906
+ <button class="tab-add" @click=${() => this.createTab()} title="New tab">
1907
+ +
1908
+ </button>
1909
+ </div>
1910
+ `;
1911
+ }
1912
+ // ==================== End Tab Management Methods ====================
1475
1913
  /**
1476
1914
  * Set status message
1477
1915
  */
@@ -1535,6 +1973,70 @@ var XShellTerminal = class extends i4 {
1535
1973
  if (this.connectionMode === "docker" && this.client && this.connected) {
1536
1974
  this.client.requestContainerList();
1537
1975
  }
1976
+ if (this.connectionMode === "join" && this.client && this.connected) {
1977
+ this.client.requestSessionList();
1978
+ }
1979
+ }
1980
+ /**
1981
+ * Refresh session list
1982
+ */
1983
+ refreshSessions() {
1984
+ if (this.client && this.connected) {
1985
+ this.client.requestSessionList();
1986
+ }
1987
+ }
1988
+ /**
1989
+ * Join an existing session
1990
+ */
1991
+ async join(sessionId, requestHistory = true) {
1992
+ if (!this.client || !this.connected) {
1993
+ throw new Error("Not connected to server");
1994
+ }
1995
+ this.loading = true;
1996
+ this.error = null;
1997
+ try {
1998
+ await this.initTerminalUI();
1999
+ const session = await this.client.join({
2000
+ sessionId,
2001
+ requestHistory,
2002
+ historyLimit: 5e4
2003
+ });
2004
+ this.sessionActive = true;
2005
+ this.sessionInfo = {
2006
+ sessionId: session.sessionId,
2007
+ shell: session.shell,
2008
+ cwd: session.cwd,
2009
+ cols: session.cols,
2010
+ rows: session.rows,
2011
+ createdAt: session.createdAt || /* @__PURE__ */ new Date(),
2012
+ container: session.container
2013
+ };
2014
+ this.clientCount = session.clientCount;
2015
+ if (this.showTabs) {
2016
+ this.syncStateToActiveTab();
2017
+ }
2018
+ this.setStatus(`Joined session (${session.clientCount} clients)`, "success");
2019
+ if (this.terminal) {
2020
+ this.terminal.focus();
2021
+ }
2022
+ return session;
2023
+ } catch (err) {
2024
+ this.error = err instanceof Error ? err.message : "Failed to join session";
2025
+ throw err;
2026
+ } finally {
2027
+ this.loading = false;
2028
+ }
2029
+ }
2030
+ /**
2031
+ * Leave current session without killing it
2032
+ */
2033
+ leave() {
2034
+ if (this.client && this.sessionInfo) {
2035
+ this.client.leave(this.sessionInfo.sessionId);
2036
+ this.sessionActive = false;
2037
+ this.sessionInfo = null;
2038
+ this.setStatus("Left session", "info");
2039
+ }
1538
2040
  }
1539
2041
  /**
1540
2042
  * Handle connect from connection panel
@@ -1544,14 +2046,18 @@ var XShellTerminal = class extends i4 {
1544
2046
  await this.connect();
1545
2047
  }
1546
2048
  if (this.connected) {
1547
- const options = {};
1548
- if (this.connectionMode === "docker" && this.selectedContainer) {
1549
- options.container = this.selectedContainer;
1550
- options.containerShell = this.selectedShell || "/bin/sh";
2049
+ if (this.connectionMode === "join" && this.selectedSessionId) {
2050
+ await this.join(this.selectedSessionId);
1551
2051
  } else {
1552
- options.shell = this.selectedShell || void 0;
2052
+ const options = {};
2053
+ if (this.connectionMode === "docker" && this.selectedContainer) {
2054
+ options.container = this.selectedContainer;
2055
+ options.containerShell = this.selectedShell || "/bin/sh";
2056
+ } else {
2057
+ options.shell = this.selectedShell || void 0;
2058
+ }
2059
+ await this.spawn(options);
1553
2060
  }
1554
- await this.spawn(options);
1555
2061
  }
1556
2062
  }
1557
2063
  /**
@@ -1567,11 +2073,13 @@ var XShellTerminal = class extends i4 {
1567
2073
  if (!this.showConnectionPanel)
1568
2074
  return A;
1569
2075
  const runningContainers = this.containers.filter((c4) => c4.state === "running");
2076
+ const acceptingSessions = this.availableSessions.filter((s4) => s4.accepting);
1570
2077
  return b2`
1571
2078
  <div class="connection-panel">
1572
2079
  <div class="connection-panel-title">
1573
2080
  <span>Connection</span>
1574
- ${this.serverInfo?.dockerEnabled ? b2`<span style="font-size: 11px; color: var(--xs-status-connected);">Docker enabled</span>` : A}
2081
+ ${this.availableSessions.length > 0 ? b2`<span style="font-size: 11px; color: var(--xs-status-connected);">${this.availableSessions.length} session(s) available</span>` : A}
2082
+ ${this.serverInfo?.dockerEnabled ? b2`<span style="font-size: 11px; color: var(--xs-text-muted);">Docker enabled</span>` : A}
1575
2083
  </div>
1576
2084
  <div class="connection-form">
1577
2085
  <div class="form-group">
@@ -1581,12 +2089,37 @@ var XShellTerminal = class extends i4 {
1581
2089
  @change=${this.handleModeChange}
1582
2090
  ?disabled=${this.sessionActive}
1583
2091
  >
1584
- <option value="local">Local Shell</option>
1585
- ${this.serverInfo?.dockerEnabled ? b2`<option value="docker">Docker Container</option>` : A}
2092
+ <option value="local">New Local Shell</option>
2093
+ ${this.serverInfo?.dockerEnabled ? b2`<option value="docker">New Docker Session</option>` : A}
2094
+ ${acceptingSessions.length > 0 ? b2`<option value="join">Join Existing Session</option>` : A}
1586
2095
  </select>
1587
2096
  </div>
1588
2097
 
1589
- ${this.connectionMode === "docker" ? b2`
2098
+ ${this.connectionMode === "join" ? b2`
2099
+ <div class="form-group">
2100
+ <label style="display: flex; justify-content: space-between; align-items: center;">
2101
+ <span>Session</span>
2102
+ <button
2103
+ style="font-size: 10px; padding: 2px 6px;"
2104
+ @click=${this.refreshSessions}
2105
+ ?disabled=${!this.connected}
2106
+ >Refresh</button>
2107
+ </label>
2108
+ <select
2109
+ .value=${this.selectedSessionId}
2110
+ @change=${(e5) => this.selectedSessionId = e5.target.value}
2111
+ ?disabled=${this.sessionActive}
2112
+ >
2113
+ ${acceptingSessions.length === 0 ? b2`<option value="">No sessions available</option>` : acceptingSessions.map((s4) => b2`
2114
+ <option value=${s4.sessionId}>
2115
+ ${s4.label || s4.sessionId.substring(0, 12)}
2116
+ (${s4.type === "local" ? s4.shell : s4.container || s4.type})
2117
+ - ${s4.clientCount} client(s)
2118
+ </option>
2119
+ `)}
2120
+ </select>
2121
+ </div>
2122
+ ` : this.connectionMode === "docker" ? b2`
1590
2123
  <div class="form-group">
1591
2124
  <label>Container</label>
1592
2125
  <select
@@ -1601,28 +2134,30 @@ var XShellTerminal = class extends i4 {
1601
2134
  </div>
1602
2135
  ` : A}
1603
2136
 
1604
- <div class="form-group">
1605
- <label>Shell</label>
1606
- <select
1607
- .value=${this.selectedShell}
1608
- @change=${(e5) => this.selectedShell = e5.target.value}
1609
- ?disabled=${this.sessionActive}
1610
- >
1611
- ${this.serverInfo?.allowedShells.length ? this.serverInfo.allowedShells.map((s4) => b2`<option value=${s4}>${s4}</option>`) : b2`
1612
- <option value="/bin/bash">/bin/bash</option>
1613
- <option value="/bin/sh">/bin/sh</option>
1614
- <option value="/bin/zsh">/bin/zsh</option>
1615
- `}
1616
- </select>
1617
- </div>
2137
+ ${this.connectionMode !== "join" ? b2`
2138
+ <div class="form-group">
2139
+ <label>Shell</label>
2140
+ <select
2141
+ .value=${this.selectedShell}
2142
+ @change=${(e5) => this.selectedShell = e5.target.value}
2143
+ ?disabled=${this.sessionActive}
2144
+ >
2145
+ ${this.serverInfo?.allowedShells.length ? this.serverInfo.allowedShells.map((s4) => b2`<option value=${s4}>${s4}</option>`) : b2`
2146
+ <option value="/bin/bash">/bin/bash</option>
2147
+ <option value="/bin/sh">/bin/sh</option>
2148
+ <option value="/bin/zsh">/bin/zsh</option>
2149
+ `}
2150
+ </select>
2151
+ </div>
2152
+ ` : A}
1618
2153
 
1619
2154
  <div class="form-group">
1620
2155
  ${!this.connected ? b2`<button class="btn-primary" @click=${this.handlePanelConnect} ?disabled=${this.loading}>
1621
2156
  ${this.loading ? "Connecting..." : "Connect"}
1622
- </button>` : !this.sessionActive ? b2`<button class="btn-primary" @click=${this.handlePanelConnect} ?disabled=${this.loading}>
1623
- ${this.loading ? "Starting..." : "Start Session"}
2157
+ </button>` : !this.sessionActive ? b2`<button class="btn-primary" @click=${this.handlePanelConnect} ?disabled=${this.loading || this.connectionMode === "join" && !this.selectedSessionId}>
2158
+ ${this.loading ? "Starting..." : this.connectionMode === "join" ? "Join Session" : "Start Session"}
1624
2159
  </button>` : b2`<button class="btn-danger" @click=${this.kill}>
1625
- Stop Session
2160
+ ${this.clientCount > 1 ? "Leave Session" : "Stop Session"}
1626
2161
  </button>`}
1627
2162
  </div>
1628
2163
  </div>
@@ -1742,10 +2277,26 @@ var XShellTerminal = class extends i4 {
1742
2277
  `}
1743
2278
 
1744
2279
  ${this.renderConnectionPanel()}
1745
-
1746
- <div class="terminal-container">
1747
- ${this.loading && !this.terminal ? b2`<div class="loading"><span class="loading-spinner">⏳</span> Loading...</div>` : this.error && !this.terminal ? b2`<div class="error">❌ ${this.error}</div>` : A}
1748
- </div>
2280
+ ${this.renderTabBar()}
2281
+
2282
+ ${this.showTabs && this.tabs.length > 0 ? b2`
2283
+ <div class="terminals-wrapper">
2284
+ ${this.tabs.map(
2285
+ (tab) => b2`
2286
+ <div
2287
+ class="tab-terminal-container ${tab.id === this.activeTabId ? "active" : ""}"
2288
+ data-tab-id=${tab.id}
2289
+ >
2290
+ ${this.loading && tab.id === this.activeTabId && !tab.terminal ? b2`<div class="loading"><span class="loading-spinner">⏳</span> Loading...</div>` : this.error && tab.id === this.activeTabId && !tab.terminal ? b2`<div class="error">❌ ${this.error}</div>` : A}
2291
+ </div>
2292
+ `
2293
+ )}
2294
+ </div>
2295
+ ` : b2`
2296
+ <div class="terminal-container">
2297
+ ${this.loading && !this.terminal ? b2`<div class="loading"><span class="loading-spinner">⏳</span> Loading...</div>` : this.error && !this.terminal ? b2`<div class="error">❌ ${this.error}</div>` : A}
2298
+ </div>
2299
+ `}
1749
2300
 
1750
2301
  ${this.renderStatusBar()}
1751
2302
  `;
@@ -1987,6 +2538,135 @@ XShellTerminal.styles = [
1987
2538
  .status-bar-success {
1988
2539
  color: var(--xs-status-connected);
1989
2540
  }
2541
+
2542
+ /* Tab bar styles */
2543
+ .tab-bar {
2544
+ display: flex;
2545
+ align-items: center;
2546
+ background: var(--xs-bg-header);
2547
+ border-bottom: 1px solid var(--xs-border);
2548
+ padding: 0 4px;
2549
+ gap: 2px;
2550
+ min-height: 32px;
2551
+ overflow-x: auto;
2552
+ }
2553
+
2554
+ .tab-bar::-webkit-scrollbar {
2555
+ height: 4px;
2556
+ }
2557
+
2558
+ .tab-bar::-webkit-scrollbar-thumb {
2559
+ background: var(--xs-border);
2560
+ border-radius: 2px;
2561
+ }
2562
+
2563
+ .tab {
2564
+ display: flex;
2565
+ align-items: center;
2566
+ gap: 6px;
2567
+ padding: 6px 12px;
2568
+ background: transparent;
2569
+ border: none;
2570
+ border-bottom: 2px solid transparent;
2571
+ color: var(--xs-text-muted);
2572
+ font-size: 12px;
2573
+ cursor: pointer;
2574
+ white-space: nowrap;
2575
+ transition: all 0.15s ease;
2576
+ }
2577
+
2578
+ .tab:hover {
2579
+ background: var(--xs-btn-hover);
2580
+ color: var(--xs-text);
2581
+ }
2582
+
2583
+ .tab.active {
2584
+ color: var(--xs-text);
2585
+ border-bottom-color: var(--xs-status-connected);
2586
+ background: var(--xs-bg);
2587
+ }
2588
+
2589
+ .tab-status {
2590
+ width: 6px;
2591
+ height: 6px;
2592
+ border-radius: 50%;
2593
+ background: var(--xs-status-disconnected);
2594
+ }
2595
+
2596
+ .tab-status.connected {
2597
+ background: var(--xs-status-connected);
2598
+ }
2599
+
2600
+ .tab-close {
2601
+ display: flex;
2602
+ align-items: center;
2603
+ justify-content: center;
2604
+ width: 16px;
2605
+ height: 16px;
2606
+ border-radius: 3px;
2607
+ background: none;
2608
+ border: none;
2609
+ color: var(--xs-text-muted);
2610
+ font-size: 14px;
2611
+ cursor: pointer;
2612
+ opacity: 0;
2613
+ transition: opacity 0.15s ease;
2614
+ }
2615
+
2616
+ .tab:hover .tab-close {
2617
+ opacity: 1;
2618
+ }
2619
+
2620
+ .tab-close:hover {
2621
+ background: var(--xs-btn-hover);
2622
+ color: var(--xs-text);
2623
+ }
2624
+
2625
+ .tab-add {
2626
+ display: flex;
2627
+ align-items: center;
2628
+ justify-content: center;
2629
+ width: 28px;
2630
+ height: 28px;
2631
+ border-radius: 4px;
2632
+ background: none;
2633
+ border: none;
2634
+ color: var(--xs-text-muted);
2635
+ font-size: 18px;
2636
+ cursor: pointer;
2637
+ margin-left: 4px;
2638
+ }
2639
+
2640
+ .tab-add:hover {
2641
+ background: var(--xs-btn-hover);
2642
+ color: var(--xs-text);
2643
+ }
2644
+
2645
+ /* Multi-terminal container */
2646
+ .terminals-wrapper {
2647
+ flex: 1;
2648
+ position: relative;
2649
+ overflow: hidden;
2650
+ }
2651
+
2652
+ .tab-terminal-container {
2653
+ position: absolute;
2654
+ top: 0;
2655
+ left: 0;
2656
+ right: 0;
2657
+ bottom: 0;
2658
+ padding: 4px;
2659
+ background: var(--xs-terminal-bg);
2660
+ display: none;
2661
+ }
2662
+
2663
+ .tab-terminal-container.active {
2664
+ display: block;
2665
+ }
2666
+
2667
+ .tab-terminal-container .xterm {
2668
+ height: 100%;
2669
+ }
1990
2670
  `
1991
2671
  ];
1992
2672
  __decorateClass([
@@ -2037,6 +2717,9 @@ __decorateClass([
2037
2717
  __decorateClass([
2038
2718
  n4({ type: Boolean, attribute: "show-status-bar" })
2039
2719
  ], XShellTerminal.prototype, "showStatusBar", 2);
2720
+ __decorateClass([
2721
+ n4({ type: Boolean, attribute: "show-tabs" })
2722
+ ], XShellTerminal.prototype, "showTabs", 2);
2040
2723
  __decorateClass([
2041
2724
  n4({ type: Number, attribute: "font-size" })
2042
2725
  ], XShellTerminal.prototype, "fontSize", 2);
@@ -2082,6 +2765,15 @@ __decorateClass([
2082
2765
  __decorateClass([
2083
2766
  r5()
2084
2767
  ], XShellTerminal.prototype, "connectionMode", 2);
2768
+ __decorateClass([
2769
+ r5()
2770
+ ], XShellTerminal.prototype, "availableSessions", 2);
2771
+ __decorateClass([
2772
+ r5()
2773
+ ], XShellTerminal.prototype, "selectedSessionId", 2);
2774
+ __decorateClass([
2775
+ r5()
2776
+ ], XShellTerminal.prototype, "clientCount", 2);
2085
2777
  __decorateClass([
2086
2778
  r5()
2087
2779
  ], XShellTerminal.prototype, "settingsMenuOpen", 2);
@@ -2091,6 +2783,12 @@ __decorateClass([
2091
2783
  __decorateClass([
2092
2784
  r5()
2093
2785
  ], XShellTerminal.prototype, "statusType", 2);
2786
+ __decorateClass([
2787
+ r5()
2788
+ ], XShellTerminal.prototype, "tabs", 2);
2789
+ __decorateClass([
2790
+ r5()
2791
+ ], XShellTerminal.prototype, "activeTabId", 2);
2094
2792
  XShellTerminal = __decorateClass([
2095
2793
  t3("x-shell-terminal")
2096
2794
  ], XShellTerminal);