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.
- package/CHANGELOG.md +69 -0
- package/README.md +189 -62
- package/dist/client/browser-bundle.js +301 -5
- package/dist/client/browser-bundle.js.map +3 -3
- package/dist/client/index.d.ts +2 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -0
- package/dist/client/index.js.map +1 -1
- package/dist/client/terminal-client.d.ts +97 -3
- package/dist/client/terminal-client.d.ts.map +1 -1
- package/dist/client/terminal-client.js +298 -4
- package/dist/client/terminal-client.js.map +1 -1
- package/dist/server/circular-buffer.d.ts +55 -0
- package/dist/server/circular-buffer.d.ts.map +1 -0
- package/dist/server/circular-buffer.js +91 -0
- package/dist/server/circular-buffer.js.map +1 -0
- package/dist/server/index.d.ts +5 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +4 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/session-manager.d.ts +201 -0
- package/dist/server/session-manager.d.ts.map +1 -0
- package/dist/server/session-manager.js +458 -0
- package/dist/server/session-manager.js.map +1 -0
- package/dist/server/terminal-server.d.ts +75 -5
- package/dist/server/terminal-server.d.ts.map +1 -1
- package/dist/server/terminal-server.js +515 -79
- package/dist/server/terminal-server.js.map +1 -1
- package/dist/shared/types.d.ts +185 -2
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/ui/browser-bundle.js +1853 -88
- package/dist/ui/browser-bundle.js.map +4 -4
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.d.ts.map +1 -1
- package/dist/ui/index.js +1 -0
- package/dist/ui/index.js.map +1 -1
- package/dist/ui/lit-shell-terminal.d.ts +225 -6
- package/dist/ui/lit-shell-terminal.d.ts.map +1 -1
- package/dist/ui/lit-shell-terminal.js +1605 -60
- package/dist/ui/lit-shell-terminal.js.map +1 -1
- package/dist/ui/styles.d.ts.map +1 -1
- package/dist/ui/styles.js +22 -0
- package/dist/ui/styles.js.map +1 -1
- package/dist/version.d.ts +6 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +6 -0
- package/dist/version.js.map +1 -0
- 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
|
-
|
|
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
|
|
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.
|
|
1500
|
+
await this.injectXtermCSS();
|
|
1120
1501
|
}
|
|
1121
1502
|
/**
|
|
1122
|
-
*
|
|
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
|
|
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
|
-
|
|
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 =
|
|
1139
|
-
this.shadowRoot.
|
|
1140
|
-
} catch (
|
|
1141
|
-
console.warn("[lit-shell] Failed to load xterm
|
|
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.
|
|
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.
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
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
|
-
|
|
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
|
|
1284
|
-
|
|
1285
|
-
this.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
<
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
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
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
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
|