lit-shell.js 0.1.4 → 1.2.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 (48) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +189 -62
  3. package/dist/client/browser-bundle.js +301 -5
  4. package/dist/client/browser-bundle.js.map +3 -3
  5. package/dist/client/index.d.ts +2 -1
  6. package/dist/client/index.d.ts.map +1 -1
  7. package/dist/client/index.js +1 -0
  8. package/dist/client/index.js.map +1 -1
  9. package/dist/client/terminal-client.d.ts +97 -3
  10. package/dist/client/terminal-client.d.ts.map +1 -1
  11. package/dist/client/terminal-client.js +298 -4
  12. package/dist/client/terminal-client.js.map +1 -1
  13. package/dist/server/circular-buffer.d.ts +55 -0
  14. package/dist/server/circular-buffer.d.ts.map +1 -0
  15. package/dist/server/circular-buffer.js +91 -0
  16. package/dist/server/circular-buffer.js.map +1 -0
  17. package/dist/server/index.d.ts +5 -1
  18. package/dist/server/index.d.ts.map +1 -1
  19. package/dist/server/index.js +4 -0
  20. package/dist/server/index.js.map +1 -1
  21. package/dist/server/session-manager.d.ts +201 -0
  22. package/dist/server/session-manager.d.ts.map +1 -0
  23. package/dist/server/session-manager.js +458 -0
  24. package/dist/server/session-manager.js.map +1 -0
  25. package/dist/server/terminal-server.d.ts +75 -5
  26. package/dist/server/terminal-server.d.ts.map +1 -1
  27. package/dist/server/terminal-server.js +515 -79
  28. package/dist/server/terminal-server.js.map +1 -1
  29. package/dist/shared/types.d.ts +185 -2
  30. package/dist/shared/types.d.ts.map +1 -1
  31. package/dist/ui/browser-bundle.js +1853 -88
  32. package/dist/ui/browser-bundle.js.map +4 -4
  33. package/dist/ui/index.d.ts +1 -0
  34. package/dist/ui/index.d.ts.map +1 -1
  35. package/dist/ui/index.js +1 -0
  36. package/dist/ui/index.js.map +1 -1
  37. package/dist/ui/lit-shell-terminal.d.ts +225 -6
  38. package/dist/ui/lit-shell-terminal.d.ts.map +1 -1
  39. package/dist/ui/lit-shell-terminal.js +1605 -60
  40. package/dist/ui/lit-shell-terminal.js.map +1 -1
  41. package/dist/ui/styles.d.ts.map +1 -1
  42. package/dist/ui/styles.js +22 -0
  43. package/dist/ui/styles.js.map +1 -1
  44. package/dist/version.d.ts +6 -0
  45. package/dist/version.d.ts.map +1 -0
  46. package/dist/version.js +6 -0
  47. package/dist/version.js.map +1 -0
  48. package/package.json +9 -4
@@ -10,6 +10,9 @@ var __decorateClass = (decorators, target, key, kind) => {
10
10
  return result;
11
11
  };
12
12
 
13
+ // src/version.ts
14
+ var VERSION = "1.2.0";
15
+
13
16
  // node_modules/@lit/reactive-element/css-tag.js
14
17
  var t = globalThis;
15
18
  var e = t.ShadowRoot && (void 0 === t.ShadyCSS || t.ShadyCSS.nativeShadow) && "adoptedStyleSheets" in Document.prototype && "replace" in CSSStyleSheet.prototype;
@@ -748,6 +751,28 @@ var buttonStyles = i`
748
751
  opacity: 0.5;
749
752
  cursor: not-allowed;
750
753
  }
754
+
755
+ button.btn-primary,
756
+ .btn-primary {
757
+ background: var(--ls-status-connected);
758
+ color: #ffffff;
759
+ }
760
+
761
+ button.btn-primary:hover,
762
+ .btn-primary:hover {
763
+ background: #16a34a;
764
+ }
765
+
766
+ button.btn-danger,
767
+ .btn-danger {
768
+ background: var(--ls-status-disconnected);
769
+ color: #ffffff;
770
+ }
771
+
772
+ button.btn-danger:hover,
773
+ .btn-danger:hover {
774
+ background: #dc2626;
775
+ }
751
776
  `;
752
777
 
753
778
  // src/client/terminal-client.ts
@@ -757,8 +782,11 @@ var TerminalClient = class {
757
782
  this.state = "disconnected";
758
783
  this.sessionId = null;
759
784
  this.sessionInfo = null;
785
+ this.serverInfo = null;
760
786
  this.reconnectAttempts = 0;
761
787
  this.reconnectTimeout = null;
788
+ this.previousSessionId = null;
789
+ this.isReconnecting = false;
762
790
  // Event handlers
763
791
  this.connectHandlers = [];
764
792
  this.disconnectHandlers = [];
@@ -766,9 +794,22 @@ var TerminalClient = class {
766
794
  this.exitHandlers = [];
767
795
  this.errorHandlers = [];
768
796
  this.spawnedHandlers = [];
769
- // Promise resolvers for spawn
797
+ this.serverInfoHandlers = [];
798
+ this.containerListHandlers = [];
799
+ // Session multiplexing handlers
800
+ this.sessionListHandlers = [];
801
+ this.joinedHandlers = [];
802
+ this.leftHandlers = [];
803
+ this.clientJoinedHandlers = [];
804
+ this.clientLeftHandlers = [];
805
+ this.sessionClosedHandlers = [];
806
+ this.reconnectWithSessionHandlers = [];
807
+ // Promise resolvers for spawn/join
770
808
  this.spawnResolve = null;
771
809
  this.spawnReject = null;
810
+ this.joinResolve = null;
811
+ this.joinReject = null;
812
+ this.listSessionsResolve = null;
772
813
  this.config = {
773
814
  url: config.url,
774
815
  reconnect: config.reconnect ?? true,
@@ -797,17 +838,25 @@ var TerminalClient = class {
797
838
  this.state = "connected";
798
839
  this.reconnectAttempts = 0;
799
840
  this.connectHandlers.forEach((handler) => handler());
841
+ if (this.isReconnecting && this.previousSessionId) {
842
+ this.checkPreviousSessionAndNotify();
843
+ }
844
+ this.isReconnecting = false;
800
845
  resolve();
801
846
  };
802
847
  this.ws.onclose = () => {
803
848
  const wasConnected = this.state === "connected";
804
849
  this.state = "disconnected";
850
+ if (this.sessionId) {
851
+ this.previousSessionId = this.sessionId;
852
+ }
805
853
  this.sessionId = null;
806
854
  this.sessionInfo = null;
807
855
  if (wasConnected) {
808
856
  this.disconnectHandlers.forEach((handler) => handler());
809
857
  }
810
858
  if (this.config.reconnect && this.reconnectAttempts < this.config.maxReconnectAttempts) {
859
+ this.isReconnecting = true;
811
860
  this.scheduleReconnect();
812
861
  }
813
862
  };
@@ -875,7 +924,8 @@ var TerminalClient = class {
875
924
  cwd: message.cwd,
876
925
  cols: message.cols,
877
926
  rows: message.rows,
878
- createdAt: /* @__PURE__ */ new Date()
927
+ createdAt: /* @__PURE__ */ new Date(),
928
+ container: message.container
879
929
  };
880
930
  this.spawnedHandlers.forEach((handler) => handler(this.sessionInfo));
881
931
  if (this.spawnResolve) {
@@ -901,6 +951,76 @@ var TerminalClient = class {
901
951
  this.spawnResolve = null;
902
952
  this.spawnReject = null;
903
953
  }
954
+ if (this.joinReject) {
955
+ this.joinReject(error);
956
+ this.joinResolve = null;
957
+ this.joinReject = null;
958
+ }
959
+ break;
960
+ case "serverInfo":
961
+ this.serverInfo = message.info;
962
+ this.serverInfoHandlers.forEach((handler) => handler(message.info));
963
+ break;
964
+ case "containerList":
965
+ this.containerListHandlers.forEach((handler) => handler(message.containers));
966
+ break;
967
+ case "sessionList":
968
+ this.sessionListHandlers.forEach(
969
+ (handler) => handler(message.sessions)
970
+ );
971
+ if (this.listSessionsResolve) {
972
+ this.listSessionsResolve(message.sessions);
973
+ this.listSessionsResolve = null;
974
+ }
975
+ break;
976
+ case "joined":
977
+ const joinedSession = message.session;
978
+ const history = message.history;
979
+ this.sessionId = message.sessionId;
980
+ this.sessionInfo = {
981
+ sessionId: joinedSession.sessionId,
982
+ shell: joinedSession.shell,
983
+ cwd: joinedSession.cwd,
984
+ cols: joinedSession.cols,
985
+ rows: joinedSession.rows,
986
+ createdAt: joinedSession.createdAt,
987
+ container: joinedSession.container
988
+ };
989
+ this.joinedHandlers.forEach((handler) => handler(joinedSession, history));
990
+ if (this.joinResolve) {
991
+ this.joinResolve(joinedSession);
992
+ this.joinResolve = null;
993
+ this.joinReject = null;
994
+ }
995
+ break;
996
+ case "left":
997
+ const leftSessionId = message.sessionId;
998
+ if (this.sessionId === leftSessionId) {
999
+ this.sessionId = null;
1000
+ this.sessionInfo = null;
1001
+ }
1002
+ this.leftHandlers.forEach((handler) => handler(leftSessionId));
1003
+ break;
1004
+ case "clientJoined":
1005
+ this.clientJoinedHandlers.forEach(
1006
+ (handler) => handler(message.sessionId, message.clientCount)
1007
+ );
1008
+ break;
1009
+ case "clientLeft":
1010
+ this.clientLeftHandlers.forEach(
1011
+ (handler) => handler(message.sessionId, message.clientCount)
1012
+ );
1013
+ break;
1014
+ case "sessionClosed":
1015
+ const closedSessionId = message.sessionId;
1016
+ const reason = message.reason;
1017
+ if (this.sessionId === closedSessionId) {
1018
+ this.sessionId = null;
1019
+ this.sessionInfo = null;
1020
+ }
1021
+ this.sessionClosedHandlers.forEach(
1022
+ (handler) => handler(closedSessionId, reason)
1023
+ );
904
1024
  break;
905
1025
  }
906
1026
  }
@@ -914,7 +1034,7 @@ var TerminalClient = class {
914
1034
  return;
915
1035
  }
916
1036
  if (this.sessionId) {
917
- reject(new Error("Session already spawned. Call kill() first."));
1037
+ reject(new Error("Session already active. Call kill() or leave() first."));
918
1038
  return;
919
1039
  }
920
1040
  this.spawnResolve = resolve;
@@ -969,7 +1089,7 @@ var TerminalClient = class {
969
1089
  );
970
1090
  }
971
1091
  /**
972
- * Kill the terminal session
1092
+ * Kill the terminal session (close and terminate)
973
1093
  */
974
1094
  kill() {
975
1095
  if (!this.ws || this.state !== "connected") {
@@ -988,6 +1108,91 @@ var TerminalClient = class {
988
1108
  this.sessionInfo = null;
989
1109
  }
990
1110
  // ==========================================
1111
+ // Session Multiplexing Methods
1112
+ // ==========================================
1113
+ /**
1114
+ * List available sessions
1115
+ */
1116
+ listSessions(filter) {
1117
+ return new Promise((resolve, reject) => {
1118
+ if (this.state !== "connected" || !this.ws) {
1119
+ reject(new Error("Not connected to server"));
1120
+ return;
1121
+ }
1122
+ this.listSessionsResolve = resolve;
1123
+ this.ws.send(
1124
+ JSON.stringify({
1125
+ type: "listSessions",
1126
+ filter
1127
+ })
1128
+ );
1129
+ });
1130
+ }
1131
+ /**
1132
+ * Join an existing session
1133
+ */
1134
+ join(options) {
1135
+ return new Promise((resolve, reject) => {
1136
+ if (this.state !== "connected" || !this.ws) {
1137
+ reject(new Error("Not connected to server"));
1138
+ return;
1139
+ }
1140
+ if (this.sessionId) {
1141
+ reject(new Error("Already in a session. Call leave() first."));
1142
+ return;
1143
+ }
1144
+ this.joinResolve = resolve;
1145
+ this.joinReject = reject;
1146
+ this.ws.send(
1147
+ JSON.stringify({
1148
+ type: "join",
1149
+ options
1150
+ })
1151
+ );
1152
+ });
1153
+ }
1154
+ /**
1155
+ * Leave the current session without killing it
1156
+ */
1157
+ leave(sessionId) {
1158
+ if (!this.ws || this.state !== "connected") {
1159
+ console.error("[lit-shell] Cannot leave: not connected");
1160
+ return;
1161
+ }
1162
+ const targetSession = sessionId || this.sessionId;
1163
+ if (!targetSession) {
1164
+ console.error("[lit-shell] Cannot leave: no active session");
1165
+ return;
1166
+ }
1167
+ this.ws.send(
1168
+ JSON.stringify({
1169
+ type: "leave",
1170
+ sessionId: targetSession
1171
+ })
1172
+ );
1173
+ if (targetSession === this.sessionId) {
1174
+ this.sessionId = null;
1175
+ this.sessionInfo = null;
1176
+ }
1177
+ }
1178
+ /**
1179
+ * Request session list and trigger onSessionList handlers
1180
+ * (Fire-and-forget version of listSessions)
1181
+ */
1182
+ requestSessionList(filter) {
1183
+ this.listSessions(filter).then((sessions) => {
1184
+ this.sessionListHandlers.forEach((handler) => {
1185
+ try {
1186
+ handler(sessions);
1187
+ } catch (e5) {
1188
+ console.error("[lit-shell] Error in sessionList handler:", e5);
1189
+ }
1190
+ });
1191
+ }).catch((err) => {
1192
+ console.error("[lit-shell] Failed to list sessions:", err);
1193
+ });
1194
+ }
1195
+ // ==========================================
991
1196
  // Event handlers
992
1197
  // ==========================================
993
1198
  /**
@@ -1026,6 +1231,67 @@ var TerminalClient = class {
1026
1231
  onSpawned(handler) {
1027
1232
  this.spawnedHandlers.push(handler);
1028
1233
  }
1234
+ /**
1235
+ * Called when server info is received
1236
+ */
1237
+ onServerInfo(handler) {
1238
+ this.serverInfoHandlers.push(handler);
1239
+ if (this.serverInfo) {
1240
+ handler(this.serverInfo);
1241
+ }
1242
+ }
1243
+ /**
1244
+ * Called when container list is received
1245
+ */
1246
+ onContainerList(handler) {
1247
+ this.containerListHandlers.push(handler);
1248
+ }
1249
+ /**
1250
+ * Called when session list is received
1251
+ */
1252
+ onSessionList(handler) {
1253
+ this.sessionListHandlers.push(handler);
1254
+ }
1255
+ /**
1256
+ * Called when successfully joined a session
1257
+ */
1258
+ onJoined(handler) {
1259
+ this.joinedHandlers.push(handler);
1260
+ }
1261
+ /**
1262
+ * Called when left a session
1263
+ */
1264
+ onLeft(handler) {
1265
+ this.leftHandlers.push(handler);
1266
+ }
1267
+ /**
1268
+ * Called when another client joins the current session
1269
+ */
1270
+ onClientJoined(handler) {
1271
+ this.clientJoinedHandlers.push(handler);
1272
+ }
1273
+ /**
1274
+ * Called when another client leaves the current session
1275
+ */
1276
+ onClientLeft(handler) {
1277
+ this.clientLeftHandlers.push(handler);
1278
+ }
1279
+ /**
1280
+ * Called when the session is closed by owner or orphan timeout
1281
+ */
1282
+ onSessionClosed(handler) {
1283
+ this.sessionClosedHandlers.push(handler);
1284
+ }
1285
+ /**
1286
+ * Request list of available containers
1287
+ */
1288
+ requestContainerList() {
1289
+ if (!this.ws || this.state !== "connected") {
1290
+ console.error("[lit-shell] Cannot request containers: not connected");
1291
+ return;
1292
+ }
1293
+ this.ws.send(JSON.stringify({ type: "listContainers" }));
1294
+ }
1029
1295
  // ==========================================
1030
1296
  // Getters
1031
1297
  // ==========================================
@@ -1059,6 +1325,57 @@ var TerminalClient = class {
1059
1325
  hasActiveSession() {
1060
1326
  return this.sessionId !== null;
1061
1327
  }
1328
+ /**
1329
+ * Get server info
1330
+ */
1331
+ getServerInfo() {
1332
+ return this.serverInfo;
1333
+ }
1334
+ /**
1335
+ * Get previous session ID (available after disconnect)
1336
+ */
1337
+ getPreviousSessionId() {
1338
+ return this.previousSessionId;
1339
+ }
1340
+ /**
1341
+ * Clear previous session ID (call after user declines to rejoin)
1342
+ */
1343
+ clearPreviousSessionId() {
1344
+ this.previousSessionId = null;
1345
+ }
1346
+ /**
1347
+ * Called when reconnected and previous session is available
1348
+ */
1349
+ onReconnectWithSession(handler) {
1350
+ this.reconnectWithSessionHandlers.push(handler);
1351
+ }
1352
+ /**
1353
+ * Check if previous session exists and notify handlers
1354
+ */
1355
+ async checkPreviousSessionAndNotify() {
1356
+ if (!this.previousSessionId)
1357
+ return;
1358
+ try {
1359
+ const sessions = await this.listSessions();
1360
+ const previousSession = sessions.find(
1361
+ (s4) => s4.sessionId === this.previousSessionId
1362
+ );
1363
+ if (previousSession && previousSession.accepting) {
1364
+ this.reconnectWithSessionHandlers.forEach((handler) => {
1365
+ try {
1366
+ handler(this.previousSessionId);
1367
+ } catch (e5) {
1368
+ console.error("[lit-shell] Error in reconnectWithSession handler:", e5);
1369
+ }
1370
+ });
1371
+ } else {
1372
+ this.previousSessionId = null;
1373
+ }
1374
+ } catch (e5) {
1375
+ console.error("[lit-shell] Failed to check previous session:", e5);
1376
+ this.previousSessionId = null;
1377
+ }
1378
+ }
1062
1379
  };
1063
1380
 
1064
1381
  // src/ui/lit-shell-terminal.ts
@@ -1074,8 +1391,16 @@ var LitShellTerminal = class extends i4 {
1074
1391
  this.noHeader = false;
1075
1392
  this.autoConnect = false;
1076
1393
  this.autoSpawn = false;
1394
+ this.container = "";
1395
+ this.containerShell = "";
1396
+ this.containerUser = "";
1397
+ this.containerCwd = "";
1398
+ this.showConnectionPanel = false;
1399
+ this.showSettings = false;
1400
+ this.showStatusBar = false;
1401
+ this.showTabs = false;
1077
1402
  this.fontSize = 14;
1078
- this.fontFamily = 'Menlo, Monaco, "Courier New", monospace';
1403
+ this.fontFamily = '"Cascadia Mono", "Cascadia Code", Consolas, "Ubuntu Mono", "DejaVu Sans Mono", "Liberation Mono", Hack, "Fira Code", "JetBrains Mono", Menlo, Monaco, "Courier New", monospace';
1079
1404
  this.client = null;
1080
1405
  this.terminal = null;
1081
1406
  this.fitAddon = null;
@@ -1084,17 +1409,73 @@ var LitShellTerminal = class extends i4 {
1084
1409
  this.loading = false;
1085
1410
  this.error = null;
1086
1411
  this.sessionInfo = null;
1412
+ this.containers = [];
1413
+ this.serverInfo = null;
1414
+ this.selectedContainer = "";
1415
+ this.selectedShell = "/bin/sh";
1416
+ this.connectionMode = "local";
1417
+ this.orphanTimeout = 36e5;
1418
+ this.useTmux = false;
1419
+ this.availableSessions = [];
1420
+ this.selectedSessionId = "";
1421
+ this.clientCount = 1;
1422
+ this.settingsMenuOpen = false;
1423
+ this.showReconnectDialog = false;
1424
+ this.reconnectSessionId = null;
1425
+ this.isMobile = false;
1426
+ this.showTouchKeyboard = true;
1427
+ this.showExtraKeyRows = false;
1428
+ this.statusMessage = "";
1429
+ this.statusType = "info";
1430
+ this.tabs = [];
1431
+ this.activeTabId = "";
1432
+ this.tabCounter = 0;
1087
1433
  // xterm.js module (loaded dynamically)
1088
1434
  this.xtermModule = null;
1089
1435
  this.fitAddonModule = null;
1090
1436
  this.resizeObserver = null;
1437
+ this.ctrlPressed = false;
1438
+ this.altPressed = false;
1091
1439
  }
1092
1440
  connectedCallback() {
1093
1441
  super.connectedCallback();
1442
+ this.setAttribute("data-version", VERSION);
1443
+ this.detectMobile();
1444
+ if (this.showTabs && this.tabs.length === 0) {
1445
+ this.createTab();
1446
+ }
1094
1447
  if (this.autoConnect && this.url) {
1095
1448
  this.connect();
1096
1449
  }
1097
1450
  }
1451
+ /**
1452
+ * Detect if running on a mobile device
1453
+ */
1454
+ detectMobile() {
1455
+ const hasTouch = navigator.maxTouchPoints > 0;
1456
+ const isMobileWidth = window.matchMedia("(max-width: 768px)").matches;
1457
+ const mobileUA = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
1458
+ navigator.userAgent
1459
+ );
1460
+ this.isMobile = hasTouch && (isMobileWidth || mobileUA);
1461
+ this.updateMobileAttribute();
1462
+ window.matchMedia("(max-width: 768px)").addEventListener("change", (e5) => {
1463
+ if (navigator.maxTouchPoints > 0) {
1464
+ this.isMobile = e5.matches;
1465
+ this.updateMobileAttribute();
1466
+ }
1467
+ });
1468
+ }
1469
+ /**
1470
+ * Update mobile attribute for CSS targeting
1471
+ */
1472
+ updateMobileAttribute() {
1473
+ if (this.isMobile) {
1474
+ this.setAttribute("mobile", "");
1475
+ } else {
1476
+ this.removeAttribute("mobile");
1477
+ }
1478
+ }
1098
1479
  disconnectedCallback() {
1099
1480
  super.disconnectedCallback();
1100
1481
  this.cleanup();
@@ -1116,29 +1497,25 @@ var LitShellTerminal = class extends i4 {
1116
1497
  throw new Error("Failed to load xterm.js. Make sure it is available.");
1117
1498
  }
1118
1499
  }
1119
- await this.loadXtermStyles();
1500
+ await this.injectXtermCSS();
1120
1501
  }
1121
1502
  /**
1122
- * Load xterm.css into the shadow DOM
1123
- * This is necessary because CSS loaded in the main document doesn't apply inside shadow DOM
1503
+ * Inject xterm.js CSS into shadow DOM
1124
1504
  */
1125
- async loadXtermStyles() {
1505
+ async injectXtermCSS() {
1126
1506
  if (!this.shadowRoot)
1127
1507
  return;
1128
1508
  if (this.shadowRoot.querySelector("#xterm-styles"))
1129
1509
  return;
1130
1510
  try {
1131
1511
  const response = await fetch("https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css");
1132
- if (!response.ok) {
1133
- throw new Error(`Failed to fetch xterm.css: ${response.status}`);
1134
- }
1135
- const cssText = await response.text();
1512
+ const css = await response.text();
1136
1513
  const style = document.createElement("style");
1137
1514
  style.id = "xterm-styles";
1138
- style.textContent = cssText;
1139
- this.shadowRoot.appendChild(style);
1140
- } catch (error) {
1141
- console.warn("[lit-shell] Failed to load xterm.css:", error);
1515
+ style.textContent = css;
1516
+ this.shadowRoot.prepend(style);
1517
+ } catch (e5) {
1518
+ console.warn("[lit-shell] Failed to load xterm CSS:", e5);
1142
1519
  }
1143
1520
  }
1144
1521
  /**
@@ -1168,33 +1545,133 @@ var LitShellTerminal = class extends i4 {
1168
1545
  });
1169
1546
  this.client.onError((err) => {
1170
1547
  this.error = err.message;
1548
+ this.setStatus(err.message, "error");
1171
1549
  this.dispatchEvent(
1172
1550
  new CustomEvent("error", { detail: { error: err }, bubbles: true, composed: true })
1173
1551
  );
1174
1552
  });
1553
+ const client = this.client;
1175
1554
  this.client.onData((data) => {
1176
- if (this.terminal) {
1555
+ if (this.showTabs) {
1556
+ const tab = this.tabs.find((t4) => t4.client === client);
1557
+ if (tab?.terminal) {
1558
+ tab.terminal.write(data);
1559
+ }
1560
+ } else if (this.terminal) {
1177
1561
  this.terminal.write(data);
1178
1562
  }
1179
1563
  });
1180
1564
  this.client.onExit((code) => {
1181
- this.sessionActive = false;
1182
- this.sessionInfo = null;
1183
- if (this.terminal) {
1184
- this.terminal.writeln("");
1185
- this.terminal.writeln(`\x1B[1;33m[Process exited with code: ${code}]\x1B[0m`);
1565
+ if (this.showTabs) {
1566
+ const tab = this.tabs.find((t4) => t4.client === client);
1567
+ if (tab) {
1568
+ tab.sessionActive = false;
1569
+ tab.sessionInfo = null;
1570
+ if (tab.terminal) {
1571
+ tab.terminal.writeln("");
1572
+ tab.terminal.writeln(`\x1B[1;33m[Process exited with code: ${code}]\x1B[0m`);
1573
+ }
1574
+ if (tab.id === this.activeTabId) {
1575
+ this.sessionActive = false;
1576
+ this.sessionInfo = null;
1577
+ }
1578
+ this.tabs = [...this.tabs];
1579
+ }
1580
+ } else {
1581
+ this.sessionActive = false;
1582
+ this.sessionInfo = null;
1583
+ if (this.terminal) {
1584
+ this.terminal.writeln("");
1585
+ this.terminal.writeln(`\x1B[1;33m[Process exited with code: ${code}]\x1B[0m`);
1586
+ }
1186
1587
  }
1187
1588
  this.dispatchEvent(
1188
1589
  new CustomEvent("exit", { detail: { exitCode: code }, bubbles: true, composed: true })
1189
1590
  );
1190
1591
  });
1191
1592
  this.client.onSpawned((info) => {
1593
+ if (this.showTabs) {
1594
+ const tab = this.tabs.find((t4) => t4.client === client);
1595
+ if (tab) {
1596
+ tab.sessionInfo = info;
1597
+ tab.sessionActive = true;
1598
+ tab.label = info.container || info.shell.split("/").pop() || "Terminal";
1599
+ this.tabs = [...this.tabs];
1600
+ }
1601
+ }
1192
1602
  this.sessionInfo = info;
1603
+ this.setStatus(`Session started: ${info.container || info.shell}`, "success");
1193
1604
  this.dispatchEvent(
1194
1605
  new CustomEvent("spawned", { detail: { session: info }, bubbles: true, composed: true })
1195
1606
  );
1196
1607
  });
1608
+ this.client.onServerInfo((info) => {
1609
+ this.serverInfo = info;
1610
+ if (info.dockerEnabled) {
1611
+ this.connectionMode = "docker";
1612
+ this.client?.requestContainerList();
1613
+ }
1614
+ this.selectedShell = info.defaultShell;
1615
+ });
1616
+ this.client.onContainerList((containers) => {
1617
+ this.containers = containers;
1618
+ if (containers.length > 0 && !this.selectedContainer) {
1619
+ this.selectedContainer = containers[0].name;
1620
+ }
1621
+ });
1622
+ this.client.onSessionList((sessions) => {
1623
+ this.availableSessions = sessions;
1624
+ if (sessions.length > 0 && !this.selectedSessionId) {
1625
+ this.selectedSessionId = sessions[0].sessionId;
1626
+ }
1627
+ });
1628
+ this.client.onClientJoined((sessionId, count) => {
1629
+ if (this.showTabs) {
1630
+ const tab = this.tabs.find((t4) => t4.client === client);
1631
+ if (tab) {
1632
+ tab.clientCount = count;
1633
+ this.tabs = [...this.tabs];
1634
+ }
1635
+ }
1636
+ this.clientCount = count;
1637
+ this.setStatus(`Client joined (${count} total)`, "info");
1638
+ });
1639
+ this.client.onClientLeft((sessionId, count) => {
1640
+ if (this.showTabs) {
1641
+ const tab = this.tabs.find((t4) => t4.client === client);
1642
+ if (tab) {
1643
+ tab.clientCount = count;
1644
+ this.tabs = [...this.tabs];
1645
+ }
1646
+ }
1647
+ this.clientCount = count;
1648
+ this.setStatus(`Client left (${count} remaining)`, "info");
1649
+ });
1650
+ this.client.onSessionClosed((sessionId, reason) => {
1651
+ if (this.showTabs) {
1652
+ const tab = this.tabs.find((t4) => t4.client === client && t4.sessionInfo?.sessionId === sessionId);
1653
+ if (tab) {
1654
+ tab.sessionActive = false;
1655
+ tab.sessionInfo = null;
1656
+ this.tabs = [...this.tabs];
1657
+ }
1658
+ }
1659
+ if (this.sessionInfo?.sessionId === sessionId) {
1660
+ this.sessionActive = false;
1661
+ this.sessionInfo = null;
1662
+ this.setStatus(`Session closed: ${reason}`, "info");
1663
+ }
1664
+ client.requestSessionList();
1665
+ });
1666
+ this.client.onReconnectWithSession((sessionId) => {
1667
+ this.reconnectSessionId = sessionId;
1668
+ this.showReconnectDialog = true;
1669
+ });
1197
1670
  await this.client.connect();
1671
+ this.client.requestSessionList();
1672
+ if (this.showTabs) {
1673
+ this.syncStateToActiveTab();
1674
+ }
1198
1675
  } catch (err) {
1199
1676
  this.error = err instanceof Error ? err.message : "Connection failed";
1200
1677
  } finally {
@@ -1228,16 +1705,27 @@ var LitShellTerminal = class extends i4 {
1228
1705
  cwd: options?.cwd || this.cwd || void 0,
1229
1706
  cols: this.terminal?.cols || this.cols,
1230
1707
  rows: this.terminal?.rows || this.rows,
1231
- env: options?.env
1708
+ env: options?.env,
1709
+ // Docker container options
1710
+ container: options?.container || this.container || void 0,
1711
+ containerShell: options?.containerShell || this.containerShell || void 0,
1712
+ containerUser: options?.containerUser || this.containerUser || void 0,
1713
+ containerCwd: options?.containerCwd || this.containerCwd || void 0
1232
1714
  };
1233
1715
  const info = await this.client.spawn(spawnOptions);
1234
1716
  this.sessionActive = true;
1235
1717
  this.sessionInfo = info;
1718
+ if (this.showTabs) {
1719
+ this.syncStateToActiveTab();
1720
+ }
1721
+ this.client.requestSessionList();
1236
1722
  if (this.terminal) {
1237
1723
  this.terminal.focus();
1238
1724
  }
1725
+ return info;
1239
1726
  } catch (err) {
1240
1727
  this.error = err instanceof Error ? err.message : "Failed to spawn session";
1728
+ throw err;
1241
1729
  } finally {
1242
1730
  this.loading = false;
1243
1731
  }
@@ -1250,7 +1738,12 @@ var LitShellTerminal = class extends i4 {
1250
1738
  return;
1251
1739
  await this.loadXterm();
1252
1740
  await this.updateComplete;
1253
- const container = this.shadowRoot?.querySelector(".terminal-container");
1741
+ let container = null;
1742
+ if (this.showTabs && this.activeTabId) {
1743
+ container = this.shadowRoot?.querySelector(`.tab-terminal-container[data-tab-id="${this.activeTabId}"]`) ?? null;
1744
+ } else {
1745
+ container = this.shadowRoot?.querySelector(".terminal-container") ?? null;
1746
+ }
1254
1747
  if (!container)
1255
1748
  return;
1256
1749
  const terminalTheme = this.getTerminalTheme();
@@ -1280,30 +1773,48 @@ var LitShellTerminal = class extends i4 {
1280
1773
  this.client.resize(cols, rows);
1281
1774
  }
1282
1775
  });
1283
- this.resizeObserver = new ResizeObserver(() => {
1284
- if (this.fitAddon) {
1285
- this.fitAddon.fit();
1286
- }
1287
- });
1776
+ if (!this.resizeObserver) {
1777
+ this.resizeObserver = new ResizeObserver(() => {
1778
+ if (this.showTabs) {
1779
+ const activeTab = this.getActiveTab();
1780
+ if (activeTab?.fitAddon) {
1781
+ activeTab.fitAddon.fit();
1782
+ }
1783
+ } else if (this.fitAddon) {
1784
+ this.fitAddon.fit();
1785
+ }
1786
+ });
1787
+ }
1288
1788
  this.resizeObserver.observe(container);
1789
+ if (this.showTabs) {
1790
+ this.syncStateToActiveTab();
1791
+ }
1289
1792
  }
1290
1793
  /**
1291
1794
  * Get terminal theme based on component theme
1292
1795
  */
1293
1796
  getTerminalTheme() {
1294
- if (this.theme === "light") {
1797
+ let effectiveTheme = this.theme;
1798
+ if (this.theme === "auto") {
1799
+ effectiveTheme = window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
1800
+ }
1801
+ if (effectiveTheme === "light") {
1295
1802
  return {
1296
1803
  background: "#ffffff",
1297
1804
  foreground: "#1f2937",
1298
1805
  cursor: "#1f2937",
1299
- selection: "#b4d5fe"
1806
+ cursorAccent: "#ffffff",
1807
+ selection: "#b4d5fe",
1808
+ selectionForeground: "#1f2937"
1300
1809
  };
1301
1810
  }
1302
1811
  return {
1303
1812
  background: "#1e1e1e",
1304
1813
  foreground: "#cccccc",
1305
1814
  cursor: "#ffffff",
1306
- selection: "#264f78"
1815
+ cursorAccent: "#1e1e1e",
1816
+ selection: "#264f78",
1817
+ selectionForeground: "#ffffff"
1307
1818
  };
1308
1819
  }
1309
1820
  /**
@@ -1366,65 +1877,793 @@ var LitShellTerminal = class extends i4 {
1366
1877
  }
1367
1878
  this.fitAddon = null;
1368
1879
  }
1369
- render() {
1880
+ // ==================== Tab Management Methods ====================
1881
+ /**
1882
+ * Get the active tab
1883
+ */
1884
+ getActiveTab() {
1885
+ return this.tabs.find((t4) => t4.id === this.activeTabId);
1886
+ }
1887
+ /**
1888
+ * Create a new tab
1889
+ */
1890
+ createTab(label) {
1891
+ this.tabCounter++;
1892
+ const tab = {
1893
+ id: `tab-${this.tabCounter}`,
1894
+ label: label || `Terminal ${this.tabCounter}`,
1895
+ client: null,
1896
+ terminal: null,
1897
+ fitAddon: null,
1898
+ connected: false,
1899
+ sessionActive: false,
1900
+ sessionInfo: null,
1901
+ clientCount: 1,
1902
+ containerEl: null
1903
+ };
1904
+ this.tabs = [...this.tabs, tab];
1905
+ this.switchTab(tab.id);
1906
+ return tab;
1907
+ }
1908
+ /**
1909
+ * Switch to a tab
1910
+ */
1911
+ switchTab(tabId) {
1912
+ const tab = this.tabs.find((t4) => t4.id === tabId);
1913
+ if (!tab)
1914
+ return;
1915
+ this.activeTabId = tabId;
1916
+ this.client = tab.client;
1917
+ this.terminal = tab.terminal;
1918
+ this.fitAddon = tab.fitAddon;
1919
+ this.connected = tab.connected;
1920
+ this.sessionActive = tab.sessionActive;
1921
+ this.sessionInfo = tab.sessionInfo;
1922
+ this.clientCount = tab.clientCount;
1923
+ this.updateComplete.then(() => {
1924
+ if (tab.terminal) {
1925
+ tab.terminal.focus();
1926
+ }
1927
+ if (tab.fitAddon) {
1928
+ tab.fitAddon.fit();
1929
+ }
1930
+ });
1931
+ }
1932
+ /**
1933
+ * Close a tab
1934
+ */
1935
+ closeTab(tabId) {
1936
+ const tabIndex = this.tabs.findIndex((t4) => t4.id === tabId);
1937
+ if (tabIndex === -1)
1938
+ return;
1939
+ const tab = this.tabs[tabIndex];
1940
+ if (tab.terminal) {
1941
+ tab.terminal.dispose();
1942
+ }
1943
+ if (tab.client) {
1944
+ tab.client.disconnect();
1945
+ }
1946
+ this.tabs = this.tabs.filter((t4) => t4.id !== tabId);
1947
+ if (this.activeTabId === tabId && this.tabs.length > 0) {
1948
+ const newIndex = Math.max(0, tabIndex - 1);
1949
+ this.switchTab(this.tabs[newIndex].id);
1950
+ }
1951
+ if (this.tabs.length === 0) {
1952
+ this.activeTabId = "";
1953
+ this.client = null;
1954
+ this.terminal = null;
1955
+ this.fitAddon = null;
1956
+ this.connected = false;
1957
+ this.sessionActive = false;
1958
+ this.sessionInfo = null;
1959
+ }
1960
+ }
1961
+ /**
1962
+ * Update the active tab's state from component state
1963
+ */
1964
+ syncStateToActiveTab() {
1965
+ const tab = this.getActiveTab();
1966
+ if (tab) {
1967
+ tab.client = this.client;
1968
+ tab.terminal = this.terminal;
1969
+ tab.fitAddon = this.fitAddon;
1970
+ tab.connected = this.connected;
1971
+ tab.sessionActive = this.sessionActive;
1972
+ tab.sessionInfo = this.sessionInfo;
1973
+ tab.clientCount = this.clientCount;
1974
+ this.tabs = [...this.tabs];
1975
+ }
1976
+ }
1977
+ /**
1978
+ * Render the tab bar
1979
+ */
1980
+ renderTabBar() {
1981
+ if (!this.showTabs)
1982
+ return A;
1370
1983
  return b2`
1371
- ${this.noHeader ? A : b2`
1372
- <div class="header">
1373
- <div class="header-title">
1374
- <span>Terminal</span>
1375
- ${this.sessionInfo ? b2`<span style="font-weight: normal; font-size: 12px; color: var(--ls-text-muted)">
1376
- ${this.sessionInfo.shell}
1377
- </span>` : A}
1378
- </div>
1379
- <div class="header-actions">
1380
- ${!this.connected ? b2`<button @click=${this.connect} ?disabled=${this.loading}>
1381
- ${this.loading ? "Connecting..." : "Connect"}
1382
- </button>` : !this.sessionActive ? b2`<button @click=${() => this.spawn()} ?disabled=${this.loading}>
1383
- ${this.loading ? "Spawning..." : "Start"}
1384
- </button>` : b2`<button @click=${this.kill}>Stop</button>`}
1385
- <button @click=${this.clear} ?disabled=${!this.sessionActive}>Clear</button>
1386
- <div class="status">
1387
- <span class="status-dot ${this.connected ? "connected" : ""}"></span>
1388
- <span>${this.connected ? "Connected" : "Disconnected"}</span>
1389
- </div>
1390
- </div>
1391
- </div>
1392
- `}
1393
-
1394
- <div class="terminal-container">
1395
- ${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}
1984
+ <div class="tab-bar">
1985
+ ${this.tabs.map(
1986
+ (tab) => b2`
1987
+ <button
1988
+ class="tab ${tab.id === this.activeTabId ? "active" : ""}"
1989
+ @click=${() => this.switchTab(tab.id)}
1990
+ >
1991
+ <span class="tab-status ${tab.sessionActive ? "connected" : ""}"></span>
1992
+ <span>${tab.label}</span>
1993
+ ${this.tabs.length > 1 ? b2`
1994
+ <button
1995
+ class="tab-close"
1996
+ @click=${(e5) => {
1997
+ e5.stopPropagation();
1998
+ this.closeTab(tab.id);
1999
+ }}
2000
+ title="Close tab"
2001
+ >
2002
+ ×
2003
+ </button>
2004
+ ` : A}
2005
+ </button>
2006
+ `
2007
+ )}
2008
+ <button class="tab-add" @click=${() => this.createTab()} title="New tab">
2009
+ +
2010
+ </button>
1396
2011
  </div>
1397
2012
  `;
1398
2013
  }
1399
- };
1400
- LitShellTerminal.styles = [
1401
- sharedStyles,
1402
- themeStyles,
1403
- buttonStyles,
1404
- i`
1405
- :host {
1406
- display: flex;
1407
- flex-direction: column;
1408
- height: 100%;
1409
- min-height: 200px;
1410
- border: 1px solid var(--ls-border);
1411
- border-radius: 4px;
1412
- overflow: hidden;
1413
- }
1414
-
1415
- .header {
1416
- display: flex;
1417
- align-items: center;
1418
- justify-content: space-between;
1419
- padding: 8px 12px;
1420
- background: var(--ls-bg-header);
1421
- border-bottom: 1px solid var(--ls-border);
1422
- }
1423
-
1424
- .header-title {
1425
- display: flex;
1426
- align-items: center;
1427
- gap: 8px;
2014
+ // ==================== End Tab Management Methods ====================
2015
+ /**
2016
+ * Set status message
2017
+ */
2018
+ setStatus(message, type = "info") {
2019
+ this.statusMessage = message;
2020
+ this.statusType = type;
2021
+ if (type !== "error") {
2022
+ setTimeout(() => {
2023
+ if (this.statusMessage === message) {
2024
+ this.statusMessage = "";
2025
+ }
2026
+ }, 5e3);
2027
+ }
2028
+ }
2029
+ /**
2030
+ * Clear status message
2031
+ */
2032
+ clearStatus() {
2033
+ this.statusMessage = "";
2034
+ this.statusType = "info";
2035
+ }
2036
+ /**
2037
+ * Handle theme change
2038
+ */
2039
+ handleThemeChange(e5) {
2040
+ const select = e5.target;
2041
+ this.theme = select.value;
2042
+ this.applyTerminalTheme();
2043
+ this.dispatchEvent(new CustomEvent("theme-change", {
2044
+ detail: { theme: this.theme },
2045
+ bubbles: true,
2046
+ composed: true
2047
+ }));
2048
+ }
2049
+ /**
2050
+ * Apply current theme to xterm.js terminal
2051
+ */
2052
+ applyTerminalTheme() {
2053
+ if (!this.terminal)
2054
+ return;
2055
+ const terminalTheme = this.getTerminalTheme();
2056
+ this.terminal.options.theme = terminalTheme;
2057
+ }
2058
+ /**
2059
+ * Apply current font size to xterm.js terminal
2060
+ */
2061
+ applyTerminalFontSize() {
2062
+ if (!this.terminal)
2063
+ return;
2064
+ this.terminal.options.fontSize = this.fontSize;
2065
+ if (this.fitAddon) {
2066
+ this.fitAddon.fit();
2067
+ }
2068
+ }
2069
+ /**
2070
+ * Handle connection mode change
2071
+ */
2072
+ handleModeChange(e5) {
2073
+ const select = e5.target;
2074
+ this.connectionMode = select.value;
2075
+ if ((this.connectionMode === "docker" || this.connectionMode === "docker-attach") && this.client && this.connected) {
2076
+ this.client.requestContainerList();
2077
+ }
2078
+ if (this.connectionMode === "join" && this.client && this.connected) {
2079
+ this.client.requestSessionList();
2080
+ }
2081
+ }
2082
+ /**
2083
+ * Refresh session list
2084
+ */
2085
+ refreshSessions() {
2086
+ if (this.client && this.connected) {
2087
+ this.client.requestSessionList();
2088
+ }
2089
+ }
2090
+ /**
2091
+ * Join an existing session
2092
+ */
2093
+ async join(sessionId, requestHistory = true) {
2094
+ if (!this.client || !this.connected) {
2095
+ throw new Error("Not connected to server");
2096
+ }
2097
+ this.loading = true;
2098
+ this.error = null;
2099
+ try {
2100
+ await this.initTerminalUI();
2101
+ const session = await this.client.join({
2102
+ sessionId,
2103
+ requestHistory,
2104
+ historyLimit: 5e4
2105
+ });
2106
+ this.sessionActive = true;
2107
+ this.sessionInfo = {
2108
+ sessionId: session.sessionId,
2109
+ shell: session.shell,
2110
+ cwd: session.cwd,
2111
+ cols: session.cols,
2112
+ rows: session.rows,
2113
+ createdAt: session.createdAt || /* @__PURE__ */ new Date(),
2114
+ container: session.container
2115
+ };
2116
+ this.clientCount = session.clientCount;
2117
+ if (this.showTabs) {
2118
+ this.syncStateToActiveTab();
2119
+ }
2120
+ this.setStatus(`Joined session (${session.clientCount} clients)`, "success");
2121
+ if (this.terminal) {
2122
+ this.terminal.focus();
2123
+ }
2124
+ return session;
2125
+ } catch (err) {
2126
+ this.error = err instanceof Error ? err.message : "Failed to join session";
2127
+ throw err;
2128
+ } finally {
2129
+ this.loading = false;
2130
+ }
2131
+ }
2132
+ /**
2133
+ * Leave current session without killing it
2134
+ */
2135
+ leave() {
2136
+ if (this.client && this.sessionInfo) {
2137
+ this.client.leave(this.sessionInfo.sessionId);
2138
+ this.sessionActive = false;
2139
+ this.sessionInfo = null;
2140
+ this.setStatus("Left session", "info");
2141
+ }
2142
+ }
2143
+ /**
2144
+ * Handle connect from connection panel
2145
+ */
2146
+ async handlePanelConnect() {
2147
+ if (!this.connected) {
2148
+ await this.connect();
2149
+ }
2150
+ if (this.connected) {
2151
+ if (this.connectionMode === "join" && this.selectedSessionId) {
2152
+ await this.join(this.selectedSessionId);
2153
+ } else if (this.connectionMode === "docker-attach" && this.selectedContainer) {
2154
+ const options = {
2155
+ container: this.selectedContainer,
2156
+ attachMode: true,
2157
+ orphanTimeout: this.orphanTimeout
2158
+ };
2159
+ await this.spawn(options);
2160
+ } else {
2161
+ const options = {
2162
+ orphanTimeout: this.orphanTimeout,
2163
+ useTmux: this.useTmux
2164
+ };
2165
+ if (this.connectionMode === "docker" && this.selectedContainer) {
2166
+ options.container = this.selectedContainer;
2167
+ options.containerShell = this.selectedShell || "/bin/sh";
2168
+ } else {
2169
+ options.shell = this.selectedShell || void 0;
2170
+ }
2171
+ await this.spawn(options);
2172
+ }
2173
+ }
2174
+ }
2175
+ /**
2176
+ * Toggle settings menu
2177
+ */
2178
+ toggleSettingsMenu() {
2179
+ this.settingsMenuOpen = !this.settingsMenuOpen;
2180
+ }
2181
+ /**
2182
+ * Handle reconnect dialog - Yes button
2183
+ */
2184
+ async handleReconnectYes() {
2185
+ if (!this.reconnectSessionId || !this.client)
2186
+ return;
2187
+ this.showReconnectDialog = false;
2188
+ try {
2189
+ await this.initTerminalUI();
2190
+ await this.join(this.reconnectSessionId, true);
2191
+ this.setStatus("Rejoined previous session", "success");
2192
+ } catch (err) {
2193
+ this.setStatus(`Failed to rejoin: ${err instanceof Error ? err.message : "Unknown error"}`, "error");
2194
+ }
2195
+ this.reconnectSessionId = null;
2196
+ this.client?.clearPreviousSessionId();
2197
+ }
2198
+ /**
2199
+ * Handle reconnect dialog - No button
2200
+ */
2201
+ handleReconnectNo() {
2202
+ this.showReconnectDialog = false;
2203
+ this.reconnectSessionId = null;
2204
+ this.client?.clearPreviousSessionId();
2205
+ }
2206
+ /**
2207
+ * Render reconnect dialog
2208
+ */
2209
+ renderReconnectDialog() {
2210
+ if (!this.showReconnectDialog)
2211
+ return A;
2212
+ return b2`
2213
+ <div class="reconnect-dialog-overlay">
2214
+ <div class="reconnect-dialog">
2215
+ <h3>Session Available</h3>
2216
+ <p>Your previous session is still active. Would you like to rejoin?</p>
2217
+ <div class="reconnect-dialog-buttons">
2218
+ <button class="btn-primary" @click=${this.handleReconnectYes}>
2219
+ Yes, Rejoin
2220
+ </button>
2221
+ <button class="btn-secondary" @click=${this.handleReconnectNo}>
2222
+ No, Start New
2223
+ </button>
2224
+ </div>
2225
+ </div>
2226
+ </div>
2227
+ `;
2228
+ }
2229
+ /**
2230
+ * Render connection panel
2231
+ */
2232
+ renderConnectionPanel() {
2233
+ if (!this.showConnectionPanel)
2234
+ return A;
2235
+ const runningContainers = this.containers.filter((c4) => c4.state === "running");
2236
+ const acceptingSessions = this.availableSessions.filter((s4) => s4.accepting);
2237
+ return b2`
2238
+ <div class="connection-panel">
2239
+ <div class="connection-panel-title">
2240
+ <span>Connection</span>
2241
+ ${this.availableSessions.length > 0 ? b2`<span style="font-size: 11px; color: var(--ls-status-connected);">${this.availableSessions.length} session(s) available</span>` : A}
2242
+ ${this.serverInfo?.dockerEnabled ? b2`<span style="font-size: 11px; color: var(--ls-text-muted);">Docker enabled</span>` : A}
2243
+ </div>
2244
+ <div class="connection-form">
2245
+ <div class="form-group">
2246
+ <label>Mode</label>
2247
+ <select
2248
+ .value=${this.connectionMode}
2249
+ @change=${this.handleModeChange}
2250
+ ?disabled=${this.sessionActive}
2251
+ >
2252
+ <option value="local">New Local Shell</option>
2253
+ ${this.serverInfo?.dockerEnabled ? b2`
2254
+ <option value="docker">Docker Exec (new shell)</option>
2255
+ <option value="docker-attach">Docker Attach (main process)</option>
2256
+ ` : A}
2257
+ ${acceptingSessions.length > 0 ? b2`<option value="join">Join Existing Session</option>` : A}
2258
+ </select>
2259
+ </div>
2260
+
2261
+ ${this.connectionMode === "join" ? b2`
2262
+ <div class="form-group">
2263
+ <label style="display: flex; justify-content: space-between; align-items: center;">
2264
+ <span>Session</span>
2265
+ <button
2266
+ style="font-size: 10px; padding: 2px 6px;"
2267
+ @click=${this.refreshSessions}
2268
+ ?disabled=${!this.connected}
2269
+ >Refresh</button>
2270
+ </label>
2271
+ <select
2272
+ .value=${this.selectedSessionId}
2273
+ @change=${(e5) => this.selectedSessionId = e5.target.value}
2274
+ ?disabled=${this.sessionActive}
2275
+ >
2276
+ ${acceptingSessions.length === 0 ? b2`<option value="">No sessions available</option>` : acceptingSessions.map((s4) => b2`
2277
+ <option value=${s4.sessionId}>
2278
+ ${s4.label || s4.sessionId.substring(0, 12)}
2279
+ (${s4.type === "local" ? s4.shell : s4.container || s4.type})
2280
+ - ${s4.clientCount} client(s)
2281
+ </option>
2282
+ `)}
2283
+ </select>
2284
+ </div>
2285
+ ` : this.connectionMode === "docker" || this.connectionMode === "docker-attach" ? b2`
2286
+ <div class="form-group">
2287
+ <label>Container</label>
2288
+ <select
2289
+ .value=${this.selectedContainer}
2290
+ @change=${(e5) => this.selectedContainer = e5.target.value}
2291
+ ?disabled=${this.sessionActive}
2292
+ >
2293
+ ${runningContainers.length === 0 ? b2`<option value="">No containers running</option>` : runningContainers.map((c4) => b2`
2294
+ <option value=${c4.name}>${c4.name} (${c4.image})</option>
2295
+ `)}
2296
+ </select>
2297
+ </div>
2298
+ ` : A}
2299
+
2300
+ ${this.connectionMode !== "join" && this.connectionMode !== "docker-attach" ? b2`
2301
+ <div class="form-group">
2302
+ <label>Shell</label>
2303
+ <select
2304
+ .value=${this.selectedShell}
2305
+ @change=${(e5) => this.selectedShell = e5.target.value}
2306
+ ?disabled=${this.sessionActive}
2307
+ >
2308
+ ${this.serverInfo?.allowedShells.length ? this.serverInfo.allowedShells.map((s4) => b2`<option value=${s4}>${s4}</option>`) : b2`
2309
+ <option value="/bin/bash">/bin/bash</option>
2310
+ <option value="/bin/sh">/bin/sh</option>
2311
+ <option value="/bin/zsh">/bin/zsh</option>
2312
+ `}
2313
+ </select>
2314
+ </div>
2315
+ ` : A}
2316
+
2317
+ ${this.connectionMode === "local" || this.connectionMode === "docker" ? b2`
2318
+ <div class="form-group">
2319
+ <label>Session Timeout</label>
2320
+ <select
2321
+ .value=${String(this.orphanTimeout)}
2322
+ @change=${(e5) => this.orphanTimeout = parseInt(e5.target.value)}
2323
+ ?disabled=${this.sessionActive}
2324
+ >
2325
+ <option value="60000">1 minute</option>
2326
+ <option value="300000">5 minutes</option>
2327
+ <option value="900000">15 minutes</option>
2328
+ <option value="3600000">1 hour</option>
2329
+ <option value="21600000">6 hours</option>
2330
+ <option value="86400000">24 hours</option>
2331
+ <option value="604800000">1 week</option>
2332
+ </select>
2333
+ </div>
2334
+ ` : A}
2335
+
2336
+ ${this.connectionMode === "docker" ? b2`
2337
+ <div class="form-group">
2338
+ <label style="display: flex; align-items: center; gap: 6px;">
2339
+ <input
2340
+ type="checkbox"
2341
+ .checked=${this.useTmux}
2342
+ @change=${(e5) => this.useTmux = e5.target.checked}
2343
+ ?disabled=${this.sessionActive}
2344
+ />
2345
+ Use tmux (persist forever)
2346
+ </label>
2347
+ </div>
2348
+ ` : A}
2349
+
2350
+ <div class="form-group">
2351
+ ${!this.connected ? b2`<button class="btn-primary" @click=${this.handlePanelConnect} ?disabled=${this.loading}>
2352
+ ${this.loading ? "Connecting..." : "Connect"}
2353
+ </button>` : !this.sessionActive ? b2`<button class="btn-primary" @click=${this.handlePanelConnect} ?disabled=${this.loading || this.connectionMode === "join" && !this.selectedSessionId || (this.connectionMode === "docker" || this.connectionMode === "docker-attach") && !this.selectedContainer}>
2354
+ ${this.loading ? "Starting..." : this.connectionMode === "join" ? "Join Session" : this.connectionMode === "docker-attach" ? "Attach" : "Start Session"}
2355
+ </button>` : b2`<button class="btn-danger" @click=${this.kill}>
2356
+ ${this.clientCount > 1 ? "Leave Session" : "Stop Session"}
2357
+ </button>`}
2358
+ </div>
2359
+ </div>
2360
+ </div>
2361
+ `;
2362
+ }
2363
+ /**
2364
+ * Render settings dropdown
2365
+ */
2366
+ renderSettingsDropdown() {
2367
+ if (!this.showSettings)
2368
+ return A;
2369
+ return b2`
2370
+ <div class="settings-dropdown">
2371
+ <button @click=${this.toggleSettingsMenu} title="Settings">
2372
+ ⚙️
2373
+ </button>
2374
+ ${this.settingsMenuOpen ? b2`
2375
+ <div class="settings-menu">
2376
+ <div class="settings-menu-item">
2377
+ <span>Theme</span>
2378
+ <select
2379
+ .value=${this.theme}
2380
+ @change=${this.handleThemeChange}
2381
+ >
2382
+ <option value="dark">Dark</option>
2383
+ <option value="light">Light</option>
2384
+ <option value="auto">Auto</option>
2385
+ </select>
2386
+ </div>
2387
+ <div class="settings-divider"></div>
2388
+ <div class="settings-menu-item">
2389
+ <span>Font Size</span>
2390
+ <select
2391
+ .value=${String(this.fontSize)}
2392
+ @change=${(e5) => {
2393
+ this.fontSize = parseInt(e5.target.value);
2394
+ this.applyTerminalFontSize();
2395
+ }}
2396
+ >
2397
+ <option value="12">12px</option>
2398
+ <option value="14">14px</option>
2399
+ <option value="16">16px</option>
2400
+ <option value="18">18px</option>
2401
+ </select>
2402
+ </div>
2403
+ <div class="settings-divider"></div>
2404
+ <div class="settings-menu-item" @click=${this.clear}>
2405
+ <span>Clear Terminal</span>
2406
+ </div>
2407
+ </div>
2408
+ ` : A}
2409
+ </div>
2410
+ `;
2411
+ }
2412
+ /**
2413
+ * Send a special key sequence to the terminal
2414
+ */
2415
+ sendKey(key) {
2416
+ if (!this.client || !this.sessionActive)
2417
+ return;
2418
+ let sequence = key;
2419
+ if (this.ctrlPressed) {
2420
+ if (key.length === 1 && key >= "a" && key <= "z") {
2421
+ sequence = String.fromCharCode(key.charCodeAt(0) - 96);
2422
+ } else if (key.length === 1 && key >= "A" && key <= "Z") {
2423
+ sequence = String.fromCharCode(key.charCodeAt(0) - 64);
2424
+ }
2425
+ this.ctrlPressed = false;
2426
+ }
2427
+ if (this.altPressed) {
2428
+ sequence = "\x1B" + sequence;
2429
+ this.altPressed = false;
2430
+ }
2431
+ this.client.write(sequence);
2432
+ this.terminal?.focus();
2433
+ }
2434
+ /**
2435
+ * Send ANSI escape sequence
2436
+ */
2437
+ sendEscape(code) {
2438
+ if (!this.client || !this.sessionActive)
2439
+ return;
2440
+ this.client.write("\x1B" + code);
2441
+ this.terminal?.focus();
2442
+ }
2443
+ /**
2444
+ * Send control character directly
2445
+ */
2446
+ sendCtrl(char) {
2447
+ if (!this.client || !this.sessionActive)
2448
+ return;
2449
+ const code = char.toUpperCase().charCodeAt(0) - 64;
2450
+ this.client.write(String.fromCharCode(code));
2451
+ this.terminal?.focus();
2452
+ }
2453
+ /**
2454
+ * Toggle Ctrl modifier
2455
+ */
2456
+ toggleCtrl() {
2457
+ this.ctrlPressed = !this.ctrlPressed;
2458
+ this.altPressed = false;
2459
+ }
2460
+ /**
2461
+ * Toggle Alt modifier
2462
+ */
2463
+ toggleAlt() {
2464
+ this.altPressed = !this.altPressed;
2465
+ this.ctrlPressed = false;
2466
+ }
2467
+ /**
2468
+ * Toggle extra key rows visibility
2469
+ */
2470
+ toggleExtraRows() {
2471
+ this.showExtraKeyRows = !this.showExtraKeyRows;
2472
+ }
2473
+ /**
2474
+ * Toggle touch keyboard visibility
2475
+ */
2476
+ toggleTouchKeyboard() {
2477
+ this.showTouchKeyboard = !this.showTouchKeyboard;
2478
+ }
2479
+ /**
2480
+ * Render touch keyboard for mobile
2481
+ */
2482
+ renderTouchKeyboard() {
2483
+ if (!this.isMobile)
2484
+ return A;
2485
+ return b2`
2486
+ <!-- Toggle bar to show/hide keyboard -->
2487
+ <div class="touch-keyboard-toggle">
2488
+ <button @click=${this.toggleTouchKeyboard} title="${this.showTouchKeyboard ? "Hide" : "Show"} keyboard">
2489
+ ${this.showTouchKeyboard ? "\u25BC" : "\u25B2"}
2490
+ </button>
2491
+ </div>
2492
+
2493
+ ${this.showTouchKeyboard ? b2`
2494
+ <div class="touch-keyboard">
2495
+ <!-- Row 1: ESC, navigation, special -->
2496
+ <div class="touch-keyboard-row">
2497
+ <button class="touch-key" @click=${() => this.sendEscape("")}>ESC</button>
2498
+ <button class="touch-key" @click=${() => this.sendKey("/")}>/</button>
2499
+ <button class="touch-key" @click=${() => this.sendKey("-")}>-</button>
2500
+ <button class="touch-key" @click=${() => this.sendEscape("[H")}>HOME</button>
2501
+ <button class="touch-key" @click=${() => this.sendEscape("[A")}>↑</button>
2502
+ <button class="touch-key" @click=${() => this.sendEscape("[F")}>END</button>
2503
+ <button class="touch-key" @click=${() => this.sendEscape("[5~")}>PGUP</button>
2504
+ </div>
2505
+
2506
+ <!-- Row 2: TAB, modifiers, arrows -->
2507
+ <div class="touch-keyboard-row">
2508
+ <button class="touch-key" @click=${() => this.sendKey(" ")}>TAB</button>
2509
+ <button class="touch-key toggle-btn ${this.ctrlPressed ? "active" : ""}" @click=${this.toggleCtrl}>CTRL</button>
2510
+ <button class="touch-key toggle-btn ${this.altPressed ? "active" : ""}" @click=${this.toggleAlt}>ALT</button>
2511
+ <button class="touch-key" @click=${() => this.sendEscape("[D")}>←</button>
2512
+ <button class="touch-key" @click=${() => this.sendEscape("[B")}>↓</button>
2513
+ <button class="touch-key" @click=${() => this.sendEscape("[C")}>→</button>
2514
+ <button class="touch-key" @click=${() => this.sendEscape("[6~")}>PGDN</button>
2515
+ </div>
2516
+
2517
+ <!-- Expandable extra rows -->
2518
+ <div class="touch-keyboard-extra ${this.showExtraKeyRows ? "expanded" : ""}">
2519
+ <!-- Row 3: Common control sequences -->
2520
+ <div class="touch-keyboard-row">
2521
+ <button class="touch-key danger" @click=${() => this.sendCtrl("C")}>^C</button>
2522
+ <button class="touch-key" @click=${() => this.sendCtrl("D")}>^D</button>
2523
+ <button class="touch-key" @click=${() => this.sendCtrl("Z")}>^Z</button>
2524
+ <button class="touch-key" @click=${() => this.sendCtrl("L")}>^L</button>
2525
+ <button class="touch-key" @click=${() => this.sendCtrl("A")}>^A</button>
2526
+ <button class="touch-key" @click=${() => this.sendCtrl("E")}>^E</button>
2527
+ <button class="touch-key" @click=${() => this.sendCtrl("R")}>^R</button>
2528
+ </div>
2529
+ </div>
2530
+
2531
+ <!-- Toggle for extra rows -->
2532
+ <div class="touch-keyboard-row">
2533
+ <button
2534
+ class="touch-key wide"
2535
+ @click=${this.toggleExtraRows}
2536
+ title="${this.showExtraKeyRows ? "Hide" : "Show"} extra keys"
2537
+ >
2538
+ ${this.showExtraKeyRows ? "\u25B2 Less" : "\u25BC More"}
2539
+ </button>
2540
+ </div>
2541
+ </div>
2542
+ ` : A}
2543
+ `;
2544
+ }
2545
+ // ==================== End Touch Keyboard Methods ====================
2546
+ /**
2547
+ * Render status bar
2548
+ */
2549
+ renderStatusBar() {
2550
+ if (!this.showStatusBar)
2551
+ return A;
2552
+ return b2`
2553
+ <div class="status-bar">
2554
+ <div class="status-bar-left">
2555
+ <span class="status-dot ${this.connected ? "connected" : ""}"></span>
2556
+ <span>${this.connected ? this.sessionActive ? "Session active" : "Connected" : "Disconnected"}</span>
2557
+ ${this.sessionInfo ? b2`
2558
+ <span style="color: var(--ls-text-muted)">|</span>
2559
+ <span>${this.sessionInfo.container || this.sessionInfo.shell}</span>
2560
+ <span style="color: var(--ls-text-muted)">${this.sessionInfo.cols}x${this.sessionInfo.rows}</span>
2561
+ ` : A}
2562
+ </div>
2563
+ <div class="status-bar-right">
2564
+ ${this.statusMessage ? b2`
2565
+ <span class="${this.statusType === "error" ? "status-bar-error" : this.statusType === "success" ? "status-bar-success" : ""}">
2566
+ ${this.statusType === "error" ? "\u26A0\uFE0F" : this.statusType === "success" ? "\u2713" : ""}
2567
+ ${this.statusMessage}
2568
+ </span>
2569
+ <button
2570
+ style="background: none; border: none; cursor: pointer; padding: 0; font-size: 10px;"
2571
+ @click=${this.clearStatus}
2572
+ title="Dismiss"
2573
+ >✕</button>
2574
+ ` : A}
2575
+ </div>
2576
+ </div>
2577
+ `;
2578
+ }
2579
+ render() {
2580
+ return b2`
2581
+ ${this.noHeader ? A : b2`
2582
+ <div class="header">
2583
+ <div class="header-title">
2584
+ <span>Terminal</span>
2585
+ ${this.sessionInfo ? b2`<span style="font-weight: normal; font-size: 12px; color: var(--ls-text-muted)">
2586
+ ${this.sessionInfo.container ? `${this.sessionInfo.container} (${this.sessionInfo.shell})` : this.sessionInfo.shell}
2587
+ </span>` : A}
2588
+ </div>
2589
+ <div class="header-actions">
2590
+ ${!this.showConnectionPanel ? b2`
2591
+ ${!this.connected ? b2`<button @click=${this.connect} ?disabled=${this.loading}>
2592
+ ${this.loading ? "Connecting..." : "Connect"}
2593
+ </button>` : !this.sessionActive ? b2`<button @click=${() => this.spawn()} ?disabled=${this.loading}>
2594
+ ${this.loading ? "Spawning..." : "Start"}
2595
+ </button>` : b2`<button @click=${this.kill}>Stop</button>`}
2596
+ ` : A}
2597
+ <button @click=${this.clear} ?disabled=${!this.sessionActive}>Clear</button>
2598
+ ${this.renderSettingsDropdown()}
2599
+ ${!this.showStatusBar ? b2`
2600
+ <div class="status">
2601
+ <span class="status-dot ${this.connected ? "connected" : ""}"></span>
2602
+ <span>${this.connected ? "Connected" : "Disconnected"}</span>
2603
+ </div>
2604
+ ` : A}
2605
+ </div>
2606
+ </div>
2607
+ `}
2608
+
2609
+ ${this.renderConnectionPanel()}
2610
+ ${this.renderTabBar()}
2611
+
2612
+ ${this.showTabs && this.tabs.length > 0 ? b2`
2613
+ <div class="terminals-wrapper">
2614
+ ${this.tabs.map(
2615
+ (tab) => b2`
2616
+ <div
2617
+ class="tab-terminal-container ${tab.id === this.activeTabId ? "active" : ""}"
2618
+ data-tab-id=${tab.id}
2619
+ >
2620
+ ${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}
2621
+ </div>
2622
+ `
2623
+ )}
2624
+ </div>
2625
+ ` : b2`
2626
+ <div class="terminal-container">
2627
+ ${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}
2628
+ </div>
2629
+ `}
2630
+
2631
+ ${this.renderStatusBar()}
2632
+ ${this.renderTouchKeyboard()}
2633
+ ${this.renderReconnectDialog()}
2634
+ `;
2635
+ }
2636
+ };
2637
+ /** lit-shell.js version */
2638
+ LitShellTerminal.VERSION = VERSION;
2639
+ LitShellTerminal.styles = [
2640
+ sharedStyles,
2641
+ themeStyles,
2642
+ buttonStyles,
2643
+ i`
2644
+ :host {
2645
+ display: flex;
2646
+ flex-direction: column;
2647
+ height: 100%;
2648
+ min-height: 200px;
2649
+ border: 1px solid var(--ls-border);
2650
+ border-radius: 4px;
2651
+ overflow: hidden;
2652
+ }
2653
+
2654
+ .header {
2655
+ display: flex;
2656
+ align-items: center;
2657
+ justify-content: space-between;
2658
+ padding: 8px 12px;
2659
+ background: var(--ls-bg-header);
2660
+ border-bottom: 1px solid var(--ls-border);
2661
+ }
2662
+
2663
+ .header-title {
2664
+ display: flex;
2665
+ align-items: center;
2666
+ gap: 8px;
1428
2667
  font-weight: 600;
1429
2668
  }
1430
2669
 
@@ -1500,6 +2739,441 @@ LitShellTerminal.styles = [
1500
2739
  :host([no-header]) .header {
1501
2740
  display: none;
1502
2741
  }
2742
+
2743
+ /* Connection panel */
2744
+ .connection-panel {
2745
+ padding: 12px;
2746
+ background: var(--ls-bg-header);
2747
+ border-bottom: 1px solid var(--ls-border);
2748
+ }
2749
+
2750
+ .connection-panel-title {
2751
+ font-weight: 600;
2752
+ margin-bottom: 12px;
2753
+ display: flex;
2754
+ align-items: center;
2755
+ gap: 8px;
2756
+ }
2757
+
2758
+ .connection-form {
2759
+ display: grid;
2760
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
2761
+ gap: 10px;
2762
+ align-items: end;
2763
+ }
2764
+
2765
+ .form-group {
2766
+ display: flex;
2767
+ flex-direction: column;
2768
+ gap: 4px;
2769
+ }
2770
+
2771
+ .form-group label {
2772
+ font-size: 11px;
2773
+ text-transform: uppercase;
2774
+ color: var(--ls-text-muted);
2775
+ letter-spacing: 0.5px;
2776
+ }
2777
+
2778
+ .form-group select,
2779
+ .form-group input {
2780
+ padding: 6px 10px;
2781
+ border: 1px solid var(--ls-border);
2782
+ border-radius: 4px;
2783
+ background: var(--ls-bg);
2784
+ color: var(--ls-text);
2785
+ font-size: 13px;
2786
+ }
2787
+
2788
+ .form-group select:focus,
2789
+ .form-group input:focus {
2790
+ outline: none;
2791
+ border-color: var(--ls-status-connected);
2792
+ }
2793
+
2794
+ /* Settings dropdown */
2795
+ .settings-dropdown {
2796
+ position: relative;
2797
+ }
2798
+
2799
+ .settings-menu {
2800
+ position: absolute;
2801
+ top: 100%;
2802
+ right: 0;
2803
+ margin-top: 4px;
2804
+ min-width: 180px;
2805
+ background: var(--ls-bg-header);
2806
+ border: 1px solid var(--ls-border);
2807
+ border-radius: 4px;
2808
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
2809
+ z-index: 100;
2810
+ padding: 8px 0;
2811
+ }
2812
+
2813
+ .settings-menu-item {
2814
+ display: flex;
2815
+ align-items: center;
2816
+ justify-content: space-between;
2817
+ padding: 8px 12px;
2818
+ font-size: 13px;
2819
+ cursor: pointer;
2820
+ }
2821
+
2822
+ .settings-menu-item:hover {
2823
+ background: var(--ls-btn-hover);
2824
+ }
2825
+
2826
+ .settings-menu-item select {
2827
+ padding: 4px 8px;
2828
+ border: 1px solid var(--ls-border);
2829
+ border-radius: 3px;
2830
+ background: var(--ls-bg);
2831
+ color: var(--ls-text);
2832
+ font-size: 12px;
2833
+ }
2834
+
2835
+ .settings-divider {
2836
+ height: 1px;
2837
+ background: var(--ls-border);
2838
+ margin: 4px 0;
2839
+ }
2840
+
2841
+ /* Status bar */
2842
+ .status-bar {
2843
+ display: flex;
2844
+ align-items: center;
2845
+ justify-content: space-between;
2846
+ padding: 4px 12px;
2847
+ background: var(--ls-bg-header);
2848
+ border-top: 1px solid var(--ls-border);
2849
+ font-size: 12px;
2850
+ color: var(--ls-text-muted);
2851
+ }
2852
+
2853
+ .status-bar-left {
2854
+ display: flex;
2855
+ align-items: center;
2856
+ gap: 12px;
2857
+ }
2858
+
2859
+ .status-bar-right {
2860
+ display: flex;
2861
+ align-items: center;
2862
+ gap: 8px;
2863
+ }
2864
+
2865
+ .status-bar-error {
2866
+ color: #ef4444;
2867
+ display: flex;
2868
+ align-items: center;
2869
+ gap: 4px;
2870
+ }
2871
+
2872
+ .status-bar-success {
2873
+ color: var(--ls-status-connected);
2874
+ }
2875
+
2876
+ /* Tab bar styles */
2877
+ .tab-bar {
2878
+ display: flex;
2879
+ align-items: center;
2880
+ background: var(--ls-bg-header);
2881
+ border-bottom: 1px solid var(--ls-border);
2882
+ padding: 0 4px;
2883
+ gap: 2px;
2884
+ min-height: 32px;
2885
+ overflow-x: auto;
2886
+ }
2887
+
2888
+ .tab-bar::-webkit-scrollbar {
2889
+ height: 4px;
2890
+ }
2891
+
2892
+ .tab-bar::-webkit-scrollbar-thumb {
2893
+ background: var(--ls-border);
2894
+ border-radius: 2px;
2895
+ }
2896
+
2897
+ .tab {
2898
+ display: flex;
2899
+ align-items: center;
2900
+ gap: 6px;
2901
+ padding: 6px 12px;
2902
+ background: transparent;
2903
+ border: none;
2904
+ border-bottom: 2px solid transparent;
2905
+ color: var(--ls-text-muted);
2906
+ font-size: 12px;
2907
+ cursor: pointer;
2908
+ white-space: nowrap;
2909
+ transition: all 0.15s ease;
2910
+ }
2911
+
2912
+ .tab:hover {
2913
+ background: var(--ls-btn-hover);
2914
+ color: var(--ls-text);
2915
+ }
2916
+
2917
+ .tab.active {
2918
+ color: var(--ls-text);
2919
+ border-bottom-color: var(--ls-status-connected);
2920
+ background: var(--ls-bg);
2921
+ }
2922
+
2923
+ .tab-status {
2924
+ width: 6px;
2925
+ height: 6px;
2926
+ border-radius: 50%;
2927
+ background: var(--ls-status-disconnected);
2928
+ }
2929
+
2930
+ .tab-status.connected {
2931
+ background: var(--ls-status-connected);
2932
+ }
2933
+
2934
+ .tab-close {
2935
+ display: flex;
2936
+ align-items: center;
2937
+ justify-content: center;
2938
+ width: 16px;
2939
+ height: 16px;
2940
+ border-radius: 3px;
2941
+ background: none;
2942
+ border: none;
2943
+ color: var(--ls-text-muted);
2944
+ font-size: 14px;
2945
+ cursor: pointer;
2946
+ opacity: 0;
2947
+ transition: opacity 0.15s ease;
2948
+ }
2949
+
2950
+ .tab:hover .tab-close {
2951
+ opacity: 1;
2952
+ }
2953
+
2954
+ .tab-close:hover {
2955
+ background: var(--ls-btn-hover);
2956
+ color: var(--ls-text);
2957
+ }
2958
+
2959
+ .tab-add {
2960
+ display: flex;
2961
+ align-items: center;
2962
+ justify-content: center;
2963
+ width: 28px;
2964
+ height: 28px;
2965
+ border-radius: 4px;
2966
+ background: none;
2967
+ border: none;
2968
+ color: var(--ls-text-muted);
2969
+ font-size: 18px;
2970
+ cursor: pointer;
2971
+ margin-left: 4px;
2972
+ }
2973
+
2974
+ .tab-add:hover {
2975
+ background: var(--ls-btn-hover);
2976
+ color: var(--ls-text);
2977
+ }
2978
+
2979
+ /* Multi-terminal container */
2980
+ .terminals-wrapper {
2981
+ flex: 1;
2982
+ position: relative;
2983
+ overflow: hidden;
2984
+ }
2985
+
2986
+ .tab-terminal-container {
2987
+ position: absolute;
2988
+ top: 0;
2989
+ left: 0;
2990
+ right: 0;
2991
+ bottom: 0;
2992
+ padding: 4px;
2993
+ background: var(--ls-terminal-bg);
2994
+ display: none;
2995
+ }
2996
+
2997
+ .tab-terminal-container.active {
2998
+ display: block;
2999
+ }
3000
+
3001
+ .tab-terminal-container .xterm {
3002
+ height: 100%;
3003
+ }
3004
+
3005
+ /* Reconnect dialog */
3006
+ .reconnect-dialog-overlay {
3007
+ position: absolute;
3008
+ top: 0;
3009
+ left: 0;
3010
+ right: 0;
3011
+ bottom: 0;
3012
+ background: rgba(0, 0, 0, 0.7);
3013
+ display: flex;
3014
+ align-items: center;
3015
+ justify-content: center;
3016
+ z-index: 1000;
3017
+ }
3018
+
3019
+ .reconnect-dialog {
3020
+ background: var(--ls-bg-header);
3021
+ border: 1px solid var(--ls-border);
3022
+ border-radius: 8px;
3023
+ padding: 24px;
3024
+ max-width: 320px;
3025
+ text-align: center;
3026
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
3027
+ }
3028
+
3029
+ .reconnect-dialog h3 {
3030
+ margin: 0 0 12px 0;
3031
+ font-size: 16px;
3032
+ color: var(--ls-text);
3033
+ }
3034
+
3035
+ .reconnect-dialog p {
3036
+ margin: 0 0 20px 0;
3037
+ font-size: 14px;
3038
+ color: var(--ls-text-muted);
3039
+ }
3040
+
3041
+ .reconnect-dialog-buttons {
3042
+ display: flex;
3043
+ gap: 12px;
3044
+ justify-content: center;
3045
+ }
3046
+
3047
+ .reconnect-dialog-buttons button {
3048
+ padding: 8px 20px;
3049
+ border-radius: 4px;
3050
+ font-size: 14px;
3051
+ cursor: pointer;
3052
+ transition: background 0.15s ease;
3053
+ }
3054
+
3055
+ .reconnect-dialog-buttons .btn-primary {
3056
+ background: var(--ls-status-connected);
3057
+ color: white;
3058
+ border: none;
3059
+ }
3060
+
3061
+ .reconnect-dialog-buttons .btn-primary:hover {
3062
+ background: #1da34d;
3063
+ }
3064
+
3065
+ .reconnect-dialog-buttons .btn-secondary {
3066
+ background: transparent;
3067
+ color: var(--ls-text-muted);
3068
+ border: 1px solid var(--ls-border);
3069
+ }
3070
+
3071
+ .reconnect-dialog-buttons .btn-secondary:hover {
3072
+ background: var(--ls-btn-hover);
3073
+ color: var(--ls-text);
3074
+ }
3075
+
3076
+ /* Touch keyboard for mobile */
3077
+ .touch-keyboard {
3078
+ display: flex;
3079
+ flex-direction: column;
3080
+ background: var(--ls-bg-header);
3081
+ border-top: 1px solid var(--ls-border);
3082
+ padding: 4px;
3083
+ gap: 4px;
3084
+ }
3085
+
3086
+ .touch-keyboard-row {
3087
+ display: flex;
3088
+ gap: 4px;
3089
+ justify-content: center;
3090
+ }
3091
+
3092
+ .touch-key {
3093
+ display: flex;
3094
+ align-items: center;
3095
+ justify-content: center;
3096
+ min-width: 40px;
3097
+ height: 36px;
3098
+ padding: 0 8px;
3099
+ background: var(--ls-bg);
3100
+ border: 1px solid var(--ls-border);
3101
+ border-radius: 4px;
3102
+ color: var(--ls-text);
3103
+ font-size: 12px;
3104
+ font-family: inherit;
3105
+ cursor: pointer;
3106
+ touch-action: manipulation;
3107
+ user-select: none;
3108
+ -webkit-user-select: none;
3109
+ transition: background 0.1s ease;
3110
+ }
3111
+
3112
+ .touch-key:active {
3113
+ background: var(--ls-btn-hover);
3114
+ }
3115
+
3116
+ .touch-key.wide {
3117
+ min-width: 50px;
3118
+ }
3119
+
3120
+ .touch-key.danger {
3121
+ color: #ef4444;
3122
+ border-color: #ef4444;
3123
+ }
3124
+
3125
+ .touch-key.toggle-btn {
3126
+ background: var(--ls-bg-header);
3127
+ min-width: 30px;
3128
+ font-size: 14px;
3129
+ }
3130
+
3131
+ .touch-key.toggle-btn.active {
3132
+ background: var(--ls-status-connected);
3133
+ color: white;
3134
+ border-color: var(--ls-status-connected);
3135
+ }
3136
+
3137
+ .touch-keyboard-toggle {
3138
+ display: flex;
3139
+ align-items: center;
3140
+ justify-content: center;
3141
+ padding: 2px;
3142
+ background: var(--ls-bg-header);
3143
+ border-top: 1px solid var(--ls-border);
3144
+ }
3145
+
3146
+ .touch-keyboard-toggle button {
3147
+ background: none;
3148
+ border: none;
3149
+ color: var(--ls-text-muted);
3150
+ font-size: 18px;
3151
+ padding: 4px 12px;
3152
+ cursor: pointer;
3153
+ }
3154
+
3155
+ .touch-keyboard-toggle button:active {
3156
+ color: var(--ls-text);
3157
+ }
3158
+
3159
+ /* Extra key rows (collapsible) */
3160
+ .touch-keyboard-extra {
3161
+ overflow: hidden;
3162
+ max-height: 0;
3163
+ opacity: 0;
3164
+ transition: max-height 0.2s ease, opacity 0.2s ease;
3165
+ }
3166
+
3167
+ .touch-keyboard-extra.expanded {
3168
+ max-height: 100px;
3169
+ opacity: 1;
3170
+ }
3171
+
3172
+ /* Hide touch keyboard on desktop */
3173
+ :host(:not([mobile])) .touch-keyboard,
3174
+ :host(:not([mobile])) .touch-keyboard-toggle {
3175
+ display: none;
3176
+ }
1503
3177
  `
1504
3178
  ];
1505
3179
  __decorateClass([
@@ -1529,6 +3203,30 @@ __decorateClass([
1529
3203
  __decorateClass([
1530
3204
  n4({ type: Boolean, attribute: "auto-spawn" })
1531
3205
  ], LitShellTerminal.prototype, "autoSpawn", 2);
3206
+ __decorateClass([
3207
+ n4({ type: String })
3208
+ ], LitShellTerminal.prototype, "container", 2);
3209
+ __decorateClass([
3210
+ n4({ type: String, attribute: "container-shell" })
3211
+ ], LitShellTerminal.prototype, "containerShell", 2);
3212
+ __decorateClass([
3213
+ n4({ type: String, attribute: "container-user" })
3214
+ ], LitShellTerminal.prototype, "containerUser", 2);
3215
+ __decorateClass([
3216
+ n4({ type: String, attribute: "container-cwd" })
3217
+ ], LitShellTerminal.prototype, "containerCwd", 2);
3218
+ __decorateClass([
3219
+ n4({ type: Boolean, attribute: "show-connection-panel" })
3220
+ ], LitShellTerminal.prototype, "showConnectionPanel", 2);
3221
+ __decorateClass([
3222
+ n4({ type: Boolean, attribute: "show-settings" })
3223
+ ], LitShellTerminal.prototype, "showSettings", 2);
3224
+ __decorateClass([
3225
+ n4({ type: Boolean, attribute: "show-status-bar" })
3226
+ ], LitShellTerminal.prototype, "showStatusBar", 2);
3227
+ __decorateClass([
3228
+ n4({ type: Boolean, attribute: "show-tabs" })
3229
+ ], LitShellTerminal.prototype, "showTabs", 2);
1532
3230
  __decorateClass([
1533
3231
  n4({ type: Number, attribute: "font-size" })
1534
3232
  ], LitShellTerminal.prototype, "fontSize", 2);
@@ -1559,11 +3257,78 @@ __decorateClass([
1559
3257
  __decorateClass([
1560
3258
  r5()
1561
3259
  ], LitShellTerminal.prototype, "sessionInfo", 2);
3260
+ __decorateClass([
3261
+ r5()
3262
+ ], LitShellTerminal.prototype, "containers", 2);
3263
+ __decorateClass([
3264
+ r5()
3265
+ ], LitShellTerminal.prototype, "serverInfo", 2);
3266
+ __decorateClass([
3267
+ r5()
3268
+ ], LitShellTerminal.prototype, "selectedContainer", 2);
3269
+ __decorateClass([
3270
+ r5()
3271
+ ], LitShellTerminal.prototype, "selectedShell", 2);
3272
+ __decorateClass([
3273
+ r5()
3274
+ ], LitShellTerminal.prototype, "connectionMode", 2);
3275
+ __decorateClass([
3276
+ r5()
3277
+ ], LitShellTerminal.prototype, "orphanTimeout", 2);
3278
+ __decorateClass([
3279
+ r5()
3280
+ ], LitShellTerminal.prototype, "useTmux", 2);
3281
+ __decorateClass([
3282
+ r5()
3283
+ ], LitShellTerminal.prototype, "availableSessions", 2);
3284
+ __decorateClass([
3285
+ r5()
3286
+ ], LitShellTerminal.prototype, "selectedSessionId", 2);
3287
+ __decorateClass([
3288
+ r5()
3289
+ ], LitShellTerminal.prototype, "clientCount", 2);
3290
+ __decorateClass([
3291
+ r5()
3292
+ ], LitShellTerminal.prototype, "settingsMenuOpen", 2);
3293
+ __decorateClass([
3294
+ r5()
3295
+ ], LitShellTerminal.prototype, "showReconnectDialog", 2);
3296
+ __decorateClass([
3297
+ r5()
3298
+ ], LitShellTerminal.prototype, "reconnectSessionId", 2);
3299
+ __decorateClass([
3300
+ r5()
3301
+ ], LitShellTerminal.prototype, "isMobile", 2);
3302
+ __decorateClass([
3303
+ r5()
3304
+ ], LitShellTerminal.prototype, "showTouchKeyboard", 2);
3305
+ __decorateClass([
3306
+ r5()
3307
+ ], LitShellTerminal.prototype, "showExtraKeyRows", 2);
3308
+ __decorateClass([
3309
+ r5()
3310
+ ], LitShellTerminal.prototype, "statusMessage", 2);
3311
+ __decorateClass([
3312
+ r5()
3313
+ ], LitShellTerminal.prototype, "statusType", 2);
3314
+ __decorateClass([
3315
+ r5()
3316
+ ], LitShellTerminal.prototype, "tabs", 2);
3317
+ __decorateClass([
3318
+ r5()
3319
+ ], LitShellTerminal.prototype, "activeTabId", 2);
3320
+ __decorateClass([
3321
+ r5()
3322
+ ], LitShellTerminal.prototype, "ctrlPressed", 2);
3323
+ __decorateClass([
3324
+ r5()
3325
+ ], LitShellTerminal.prototype, "altPressed", 2);
1562
3326
  LitShellTerminal = __decorateClass([
1563
3327
  t3("lit-shell-terminal")
1564
3328
  ], LitShellTerminal);
1565
3329
  export {
1566
3330
  LitShellTerminal,
3331
+ VERSION,
1567
3332
  buttonStyles,
1568
3333
  sharedStyles,
1569
3334
  themeStyles