refactor: 拆分 claude-dev-stack 为 windows-dev-stack 和 wsl-dev-stack
将原 claude-dev-stack 目录拆分为独立的 Windows 和 WSL 部署栈,便于分别维护和使用。 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,382 @@
|
||||
@tool
|
||||
extends VBoxContainer
|
||||
|
||||
var websocket_server: Node = null
|
||||
var command_router: Node = null
|
||||
|
||||
const MAX_LOG_ENTRIES := 200
|
||||
const COLOR_CONNECTED := Color(0.2, 0.9, 0.2)
|
||||
const COLOR_DISCONNECTED := Color(0.9, 0.2, 0.2)
|
||||
const COLOR_STALE := Color(1.0, 0.7, 0.2)
|
||||
const COLOR_SUCCESS := Color(0.6, 1, 0.6)
|
||||
const COLOR_ERROR := Color(1, 0.6, 0.6)
|
||||
const COLOR_DIM := Color(0.6, 0.6, 0.6)
|
||||
|
||||
const BASE_PORT := 6505
|
||||
const MAX_PORT := 6509
|
||||
|
||||
# Header
|
||||
var _status_icon: Label
|
||||
var _status_label: Label
|
||||
var _client_count_label: Label
|
||||
|
||||
# Tabs
|
||||
var _tab_container: TabContainer
|
||||
|
||||
# Activity tab
|
||||
var _show_details_check: CheckBox
|
||||
var _log_container: VBoxContainer
|
||||
var _log_scroll: ScrollContainer
|
||||
|
||||
# Clients tab
|
||||
var _port_labels: Dictionary = {} # port -> {icon: Label, label: Label}
|
||||
|
||||
# Tools tab
|
||||
var _filter_edit: LineEdit
|
||||
var _tools_container: VBoxContainer
|
||||
var _tool_checkboxes: Dictionary = {} # method_name -> CheckBox
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_build_ui()
|
||||
|
||||
|
||||
func setup(ws_server: Node, cmd_router: Node = null) -> void:
|
||||
websocket_server = ws_server
|
||||
command_router = cmd_router
|
||||
|
||||
if websocket_server:
|
||||
websocket_server.client_connected.connect(_on_client_connected)
|
||||
websocket_server.client_disconnected.connect(_on_client_disconnected)
|
||||
if websocket_server.has_signal("command_completed"):
|
||||
websocket_server.command_completed.connect(_on_command_completed)
|
||||
else:
|
||||
websocket_server.command_executed.connect(_on_command_executed)
|
||||
|
||||
if command_router:
|
||||
_populate_tools_list()
|
||||
|
||||
|
||||
func _build_ui() -> void:
|
||||
# Header bar
|
||||
var header := HBoxContainer.new()
|
||||
add_child(header)
|
||||
|
||||
_status_icon = Label.new()
|
||||
_status_icon.text = "●"
|
||||
_status_icon.add_theme_color_override("font_color", COLOR_DISCONNECTED)
|
||||
header.add_child(_status_icon)
|
||||
|
||||
_status_label = Label.new()
|
||||
_status_label.text = " MCP Pro: Waiting for connection..."
|
||||
header.add_child(_status_label)
|
||||
|
||||
var spacer := Control.new()
|
||||
spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
header.add_child(spacer)
|
||||
|
||||
_client_count_label = Label.new()
|
||||
_client_count_label.text = "Clients: 0"
|
||||
header.add_child(_client_count_label)
|
||||
|
||||
# Separator
|
||||
var sep := HSeparator.new()
|
||||
add_child(sep)
|
||||
|
||||
# TabContainer
|
||||
_tab_container = TabContainer.new()
|
||||
_tab_container.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
add_child(_tab_container)
|
||||
|
||||
_build_activity_tab()
|
||||
_build_clients_tab()
|
||||
_build_tools_tab()
|
||||
|
||||
|
||||
func _build_activity_tab() -> void:
|
||||
var vbox := VBoxContainer.new()
|
||||
vbox.name = "Activity"
|
||||
vbox.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
_tab_container.add_child(vbox)
|
||||
|
||||
# Controls row
|
||||
var controls := HBoxContainer.new()
|
||||
vbox.add_child(controls)
|
||||
|
||||
_show_details_check = CheckBox.new()
|
||||
_show_details_check.text = "Show Response Details"
|
||||
_show_details_check.button_pressed = false
|
||||
_show_details_check.toggled.connect(_on_show_details_toggled)
|
||||
controls.add_child(_show_details_check)
|
||||
|
||||
var ctrl_spacer := Control.new()
|
||||
ctrl_spacer.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
controls.add_child(ctrl_spacer)
|
||||
|
||||
var clear_btn := Button.new()
|
||||
clear_btn.text = "Clear"
|
||||
clear_btn.pressed.connect(_on_clear_log)
|
||||
controls.add_child(clear_btn)
|
||||
|
||||
# Log scroll
|
||||
_log_scroll = ScrollContainer.new()
|
||||
_log_scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
_log_scroll.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_log_scroll.custom_minimum_size.y = 80
|
||||
vbox.add_child(_log_scroll)
|
||||
|
||||
_log_container = VBoxContainer.new()
|
||||
_log_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_log_scroll.add_child(_log_container)
|
||||
|
||||
|
||||
func _build_clients_tab() -> void:
|
||||
var vbox := VBoxContainer.new()
|
||||
vbox.name = "Clients"
|
||||
_tab_container.add_child(vbox)
|
||||
|
||||
for p in range(BASE_PORT, MAX_PORT + 1):
|
||||
var row := HBoxContainer.new()
|
||||
vbox.add_child(row)
|
||||
|
||||
var icon := Label.new()
|
||||
icon.text = "○"
|
||||
icon.add_theme_color_override("font_color", COLOR_DISCONNECTED)
|
||||
row.add_child(icon)
|
||||
|
||||
var lbl := Label.new()
|
||||
lbl.text = " Port %d — Disconnected" % p
|
||||
row.add_child(lbl)
|
||||
|
||||
_port_labels[p] = {"icon": icon, "label": lbl}
|
||||
|
||||
|
||||
func _build_tools_tab() -> void:
|
||||
var vbox := VBoxContainer.new()
|
||||
vbox.name = "Tools"
|
||||
vbox.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
_tab_container.add_child(vbox)
|
||||
|
||||
# Controls
|
||||
var controls := HBoxContainer.new()
|
||||
vbox.add_child(controls)
|
||||
|
||||
_filter_edit = LineEdit.new()
|
||||
_filter_edit.placeholder_text = "Filter tools..."
|
||||
_filter_edit.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
_filter_edit.text_changed.connect(_on_filter_changed)
|
||||
controls.add_child(_filter_edit)
|
||||
|
||||
var enable_all_btn := Button.new()
|
||||
enable_all_btn.text = "Enable All"
|
||||
enable_all_btn.pressed.connect(_on_enable_all)
|
||||
controls.add_child(enable_all_btn)
|
||||
|
||||
var disable_all_btn := Button.new()
|
||||
disable_all_btn.text = "Disable All"
|
||||
disable_all_btn.pressed.connect(_on_disable_all)
|
||||
controls.add_child(disable_all_btn)
|
||||
|
||||
# Scroll
|
||||
var scroll := ScrollContainer.new()
|
||||
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
||||
scroll.custom_minimum_size.y = 80
|
||||
vbox.add_child(scroll)
|
||||
|
||||
_tools_container = VBoxContainer.new()
|
||||
_tools_container.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
||||
scroll.add_child(_tools_container)
|
||||
|
||||
|
||||
func _populate_tools_list() -> void:
|
||||
if not command_router:
|
||||
return
|
||||
|
||||
# Clear existing
|
||||
for child in _tools_container.get_children():
|
||||
child.queue_free()
|
||||
_tool_checkboxes.clear()
|
||||
|
||||
var methods: Array = command_router.get_available_methods()
|
||||
methods.sort()
|
||||
|
||||
for method_name: String in methods:
|
||||
var cb := CheckBox.new()
|
||||
cb.text = method_name
|
||||
cb.button_pressed = not command_router.is_tool_disabled(method_name)
|
||||
cb.toggled.connect(_on_tool_toggled.bind(method_name))
|
||||
_tools_container.add_child(cb)
|
||||
_tool_checkboxes[method_name] = cb
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if not websocket_server:
|
||||
return
|
||||
|
||||
var count: int = websocket_server.get_client_count()
|
||||
_client_count_label.text = "Clients: %d" % count
|
||||
|
||||
var any_stale := false
|
||||
if websocket_server.has_method("is_port_stale"):
|
||||
for p in range(BASE_PORT, MAX_PORT + 1):
|
||||
if websocket_server.is_port_stale(p):
|
||||
any_stale = true
|
||||
break
|
||||
|
||||
if count > 0:
|
||||
_status_icon.add_theme_color_override("font_color", COLOR_CONNECTED)
|
||||
_status_label.text = " MCP Pro: Connected"
|
||||
elif any_stale:
|
||||
_status_icon.add_theme_color_override("font_color", COLOR_STALE)
|
||||
_status_label.text = " MCP Pro: ⚠ Reconnecting (stale connection detected)..."
|
||||
else:
|
||||
_status_icon.add_theme_color_override("font_color", COLOR_DISCONNECTED)
|
||||
_status_label.text = " MCP Pro: Waiting for connection..."
|
||||
|
||||
# Update clients tab
|
||||
_update_clients_tab()
|
||||
|
||||
|
||||
func _update_clients_tab() -> void:
|
||||
var connected_ports: Array[int] = []
|
||||
if websocket_server.has_method("get_connected_ports"):
|
||||
connected_ports = websocket_server.get_connected_ports()
|
||||
|
||||
for p: int in _port_labels:
|
||||
var info: Dictionary = _port_labels[p]
|
||||
var icon: Label = info["icon"]
|
||||
var lbl: Label = info["label"]
|
||||
|
||||
var is_stale := false
|
||||
if websocket_server.has_method("is_port_stale"):
|
||||
is_stale = websocket_server.is_port_stale(p)
|
||||
|
||||
if p in connected_ports:
|
||||
var time_str := ""
|
||||
if websocket_server.has_method("get_port_connect_time"):
|
||||
var elapsed: float = websocket_server.get_port_connect_time(p)
|
||||
if elapsed >= 0:
|
||||
var mins := int(elapsed) / 60
|
||||
var secs := int(elapsed) % 60
|
||||
time_str = " (%dm %02ds)" % [mins, secs]
|
||||
|
||||
var idle_str := ""
|
||||
if websocket_server.has_method("get_port_idle_time"):
|
||||
var idle: float = websocket_server.get_port_idle_time(p)
|
||||
if idle >= 2.0:
|
||||
idle_str = " · idle %.0fs" % idle
|
||||
|
||||
icon.text = "●"
|
||||
icon.add_theme_color_override("font_color", COLOR_CONNECTED)
|
||||
lbl.text = " Port %d — Connected%s%s" % [p, time_str, idle_str]
|
||||
elif is_stale:
|
||||
icon.text = "◐"
|
||||
icon.add_theme_color_override("font_color", COLOR_STALE)
|
||||
lbl.text = " Port %d — ⚠ Stale (reconnecting)" % p
|
||||
else:
|
||||
icon.text = "○"
|
||||
icon.add_theme_color_override("font_color", COLOR_DISCONNECTED)
|
||||
lbl.text = " Port %d — Disconnected" % p
|
||||
|
||||
|
||||
# --- Activity callbacks ---
|
||||
|
||||
func _on_client_connected() -> void:
|
||||
_add_log("Client connected", COLOR_CONNECTED)
|
||||
|
||||
|
||||
func _on_client_disconnected() -> void:
|
||||
_add_log("Client disconnected", COLOR_DISCONNECTED)
|
||||
|
||||
|
||||
func _on_command_executed(method: String, ok: bool) -> void:
|
||||
var color := COLOR_SUCCESS if ok else COLOR_ERROR
|
||||
var status_icon := "OK" if ok else "ERR"
|
||||
_add_log("[%s] %s" % [status_icon, method], color)
|
||||
|
||||
|
||||
func _on_command_completed(method: String, ok: bool, response: String, source_port: int) -> void:
|
||||
var color := COLOR_SUCCESS if ok else COLOR_ERROR
|
||||
var status_icon := "OK" if ok else "ERR"
|
||||
_add_log("[%s] %s (port %d)" % [status_icon, method, source_port], color, response)
|
||||
|
||||
|
||||
func _on_clear_log() -> void:
|
||||
for child in _log_container.get_children():
|
||||
child.queue_free()
|
||||
|
||||
|
||||
func _on_show_details_toggled(on: bool) -> void:
|
||||
for entry in _log_container.get_children():
|
||||
if entry is VBoxContainer and entry.get_child_count() > 1:
|
||||
entry.get_child(1).visible = on
|
||||
|
||||
|
||||
func _add_log(text: String, color: Color = Color.WHITE, response: String = "") -> void:
|
||||
if _log_container == null:
|
||||
return
|
||||
|
||||
var entry := VBoxContainer.new()
|
||||
_log_container.add_child(entry)
|
||||
|
||||
var label := Label.new()
|
||||
var time_str := Time.get_time_string_from_system()
|
||||
label.text = "[%s] %s" % [time_str, text]
|
||||
label.add_theme_color_override("font_color", color)
|
||||
label.add_theme_font_size_override("font_size", 12)
|
||||
entry.add_child(label)
|
||||
|
||||
if not response.is_empty():
|
||||
var detail := RichTextLabel.new()
|
||||
var preview := response.substr(0, 500)
|
||||
if response.length() > 500:
|
||||
preview += "..."
|
||||
detail.text = preview
|
||||
detail.fit_content = true
|
||||
detail.scroll_active = false
|
||||
detail.add_theme_color_override("default_color", COLOR_DIM)
|
||||
detail.add_theme_font_size_override("normal_font_size", 11)
|
||||
detail.custom_minimum_size.y = 0
|
||||
detail.visible = _show_details_check.button_pressed if _show_details_check else false
|
||||
entry.add_child(detail)
|
||||
|
||||
# Limit entries
|
||||
while _log_container.get_child_count() > MAX_LOG_ENTRIES:
|
||||
var old: Node = _log_container.get_child(0)
|
||||
_log_container.remove_child(old)
|
||||
old.queue_free()
|
||||
|
||||
# Auto scroll to bottom
|
||||
_auto_scroll.call_deferred()
|
||||
|
||||
|
||||
func _auto_scroll() -> void:
|
||||
if _log_scroll:
|
||||
_log_scroll.scroll_vertical = int(_log_scroll.get_v_scroll_bar().max_value)
|
||||
|
||||
|
||||
# --- Tools callbacks ---
|
||||
|
||||
func _on_filter_changed(filter: String) -> void:
|
||||
for method_name: String in _tool_checkboxes:
|
||||
var cb: CheckBox = _tool_checkboxes[method_name]
|
||||
cb.visible = filter.is_empty() or method_name.containsn(filter)
|
||||
|
||||
|
||||
func _on_tool_toggled(enabled: bool, method_name: String) -> void:
|
||||
if command_router and command_router.has_method("set_tool_disabled"):
|
||||
command_router.set_tool_disabled(method_name, not enabled)
|
||||
|
||||
|
||||
func _on_enable_all() -> void:
|
||||
if command_router and command_router.has_method("set_all_tools_disabled"):
|
||||
command_router.set_all_tools_disabled(false)
|
||||
for method_name: String in _tool_checkboxes:
|
||||
_tool_checkboxes[method_name].set_pressed_no_signal(true)
|
||||
|
||||
|
||||
func _on_disable_all() -> void:
|
||||
if command_router and command_router.has_method("set_all_tools_disabled"):
|
||||
command_router.set_all_tools_disabled(true)
|
||||
for method_name: String in _tool_checkboxes:
|
||||
_tool_checkboxes[method_name].set_pressed_no_signal(false)
|
||||
Reference in New Issue
Block a user