-
Notifications
You must be signed in to change notification settings - Fork 15
Expand file tree
/
Copy path__init__.py
More file actions
606 lines (511 loc) · 26 KB
/
__init__.py
File metadata and controls
606 lines (511 loc) · 26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
import os
import sys
import time
import atexit
import hashlib
import logging
import requests
import threading
from collections import defaultdict
import traceback
import asyncio
from watchdog.observers.polling import PollingObserver as Observer
from watchdog.events import FileSystemEventHandler
from aiohttp import web
import folder_paths
from nodes import load_custom_node
from comfy_execution import caching
from server import PromptServer
import json
import nodes
from .Nodes.Terminal import *
RELOADED_CLASS_TYPES: dict = {}
CUSTOM_NODE_ROOT: list[str] = folder_paths.folder_names_and_paths["custom_nodes"][0]
CONFIG_PATH = os.path.join(os.path.dirname(__file__), "config.json")
def load_exclude_modules() -> set[str]:
try:
with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
config = json.load(f)
return set(config.get("exclude_modules", set()))
except Exception as e:
print(f"\033[91m[LG_HotReload] Error loading config: {str(e)}\033[0m")
return set()
def save_exclude_modules(modules: set[str]):
try:
with open(CONFIG_PATH, 'w', encoding='utf-8') as f:
json.dump({"exclude_modules": list(modules)}, f, indent=4)
print(f"\033[92m[LG_HotReload] Exclude modules config saved\033[0m")
except Exception as e:
print(f"\033[91m[LG_HotReload] Error saving config: {str(e)}\033[0m")
EXCLUDE_MODULES: set[str] = load_exclude_modules()
@PromptServer.instance.routes.get("/hotreload/get_exclude_modules")
async def get_exclude_modules(request):
return web.json_response({"exclude_modules": list(EXCLUDE_MODULES)})
@PromptServer.instance.routes.post("/hotreload/update_exclude_modules")
async def update_exclude_modules(request):
try:
data = await request.json()
modules = set(data.get("exclude_modules", []))
global EXCLUDE_MODULES
EXCLUDE_MODULES = modules
save_exclude_modules(modules)
return web.json_response({"status": "success"})
except Exception as e:
return web.json_response({"status": "error", "message": str(e)}, status=500)
@PromptServer.instance.routes.get("/hotreload/get_all_modules")
async def get_all_modules(request):
try:
modules = []
for item in os.listdir(CUSTOM_NODE_ROOT[0]):
item_path = os.path.join(CUSTOM_NODE_ROOT[0], item)
if os.path.isdir(item_path) and not item.startswith('.'):
if os.path.exists(os.path.join(item_path, '__init__.py')):
modules.append(item)
return web.json_response({"modules": modules})
except Exception as e:
return web.json_response({"status": "error", "message": str(e)}, status=500)
@PromptServer.instance.routes.get("/extensions/{module_name}/{path:.*}")
async def dynamic_extensions_handler(request):
"""处理动态加载的插件的WEB_DIRECTORY文件访问"""
module_name = request.match_info['module_name']
file_path = request.match_info['path']
# 优先从 EXTENSION_WEB_DIRS 中查找(自定义节点,支持热更新)
if module_name in nodes.EXTENSION_WEB_DIRS:
web_dir = nodes.EXTENSION_WEB_DIRS[module_name]
full_path = os.path.join(web_dir, file_path)
if os.path.isfile(full_path):
return web.FileResponse(full_path)
# 如果不在 EXTENSION_WEB_DIRS 中,尝试从 ComfyUI 前端包的 extensions 目录查找(系统扩展如 core)
# PromptServer.instance.web_root 指向前端包的静态文件目录
# 例如:E:\python3.11\Lib\site-packages\comfyui_frontend_package\static
if hasattr(PromptServer.instance, 'web_root') and PromptServer.instance.web_root:
web_root = PromptServer.instance.web_root
full_path = os.path.join(web_root, "extensions", module_name, file_path)
if os.path.isfile(full_path):
return web.FileResponse(full_path)
# 如果都找不到,返回404
raise web.HTTPNotFound()
# 存储动态路由映射
DYNAMIC_API_ROUTES = {}
def is_module_match(handler_module: str, module_name: str, sys_module_name: str = None) -> bool:
"""
检查 handler 的模块名是否匹配目标模块
Args:
handler_module: handler.__module__ 的值
module_name: 模块名(如 "comfyui-clear-screen")
sys_module_name: sys.modules 中的模块名(路径中的 "." 被替换为 "_x_")
Returns:
是否匹配
"""
if not handler_module:
return False
# 直接匹配
if module_name == handler_module:
return True
# 匹配 sys_module_name(如果提供)
if sys_module_name and sys_module_name == handler_module:
return True
# 匹配各种路径格式
patterns = [
f"custom_nodes.{module_name}",
f"custom_nodes\\{module_name}",
f"custom_nodes/{module_name}",
f"\\{module_name}",
f"/{module_name}",
]
for pattern in patterns:
if handler_module.endswith(pattern) or pattern in handler_module:
return True
# 如果 sys_module_name 提供了,也检查它的各种格式
if sys_module_name:
if sys_module_name in handler_module or handler_module in sys_module_name:
return True
return False
@PromptServer.instance.routes.get("/api/{path:.*}")
async def dynamic_api_handler(request):
"""动态处理热更新的API路由"""
path = "/" + request.match_info['path'] # 重构完整路径
method = request.method.upper()
# 查找动态注册的处理器
route_key = f"{method}:{path}"
if route_key in DYNAMIC_API_ROUTES:
handler = DYNAMIC_API_ROUTES[route_key]
return await handler(request)
# 如果没找到动态路由,让系统继续处理
raise web.HTTPNotFound()
def register_module_routes(module_name, sys_module_name=None):
"""注册模块的所有路由到动态路由表"""
# 清理旧路由 - 从 DYNAMIC_API_ROUTES(通过模块路径匹配)
keys_to_remove = []
for route_key, handler in DYNAMIC_API_ROUTES.items():
if hasattr(handler, '__module__'):
handler_module = handler.__module__
# 使用统一的匹配函数
if is_module_match(handler_module, module_name, sys_module_name):
keys_to_remove.append(route_key)
for key in keys_to_remove:
del DYNAMIC_API_ROUTES[key]
# 注册新路由到动态路由表
registered_count = 0
for route in PromptServer.instance.routes:
if hasattr(route, 'handler') and hasattr(route.handler, '__module__'):
handler_module = route.handler.__module__
# 使用统一的匹配函数
if is_module_match(handler_module, module_name, sys_module_name):
route_key = f"{route.method}:{route.path}"
DYNAMIC_API_ROUTES[route_key] = route.handler
registered_count += 1
if (HOTRELOAD_EXCLUDE := os.getenv("HOTRELOAD_EXCLUDE", None)) is not None:
EXCLUDE_MODULES.update(x for x in HOTRELOAD_EXCLUDE.split(',') if x)
HOTRELOAD_OBSERVE_ONLY: set[str] = set(x for x in os.getenv("HOTRELOAD_OBSERVE_ONLY", '').split(',') if x)
HOTRELOAD_EXTENSIONS: set[str] = set(x.strip() for x in os.getenv("HOTRELOAD_EXTENSIONS", '.py').split(',') if x)
try:
DEBOUNCE_TIME: float = float(os.getenv("HOTRELOAD_DEBOUNCE_TIME", 1.0))
except ValueError:
DEBOUNCE_TIME = 1.0
def hash_file(file_path: str) -> str:
try:
with open(file_path, 'rb') as f:
return hashlib.md5(f.read()).hexdigest()
except Exception as e:
logging.error(f"Error reading file {file_path}: {e}")
return None
def is_hidden_file_windows(file_path: str) -> bool:
try:
import ctypes
attribute = ctypes.windll.kernel32.GetFileAttributesW(file_path)
if attribute == -1:
return False
return attribute & 0x2 != 0
except Exception as e:
logging.error(f"Error checking if file is hidden on Windows: {e}")
return False
def is_hidden_file(file_path: str) -> bool:
file_path = os.path.abspath(file_path)
if sys.platform.startswith('win'):
while file_path and file_path != os.path.dirname(file_path):
if is_hidden_file_windows(file_path):
return True
file_path = os.path.dirname(file_path)
else:
while file_path and file_path != os.path.dirname(file_path):
if os.path.basename(file_path).startswith('.'):
return True
file_path = os.path.dirname(file_path)
return False
def dfs(item_list: list, searches: set) -> bool:
for item in item_list:
if isinstance(item, (frozenset, tuple)) and dfs(item, searches):
return True
elif item in searches:
return True
return False
class DebouncedHotReloader(FileSystemEventHandler):
def __init__(self, delay: float = 1.0):
super().__init__()
self.__delay: float = delay
self.__last_modified: defaultdict[str, float] = defaultdict(float)
self.__reload_timers: dict[str, threading.Timer] = {}
self.__hashes: dict[str, str] = {}
self.__lock: threading.Lock = threading.Lock()
# 添加最后成功重载时间记录
self.__last_successful_reload: defaultdict[float] = defaultdict(float)
self.__successful_reload_cooldown = 5.0 # 成功重载后的冷却时间(秒)
def __reload(self, module_name: str) -> web.Response:
with self.__lock:
try:
print(f'\n\033[94m[LG_HotReload] 开始重载模块: {module_name}\033[0m')
# 计算 sys_module_name(与 load_custom_node 中的逻辑一致)
# load_custom_node 会将路径中的 "." 替换为 "_x_"
module_path_for_sys = os.path.join(CUSTOM_NODE_ROOT[0], module_name)
sys_module_name = module_path_for_sys.replace(".", "_x_")
original_routes_count = len(PromptServer.instance.routes)
# 收集需要保留的路由
routes_to_keep = []
routes_removed_count = 0
for route in PromptServer.instance.routes:
should_remove = False
if hasattr(route, 'handler') and hasattr(route.handler, '__module__'):
handler_module = route.handler.__module__
# 使用统一的匹配函数
if is_module_match(handler_module, module_name, sys_module_name):
should_remove = True
routes_removed_count += 1
route_key = f"{route.method}:{route.path}"
if not should_remove:
routes_to_keep.append(route)
# 重建路由表
if routes_removed_count > 0:
# 由于RouteTableDef不支持直接删除路由,我们采用替换策略
# 直接使用_items清理路由
try:
PromptServer.instance.routes._items.clear()
PromptServer.instance.routes._items.extend(routes_to_keep)
except Exception as e:
print(f'\033[91m[LG_HotReload] 路由清理失败: {str(e)}\033[0m')
traceback.print_exc()
else:
print(f'\033[96m[LG_HotReload] 未发现需要清理的路由\033[0m')
module_path = os.path.join(CUSTOM_NODE_ROOT[0], module_name)
# 收集需要重新加载的所有模块
modules_to_reload = set()
for name, module in list(sys.modules.items()):
if hasattr(module, '__file__') and module.__file__ and \
module.__file__.startswith(module_path):
modules_to_reload.add(name)
# 删除所有相关模块
for name in modules_to_reload:
if name in sys.modules:
del sys.modules[name]
# 重新加载自定义节点
# 追踪load_custom_node前的路由数量
routes_before_load = len(PromptServer.instance.routes)
try:
# 使用 asyncio.run 来同步调用异步函数
success = asyncio.run(load_custom_node(module_path))
except Exception as e:
print(f'\033[91m[LG_HotReload] 调用 load_custom_node 失败: {str(e)}\033[0m')
success = False
# 追踪load_custom_node后的路由数量
routes_after_load = len(PromptServer.instance.routes)
if not success:
print(f'\033[91m[LG_HotReload] 加载模块失败: {module_name}\033[0m')
return web.Response(text='FAILED')
# 关键步骤:同步新路由到 aiohttp 的 router
# 通过直接替换 handler 来实现热重载
try:
# 获取新添加的路由
new_routes_count = routes_after_load - routes_before_load
if new_routes_count > 0 and hasattr(PromptServer.instance, 'app') and PromptServer.instance.app:
new_routes = list(PromptServer.instance.routes)[-new_routes_count:]
router = PromptServer.instance.app.router
for route in new_routes:
if hasattr(route, 'method') and hasattr(route, 'path') and hasattr(route, 'handler'):
if hasattr(route.handler, '__module__'):
handler_module = route.handler.__module__
if is_module_match(handler_module, module_name, sys_module_name):
handler_id = id(route.handler)
# 查找并替换旧的 handler
for resource in list(router._resources):
resource_path = getattr(resource, '_path', None) or getattr(resource, 'canonical', None)
# 匹配路径(包括 /api 前缀的版本)
if resource_path and (resource_path == route.path or resource_path == f"/api{route.path}"):
for route_obj in resource:
if hasattr(route_obj, 'handler') and hasattr(route_obj.handler, '__module__'):
route_handler_module = route_obj.handler.__module__
if is_module_match(route_handler_module, module_name, sys_module_name):
# 直接替换 handler(保留路由缓存结构)
if hasattr(route_obj, '_handler'):
old_id = id(route_obj._handler)
route_obj._handler = route.handler
except Exception as e:
print(f'\033[91m[LG_HotReload] 路由同步失败: {str(e)}\033[0m')
traceback.print_exc()
# 确保模块被正确注册到sys.modules中
try:
import importlib.util
# 构建完整的模块名(包含custom_nodes前缀)
full_module_name = f"custom_nodes.{module_name}"
if os.path.isfile(module_path):
# 处理单个.py文件
spec = importlib.util.spec_from_file_location(full_module_name, module_path)
else:
# 处理模块目录
init_path = os.path.join(module_path, '__init__.py')
spec = importlib.util.spec_from_file_location(full_module_name, init_path)
if spec:
# 追踪模块注册前的路由数量
routes_before_register = len(PromptServer.instance.routes)
# load_custom_node已经执行了模块代码,这里只需要注册到sys.modules
# 获取已经加载的模块(通过load_custom_node加载)
loaded_module = None
# 尝试从sys.modules中找到已加载的模块
for mod_name, mod in sys.modules.items():
if (hasattr(mod, '__file__') and mod.__file__ and
mod.__file__.startswith(module_path)):
loaded_module = mod
break
if loaded_module:
# 使用已加载的模块,避免重复执行
sys.modules[full_module_name] = loaded_module
sys.modules[module_name] = loaded_module
else:
# 如果找不到已加载的模块,则正常加载(备用方案)
module = importlib.util.module_from_spec(spec)
sys.modules[full_module_name] = module
sys.modules[module_name] = module
spec.loader.exec_module(module)
except Exception as e:
print(f'\033[91m[LG_HotReload] 重新注册模块失败: {str(e)}\033[0m')
traceback.print_exc()
# 确保节点被正确注册到全局的 NODE_CLASS_MAPPINGS 中
module = sys.modules.get(module_name)
if module and hasattr(module, 'NODE_CLASS_MAPPINGS'):
# 先清理旧的节点映射
for name in list(nodes.NODE_CLASS_MAPPINGS.keys()):
if name in module.NODE_CLASS_MAPPINGS:
del nodes.NODE_CLASS_MAPPINGS[name]
# 重新注册节点
for name, node_cls in module.NODE_CLASS_MAPPINGS.items():
nodes.NODE_CLASS_MAPPINGS[name] = node_cls
node_cls.RELATIVE_PYTHON_MODULE = f"custom_nodes.{module_name}"
if hasattr(module, 'NODE_DISPLAY_NAME_MAPPINGS'):
nodes.NODE_DISPLAY_NAME_MAPPINGS.update(module.NODE_DISPLAY_NAME_MAPPINGS)
# 更新节点类型
if module and hasattr(module, 'NODE_CLASS_MAPPINGS'):
for key in module.NODE_CLASS_MAPPINGS.keys():
RELOADED_CLASS_TYPES[key] = 3
# 重新注册API路由(到动态路由表)
register_module_routes(module_name, sys_module_name)
print(f'\033[92m[LG_HotReload] 模块重载成功: {module_name}\033[0m')
return web.Response(text='OK')
except Exception as e:
logging.error(f"Failed to reload module {module_name}: {e}")
traceback.print_exc()
return web.Response(text='FAILED')
def on_created(self, event):
if event.is_directory:
return
self.handle_file_event(event.src_path)
def on_deleted(self, event):
if event.is_directory:
return
self.handle_file_event(event.src_path)
def handle_file_event(self, file_path: str):
if not any(ext == '*' for ext in HOTRELOAD_EXTENSIONS):
if not any(file_path.endswith(ext) for ext in HOTRELOAD_EXTENSIONS):
return
if is_hidden_file(file_path):
return
relative_path: str = os.path.relpath(file_path, CUSTOM_NODE_ROOT[0])
root_dir: str = relative_path.split(os.path.sep)[0]
if HOTRELOAD_OBSERVE_ONLY and root_dir not in HOTRELOAD_OBSERVE_ONLY:
return
elif root_dir in EXCLUDE_MODULES:
return
self.schedule_reload(root_dir, file_path)
def on_modified(self, event):
if event.is_directory:
return
self.handle_file_event(event.src_path)
def schedule_reload(self, module_name: str, file_path: str):
current_time: float = time.time()
self.__last_modified[module_name] = current_time
with self.__lock:
if module_name in self.__reload_timers:
self.__reload_timers[module_name].cancel()
timer = threading.Timer(
self.__delay,
self.check_and_reload,
args=[module_name, current_time, file_path]
)
self.__reload_timers[module_name] = timer
timer.start()
def check_and_reload(self, module_name: str, scheduled_time: float, file_path: str):
with self.__lock:
if self.__last_modified[module_name] != scheduled_time:
return
# 检查是否在冷却期内
current_time = time.time()
if (current_time - self.__last_successful_reload[module_name]) < self.__successful_reload_cooldown:
print(f"\033[93m[LG_HotReload] Module {module_name} was recently reloaded, skipping...\033[0m")
return
try:
# 获取重载前的节点信息
old_nodes = set()
old_module = sys.modules.get(module_name)
if old_module and hasattr(old_module, 'NODE_CLASS_MAPPINGS'):
old_nodes = set(old_module.NODE_CLASS_MAPPINGS.keys())
# 重载模块
self.__reload(module_name)
# 添加调试信息
print(f'\033[94m[LG_HotReload] 检查节点注册状态:\033[0m')
module = sys.modules.get(module_name)
if module and hasattr(module, 'NODE_CLASS_MAPPINGS'):
for node_class in module.NODE_CLASS_MAPPINGS.keys():
if node_class in nodes.NODE_CLASS_MAPPINGS:
print(f'\033[92m[LG_HotReload] 节点 {node_class} 已成功注册\033[0m')
else:
print(f'\033[91m[LG_HotReload] 节点 {node_class} 注册失败\033[0m')
# 获取重载后的节点信息
new_nodes = set()
if module and hasattr(module, 'NODE_CLASS_MAPPINGS'):
new_nodes = set(module.NODE_CLASS_MAPPINGS.keys())
# 计算节点变化
added_nodes = new_nodes - old_nodes
removed_nodes = old_nodes - new_nodes
updated_nodes = new_nodes & old_nodes
# 确定文件变更类型
action = "deleted" if not os.path.exists(file_path) else "added" if file_path not in self.__hashes else "modified"
# 发送更新消息给前端
update_message = {
"type": "hot_reload_update",
"data": {
"module": module_name,
"action": action,
"file": file_path,
"timestamp": time.time(),
"changes": {
"added": list(added_nodes),
"removed": list(removed_nodes),
"updated": list(updated_nodes)
}
}
}
if hasattr(PromptServer.instance, "send_sync"):
PromptServer.instance.send_sync(
"hot_reload_update",
update_message["data"]
)
self.__last_successful_reload[module_name] = time.time()
print(f'\033[92m[LG_HotReload] Successfully reloaded module: {module_name}\033[0m')
except requests.RequestException as e:
print(f'\033[91m[LG_HotReload] Reload failed: {e}\033[0m')
except Exception as e:
print(f'\033[91m[LG_HotReload] Error occurred: {e}\033[0m')
traceback.print_exc()
class HotReloaderService:
def __init__(self, delay: float = 1.0):
self.__observer: Observer = None
self.__reloader: DebouncedHotReloader = DebouncedHotReloader(delay)
def start(self):
self.__observer = Observer()
self.__observer.schedule(self.__reloader, CUSTOM_NODE_ROOT[0], recursive=True)
self.__observer.start()
def stop(self):
if self.__observer:
self.__observer.stop()
self.__observer.join()
def monkeypatch():
original_set_prompt = caching.BasicCache.set_prompt
def set_prompt(self, dynprompt, node_ids, is_changed_cache):
if not hasattr(self, 'cache_key_set'):
RELOADED_CLASS_TYPES.clear()
return original_set_prompt(self, dynprompt, node_ids, is_changed_cache)
found_keys = []
for key, item_list in self.cache_key_set.keys.items():
if dfs(item_list, RELOADED_CLASS_TYPES):
found_keys.append(key)
if len(found_keys):
for value_key in list(RELOADED_CLASS_TYPES.keys()):
RELOADED_CLASS_TYPES[value_key] -= 1
if RELOADED_CLASS_TYPES[value_key] == 0:
del RELOADED_CLASS_TYPES[value_key]
for key in found_keys:
cache_key = self.cache_key_set.get_data_key(key)
if cache_key and cache_key in self.cache:
del self.cache[cache_key]
del self.cache_key_set.keys[key]
del self.cache_key_set.subcache_keys[key]
return original_set_prompt(self, dynprompt, node_ids, is_changed_cache)
caching.HierarchicalCache.set_prompt = set_prompt
def setup():
logging.info("[LG_HotReload] Monkey patching comfy_execution.caching.BasicCache")
monkeypatch()
hot_reloader_service = HotReloaderService(delay=DEBOUNCE_TIME)
atexit.register(hot_reloader_service.stop)
hot_reloader_service.start()
setup()
WEB_DIRECTORY = "./web"
NODE_CLASS_MAPPINGS = {"HotReload_Terminal": HotReload_Terminal}
NODE_DISPLAY_NAME_MAPPINGS = {"HotReload_Terminal": "Terminal"}