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,220 @@
|
|
|
1
|
+
"""Placed sign-in popups — Wayland refuses window positioning, so auth pages
|
|
2
|
+
open as a chromeless Chromium `--app` window forced onto XWAYLAND
|
|
3
|
+
(`--ozone-platform=x11`, where placement IS allowed) and a placement thread
|
|
4
|
+
parks it beside the pointer — i.e. over the button that opened it — with
|
|
5
|
+
xdotool, sized for a login form.
|
|
6
|
+
|
|
7
|
+
A dedicated profile (`~/.lsd/oauth-browser`) does two jobs: it guarantees a
|
|
8
|
+
FRESH browser instance (flags would be swallowed by an already-running
|
|
9
|
+
Wayland Chrome otherwise) and it keeps its own Google session between
|
|
10
|
+
sign-ins — the first one asks for credentials, later ones are one click.
|
|
11
|
+
|
|
12
|
+
`shim_env` covers the flow the studio doesn't open itself: `claude auth
|
|
13
|
+
login` calls xdg-open on its own, so its PATH gets a shim dir whose
|
|
14
|
+
xdg-open launches the same placed popup (`place_async` watches for it).
|
|
15
|
+
|
|
16
|
+
Fallback at every step — popups disabled (Toggles.InternetAccounts.
|
|
17
|
+
use_oauth_popup), no Chromium-family browser, no DISPLAY, the browser
|
|
18
|
+
dying before a window appears — is plain copilot.open_url. Every spawn is
|
|
19
|
+
posix_spawn-style (absolute paths, close_fds=False — never fork the
|
|
20
|
+
studio's CUDA/GL address space).
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import os
|
|
25
|
+
import shutil
|
|
26
|
+
import stat
|
|
27
|
+
import subprocess
|
|
28
|
+
import threading
|
|
29
|
+
import time
|
|
30
|
+
from pathlib import Path
|
|
31
|
+
|
|
32
|
+
# Protocol constants (not knobs): the WM_CLASS the popup is found/closed by,
|
|
33
|
+
# and where the shim + browser profile go.
|
|
34
|
+
WINDOW_CLASS = "lsd-oauth"
|
|
35
|
+
PROFILE_DIR = Path.home() / ".lsd" / "oauth-browser"
|
|
36
|
+
SHIM_DIR = Path.home() / ".lsd" / "oauth-shim"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def find_browser(explicit=""):
|
|
40
|
+
"""A Chromium-family browser (only they have --app / --ozone-platform)."""
|
|
41
|
+
if explicit:
|
|
42
|
+
found = shutil.which(explicit) or (explicit if Path(explicit).is_file() else None)
|
|
43
|
+
return found
|
|
44
|
+
for name in ("google-chrome", "chromium", "chromium-browser"):
|
|
45
|
+
found = shutil.which(name)
|
|
46
|
+
if found:
|
|
47
|
+
return found
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def popup_available() -> bool:
|
|
52
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
53
|
+
return (bool(Toggles.InternetAccounts.use_oauth_popup)
|
|
54
|
+
and bool(os.environ.get("DISPLAY"))
|
|
55
|
+
and find_browser(Toggles.InternetAccounts.oauth_popup_browser) is not None
|
|
56
|
+
and shutil.which("xdotool") is not None)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class PopupHandle:
|
|
60
|
+
def __init__(self, process):
|
|
61
|
+
self.process = process
|
|
62
|
+
self.placed = False
|
|
63
|
+
|
|
64
|
+
def close(self):
|
|
65
|
+
"""End of the flow: the popup's job is done — close it."""
|
|
66
|
+
if self.process is not None and self.process.poll() is None:
|
|
67
|
+
try:
|
|
68
|
+
self.process.terminate()
|
|
69
|
+
except OSError:
|
|
70
|
+
pass
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def open_auth_popup(url, browser=None, xdotool=None, size=None, place_timeout_s=15.0):
|
|
74
|
+
"""Open `url` as a placed popup; falls back to copilot.open_url and
|
|
75
|
+
returns None when it can't. Returns a PopupHandle (close() on flow end)
|
|
76
|
+
when the popup browser was launched."""
|
|
77
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
78
|
+
browser = browser or (find_browser(Toggles.InternetAccounts.oauth_popup_browser)
|
|
79
|
+
if Toggles.InternetAccounts.use_oauth_popup else None)
|
|
80
|
+
xdotool = xdotool or shutil.which("xdotool")
|
|
81
|
+
if not browser or not xdotool or not os.environ.get("DISPLAY"):
|
|
82
|
+
_fallback(url)
|
|
83
|
+
return None
|
|
84
|
+
width, height = size or Toggles.InternetAccounts.oauth_popup_size
|
|
85
|
+
PROFILE_DIR.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
try:
|
|
87
|
+
process = subprocess.Popen(
|
|
88
|
+
[browser, f"--app={url}", "--ozone-platform=x11", f"--class={WINDOW_CLASS}",
|
|
89
|
+
f"--window-size={width},{height}", f"--user-data-dir={PROFILE_DIR}",
|
|
90
|
+
"--no-first-run", "--no-default-browser-check"],
|
|
91
|
+
close_fds=False, stdin=subprocess.DEVNULL,
|
|
92
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
93
|
+
except OSError:
|
|
94
|
+
_fallback(url)
|
|
95
|
+
return None
|
|
96
|
+
handle = PopupHandle(process)
|
|
97
|
+
threading.Thread(target=_place, args=(handle, url, xdotool, (width, height), place_timeout_s),
|
|
98
|
+
daemon=True, name="oauth-popup-place").start()
|
|
99
|
+
return handle
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def place_async(xdotool=None, size=None, place_timeout_s=20.0):
|
|
103
|
+
"""Watch for a popup some OTHER process launches (the xdg-open shim under
|
|
104
|
+
`claude auth login`) and park it like open_auth_popup does."""
|
|
105
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
106
|
+
xdotool = xdotool or shutil.which("xdotool")
|
|
107
|
+
if xdotool is None:
|
|
108
|
+
return
|
|
109
|
+
handle = PopupHandle(None)
|
|
110
|
+
threading.Thread(target=_place,
|
|
111
|
+
args=(handle, None, xdotool, size or Toggles.InternetAccounts.oauth_popup_size,
|
|
112
|
+
place_timeout_s),
|
|
113
|
+
daemon=True, name="oauth-popup-place").start()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def close_popups(xdotool=None):
|
|
117
|
+
"""Close every lsd-oauth window (flows we don't hold a Popen for)."""
|
|
118
|
+
xdotool = xdotool or shutil.which("xdotool")
|
|
119
|
+
if xdotool is None:
|
|
120
|
+
return
|
|
121
|
+
try:
|
|
122
|
+
found = subprocess.run([xdotool, "search", "--class", WINDOW_CLASS],
|
|
123
|
+
capture_output=True, text=True, timeout=5, close_fds=False)
|
|
124
|
+
for window_id in found.stdout.split():
|
|
125
|
+
subprocess.run([xdotool, "windowclose", window_id],
|
|
126
|
+
timeout=5, close_fds=False,
|
|
127
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
|
|
128
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
129
|
+
pass
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
# ── the xdg-open shim for `claude auth login` ─────────────────────────────
|
|
133
|
+
|
|
134
|
+
def write_shim(browser=None):
|
|
135
|
+
"""`~/.lsd/oauth-shim/xdg-open`: launches the placed popup for whatever
|
|
136
|
+
URL Claude Code opens. Regenerated per use so browser/size changes land."""
|
|
137
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
138
|
+
browser = browser or find_browser(Toggles.InternetAccounts.oauth_popup_browser)
|
|
139
|
+
if browser is None:
|
|
140
|
+
return None
|
|
141
|
+
width, height = Toggles.InternetAccounts.oauth_popup_size
|
|
142
|
+
PROFILE_DIR.mkdir(parents=True, exist_ok=True)
|
|
143
|
+
SHIM_DIR.mkdir(parents=True, exist_ok=True)
|
|
144
|
+
shim = SHIM_DIR / "xdg-open"
|
|
145
|
+
shim.write_text(
|
|
146
|
+
"#!/bin/sh\n"
|
|
147
|
+
"# latent-descent oauth shim: Claude Code's browser-open, as a placed popup\n"
|
|
148
|
+
f'exec "{browser}" "--app=$1" --ozone-platform=x11 --class={WINDOW_CLASS} '
|
|
149
|
+
f"--window-size={width},{height} \"--user-data-dir={PROFILE_DIR}\" "
|
|
150
|
+
"--no-first-run --no-default-browser-check "
|
|
151
|
+
">/dev/null 2>&1 &\n")
|
|
152
|
+
shim.chmod(shim.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
153
|
+
return shim
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def shim_env(base_env):
|
|
157
|
+
"""`base_env` with the shim first on PATH (and as $BROWSER) — pass to the
|
|
158
|
+
`claude auth login` subprocess; unchanged when popups can't happen."""
|
|
159
|
+
if not popup_available() or write_shim() is None:
|
|
160
|
+
return dict(base_env)
|
|
161
|
+
env = dict(base_env)
|
|
162
|
+
env["PATH"] = f"{SHIM_DIR}:{env.get('PATH', '')}"
|
|
163
|
+
env["BROWSER"] = str(SHIM_DIR / "xdg-open")
|
|
164
|
+
return env
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ── placement ─────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
def _place(handle, url, xdotool, size, timeout_s):
|
|
170
|
+
"""Wait for the popup's X window, then park it beside the pointer
|
|
171
|
+
(clamped to the display span) and raise it. If the browser died before
|
|
172
|
+
a window appeared (bad flags, broken profile) fall back to xdg-open."""
|
|
173
|
+
width, height = size
|
|
174
|
+
# Within this margin of any display edge the popup is pushed inward.
|
|
175
|
+
margin = 16
|
|
176
|
+
# The popup is this far above the pointer so the title area isn't under it.
|
|
177
|
+
pointer_lift = 60
|
|
178
|
+
deadline = time.monotonic() + timeout_s
|
|
179
|
+
|
|
180
|
+
def run(*args):
|
|
181
|
+
return subprocess.run([xdotool, *args], capture_output=True, text=True,
|
|
182
|
+
timeout=10, close_fds=False)
|
|
183
|
+
|
|
184
|
+
window_id = None
|
|
185
|
+
while time.monotonic() < deadline and window_id is None:
|
|
186
|
+
try:
|
|
187
|
+
found = run("search", "--onlyvisible", "--class", WINDOW_CLASS)
|
|
188
|
+
ids = found.stdout.split()
|
|
189
|
+
if ids:
|
|
190
|
+
window_id = ids[-1]
|
|
191
|
+
break
|
|
192
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
193
|
+
return
|
|
194
|
+
if (handle.process is not None and handle.process.poll() not in (None, 0)
|
|
195
|
+
and url is not None):
|
|
196
|
+
_fallback(url) # the popup was but not showing anything
|
|
197
|
+
return
|
|
198
|
+
time.sleep(0.25)
|
|
199
|
+
if window_id is None:
|
|
200
|
+
return
|
|
201
|
+
try:
|
|
202
|
+
mouse = run("getmouselocation", "--shell").stdout
|
|
203
|
+
position = dict(line.split("=", 1) for line in mouse.split() if "=" in line)
|
|
204
|
+
screen = run("getdisplaygeometry").stdout.split()
|
|
205
|
+
screen_width, screen_height = int(screen[0]), int(screen[1])
|
|
206
|
+
x = max(margin, min(int(position.get("X", 0)) - width // 2, screen_width - width - margin))
|
|
207
|
+
y = max(margin, min(int(position.get("Y", 0)) - pointer_lift, screen_height - height - margin))
|
|
208
|
+
run("windowmove", window_id, str(x), str(y))
|
|
209
|
+
run("windowactivate", window_id)
|
|
210
|
+
handle.placed = True
|
|
211
|
+
except (OSError, subprocess.TimeoutExpired, ValueError, IndexError):
|
|
212
|
+
pass
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _fallback(url):
|
|
216
|
+
try:
|
|
217
|
+
from meltygui.completion.providers.copilot import open_url
|
|
218
|
+
open_url(url)
|
|
219
|
+
except Exception:
|
|
220
|
+
pass
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
"""Ollama FIM provider — native fill-in-the-middle through a local Ollama
|
|
2
|
+
server (`/api/generate` with `suffix`, streamed). Any FIM-trained model
|
|
3
|
+
works (`qwen2.5-coder:*`, `codellama:*-code`, `deepseek-coder*`,
|
|
4
|
+
`starcoder2`); a chat-only model ignores `suffix` and completes the prefix.
|
|
5
|
+
|
|
6
|
+
Session: one keep-alive HTTP client per host (an Internet Accounts entry of
|
|
7
|
+
kind "ollama"). The account also carries the DEVICE the model should live
|
|
8
|
+
on (`device`: "auto" | "cpu" | "gpu:N" in Ollama's own GPU numbering) —
|
|
9
|
+
every request passes it as llama.cpp options (`main_gpu` / `num_gpu: 0`),
|
|
10
|
+
which is how Ollama decides placement (verified 2026-08-22: main_gpu=2 put
|
|
11
|
+
the model on the H100). Ollama numbers GPUs in CUDA-runtime order —
|
|
12
|
+
`gpu_inventory()` reads that order (names + live memory) from `torch.cuda`,
|
|
13
|
+
which shares the process's already-initialized CUDA runtime and is thread-
|
|
14
|
+
safe, so the probe never spins up a second CUDA context (a bare
|
|
15
|
+
`pycuda.driver.init()` on a probe thread could race the render thread's GL
|
|
16
|
+
context at boot and hang the load) and never shells out to nvidia-smi.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import json
|
|
21
|
+
import threading
|
|
22
|
+
import time
|
|
23
|
+
|
|
24
|
+
from meltygui.completion.fim import FimRequest
|
|
25
|
+
from meltygui.completion.fim import FimResult
|
|
26
|
+
from meltygui.completion.fim import FimSession
|
|
27
|
+
from meltygui.completion.fim import fim_provider
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class OllamaSession(FimSession):
|
|
31
|
+
"""One keep-alive client for the Ollama host of Internet Accounts entry
|
|
32
|
+
`account` (kind "ollama": `host`); an explicit `host` overrides it."""
|
|
33
|
+
KIND = "ollama"
|
|
34
|
+
|
|
35
|
+
def __init__(self, account="default", host=None, timeout_s=30.0):
|
|
36
|
+
import httpx
|
|
37
|
+
from meltygui.accounts.internet_accounts import account_field
|
|
38
|
+
self.account = account
|
|
39
|
+
host = host or account_field("ollama", account, "host") or "http://localhost:11434"
|
|
40
|
+
self.host = host.rstrip("/")
|
|
41
|
+
# Short CONNECT timeout so a down/unreachable server fails fast instead
|
|
42
|
+
# of blocking a gui thread for the long read timeout; generation
|
|
43
|
+
# itself keeps the long read timeout.
|
|
44
|
+
self.client = httpx.Client(base_url=self.host,
|
|
45
|
+
timeout=httpx.Timeout(timeout_s, connect=2.0))
|
|
46
|
+
self._status = ("ready",)
|
|
47
|
+
|
|
48
|
+
def status(self):
|
|
49
|
+
return self._status
|
|
50
|
+
|
|
51
|
+
def close(self):
|
|
52
|
+
try:
|
|
53
|
+
self.client.close()
|
|
54
|
+
except Exception:
|
|
55
|
+
pass
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
59
|
+
# Helpers
|
|
60
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
def device_options(device) -> dict:
|
|
63
|
+
"""llama.cpp options for an account's `device` setting."""
|
|
64
|
+
if not device or device == "auto":
|
|
65
|
+
return {}
|
|
66
|
+
if device == "cpu":
|
|
67
|
+
return {"num_gpu": 0}
|
|
68
|
+
if device.startswith("gpu:"):
|
|
69
|
+
try:
|
|
70
|
+
return {"main_gpu": int(device[4:])}
|
|
71
|
+
except ValueError:
|
|
72
|
+
return {}
|
|
73
|
+
return {}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
_inventory_cache = [0.0, None]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def gpu_inventory(max_age=3.0) -> list:
|
|
80
|
+
"""[{ollama_index, name, short, used_mib, total_mib}] in OLLAMA (CUDA
|
|
81
|
+
runtime) order — read from torch.cuda, which shares the process's
|
|
82
|
+
already-initialized CUDA runtime (same device order Ollama's llama.cpp
|
|
83
|
+
sees) and is thread-safe. Deliberately NOT pycuda: a bare
|
|
84
|
+
`pycuda.driver.init()` on a probe thread can race the render thread's
|
|
85
|
+
CUDA/GL context creation at boot and hang the load. No subprocess
|
|
86
|
+
either — no nvidia-smi. Best-effort; cached briefly."""
|
|
87
|
+
now = time.monotonic()
|
|
88
|
+
if _inventory_cache[1] is not None and now - _inventory_cache[0] < max_age:
|
|
89
|
+
return _inventory_cache[1]
|
|
90
|
+
gpus = []
|
|
91
|
+
try:
|
|
92
|
+
import torch
|
|
93
|
+
if torch.cuda.is_available():
|
|
94
|
+
for i in range(torch.cuda.device_count()):
|
|
95
|
+
p = torch.cuda.get_device_properties(i)
|
|
96
|
+
try:
|
|
97
|
+
free, total = torch.cuda.mem_get_info(i)
|
|
98
|
+
except Exception:
|
|
99
|
+
free, total = 0, int(getattr(p, "total_memory", 0))
|
|
100
|
+
name = p.name.replace("NVIDIA ", "").replace("GeForce ", "")
|
|
101
|
+
gpus.append({"ollama_index": i, "name": name,
|
|
102
|
+
"short": name.replace(" NVL", "").replace("RTX ", ""),
|
|
103
|
+
"used_mib": int((total - free) / 1048576),
|
|
104
|
+
"total_mib": int(total / 1048576), "order": "cuda"})
|
|
105
|
+
except Exception:
|
|
106
|
+
gpus = []
|
|
107
|
+
_inventory_cache[0] = now
|
|
108
|
+
_inventory_cache[1] = gpus
|
|
109
|
+
return gpus
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _gpu_for_vram(gpus, size_vram) -> dict | None:
|
|
113
|
+
"""The GPU a model of `size_vram` bytes most likely sits on — the one
|
|
114
|
+
whose used memory best matches (torch mem_get_info, no per-process
|
|
115
|
+
attribution needed). Good enough for one resident model."""
|
|
116
|
+
want = size_vram / 1048576
|
|
117
|
+
best, best_d = None, None
|
|
118
|
+
for g in gpus:
|
|
119
|
+
if g["used_mib"] >= 0.4 * want:
|
|
120
|
+
d = abs(g["used_mib"] - want)
|
|
121
|
+
if best_d is None or d < best_d:
|
|
122
|
+
best, best_d = g, d
|
|
123
|
+
return best
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def device_label(device, gpus=None) -> str:
|
|
127
|
+
"""Label for a device setting. `gpus` is the CACHED inventory (or None) —
|
|
128
|
+
this NEVER queries hardware, so it is safe on the render thread. Without
|
|
129
|
+
an inventory a GPU shows as a bare "GPUn" until a probe fills the names."""
|
|
130
|
+
if not device or device == "auto":
|
|
131
|
+
return "auto"
|
|
132
|
+
if device == "cpu":
|
|
133
|
+
return "CPU"
|
|
134
|
+
if device.startswith("gpu:"):
|
|
135
|
+
try:
|
|
136
|
+
i = int(device[4:])
|
|
137
|
+
except ValueError:
|
|
138
|
+
return device
|
|
139
|
+
for g in (gpus or ()):
|
|
140
|
+
if g["ollama_index"] == i:
|
|
141
|
+
return f"GPU{i} {g['short']}"
|
|
142
|
+
return f"GPU{i}"
|
|
143
|
+
return device
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def device_choices(gpus=None) -> list:
|
|
147
|
+
"""Device options from the CACHED inventory (or None) — no hardware
|
|
148
|
+
query. Falls back to auto/cpu only until a probe supplies the GPUs."""
|
|
149
|
+
return ["auto"] + [f"gpu:{g['ollama_index']}" for g in (gpus or ())] + ["cpu"]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
153
|
+
# Model management (used by the Internet Accounts window)
|
|
154
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
155
|
+
|
|
156
|
+
def list_models(client) -> list:
|
|
157
|
+
"""[{name, size, loaded, size_vram, expires_at, where}] — /api/tags
|
|
158
|
+
joined with /api/ps and the runners' GPU placement."""
|
|
159
|
+
tags = client.get("/api/tags", timeout=5.0).json().get("models") or []
|
|
160
|
+
try:
|
|
161
|
+
running = {m["name"]: m for m in (client.get("/api/ps", timeout=5.0).json().get("models") or [])}
|
|
162
|
+
except Exception:
|
|
163
|
+
running = {}
|
|
164
|
+
gpus = gpu_inventory() if running else []
|
|
165
|
+
out = []
|
|
166
|
+
for t in tags:
|
|
167
|
+
name = t.get("name", "?")
|
|
168
|
+
r = running.get(name)
|
|
169
|
+
loaded = r is not None
|
|
170
|
+
vram = int(r.get("size_vram") or 0) if r else 0
|
|
171
|
+
w = None
|
|
172
|
+
if loaded:
|
|
173
|
+
if vram == 0:
|
|
174
|
+
w = "CPU"
|
|
175
|
+
else:
|
|
176
|
+
g = _gpu_for_vram(gpus, vram)
|
|
177
|
+
w = f"GPU{g['ollama_index']} {g['short']}" if g else "GPU"
|
|
178
|
+
out.append({"name": name, "size": int(t.get("size") or 0), "loaded": loaded,
|
|
179
|
+
"size_vram": vram, "expires_at": (r or {}).get("expires_at"), "where": w,
|
|
180
|
+
"family": ((t.get("details") or {}).get("family") or "")})
|
|
181
|
+
out.sort(key=lambda m: (not m["loaded"], m["name"]))
|
|
182
|
+
return out
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def load_model(client, model, device="auto", keep_alive="30m"):
|
|
186
|
+
"""Load `model` onto `device` (an empty generate with keep_alive) — also
|
|
187
|
+
how a loaded model is MOVED: Ollama reloads when the options change."""
|
|
188
|
+
payload = {"model": model, "keep_alive": keep_alive, "options": device_options(device)}
|
|
189
|
+
r = client.post("/api/generate", json=payload, timeout=600.0)
|
|
190
|
+
if r.status_code != 200:
|
|
191
|
+
raise RuntimeError(f"ollama {r.status_code}: {r.text[:200]}")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def unload_model(client, model):
|
|
195
|
+
r = client.post("/api/generate", json={"model": model, "keep_alive": 0}, timeout=60.0)
|
|
196
|
+
if r.status_code != 200:
|
|
197
|
+
raise RuntimeError(f"ollama {r.status_code}: {r.text[:200]}")
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
201
|
+
# Provider
|
|
202
|
+
# ──────────────────────────────────────────────────────────────────────────
|
|
203
|
+
|
|
204
|
+
def _window(req: FimRequest, prefix_chars: int, suffix_chars: int):
|
|
205
|
+
prefix = req.annotated_prefix()
|
|
206
|
+
if len(prefix) > prefix_chars:
|
|
207
|
+
cut = prefix.rfind("\n", 0, len(prefix) - prefix_chars)
|
|
208
|
+
prefix = prefix[cut + 1:] if cut >= 0 else prefix[-prefix_chars:]
|
|
209
|
+
suffix = req.suffix
|
|
210
|
+
if len(suffix) > suffix_chars:
|
|
211
|
+
cut = suffix.find("\n", suffix_chars)
|
|
212
|
+
suffix = suffix[:cut] if cut >= 0 else suffix[:suffix_chars]
|
|
213
|
+
return prefix, suffix
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
@fim_provider(name="ollama", session=OllamaSession)
|
|
217
|
+
def ollama_fim(req: FimRequest, session: OllamaSession, model="qwen2.5-coder:7b",
|
|
218
|
+
prefix_chars=6000, suffix_chars=1500, context_chars=3000,
|
|
219
|
+
temperature=0.2, device=None, keep_alive=None) -> FimResult:
|
|
220
|
+
"""Local FIM. Stable context rides ahead of the prefix as commented
|
|
221
|
+
blocks (FIM models have no side channel for it); the run block is
|
|
222
|
+
inlined through `annotated_prefix`. `device` / `keep_alive` default to
|
|
223
|
+
the account's settings (Internet Accounts → Ollama)."""
|
|
224
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
225
|
+
from meltygui.accounts.internet_accounts import account_field
|
|
226
|
+
if device is None:
|
|
227
|
+
device = account_field("ollama", session.account, "device", "auto")
|
|
228
|
+
if keep_alive is None:
|
|
229
|
+
keep_alive = Toggles.Fim.ollama_keep_alive
|
|
230
|
+
prefix, suffix = _window(req, prefix_chars, suffix_chars)
|
|
231
|
+
ctx_text = req.context.render(("stable",)) if req.context is not None else ""
|
|
232
|
+
if ctx_text:
|
|
233
|
+
ctx_text = ctx_text[-context_chars:]
|
|
234
|
+
commented = "\n".join("# " + ln if ln.strip() else "#" for ln in ctx_text.split("\n"))
|
|
235
|
+
prefix = "# --- context ---\n" + commented + "\n# --- end context ---\n\n" + prefix
|
|
236
|
+
options = {"num_predict": max(16, req.max_tokens), "temperature": temperature}
|
|
237
|
+
options.update(device_options(device))
|
|
238
|
+
payload = {"model": model, "prompt": prefix, "suffix": suffix, "stream": True,
|
|
239
|
+
"keep_alive": keep_alive, "options": options}
|
|
240
|
+
acc = []
|
|
241
|
+
try:
|
|
242
|
+
try:
|
|
243
|
+
reason = _generate(session, payload, req, acc)
|
|
244
|
+
except _Unsupported as e:
|
|
245
|
+
# Not a FIM model - retry prefix-only (and with thinking off for a
|
|
246
|
+
# reasoning model, whose output would otherwise all be thinking).
|
|
247
|
+
# Quality is lower - the model can't see the suffix - but the
|
|
248
|
+
# provider still works; the status says so.
|
|
249
|
+
if "insert" in e.what:
|
|
250
|
+
payload.pop("suffix", None)
|
|
251
|
+
reason = None
|
|
252
|
+
if "insert" in e.what or "think" in e.what:
|
|
253
|
+
payload["think"] = False
|
|
254
|
+
try:
|
|
255
|
+
reason = _generate(session, payload, req, acc)
|
|
256
|
+
except _Unsupported as e2:
|
|
257
|
+
if "think" not in e2.what:
|
|
258
|
+
raise
|
|
259
|
+
payload.pop("think", None)
|
|
260
|
+
reason = _generate(session, payload, req, acc)
|
|
261
|
+
session._status = ("ready", f"{model}: no FIM support, prefix-only")
|
|
262
|
+
return FimResult("".join(acc), provider="ollama", truncated=(reason == "length"))
|
|
263
|
+
except Exception as e:
|
|
264
|
+
session._status = ("error", str(e))
|
|
265
|
+
raise
|
|
266
|
+
session._status = ("ready",)
|
|
267
|
+
# done_reason "length" = hit num_predict (more to come → continue on Tab);
|
|
268
|
+
# "stop" = the model emitted an end token (done, don't auto-continue).
|
|
269
|
+
return FimResult("".join(acc), provider="ollama", truncated=(reason == "length"))
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
class _Unsupported(RuntimeError):
|
|
273
|
+
def __init__(self, what):
|
|
274
|
+
super().__init__(f"ollama: {what}")
|
|
275
|
+
self.what = what
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _generate(session, payload, req, acc):
|
|
279
|
+
"""Stream one /api/generate call into `acc` (list of pieces), emitting
|
|
280
|
+
the running text. Returns the final `done_reason` ("stop" | "length" |
|
|
281
|
+
None). Raises _Unsupported for the model-capability errors ("does not
|
|
282
|
+
support insert/thinking") so the caller can adapt."""
|
|
283
|
+
acc.clear()
|
|
284
|
+
saw_thinking = False
|
|
285
|
+
done_reason = None
|
|
286
|
+
with session.client.stream("POST", "/api/generate", json=payload) as resp:
|
|
287
|
+
if resp.status_code != 200:
|
|
288
|
+
body = resp.read().decode("utf-8", "replace")[:300]
|
|
289
|
+
if "does not support" in body:
|
|
290
|
+
raise _Unsupported(body)
|
|
291
|
+
raise RuntimeError(f"ollama {resp.status_code}: {body}")
|
|
292
|
+
for line in resp.iter_lines():
|
|
293
|
+
if req.cancelled.is_set():
|
|
294
|
+
break
|
|
295
|
+
if not line:
|
|
296
|
+
continue
|
|
297
|
+
try:
|
|
298
|
+
msg = json.loads(line)
|
|
299
|
+
except ValueError:
|
|
300
|
+
continue
|
|
301
|
+
if msg.get("error"):
|
|
302
|
+
err = str(msg["error"])
|
|
303
|
+
if "does not support" in err:
|
|
304
|
+
raise _Unsupported(err)
|
|
305
|
+
raise RuntimeError(f"ollama: {err}")
|
|
306
|
+
if msg.get("thinking"):
|
|
307
|
+
saw_thinking = True
|
|
308
|
+
piece = msg.get("response", "")
|
|
309
|
+
if piece:
|
|
310
|
+
acc.append(piece)
|
|
311
|
+
req.emit("".join(acc))
|
|
312
|
+
if msg.get("done"):
|
|
313
|
+
done_reason = msg.get("done_reason")
|
|
314
|
+
break
|
|
315
|
+
if saw_thinking and not acc and "think" not in payload and not req.cancelled.is_set():
|
|
316
|
+
# A reasoning model spent the entire budget thinking (happens when the
|
|
317
|
+
# suffix is empty, so Ollama didn't reject the insert) - retry with
|
|
318
|
+
# thinking off so the tokens go to the completion.
|
|
319
|
+
raise _Unsupported("thinking consumed the budget")
|
|
320
|
+
return done_reason
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Named FIM profiles: a provider plus configuration. Every provider already
|
|
2
|
+
registers a default profile under its own name ("claude", "ollama"); add
|
|
3
|
+
variants here. Kwargs that name a parameter of the provider's session
|
|
4
|
+
class select/construct the SESSION (so two profiles with different session
|
|
5
|
+
kwargs get two live sessions — e.g. two accounts); the rest override the
|
|
6
|
+
provider function's per-request params.
|
|
7
|
+
|
|
8
|
+
fim_profile("claude-fast", claude_fim, model="claude-haiku-4-5")
|
|
9
|
+
fim_profile("copilot-work", copilot_fim, config_dir="~/.config/github-copilot-work")
|
|
10
|
+
|
|
11
|
+
Pick a profile per editor with `draw_text(..., fim="claude-fast")` or
|
|
12
|
+
globally with `Toggles.Fim.profile`.
|
|
13
|
+
"""
|
|
14
|
+
from meltygui.completion.fim import fim_profile
|
|
15
|
+
from meltygui.completion.providers.claude import claude_fim
|
|
16
|
+
from meltygui.completion.providers.copilot import copilot_fim
|
|
17
|
+
from meltygui.completion.providers.ollama import ollama_fim
|
|
18
|
+
|
|
19
|
+
fim_profile("claude-fast", claude_fim, model="claude-haiku-4-5", effort=None)
|
|
20
|
+
fim_profile("ollama-qwen", ollama_fim, model="qwen2.5-coder:7b")
|
|
21
|
+
# A second github login: add a "copilot" account in Internet Accounts with
|
|
22
|
+
# its own config dir, then point a profile at it by account id.
|
|
23
|
+
fim_profile("copilot-2", copilot_fim, account="copilot-2")
|
meltygui/core/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Core: rendering plumbing and shared runtime
|
|
2
|
+
|
|
3
|
+
Core makes reusable render functions work: it supplies their inputs, tracks state,
|
|
4
|
+
runs converters, dispatches events, caches drawing and connects windows to the OS.
|
|
5
|
+
`Melty` owns the shared runtime. Feature rendering belongs in `view/<feature>_view.py`;
|
|
6
|
+
feature adapters belong in `model/<feature>_model.py`; injected feature state belongs
|
|
7
|
+
in `state/<feature>_state.py`.
|
|
8
|
+
|
|
9
|
+
## Where to start
|
|
10
|
+
|
|
11
|
+
The root keeps five Python modules: `core_render.py`, `melty.py`,
|
|
12
|
+
`definition_hotswap.py`, `module_names.py`, and the package initializer.
|
|
13
|
+
`module_map.json` translates identifiers in older saved sessions.
|
|
14
|
+
Everything else is grouped by the runtime responsibility it serves:
|
|
15
|
+
|
|
16
|
+
| Folder | Responsibility and main entry points |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `input/` | Event delivery, devices, hit testing, drag/drop and selection: `input_handler.py`, `collision.py`, `drag_drop_core.py` |
|
|
19
|
+
| `rendering/` | Render dispatch, registration, parameter injection support, modes and decorators: `render_dispatch.py`, `parameter_core.py`, `mode.py` |
|
|
20
|
+
| `conversion/` | Dict-like objects, conversion graphs, hosting and persistence: `dict_conversion.py`, `render_host.py`, `load_save_v2.py` |
|
|
21
|
+
| `cache/` | Drawing caches and invalidation: `tile_cache.py`, `invalidation_tracker.py` |
|
|
22
|
+
| `windowing/` | Surface lifecycle, native windows, chrome and platform backends: `surface.py`, `window_api.py`, `backends/` |
|
|
23
|
+
| `graphics/` | Shared GL resources, shaders, overlays, capture and tensor/graph integration: `gl_state.py`, `shader_func.py`, `lut_core.py`, `cuda_context_core.py`, `cuda_interop_core.py`, `cuda_kernel_core.py` |
|
|
24
|
+
| `layout/` | Cursor, grid, column, header and dropdown plumbing |
|
|
25
|
+
| `styling/` | Shared styles, colours, fonts and font warmup |
|
|
26
|
+
| `files/` | Filesystem polling, metadata and file/import-tree integration |
|
|
27
|
+
| `runtime/` | App/session lifecycle, scheduling, settings and shared process helpers |
|
|
28
|
+
| `diagnostics/` | Notifications, profiling, tracing, inspection and diagnostics integration |
|
|
29
|
+
| `automation/` | Orchestration, actions, queries, search and MCP integration |
|
|
30
|
+
| `services/` | Terminal, chat and account runtime integration |
|
|
31
|
+
|
|
32
|
+
These folders organize wiring; they do not turn feature algorithms or local
|
|
33
|
+
presentation into core code. Some inherited integration modules remain mixed;
|
|
34
|
+
see [the outstanding ownership review](../../docs/ARCHITECTURE_DEBT.md).
|
|
35
|
+
|
|
36
|
+
## Shared presentation inputs
|
|
37
|
+
|
|
38
|
+
Render functions can declare `ui_scale` and `font_manager` in their signatures,
|
|
39
|
+
alongside the existing `style_manager` input. Core supplies the current runtime
|
|
40
|
+
scale and font manager; explicit scale/font-manager overrides are supported for
|
|
41
|
+
previews. These dependencies are excluded from saved view parameters and the
|
|
42
|
+
parameter controls. Views can use their own `draw_state.depth_and_layer` for
|
|
43
|
+
local drawing depth. This keeps feature presentation independent of `Melty`
|
|
44
|
+
lookups without making callers pass the same plumbing repeatedly.
|
|
45
|
+
|
|
46
|
+
`keyboard_available` is true when no text editor owns keyboard focus;
|
|
47
|
+
`pointer_buttons_down` reports whether any primary pointer button is held.
|
|
48
|
+
Core supplies these only to views declaring them. Like scale and font context,
|
|
49
|
+
they allow explicit overrides and are excluded from saved parameter controls.
|
|
50
|
+
|
|
51
|
+
Palette consumers declare `luts`. Core injects the shared `LutPalette` from
|
|
52
|
+
`Melty.luts`, or accepts an explicit override, and subscribes cached consumers
|
|
53
|
+
before the render-cache gate. `luts.texture(name)` is an integer-like texture ID:
|
|
54
|
+
the model handles lazy uploads, updates and per-context storage. There is no
|
|
55
|
+
palette host or separate resource service. `GLState` releases context resources
|
|
56
|
+
when a surface closes. Palette values and proxies belong in `model/lut_model.py`;
|
|
57
|
+
selection and swatches belong in `view/lut_view.py`.
|
|
58
|
+
|
|
59
|
+
## CUDA interop ownership
|
|
60
|
+
|
|
61
|
+
`cuda_context_core.py` owns primary-context leases and scoped device activation
|
|
62
|
+
for voxel kernels, line kernels and GL interop. Runtime state lives on
|
|
63
|
+
`Melty.cuda_interop`; the existing field also holds the per-device context pool.
|
|
64
|
+
`cuda_interop_core.py` selects the GL-compatible device and manages registered
|
|
65
|
+
buffers, mapping and copies through that shared context manager.
|
|
66
|
+
`model/cuda_texture_model.py` owns versioned tensor uploads as `GLTexture` values.
|
|
67
|
+
Their composite allocations use the caller's `GLState`, including partial-allocation
|
|
68
|
+
cleanup and deferred unregistration retries. Feature renderers do not own CUDA
|
|
69
|
+
context setup. `cuda_kernel_core.py` owns compilation and cached modules. Voxel and line CUDA
|
|
70
|
+
presentation live in `view/voxel_cuda_view.py` and `view/graph_cuda_view.py`.
|
|
71
|
+
|
|
72
|
+
## Why mode has three files
|
|
73
|
+
|
|
74
|
+
`rendering/mode.py` defines the real enum and its renderer/converter policies. `rendering/modes.py`
|
|
75
|
+
provides lazy `Modes.X` handles so decorators can refer to modes before their
|
|
76
|
+
renderers finish importing. `rendering/mode_defaults.py` holds shared type-to-mode defaults,
|
|
77
|
+
including delayed registration for optional dependencies. Combining these at
|
|
78
|
+
import time would recreate the mode/renderer import cycle.
|
|
79
|
+
|
|
80
|
+
The public package exports remain available from `meltygui`. Internal imports
|
|
81
|
+
use current modules; there are no legacy import aliases, forwarding shims or
|
|
82
|
+
virtual historical namespaces. Definition hotswap preserves live objects and
|
|
83
|
+
state. Saved-name translation belongs to session loading, and source navigation
|
|
84
|
+
resolves actual imports. Update the editor and its Pro dependency with source moves.
|
|
85
|
+
|
|
86
|
+
See [the move inventory and checks](../../docs/CORE_RELOCATION.md). The mixed
|
|
87
|
+
`state/new_core_model.py` and the text-editor implementation await the separate
|
|
88
|
+
editor/state refactor.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Shared framework wiring."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Shared framework automation machinery."""
|