meltygui 0.1.0__py3-none-any.whl
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.
- meltygui/__init__.py +107 -0
- meltygui/accounts/__init__.py +0 -0
- meltygui/accounts/internet_accounts.py +1355 -0
- meltygui/chat/__init__.py +91 -0
- meltygui/chat/activity.py +75 -0
- meltygui/chat/backends.py +36 -0
- meltygui/chat/chat_interface.py +732 -0
- meltygui/chat/chat_proxy.py +352 -0
- meltygui/chat/codex_proxy.py +592 -0
- meltygui/chat/codex_settings.py +100 -0
- meltygui/chat/codex_transport.py +60 -0
- meltygui/chat/command_parser.py +204 -0
- meltygui/chat/images.py +227 -0
- meltygui/chat/messages.py +417 -0
- meltygui/chat/metadata.py +139 -0
- meltygui/chat/writer_locks.py +64 -0
- meltygui/code/__init__.py +0 -0
- meltygui/code/basic_converters.py +533 -0
- meltygui/code/chain_converters.py +2111 -0
- meltygui/code/code_checks.py +2209 -0
- meltygui/code/core_syntax.py +1430 -0
- meltygui/code/file_converters.py +1933 -0
- meltygui/code/fileref.py +702 -0
- meltygui/code/hotswap_guard.py +144 -0
- meltygui/code/libcst_conversion.py +9724 -0
- meltygui/code/live_instrument.py +392 -0
- meltygui/code/live_view.py +2490 -0
- meltygui/code/melty_scan.py +2684 -0
- meltygui/code/new_codecs.py +1255 -0
- meltygui/code/new_converters.py +3017 -0
- meltygui/code/project_code.py +278 -0
- meltygui/code/source_context.py +63 -0
- meltygui/code/symbol_roster.py +1588 -0
- meltygui/code/syntax_check.py +34 -0
- meltygui/code/syntax_check_worker.py +114 -0
- meltygui/completion/__init__.py +0 -0
- meltygui/completion/fim.py +1232 -0
- meltygui/completion/fim_context.py +481 -0
- meltygui/completion/providers/__init__.py +0 -0
- meltygui/completion/providers/anthropic_oauth.py +446 -0
- meltygui/completion/providers/anthropic_requests.py +69 -0
- meltygui/completion/providers/claude.py +203 -0
- meltygui/completion/providers/claude_usage.py +499 -0
- meltygui/completion/providers/codex_accounts.py +180 -0
- meltygui/completion/providers/copilot.py +617 -0
- meltygui/completion/providers/oauth_popup.py +220 -0
- meltygui/completion/providers/ollama.py +320 -0
- meltygui/completion/providers/profiles.py +23 -0
- meltygui/core/README.md +88 -0
- meltygui/core/__init__.py +1 -0
- meltygui/core/automation/__init__.py +1 -0
- meltygui/core/automation/action_core.py +153 -0
- meltygui/core/automation/collection_action.py +45 -0
- meltygui/core/automation/mcp_eval.py +167 -0
- meltygui/core/automation/mcp_hotswap.py +107 -0
- meltygui/core/automation/mcp_query.py +552 -0
- meltygui/core/automation/mcp_server.py +657 -0
- meltygui/core/automation/orchestration_core.py +2636 -0
- meltygui/core/automation/query_core.py +124 -0
- meltygui/core/automation/search_core.py +26 -0
- meltygui/core/automation/selector_core.py +340 -0
- meltygui/core/automation/value_core.py +1752 -0
- meltygui/core/cache/__init__.py +1 -0
- meltygui/core/cache/cache_diagnostics.py +0 -0
- meltygui/core/cache/invalidation_decoration.py +153 -0
- meltygui/core/cache/invalidation_tracker.py +43 -0
- meltygui/core/cache/tile_cache.py +5968 -0
- meltygui/core/conversion/__init__.py +1 -0
- meltygui/core/conversion/bubbling.py +599 -0
- meltygui/core/conversion/cache_tree.py +188 -0
- meltygui/core/conversion/chain.py +113 -0
- meltygui/core/conversion/converter_register.py +145 -0
- meltygui/core/conversion/data_decoration.py +67 -0
- meltygui/core/conversion/dict_conversion.py +1815 -0
- meltygui/core/conversion/dict_conversion_util.py +177 -0
- meltygui/core/conversion/dynamic_obj.py +89 -0
- meltygui/core/conversion/graph_compare.py +183 -0
- meltygui/core/conversion/load_save_v2.py +1032 -0
- meltygui/core/conversion/missing_saved_class.py +39 -0
- meltygui/core/conversion/path_finder.py +606 -0
- meltygui/core/conversion/render_host.py +1083 -0
- meltygui/core/core_render.py +6537 -0
- meltygui/core/definition_hotswap.py +231 -0
- meltygui/core/diagnostics/__init__.py +1 -0
- meltygui/core/diagnostics/attribute_churn.py +38 -0
- meltygui/core/diagnostics/fps_counter.py +39 -0
- meltygui/core/diagnostics/gpu_frame_timer.py +103 -0
- meltygui/core/diagnostics/inspection_core.py +169 -0
- meltygui/core/diagnostics/monitor_core.py +105 -0
- meltygui/core/diagnostics/notifications.py +706 -0
- meltygui/core/diagnostics/perf_trace.py +281 -0
- meltygui/core/diagnostics/profile_decoration.py +88 -0
- meltygui/core/diagnostics/resize_trace.py +62 -0
- meltygui/core/diagnostics/screenshot_core.py +247 -0
- meltygui/core/diagnostics/session_status.py +98 -0
- meltygui/core/diagnostics/trace_core.py +445 -0
- meltygui/core/files/__init__.py +1 -0
- meltygui/core/files/file_core.py +208 -0
- meltygui/core/files/file_explorer_core.py +104 -0
- meltygui/core/files/file_tree_core.py +198 -0
- meltygui/core/files/file_watch_core.py +43 -0
- meltygui/core/files/import_graph_core.py +43 -0
- meltygui/core/files/metadata_core.py +51 -0
- meltygui/core/graphics/__init__.py +1 -0
- meltygui/core/graphics/cuda_context_core.py +166 -0
- meltygui/core/graphics/cuda_interop_core.py +136 -0
- meltygui/core/graphics/cuda_kernel_core.py +91 -0
- meltygui/core/graphics/framebuffer_recorder.py +337 -0
- meltygui/core/graphics/gl_state.py +658 -0
- meltygui/core/graphics/lut_core.py +52 -0
- meltygui/core/graphics/overlay_renderer.py +984 -0
- meltygui/core/graphics/scene_target.py +180 -0
- meltygui/core/graphics/screenshot.py +439 -0
- meltygui/core/graphics/shader_func.py +478 -0
- meltygui/core/graphics/tensor_core.py +45 -0
- meltygui/core/graphics/text_texture.py +329 -0
- meltygui/core/graphics/wayland_color.py +635 -0
- meltygui/core/input/__init__.py +1 -0
- meltygui/core/input/collision.py +165 -0
- meltygui/core/input/drag_drop_core.py +1525 -0
- meltygui/core/input/hypr_left_drag.py +323 -0
- meltygui/core/input/input_core.py +245 -0
- meltygui/core/input/input_handler.py +1101 -0
- meltygui/core/input/mouse_cursor.py +355 -0
- meltygui/core/input/pynput_backend.py +1054 -0
- meltygui/core/input/space_mouse.py +338 -0
- meltygui/core/input/touchpad_backend.py +393 -0
- meltygui/core/input/view_selection.py +177 -0
- meltygui/core/layout/__init__.py +1 -0
- meltygui/core/layout/column_core.py +2153 -0
- meltygui/core/layout/cursor_core.py +161 -0
- meltygui/core/layout/dropdown_core.py +278 -0
- meltygui/core/layout/edge_constraints.py +155 -0
- meltygui/core/layout/grid_core.py +117 -0
- meltygui/core/layout/header_core.py +23 -0
- meltygui/core/layout/header_runtime.py +67 -0
- meltygui/core/layout/layout_core.py +87 -0
- meltygui/core/layout/tile_manager_core.py +591 -0
- meltygui/core/melty.py +6726 -0
- meltygui/core/module_map.json +896 -0
- meltygui/core/module_names.py +19 -0
- meltygui/core/rendering/__init__.py +1 -0
- meltygui/core/rendering/core_decoration.py +431 -0
- meltygui/core/rendering/core_render_helpers.py +328 -0
- meltygui/core/rendering/func_metadata.py +398 -0
- meltygui/core/rendering/mode.py +818 -0
- meltygui/core/rendering/mode_defaults.py +41 -0
- meltygui/core/rendering/modes.py +136 -0
- meltygui/core/rendering/parameter_core.py +1665 -0
- meltygui/core/rendering/render_dispatch.py +1891 -0
- meltygui/core/rendering/render_funcs.py +273 -0
- meltygui/core/rendering/shaped.py +312 -0
- meltygui/core/rendering/window_decoration.py +25 -0
- meltygui/core/runtime/__init__.py +1 -0
- meltygui/core/runtime/app.py +735 -0
- meltygui/core/runtime/app_session.py +140 -0
- meltygui/core/runtime/background.py +564 -0
- meltygui/core/runtime/extensions.py +53 -0
- meltygui/core/runtime/gc_manager.py +1163 -0
- meltygui/core/runtime/lifecycle.py +21 -0
- meltygui/core/runtime/paths.py +27 -0
- meltygui/core/runtime/settings.py +14 -0
- meltygui/core/runtime/singleton.py +16 -0
- meltygui/core/runtime/thread_safe_bool.py +24 -0
- meltygui/core/runtime/thread_signal.py +30 -0
- meltygui/core/runtime/toggles.py +3115 -0
- meltygui/core/services/__init__.py +1 -0
- meltygui/core/services/account_core.py +10 -0
- meltygui/core/services/chat_core.py +19 -0
- meltygui/core/services/claude_terminal_core.py +346 -0
- meltygui/core/services/terminal_core.py +458 -0
- meltygui/core/services/terminal_runtime.py +78 -0
- meltygui/core/styling/__init__.py +1 -0
- meltygui/core/styling/color_core.py +46 -0
- meltygui/core/styling/fonts.py +639 -0
- meltygui/core/styling/global_style.py +338 -0
- meltygui/core/styling/style.py +198 -0
- meltygui/core/styling/style_core.py +522 -0
- meltygui/core/styling/warm_start.py +149 -0
- meltygui/core/windowing/__init__.py +1 -0
- meltygui/core/windowing/backends/PYIMGUI_LICENSE +28 -0
- meltygui/core/windowing/backends/__init__.py +1 -0
- meltygui/core/windowing/backends/imgui_renderer.py +138 -0
- meltygui/core/windowing/backends/native_wayland.py +850 -0
- meltygui/core/windowing/backends/protocols/xdg-decoration-unstable-v1.xml +156 -0
- meltygui/core/windowing/backends/protocols/xdg-shell.xml +1420 -0
- meltygui/core/windowing/backends/wayland_protocol.py +101 -0
- meltygui/core/windowing/dock_core.py +182 -0
- meltygui/core/windowing/frame_geometry.py +42 -0
- meltygui/core/windowing/geometry_feed.py +864 -0
- meltygui/core/windowing/glfw_utils.py +1343 -0
- meltygui/core/windowing/os_frame.py +1552 -0
- meltygui/core/windowing/surface.py +643 -0
- meltygui/core/windowing/titlebar.py +1560 -0
- meltygui/core/windowing/titlebar_buttons.py +281 -0
- meltygui/core/windowing/wayland_move.py +932 -0
- meltygui/core/windowing/window_api.py +62 -0
- meltygui/core/windowing/window_constants.py +339 -0
- meltygui/core/windowing/window_visibility.py +162 -0
- meltygui/debug/__init__.py +0 -0
- meltygui/debug/app_view_utils.py +9 -0
- meltygui/editor/__init__.py +0 -0
- meltygui/editor/bash_syntax.py +30 -0
- meltygui/editor/code_line_fast.py +152 -0
- meltygui/editor/diff.py +139 -0
- meltygui/editor/external_changes.py +159 -0
- meltygui/editor/file_header.py +41 -0
- meltygui/editor/live_usage.py +169 -0
- meltygui/editor/live_view_views.py +1803 -0
- meltygui/editor/pending_save.py +1351 -0
- meltygui/editor/roster_tints.py +547 -0
- meltygui/editor/source_preview.py +16 -0
- meltygui/editor/source_tools.py +10 -0
- meltygui/editor/source_ui.py +70 -0
- meltygui/editor/spell_check.py +77 -0
- meltygui/editor/text_editor.py +9076 -0
- meltygui/editor/usage_picker.py +538 -0
- meltygui/events/__init__.py +0 -0
- meltygui/events/example.py +118 -0
- meltygui/examples/__init__.py +0 -0
- meltygui/examples/columns_demo.py +33 -0
- meltygui/examples/columns_window_demo.py +60 -0
- meltygui/examples/context_menu_demo.py +24 -0
- meltygui/examples/context_menu_window_demo.py +49 -0
- meltygui/examples/gui_playground.py +127 -0
- meltygui/examples/live_view_playground.py +256 -0
- meltygui/examples/lora.py +43 -0
- meltygui/examples/lora_data.py +59 -0
- meltygui/examples/lora_policies.py +67 -0
- meltygui/examples/mode_demo.py +49 -0
- meltygui/examples/modifies_demo.py +119 -0
- meltygui/examples/scalar_policies.py +60 -0
- meltygui/examples/style_layouts.py +149 -0
- meltygui/examples/tile_manager_demo.py +74 -0
- meltygui/examples/tint_demo.py +160 -0
- meltygui/examples/tint_functions.py +56 -0
- meltygui/examples/trace_demo.py +88 -0
- meltygui/examples/two_windows.py +36 -0
- meltygui/files/__init__.py +0 -0
- meltygui/files/fast_file_explorer.py +439 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/extension.js +237 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/lsd-window-geometry@latent-descent.iml +9 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/metadata.json +7 -0
- meltygui/graphics/__init__.py +6 -0
- meltygui/graphics/base.py +85 -0
- meltygui/graphics/examples.py +507 -0
- meltygui/graphics/executor.py +520 -0
- meltygui/graphics/filter.py +667 -0
- meltygui/graphics/filter.pyi +856 -0
- meltygui/graphics/generate_stubs.py +22 -0
- meltygui/graphics/registry.py +203 -0
- meltygui/graphics/shader_compiler.py +155 -0
- meltygui/graphics/shaders.py +1302 -0
- meltygui/graphics/stub_generator.py +250 -0
- meltygui/graphics/texture_manager.py +170 -0
- meltygui/graphics/texture_min_max.py +295 -0
- meltygui/hdr_color.py +757 -0
- meltygui/image_load.py +308 -0
- meltygui/model/__init__.py +1 -0
- meltygui/model/account_model.py +142 -0
- meltygui/model/camera_model.py +159 -0
- meltygui/model/chat_model.py +86 -0
- meltygui/model/code_model.py +62 -0
- meltygui/model/code_proxy_model.py +876 -0
- meltygui/model/collection_model.py +41 -0
- meltygui/model/color_model.py +127 -0
- meltygui/model/cuda_tensor_model.py +36 -0
- meltygui/model/cuda_texture_model.py +149 -0
- meltygui/model/dropdown_model.py +137 -0
- meltygui/model/file_metadata_model.py +73 -0
- meltygui/model/file_model.py +289 -0
- meltygui/model/format_model.py +390 -0
- meltygui/model/graph_model.py +98 -0
- meltygui/model/icon_model.py +1024 -0
- meltygui/model/import_graph_model.py +468 -0
- meltygui/model/layout_model.py +15 -0
- meltygui/model/lut_model.py +266 -0
- meltygui/model/search_model.py +173 -0
- meltygui/model/tensor_model.py +402 -0
- meltygui/model/terminal_model.py +329 -0
- meltygui/model/texture_model.py +146 -0
- meltygui/model/tile_model.py +24 -0
- meltygui/model/trace_model.py +198 -0
- meltygui/model/trace_report_model.py +146 -0
- meltygui/models/__init__.py +0 -0
- meltygui/models/file_meta.py +613 -0
- meltygui/models/function_console.py +184 -0
- meltygui/models/orchestration.py +48 -0
- meltygui/pbr.py +1576 -0
- meltygui/png_unfilter.c +49 -0
- meltygui/resources/JetBrainsMono-Regular.ttf +0 -0
- meltygui/resources/THIRD_PARTY_NOTICES.md +21 -0
- meltygui/resources/dejavu/DejaVuSans-Bold.ttf +0 -0
- meltygui/resources/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
- meltygui/resources/dejavu/DejaVuSans.ttf +0 -0
- meltygui/resources/dejavu/LICENSE.txt +78 -0
- meltygui/resources/fontawesome-LICENSE.txt +121 -0
- meltygui/resources/fontawesome-webfont.ttf +0 -0
- meltygui/resources/hdri/studio_small_09_1k.hdr +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Bold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraBold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraLight.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Light.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Medium.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-SemiBold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Thin.ttf +0 -0
- meltygui/resources/jetbrains-weights/OFL.txt +93 -0
- meltygui/resources/jetbrains-weights/README.md +2 -0
- meltygui/state/__init__.py +0 -0
- meltygui/state/account_state.py +22 -0
- meltygui/state/animation_state.py +97 -0
- meltygui/state/annotation_state.py +22 -0
- meltygui/state/chat_state.py +36 -0
- meltygui/state/code_state.py +10 -0
- meltygui/state/core_enums.py +59 -0
- meltygui/state/core_markers.py +115 -0
- meltygui/state/core_undo.py +1075 -0
- meltygui/state/file_state.py +75 -0
- meltygui/state/graph_state.py +26 -0
- meltygui/state/inspection_state.py +47 -0
- meltygui/state/menu_state.py +10 -0
- meltygui/state/model_enums.py +11 -0
- meltygui/state/new_core_model.py +2394 -0
- meltygui/state/orchestration_state.py +14 -0
- meltygui/state/query_state.py +24 -0
- meltygui/state/tensor_state.py +10 -0
- meltygui/state/terminal_state.py +23 -0
- meltygui/state/trace_state.py +32 -0
- meltygui/state/voxel_state.py +12 -0
- meltygui/text_index.py +816 -0
- meltygui/utils/__init__.py +0 -0
- meltygui/utils/jump_to_code.py +344 -0
- meltygui/utils/pkl_inspect.py +90 -0
- meltygui/utils/render_utils.py +1419 -0
- meltygui/view/__init__.py +1 -0
- meltygui/view/account_view.py +524 -0
- meltygui/view/action_view.py +73 -0
- meltygui/view/chat_decoration_view.py +112 -0
- meltygui/view/chat_view.py +1703 -0
- meltygui/view/code_view.py +2677 -0
- meltygui/view/collection_view.py +1185 -0
- meltygui/view/color_view.py +824 -0
- meltygui/view/control_view.py +559 -0
- meltygui/view/decoration_view.py +439 -0
- meltygui/view/diagnostic_view.py +177 -0
- meltygui/view/dropdown_view.py +1082 -0
- meltygui/view/file_view.py +1599 -0
- meltygui/view/graph_cuda_view.py +105 -0
- meltygui/view/graph_view.py +882 -0
- meltygui/view/header_view.py +848 -0
- meltygui/view/input_view.py +238 -0
- meltygui/view/inspection_view.py +1783 -0
- meltygui/view/layout_view.py +596 -0
- meltygui/view/lut_view.py +45 -0
- meltygui/view/menu_view.py +212 -0
- meltygui/view/orchestration_view.py +658 -0
- meltygui/view/query_view.py +118 -0
- meltygui/view/search_view.py +342 -0
- meltygui/view/tab_view.py +258 -0
- meltygui/view/tensor_view.py +430 -0
- meltygui/view/terminal_view.py +355 -0
- meltygui/view/text_view.py +8031 -0
- meltygui/view/texture_view.py +480 -0
- meltygui/view/tile_view.py +38 -0
- meltygui/view/trace_view.py +923 -0
- meltygui/view/voxel_cuda_view.py +824 -0
- meltygui/view/voxel_view.py +1860 -0
- meltygui/view/window_view.py +111 -0
- meltygui-0.1.0.dist-info/METADATA +143 -0
- meltygui-0.1.0.dist-info/RECORD +372 -0
- meltygui-0.1.0.dist-info/WHEEL +4 -0
- meltygui-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,657 @@
|
|
|
1
|
+
"""In-process MCP server for the launcher (model_server).
|
|
2
|
+
|
|
3
|
+
The launcher process is the long-lived supervisor: it stays up across studio
|
|
4
|
+
sessions (and is itself kept alive by the run loop). Hosting the MCP server here
|
|
5
|
+
— rather than in the short-lived Melty studio session — means it's reachable
|
|
6
|
+
whenever the launcher is, with no process killing and no race with the run loop.
|
|
7
|
+
|
|
8
|
+
Two entry points, both called from ``model_server.py``'s ``__main__``:
|
|
9
|
+
|
|
10
|
+
* ``install_log_tee()`` — tee stdout/stderr to a logfile so ``get_logs`` can
|
|
11
|
+
read the console across studio open/close.
|
|
12
|
+
* ``start_launcher_mcp(model_server)`` — run a FastMCP streamable-http server in
|
|
13
|
+
a daemon thread, exposing tools that drive the launcher in-place:
|
|
14
|
+
``get_logs``, ``status``, ``launch``, ``restart``.
|
|
15
|
+
|
|
16
|
+
Tools call thread-safe methods on the ModelServer (``mcp_launch`` etc.), which
|
|
17
|
+
queue work on the existing task queue — the same path as the Ctrl+Enter re-run.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import functools
|
|
21
|
+
import logging
|
|
22
|
+
import re
|
|
23
|
+
import sys
|
|
24
|
+
import threading
|
|
25
|
+
import time
|
|
26
|
+
import traceback
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
# Project root: src/lsd/gl_gui/mcp_server.py -> parents[3] == latent-descent/
|
|
30
|
+
from meltygui.core.runtime.paths import cache_root
|
|
31
|
+
_ROOT = cache_root()
|
|
32
|
+
STATE_DIR = _ROOT
|
|
33
|
+
LOG_PATH = STATE_DIR / "console.log"
|
|
34
|
+
|
|
35
|
+
import os
|
|
36
|
+
|
|
37
|
+
from meltygui.core.rendering.window_decoration import window
|
|
38
|
+
|
|
39
|
+
PORT = int(os.environ.get("MELTY_MCP_PORT", "8787"))
|
|
40
|
+
HOST = "127.0.0.1"
|
|
41
|
+
|
|
42
|
+
_tee_installed = False
|
|
43
|
+
_mcp_started = False
|
|
44
|
+
|
|
45
|
+
# The uvicorn server + its event loop, captured in _run so the Melty lifecycle
|
|
46
|
+
# handlers can reach in and drop connections without stopping the listener (the
|
|
47
|
+
# launcher can stay reachable across studio sessions, e.g. for `launch`/`status`
|
|
48
|
+
# while idle).
|
|
49
|
+
_uvicorn_server = None
|
|
50
|
+
_uvicorn_loop = None
|
|
51
|
+
# Set while Melty is tearing down: the gate closes during the drop so a fresh
|
|
52
|
+
# request can't re-hang the launcher. Auto-clears shortly after the drop (see
|
|
53
|
+
# notify_melty_shutdown), so the launcher is reachable again while idle.
|
|
54
|
+
_draining = threading.Event()
|
|
55
|
+
# Drop requests until Melty has painted this many frames - see _serving_ready.
|
|
56
|
+
WARMUP_FRAMES = 3
|
|
57
|
+
|
|
58
|
+
# Cap stored result/error text so a chatty tool (get_logs returning 200 lines)
|
|
59
|
+
# can't balloon the in-memory log.
|
|
60
|
+
_LOG_RESULT_CAP = 2000
|
|
61
|
+
|
|
62
|
+
_ANSI = re.compile(r"\x1b\[[0-9;]*m")
|
|
63
|
+
# Markers that begin an error region, in this codebase's three formats:
|
|
64
|
+
# - "Exception Trace": print_stack_trace(exception=...) - a ─-bar-delimited
|
|
65
|
+
# block with frames, variable-watch tables, and a final "ExcType: msg".
|
|
66
|
+
# - "Traceback (most recent call last):": standard (thread handlers, log_exc).
|
|
67
|
+
# - "Error Caught": the older print_colored_traceback banner.
|
|
68
|
+
_PST_TITLE = "Exception Trace"
|
|
69
|
+
_FALLBACK_MARKERS = ("Traceback (most recent call last):", "Error Caught")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _is_bar(line):
|
|
73
|
+
s = line.strip()
|
|
74
|
+
return len(s) >= 20 and set(s) == {"─"}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _extract_last_error(text, cap=200):
|
|
78
|
+
"""Return the most recent error block from `text` (ANSI stripped), or None.
|
|
79
|
+
|
|
80
|
+
Picks whichever error format appears latest in the log.
|
|
81
|
+
"""
|
|
82
|
+
lines = [_ANSI.sub("", l) for l in text.splitlines()]
|
|
83
|
+
|
|
84
|
+
# Latest marker of any format.
|
|
85
|
+
pst = std = None
|
|
86
|
+
for i in range(len(lines) - 1, -1, -1):
|
|
87
|
+
if pst is None and _PST_TITLE in lines[i]:
|
|
88
|
+
pst = i
|
|
89
|
+
if std is None and any(mk in lines[i] for mk in _FALLBACK_MARKERS):
|
|
90
|
+
std = i
|
|
91
|
+
if pst is not None and std is not None:
|
|
92
|
+
break
|
|
93
|
+
if pst is None and std is None:
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
# print_stack_trace block: from the ─-bar above the title to the closing bar.
|
|
97
|
+
if pst is not None and (std is None or pst > std):
|
|
98
|
+
bars = [i for i, l in enumerate(lines) if _is_bar(l)]
|
|
99
|
+
before = [b for b in bars if b < pst]
|
|
100
|
+
after = [b for b in bars if b > pst]
|
|
101
|
+
b0 = before[-1] if before else pst
|
|
102
|
+
# after[0] is the bar right after the title; after[1] is the closing bar.
|
|
103
|
+
b2 = after[1] if len(after) >= 2 else (after[0] if after else len(lines) - 1)
|
|
104
|
+
block = lines[b0:b2 + 1]
|
|
105
|
+
return "\n".join(block[:cap])
|
|
106
|
+
|
|
107
|
+
# Fallback: cut from the marker to its exception message line.
|
|
108
|
+
block, seen_frame, j = [], False, std
|
|
109
|
+
while j < len(lines) and len(block) < cap:
|
|
110
|
+
l = lines[j]
|
|
111
|
+
s = l.strip()
|
|
112
|
+
is_marker = any(mk in l for mk in _FALLBACK_MARKERS)
|
|
113
|
+
is_file = s.startswith('File "')
|
|
114
|
+
is_indented = l[:1] in (" ", "\t")
|
|
115
|
+
if is_marker or is_file or is_indented or s == "":
|
|
116
|
+
block.append(l)
|
|
117
|
+
if is_file or is_indented:
|
|
118
|
+
seen_frame = True
|
|
119
|
+
j += 1
|
|
120
|
+
continue
|
|
121
|
+
block.append(l) # the message line
|
|
122
|
+
if seen_frame:
|
|
123
|
+
break
|
|
124
|
+
j += 1
|
|
125
|
+
while block and not block[-1].strip():
|
|
126
|
+
block.pop()
|
|
127
|
+
return "\n".join(block) if block else None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class _Tee:
|
|
131
|
+
"""Write to the real stream and the logfile at once. Thread-safe.
|
|
132
|
+
|
|
133
|
+
Falls back gracefully if either sink raises so logging can never take the
|
|
134
|
+
process down.
|
|
135
|
+
"""
|
|
136
|
+
|
|
137
|
+
def __init__(self, stream, fh, lock):
|
|
138
|
+
self._stream = stream
|
|
139
|
+
self._fh = fh
|
|
140
|
+
self._lock = lock
|
|
141
|
+
|
|
142
|
+
def write(self, data):
|
|
143
|
+
with self._lock:
|
|
144
|
+
try:
|
|
145
|
+
self._stream.write(data)
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
try:
|
|
149
|
+
self._fh.write(data)
|
|
150
|
+
self._fh.flush()
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
def flush(self):
|
|
155
|
+
try:
|
|
156
|
+
self._stream.flush()
|
|
157
|
+
except Exception:
|
|
158
|
+
pass
|
|
159
|
+
try:
|
|
160
|
+
self._fh.flush()
|
|
161
|
+
except Exception:
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
def __getattr__(self, name):
|
|
165
|
+
return getattr(self._stream, name)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class _HttpRequestLine(logging.Handler):
|
|
169
|
+
"""Prints httpx's per-response INFO record as ONE short console line:
|
|
170
|
+
|
|
171
|
+
[http] 20:26:36 GET localhost:11434/api/tags 200
|
|
172
|
+
|
|
173
|
+
httpx logs `'HTTP Request: %s %s "%s %d %s"'` with args (method, url,
|
|
174
|
+
http_version, status, reason); the scheme is dropped and the reason kept
|
|
175
|
+
only for non-2xx/3xx answers (`… 429 Too Many Requests`). Any other record
|
|
176
|
+
on the logger falls back to its plain message. Goes through print() so
|
|
177
|
+
the log tee mirrors it into console.log like every other line.
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
def emit(self, record):
|
|
181
|
+
try:
|
|
182
|
+
args = record.args if isinstance(record.args, tuple) else ()
|
|
183
|
+
if len(args) == 5 and str(record.msg).startswith("HTTP Request:"):
|
|
184
|
+
method, url, _version, status, reason = args
|
|
185
|
+
url = re.sub(r"^https?://", "", str(url))
|
|
186
|
+
status = int(status)
|
|
187
|
+
tail = f"{status}" if status < 400 else f"{status} {reason}"
|
|
188
|
+
text = f"{method} {url} {tail}"
|
|
189
|
+
else:
|
|
190
|
+
text = record.getMessage()
|
|
191
|
+
print(f"[http] {time.strftime('%H:%M:%S')} {text}")
|
|
192
|
+
except Exception:
|
|
193
|
+
pass
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
_http_logging_installed = False
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def install_concise_http_logging():
|
|
200
|
+
"""Route the `httpx` logger to _HttpRequestLine and stop it propagating,
|
|
201
|
+
so the request lines never reach whatever handler sits on the root.
|
|
202
|
+
Idempotent."""
|
|
203
|
+
global _http_logging_installed
|
|
204
|
+
if _http_logging_installed:
|
|
205
|
+
return
|
|
206
|
+
_http_logging_installed = True
|
|
207
|
+
http_logger = logging.getLogger("httpx")
|
|
208
|
+
http_logger.setLevel(logging.INFO)
|
|
209
|
+
http_logger.propagate = False
|
|
210
|
+
http_logger.addHandler(_HttpRequestLine())
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def install_log_tee():
|
|
214
|
+
"""Mirror stdout/stderr into LOG_PATH (fresh per process). Idempotent."""
|
|
215
|
+
global _tee_installed
|
|
216
|
+
if _tee_installed:
|
|
217
|
+
return
|
|
218
|
+
_tee_installed = True
|
|
219
|
+
try:
|
|
220
|
+
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
|
221
|
+
fh = open(LOG_PATH, "w", buffering=1)
|
|
222
|
+
lock = threading.Lock()
|
|
223
|
+
sys.stdout = _Tee(sys.__stdout__, fh, lock)
|
|
224
|
+
sys.stderr = _Tee(sys.__stderr__, fh, lock)
|
|
225
|
+
except Exception as e:
|
|
226
|
+
print(f"[mcp] could not install log tee: {e}")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
# --- MCP activity toasts ----------------------------------------------------
|
|
230
|
+
# Every tool call pops a toast in the studio (tag "MCP") so Claude and
|
|
231
|
+
# the live process is visible. Color-coded per tool, red on error. notify() fires
|
|
232
|
+
# UNCONDITIONALLY on every call: it's thread-safe, no-op in its render wake when
|
|
233
|
+
# no window exists, and an idle-time call just appends to the bounded deque
|
|
234
|
+
# (maxlen 20) and shows on the next session - no studio gate.
|
|
235
|
+
MCP_TOOL_TINTS = {
|
|
236
|
+
"get_logs": (0.55, 0.70, 0.95, 1.0),
|
|
237
|
+
"status": (0.45, 0.80, 1.00, 1.0),
|
|
238
|
+
"last_error": (1.00, 0.65, 0.30, 1.0),
|
|
239
|
+
"launch": (0.40, 1.00, 0.55, 1.0),
|
|
240
|
+
"restart": (1.00, 0.80, 0.35, 1.0),
|
|
241
|
+
"restart_launcher": (1.00, 0.55, 0.40, 1.0),
|
|
242
|
+
"hotswap": (0.55, 0.90, 1.00, 1.0),
|
|
243
|
+
"recompile_external_changes": (0.55, 1.00, 0.75, 1.0),
|
|
244
|
+
"screenshot": (0.80, 0.60, 1.00, 1.0),
|
|
245
|
+
"list_windows": (0.70, 0.75, 0.85, 1.0),
|
|
246
|
+
"eval_python": (0.45, 0.95, 0.80, 1.0),
|
|
247
|
+
"find_views": (0.60, 0.85, 0.95, 1.0),
|
|
248
|
+
"describe_view": (0.60, 0.85, 0.95, 1.0),
|
|
249
|
+
"hit_test": (0.95, 0.75, 0.55, 1.0),
|
|
250
|
+
"param_sources": (0.85, 0.70, 0.95, 1.0),
|
|
251
|
+
"tile_cache": (0.70, 0.95, 0.60, 1.0),
|
|
252
|
+
}
|
|
253
|
+
MCP_DEFAULT_TINT = (0.70, 0.72, 0.82, 1.0)
|
|
254
|
+
MCP_ERROR_TINT = (1.00, 0.35, 0.35, 1.0)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _mcp_toast_text(name, call_args):
|
|
258
|
+
"""Compact one-line summary of a tool call for the notification toast:
|
|
259
|
+
the tool name plus its args, each value collapsed to a single line and
|
|
260
|
+
truncated so a big `source`/`code` payload can't blow up the toast."""
|
|
261
|
+
def short(v):
|
|
262
|
+
s = " ".join(str(v).split())
|
|
263
|
+
return s if len(s) <= 40 else s[:40] + "..."
|
|
264
|
+
detail = ", ".join(f"{k}={short(v)}" for k, v in call_args.items())
|
|
265
|
+
return f"{name}({detail})" if detail else f"{name}()"
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def start_launcher_mcp(model_server, host=HOST, port=PORT):
|
|
269
|
+
"""Start the launcher MCP server in a daemon thread. Idempotent."""
|
|
270
|
+
global _mcp_started
|
|
271
|
+
if _mcp_started:
|
|
272
|
+
return
|
|
273
|
+
_mcp_started = True
|
|
274
|
+
|
|
275
|
+
install_log_tee()
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
import asyncio
|
|
279
|
+
import uvicorn
|
|
280
|
+
from mcp.server.fastmcp import FastMCP, Image
|
|
281
|
+
except Exception as e:
|
|
282
|
+
print(f"[mcp] not starting — dependencies unavailable: {e}")
|
|
283
|
+
return
|
|
284
|
+
|
|
285
|
+
# Keep MCP/uvicorn/etc logging out of the launcher console (and the
|
|
286
|
+
# tee'd logfile that get_logs reads).
|
|
287
|
+
for _name in ("uvicorn", "uvicorn.error", "uvicorn.access",
|
|
288
|
+
"mcp", "mcp.server", "sse_starlette"):
|
|
289
|
+
logging.getLogger(_name).setLevel(logging.WARNING)
|
|
290
|
+
|
|
291
|
+
# FastMCP's constructor calls logging.basicConfig(level=INFO) with a rich
|
|
292
|
+
# handler on the ROOT logger, which then rendered every library INFO
|
|
293
|
+
# record - httpx's `HTTP Request: GET http://... "HTTP/1.1 200 OK"` for each
|
|
294
|
+
# Ollama probe / Anthropic call - as a wide rich line with file-link
|
|
295
|
+
# escapes, or one character per line when it misjudged the tee's width.
|
|
296
|
+
# Snapshot the root logger before the constructor and set it back, then
|
|
297
|
+
# give httpx its own one-line handler (install_concise_http_logging).
|
|
298
|
+
root_logger = logging.getLogger()
|
|
299
|
+
root_handlers, root_level = list(root_logger.handlers), root_logger.level
|
|
300
|
+
mcp = FastMCP("meltygui", host=host, port=port)
|
|
301
|
+
for handler in list(root_logger.handlers):
|
|
302
|
+
if handler not in root_handlers:
|
|
303
|
+
root_logger.removeHandler(handler)
|
|
304
|
+
root_logger.setLevel(root_level)
|
|
305
|
+
install_concise_http_logging()
|
|
306
|
+
|
|
307
|
+
def logged_tool():
|
|
308
|
+
"""Like ``mcp.tool()`` but records each call to ``MCPServerLog.logs``.
|
|
309
|
+
|
|
310
|
+
``functools.wraps`` copies ``__wrapped__`` so FastMCP's
|
|
311
|
+
``inspect.signature`` still resolves the original signature, name, and
|
|
312
|
+
docstring — the tool schema is unchanged.
|
|
313
|
+
"""
|
|
314
|
+
|
|
315
|
+
def deco(fn):
|
|
316
|
+
@functools.wraps(fn)
|
|
317
|
+
def wrapper(*args, **kwargs):
|
|
318
|
+
# Resolve notify/MCPServerLog PER CALL, not at registration. The
|
|
319
|
+
# tool wrappers are registered once at launcher boot; a later
|
|
320
|
+
# hotswap of notifications.py / monitor.py re-imports the module and
|
|
321
|
+
# makes a NEW NotificationCenter class (with fresh deques) that the
|
|
322
|
+
# render loop reads. A registered-level `import` would keep
|
|
323
|
+
# appending to the OLD, pre-hotswap class - toasts land in an
|
|
324
|
+
# orphaned deque nothing draws (the post-hotswap silent-toast bug).
|
|
325
|
+
# Re-importing here always hits the live sys.modules entry.
|
|
326
|
+
from meltygui.core.diagnostics.monitor_core import MCPServerLog
|
|
327
|
+
from meltygui.core.diagnostics.notifications import notify
|
|
328
|
+
# Bind positional args to names so the log is self-describing.
|
|
329
|
+
call_args = dict(kwargs)
|
|
330
|
+
names = list(fn.__code__.co_varnames[:fn.__code__.co_argcount])
|
|
331
|
+
for name, val in zip(names, args):
|
|
332
|
+
call_args[name] = val
|
|
333
|
+
try:
|
|
334
|
+
result = fn(*args, **kwargs)
|
|
335
|
+
except Exception:
|
|
336
|
+
MCPServerLog.record(fn.__name__, call_args,
|
|
337
|
+
error=traceback.format_exc())
|
|
338
|
+
notify(_mcp_toast_text(fn.__name__, call_args) + " (error)",
|
|
339
|
+
tint=MCP_ERROR_TINT, tag="MCP")
|
|
340
|
+
raise
|
|
341
|
+
MCPServerLog.record(fn.__name__, call_args, result=result)
|
|
342
|
+
notify(_mcp_toast_text(fn.__name__, call_args),
|
|
343
|
+
tint=MCP_TOOL_TINTS.get(fn.__name__, MCP_DEFAULT_TINT),
|
|
344
|
+
tag="MCP")
|
|
345
|
+
return result
|
|
346
|
+
|
|
347
|
+
return mcp.tool()(wrapper)
|
|
348
|
+
|
|
349
|
+
return deco
|
|
350
|
+
|
|
351
|
+
@logged_tool()
|
|
352
|
+
def get_logs(lines: int = 200) -> str:
|
|
353
|
+
"""Return the last `lines` lines of the launcher's console output.
|
|
354
|
+
|
|
355
|
+
Captures stdout + stderr for the launcher and any studio session it
|
|
356
|
+
runs. Use this to read tracebacks, training progress, or warnings.
|
|
357
|
+
"""
|
|
358
|
+
if not LOG_PATH.exists():
|
|
359
|
+
return "(no log file yet)"
|
|
360
|
+
rows = LOG_PATH.read_text(errors="replace").splitlines()
|
|
361
|
+
if not rows:
|
|
362
|
+
return "(log is empty)"
|
|
363
|
+
return "\n".join(rows[-max(1, lines):])
|
|
364
|
+
|
|
365
|
+
@logged_tool()
|
|
366
|
+
def status() -> str:
|
|
367
|
+
"""Report the launcher PID, whether a studio session is running, and —
|
|
368
|
+
if it isn't — why the last session stopped (user_quit / restart / crash).
|
|
369
|
+
|
|
370
|
+
Use the reason to tell an expected exit (the user closed or restarted the
|
|
371
|
+
window) from a crash worth investigating, instead of treating every idle
|
|
372
|
+
studio as a bug.
|
|
373
|
+
"""
|
|
374
|
+
import meltygui.core.diagnostics.session_status as session_status
|
|
375
|
+
return f"{model_server.mcp_status()}; {session_status.summary()}"
|
|
376
|
+
|
|
377
|
+
@logged_tool()
|
|
378
|
+
def last_error() -> str:
|
|
379
|
+
"""Return the most recent traceback from the console log, or a note that
|
|
380
|
+
the run looks clean. Faster than scanning get_logs after a crash."""
|
|
381
|
+
if not LOG_PATH.exists():
|
|
382
|
+
return "(no log file yet)"
|
|
383
|
+
block = _extract_last_error(LOG_PATH.read_text(errors="replace"))
|
|
384
|
+
if block is None:
|
|
385
|
+
return "no traceback found in the current run's log (looks clean)"
|
|
386
|
+
return block
|
|
387
|
+
|
|
388
|
+
@logged_tool()
|
|
389
|
+
def launch() -> str:
|
|
390
|
+
"""Open the latent-descent studio by replaying the last run.
|
|
391
|
+
|
|
392
|
+
No-op (with a message) if a studio session is already running.
|
|
393
|
+
"""
|
|
394
|
+
return model_server.mcp_launch()
|
|
395
|
+
|
|
396
|
+
@logged_tool()
|
|
397
|
+
def restart() -> str:
|
|
398
|
+
"""Restart the studio session in place: interrupt the running session
|
|
399
|
+
(if any) and replay the last run. The launcher process stays up, so the
|
|
400
|
+
MCP connection survives — just call get_logs afterward.
|
|
401
|
+
"""
|
|
402
|
+
return model_server.mcp_restart()
|
|
403
|
+
|
|
404
|
+
@logged_tool()
|
|
405
|
+
def hotswap(path: str, source: str = "") -> str:
|
|
406
|
+
"""Recompile a project source file and hotswap it into the running studio
|
|
407
|
+
process — apply code changes live, no full restart.
|
|
408
|
+
|
|
409
|
+
path: absolute or project-relative path to a .py file whose module is
|
|
410
|
+
already imported in the running process.
|
|
411
|
+
source: optional full new file contents. Omit it to reload the file's
|
|
412
|
+
current on-disk contents (e.g. after editing it on disk). If
|
|
413
|
+
given, it is hotswapped first and written to disk only on a clean
|
|
414
|
+
compile, so a syntax error never leaves broken code on disk.
|
|
415
|
+
|
|
416
|
+
The whole module is reloaded: every function/class in the file is patched
|
|
417
|
+
in place, so imported names and live instances keep working. A swap that
|
|
418
|
+
compiles but throws at runtime is auto-reverted by the editor's hotswap
|
|
419
|
+
guard. Library/stdlib paths are refused. Returns a status line.
|
|
420
|
+
"""
|
|
421
|
+
from meltygui.core.diagnostics.notifications import notify
|
|
422
|
+
notify(f"hotswap requested: {path}", tint=MCP_TOOL_TINTS.get("hotswap", MCP_DEFAULT_TINT), tag="MCP")
|
|
423
|
+
import meltygui.core.automation.mcp_hotswap as mcp_hotswap
|
|
424
|
+
return mcp_hotswap.hotswap_file(path, source or None)
|
|
425
|
+
|
|
426
|
+
@logged_tool()
|
|
427
|
+
def recompile_external_changes() -> str:
|
|
428
|
+
"""Recompile everything the studio has queued — pending edits AND
|
|
429
|
+
tracked external changes — the SAME code path as clicking the Pending
|
|
430
|
+
Saves window's recompile button (PendingSave.recompile_all_ui drives
|
|
431
|
+
the button's own runner draw_state: busy spinner while running, then
|
|
432
|
+
the fading check mark + summary), so the button and this tool always
|
|
433
|
+
behave identically. The window is revealed so the result is visible.
|
|
434
|
+
|
|
435
|
+
External changes are first decomposed into per-span pending entries
|
|
436
|
+
and merged with any overlapping pending edits — rebase / per-span
|
|
437
|
+
3-way merge / adopt (MERGED / ADOPTED / CONFLICT lines land in the
|
|
438
|
+
window's persistent merge display); the external window keeps showing
|
|
439
|
+
absorbed drift until the user dismisses it. Each entry then hotswaps
|
|
440
|
+
in place with hotswap-guard rollback. Returns the same summary string
|
|
441
|
+
the button shows.
|
|
442
|
+
"""
|
|
443
|
+
from meltygui.core.diagnostics.notifications import notify
|
|
444
|
+
notify("recompile requested",
|
|
445
|
+
tint=MCP_TOOL_TINTS.get("recompile_external_changes", MCP_DEFAULT_TINT), tag="MCP")
|
|
446
|
+
from meltygui.editor.pending_save import PendingSave
|
|
447
|
+
return PendingSave.recompile_all_ui()
|
|
448
|
+
|
|
449
|
+
@logged_tool()
|
|
450
|
+
def screenshot(window: str):
|
|
451
|
+
"""Capture one Melty studio window by name and return it as a PNG image.
|
|
452
|
+
|
|
453
|
+
Pass the window's title (exact, else case-insensitive substring). Use
|
|
454
|
+
list_windows to see what's open. Captures a single window rather than
|
|
455
|
+
the whole display, which may span an ultra-wide monitor.
|
|
456
|
+
"""
|
|
457
|
+
from meltygui.core.diagnostics.notifications import notify
|
|
458
|
+
notify(f"screenshot requested: {window}", tint=MCP_TOOL_TINTS.get("screenshot", MCP_DEFAULT_TINT), tag="MCP")
|
|
459
|
+
if not model_server._studio_running():
|
|
460
|
+
return "no studio session running — call launch first"
|
|
461
|
+
from meltygui.core.graphics.screenshot import request_capture
|
|
462
|
+
path, error = request_capture(window)
|
|
463
|
+
if error:
|
|
464
|
+
return f"screenshot failed: {error}"
|
|
465
|
+
return Image(path=path)
|
|
466
|
+
|
|
467
|
+
@logged_tool()
|
|
468
|
+
def list_windows() -> str:
|
|
469
|
+
"""List the names of currently-open Melty studio windows."""
|
|
470
|
+
if not model_server._studio_running():
|
|
471
|
+
return "no studio session running — call launch first"
|
|
472
|
+
from meltygui.core.graphics.screenshot import list_window_names
|
|
473
|
+
names = list_window_names()
|
|
474
|
+
return "\n".join(sorted(set(names))) if names else "(no named windows open)"
|
|
475
|
+
|
|
476
|
+
@logged_tool()
|
|
477
|
+
def eval_python(code: str) -> str:
|
|
478
|
+
"""Execute Python in the live launcher process; returns stdout + result.
|
|
479
|
+
|
|
480
|
+
Runs on the studio render thread when a session is running (safe to
|
|
481
|
+
read/poke Melty + imgui state), else inline on the launcher thread.
|
|
482
|
+
In scope: `Melty`, `server`/`model_server`, and (when a studio is up)
|
|
483
|
+
`vis` (the studio) and `app`/`root` (the root AppModel). Import anything
|
|
484
|
+
else. A trailing expression's repr is returned. Arbitrary in-process
|
|
485
|
+
code — for inspecting/poking live state while iterating.
|
|
486
|
+
"""
|
|
487
|
+
from meltygui.core.automation.mcp_eval import request_eval
|
|
488
|
+
return request_eval(code, model_server)
|
|
489
|
+
|
|
490
|
+
# --- Typed state queries (mcp_query.py): JSON read on the render thread ---
|
|
491
|
+
|
|
492
|
+
def _query(collect):
|
|
493
|
+
if not model_server._studio_running():
|
|
494
|
+
return "no studio session running — call launch first"
|
|
495
|
+
from meltygui.core.automation.mcp_query import run_query
|
|
496
|
+
return run_query(collect, model_server)
|
|
497
|
+
|
|
498
|
+
@logged_tool()
|
|
499
|
+
def find_views(func: str = "", name: str = "", window: str = "",
|
|
500
|
+
include_closed: bool = False, limit: int = 50) -> str:
|
|
501
|
+
"""Find live Melty views (draw_states) by case-insensitive substring:
|
|
502
|
+
`func` on the render function's qualname, `name` on the view name /
|
|
503
|
+
tile_id, `window` on the ROOT window title (see list_windows). Rows
|
|
504
|
+
are front-most first with rect, clip, closed / hidden / hovered flags,
|
|
505
|
+
layer, z_pos, parent window and the input value's type. Use the
|
|
506
|
+
returned `tile_id` with describe_view / param_sources / tile_cache."""
|
|
507
|
+
from meltygui.core.automation.mcp_query import collect_find_views
|
|
508
|
+
return _query(lambda: collect_find_views(func, name, window, include_closed, limit))
|
|
509
|
+
|
|
510
|
+
@logged_tool()
|
|
511
|
+
def describe_view(view: str, children_depth: int = 1) -> str:
|
|
512
|
+
"""One view in full: summary, resolved kwargs, diverged auto_params,
|
|
513
|
+
event_rect scopes, this frame's event subscriptions and cursor
|
|
514
|
+
registration, its tile-cache entry (dirty, last clean / invalidated
|
|
515
|
+
frame, last bump reason), window_pos / content size / scroll, the
|
|
516
|
+
render-tree ancestors and window chain, and children to
|
|
517
|
+
`children_depth`. `view` = a tile_id (exact or unique substring) or
|
|
518
|
+
a draw_state id prefix."""
|
|
519
|
+
from meltygui.core.automation.mcp_query import collect_describe_view
|
|
520
|
+
return _query(lambda: collect_describe_view(view, children_depth))
|
|
521
|
+
|
|
522
|
+
@logged_tool()
|
|
523
|
+
def hit_test(x: float, y: float) -> str:
|
|
524
|
+
"""The BVH stack at screen point (x, y), front to back, each view
|
|
525
|
+
with its z_pos / priority and the event subscriptions + cursor shape
|
|
526
|
+
registered for it. Subscriptions exist only for views under the REAL
|
|
527
|
+
pointer (`pointer`, `pointer_matches_point`); elsewhere the stack is
|
|
528
|
+
exact but subscriptions are empty. Also: Melty.hovered_ds, the
|
|
529
|
+
resolved cursor shape, drag capture and blocker views."""
|
|
530
|
+
from meltygui.core.automation.mcp_query import collect_hit_test
|
|
531
|
+
return _query(lambda: collect_hit_test(x, y))
|
|
532
|
+
|
|
533
|
+
@logged_tool()
|
|
534
|
+
def param_sources(view: str, param: str = "") -> str:
|
|
535
|
+
"""The context menu's inputs tab as data: for each parameter of the
|
|
536
|
+
view (or just `param`) the value it reads, the DRIVING source (the
|
|
537
|
+
SourcePriority pick) and every source that sets it in priority order
|
|
538
|
+
(kind, writable, value). `sources` lists the sources with file:line."""
|
|
539
|
+
from meltygui.core.automation.mcp_query import collect_param_sources
|
|
540
|
+
return _query(lambda: collect_param_sources(view, param))
|
|
541
|
+
|
|
542
|
+
@logged_tool()
|
|
543
|
+
def tile_cache(view: str = "", history_frames: int = 0, limit: int = 100) -> str:
|
|
544
|
+
"""Blit tile-cache state. With `view`: that tile (dirty, clean /
|
|
545
|
+
invalidated cache frames, last bump, blit_served_frame, tracker note)
|
|
546
|
+
and its invalidations over the last `history_frames` frames. Without:
|
|
547
|
+
tile totals, per-frame body_runs / cache_hits / captures (last 10
|
|
548
|
+
frames, or `history_frames`), the latest `limit` invalidations and the
|
|
549
|
+
top invalidators over the window — a per-frame invalidator shows up
|
|
550
|
+
here with a count near the frame count."""
|
|
551
|
+
from meltygui.core.automation.mcp_query import collect_tile_cache
|
|
552
|
+
return _query(lambda: collect_tile_cache(view, history_frames, limit))
|
|
553
|
+
|
|
554
|
+
@logged_tool()
|
|
555
|
+
def restart_launcher() -> str:
|
|
556
|
+
"""Fully restart the launcher process (not just the studio session).
|
|
557
|
+
|
|
558
|
+
Use this to pick up newly-added MCP tools or changes to launcher/MCP
|
|
559
|
+
startup code — an in-place `restart` only replays the studio and can't
|
|
560
|
+
register new tools. The run loop relaunches the process; reconnect after
|
|
561
|
+
~15-20s. For ordinary rendering/converter code edits, prefer `restart`.
|
|
562
|
+
"""
|
|
563
|
+
return model_server.mcp_restart_launcher()
|
|
564
|
+
|
|
565
|
+
def _run():
|
|
566
|
+
global _uvicorn_server, _uvicorn_loop
|
|
567
|
+
try:
|
|
568
|
+
loop = asyncio.new_event_loop()
|
|
569
|
+
asyncio.set_event_loop(loop)
|
|
570
|
+
app = _make_gate(mcp.streamable_http_app())
|
|
571
|
+
# timeout_graceful_shutdown=0: if the server ever does stop, never
|
|
572
|
+
# wait on connections (we drop them explicitly via notify_melty_shutdown).
|
|
573
|
+
config = uvicorn.Config(app, host=host, port=port, log_level="warning",
|
|
574
|
+
timeout_graceful_shutdown=0)
|
|
575
|
+
server = uvicorn.Server(config)
|
|
576
|
+
server.install_signal_handlers = lambda: None # off the main thread
|
|
577
|
+
_uvicorn_server = server
|
|
578
|
+
_uvicorn_loop = loop
|
|
579
|
+
loop.run_until_complete(server.serve())
|
|
580
|
+
except Exception as e:
|
|
581
|
+
print(f"[mcp] server thread crashed: {e}")
|
|
582
|
+
|
|
583
|
+
threading.Thread(target=_run, daemon=True, name="launcher-mcp").start()
|
|
584
|
+
print(f"[mcp] launcher MCP listening on http://{host}:{port}/mcp")
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def _serving_ready():
|
|
588
|
+
"""True when the server should accept requests.
|
|
589
|
+
|
|
590
|
+
Rejects during a Melty teardown drain, and until Melty has painted
|
|
591
|
+
WARMUP_FRAMES frames (Melty.init_complete() == frame_count > 2) so clients
|
|
592
|
+
can't poke a process whose GUI hasn't initialized. Fails OPEN if Melty isn't
|
|
593
|
+
importable yet — the launcher tools (status/launch) must stay reachable to
|
|
594
|
+
bring the studio up.
|
|
595
|
+
"""
|
|
596
|
+
if _draining.is_set():
|
|
597
|
+
return False
|
|
598
|
+
try:
|
|
599
|
+
from meltygui.core.melty import Melty
|
|
600
|
+
return Melty.init_complete()
|
|
601
|
+
except Exception:
|
|
602
|
+
return True
|
|
603
|
+
|
|
604
|
+
|
|
605
|
+
def _make_gate(app):
|
|
606
|
+
"""ASGI wrapper rejecting HTTP requests with 503 until _serving_ready().
|
|
607
|
+
|
|
608
|
+
Non-HTTP scopes (lifespan) pass through untouched so uvicorn startup/shutdown
|
|
609
|
+
events still fire.
|
|
610
|
+
"""
|
|
611
|
+
async def gate(scope, receive, send):
|
|
612
|
+
if scope.get("type") == "http" and not _serving_ready():
|
|
613
|
+
await send({
|
|
614
|
+
"type": "http.response.start",
|
|
615
|
+
"status": 503,
|
|
616
|
+
"headers": [(b"content-type", b"text/plain; charset=utf-8"),
|
|
617
|
+
(b"connection", b"close")],
|
|
618
|
+
})
|
|
619
|
+
await send({"type": "http.response.body", "body": b"meltygui not ready"})
|
|
620
|
+
return
|
|
621
|
+
await app(scope, receive, send)
|
|
622
|
+
|
|
623
|
+
return gate
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def notify_melty_shutdown():
|
|
627
|
+
"""Drop all active MCP connections immediately, so a hanging client can't
|
|
628
|
+
|
|
629
|
+
block Melty's teardown. Keeps the listener bound (the launcher stays
|
|
630
|
+
reachable for the next session / idle `launch`), and clears the drain shortly
|
|
631
|
+
after so new requests are served again. Safe to call if the server never
|
|
632
|
+
started.
|
|
633
|
+
"""
|
|
634
|
+
server, loop = _uvicorn_server, _uvicorn_loop
|
|
635
|
+
if server is None or loop is None or loop.is_closed():
|
|
636
|
+
return
|
|
637
|
+
_draining.set()
|
|
638
|
+
|
|
639
|
+
def _drop():
|
|
640
|
+
try:
|
|
641
|
+
# Same primitives uvicorn's own Server.shutdown uses: ask every live
|
|
642
|
+
# connection to close, and cancel any in-flight request task that
|
|
643
|
+
# would otherwise keep the teardown waiting.
|
|
644
|
+
for conn in list(getattr(server.server_state, "connections", ())):
|
|
645
|
+
conn.shutdown()
|
|
646
|
+
for task in list(getattr(server.server_state, "tasks", ())):
|
|
647
|
+
task.cancel()
|
|
648
|
+
except Exception as e:
|
|
649
|
+
print(f"[mcp] error dropping connections on meltygui shutdown: {e}")
|
|
650
|
+
finally:
|
|
651
|
+
loop.call_later(1.0, _draining.clear)
|
|
652
|
+
|
|
653
|
+
try:
|
|
654
|
+
loop.call_soon_threadsafe(_drop)
|
|
655
|
+
except RuntimeError:
|
|
656
|
+
# Loop already gone - nothing to drop.
|
|
657
|
+
_draining.clear()
|