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.
Files changed (372) hide show
  1. meltygui/__init__.py +107 -0
  2. meltygui/accounts/__init__.py +0 -0
  3. meltygui/accounts/internet_accounts.py +1355 -0
  4. meltygui/chat/__init__.py +91 -0
  5. meltygui/chat/activity.py +75 -0
  6. meltygui/chat/backends.py +36 -0
  7. meltygui/chat/chat_interface.py +732 -0
  8. meltygui/chat/chat_proxy.py +352 -0
  9. meltygui/chat/codex_proxy.py +592 -0
  10. meltygui/chat/codex_settings.py +100 -0
  11. meltygui/chat/codex_transport.py +60 -0
  12. meltygui/chat/command_parser.py +204 -0
  13. meltygui/chat/images.py +227 -0
  14. meltygui/chat/messages.py +417 -0
  15. meltygui/chat/metadata.py +139 -0
  16. meltygui/chat/writer_locks.py +64 -0
  17. meltygui/code/__init__.py +0 -0
  18. meltygui/code/basic_converters.py +533 -0
  19. meltygui/code/chain_converters.py +2111 -0
  20. meltygui/code/code_checks.py +2209 -0
  21. meltygui/code/core_syntax.py +1430 -0
  22. meltygui/code/file_converters.py +1933 -0
  23. meltygui/code/fileref.py +702 -0
  24. meltygui/code/hotswap_guard.py +144 -0
  25. meltygui/code/libcst_conversion.py +9724 -0
  26. meltygui/code/live_instrument.py +392 -0
  27. meltygui/code/live_view.py +2490 -0
  28. meltygui/code/melty_scan.py +2684 -0
  29. meltygui/code/new_codecs.py +1255 -0
  30. meltygui/code/new_converters.py +3017 -0
  31. meltygui/code/project_code.py +278 -0
  32. meltygui/code/source_context.py +63 -0
  33. meltygui/code/symbol_roster.py +1588 -0
  34. meltygui/code/syntax_check.py +34 -0
  35. meltygui/code/syntax_check_worker.py +114 -0
  36. meltygui/completion/__init__.py +0 -0
  37. meltygui/completion/fim.py +1232 -0
  38. meltygui/completion/fim_context.py +481 -0
  39. meltygui/completion/providers/__init__.py +0 -0
  40. meltygui/completion/providers/anthropic_oauth.py +446 -0
  41. meltygui/completion/providers/anthropic_requests.py +69 -0
  42. meltygui/completion/providers/claude.py +203 -0
  43. meltygui/completion/providers/claude_usage.py +499 -0
  44. meltygui/completion/providers/codex_accounts.py +180 -0
  45. meltygui/completion/providers/copilot.py +617 -0
  46. meltygui/completion/providers/oauth_popup.py +220 -0
  47. meltygui/completion/providers/ollama.py +320 -0
  48. meltygui/completion/providers/profiles.py +23 -0
  49. meltygui/core/README.md +88 -0
  50. meltygui/core/__init__.py +1 -0
  51. meltygui/core/automation/__init__.py +1 -0
  52. meltygui/core/automation/action_core.py +153 -0
  53. meltygui/core/automation/collection_action.py +45 -0
  54. meltygui/core/automation/mcp_eval.py +167 -0
  55. meltygui/core/automation/mcp_hotswap.py +107 -0
  56. meltygui/core/automation/mcp_query.py +552 -0
  57. meltygui/core/automation/mcp_server.py +657 -0
  58. meltygui/core/automation/orchestration_core.py +2636 -0
  59. meltygui/core/automation/query_core.py +124 -0
  60. meltygui/core/automation/search_core.py +26 -0
  61. meltygui/core/automation/selector_core.py +340 -0
  62. meltygui/core/automation/value_core.py +1752 -0
  63. meltygui/core/cache/__init__.py +1 -0
  64. meltygui/core/cache/cache_diagnostics.py +0 -0
  65. meltygui/core/cache/invalidation_decoration.py +153 -0
  66. meltygui/core/cache/invalidation_tracker.py +43 -0
  67. meltygui/core/cache/tile_cache.py +5968 -0
  68. meltygui/core/conversion/__init__.py +1 -0
  69. meltygui/core/conversion/bubbling.py +599 -0
  70. meltygui/core/conversion/cache_tree.py +188 -0
  71. meltygui/core/conversion/chain.py +113 -0
  72. meltygui/core/conversion/converter_register.py +145 -0
  73. meltygui/core/conversion/data_decoration.py +67 -0
  74. meltygui/core/conversion/dict_conversion.py +1815 -0
  75. meltygui/core/conversion/dict_conversion_util.py +177 -0
  76. meltygui/core/conversion/dynamic_obj.py +89 -0
  77. meltygui/core/conversion/graph_compare.py +183 -0
  78. meltygui/core/conversion/load_save_v2.py +1032 -0
  79. meltygui/core/conversion/missing_saved_class.py +39 -0
  80. meltygui/core/conversion/path_finder.py +606 -0
  81. meltygui/core/conversion/render_host.py +1083 -0
  82. meltygui/core/core_render.py +6537 -0
  83. meltygui/core/definition_hotswap.py +231 -0
  84. meltygui/core/diagnostics/__init__.py +1 -0
  85. meltygui/core/diagnostics/attribute_churn.py +38 -0
  86. meltygui/core/diagnostics/fps_counter.py +39 -0
  87. meltygui/core/diagnostics/gpu_frame_timer.py +103 -0
  88. meltygui/core/diagnostics/inspection_core.py +169 -0
  89. meltygui/core/diagnostics/monitor_core.py +105 -0
  90. meltygui/core/diagnostics/notifications.py +706 -0
  91. meltygui/core/diagnostics/perf_trace.py +281 -0
  92. meltygui/core/diagnostics/profile_decoration.py +88 -0
  93. meltygui/core/diagnostics/resize_trace.py +62 -0
  94. meltygui/core/diagnostics/screenshot_core.py +247 -0
  95. meltygui/core/diagnostics/session_status.py +98 -0
  96. meltygui/core/diagnostics/trace_core.py +445 -0
  97. meltygui/core/files/__init__.py +1 -0
  98. meltygui/core/files/file_core.py +208 -0
  99. meltygui/core/files/file_explorer_core.py +104 -0
  100. meltygui/core/files/file_tree_core.py +198 -0
  101. meltygui/core/files/file_watch_core.py +43 -0
  102. meltygui/core/files/import_graph_core.py +43 -0
  103. meltygui/core/files/metadata_core.py +51 -0
  104. meltygui/core/graphics/__init__.py +1 -0
  105. meltygui/core/graphics/cuda_context_core.py +166 -0
  106. meltygui/core/graphics/cuda_interop_core.py +136 -0
  107. meltygui/core/graphics/cuda_kernel_core.py +91 -0
  108. meltygui/core/graphics/framebuffer_recorder.py +337 -0
  109. meltygui/core/graphics/gl_state.py +658 -0
  110. meltygui/core/graphics/lut_core.py +52 -0
  111. meltygui/core/graphics/overlay_renderer.py +984 -0
  112. meltygui/core/graphics/scene_target.py +180 -0
  113. meltygui/core/graphics/screenshot.py +439 -0
  114. meltygui/core/graphics/shader_func.py +478 -0
  115. meltygui/core/graphics/tensor_core.py +45 -0
  116. meltygui/core/graphics/text_texture.py +329 -0
  117. meltygui/core/graphics/wayland_color.py +635 -0
  118. meltygui/core/input/__init__.py +1 -0
  119. meltygui/core/input/collision.py +165 -0
  120. meltygui/core/input/drag_drop_core.py +1525 -0
  121. meltygui/core/input/hypr_left_drag.py +323 -0
  122. meltygui/core/input/input_core.py +245 -0
  123. meltygui/core/input/input_handler.py +1101 -0
  124. meltygui/core/input/mouse_cursor.py +355 -0
  125. meltygui/core/input/pynput_backend.py +1054 -0
  126. meltygui/core/input/space_mouse.py +338 -0
  127. meltygui/core/input/touchpad_backend.py +393 -0
  128. meltygui/core/input/view_selection.py +177 -0
  129. meltygui/core/layout/__init__.py +1 -0
  130. meltygui/core/layout/column_core.py +2153 -0
  131. meltygui/core/layout/cursor_core.py +161 -0
  132. meltygui/core/layout/dropdown_core.py +278 -0
  133. meltygui/core/layout/edge_constraints.py +155 -0
  134. meltygui/core/layout/grid_core.py +117 -0
  135. meltygui/core/layout/header_core.py +23 -0
  136. meltygui/core/layout/header_runtime.py +67 -0
  137. meltygui/core/layout/layout_core.py +87 -0
  138. meltygui/core/layout/tile_manager_core.py +591 -0
  139. meltygui/core/melty.py +6726 -0
  140. meltygui/core/module_map.json +896 -0
  141. meltygui/core/module_names.py +19 -0
  142. meltygui/core/rendering/__init__.py +1 -0
  143. meltygui/core/rendering/core_decoration.py +431 -0
  144. meltygui/core/rendering/core_render_helpers.py +328 -0
  145. meltygui/core/rendering/func_metadata.py +398 -0
  146. meltygui/core/rendering/mode.py +818 -0
  147. meltygui/core/rendering/mode_defaults.py +41 -0
  148. meltygui/core/rendering/modes.py +136 -0
  149. meltygui/core/rendering/parameter_core.py +1665 -0
  150. meltygui/core/rendering/render_dispatch.py +1891 -0
  151. meltygui/core/rendering/render_funcs.py +273 -0
  152. meltygui/core/rendering/shaped.py +312 -0
  153. meltygui/core/rendering/window_decoration.py +25 -0
  154. meltygui/core/runtime/__init__.py +1 -0
  155. meltygui/core/runtime/app.py +735 -0
  156. meltygui/core/runtime/app_session.py +140 -0
  157. meltygui/core/runtime/background.py +564 -0
  158. meltygui/core/runtime/extensions.py +53 -0
  159. meltygui/core/runtime/gc_manager.py +1163 -0
  160. meltygui/core/runtime/lifecycle.py +21 -0
  161. meltygui/core/runtime/paths.py +27 -0
  162. meltygui/core/runtime/settings.py +14 -0
  163. meltygui/core/runtime/singleton.py +16 -0
  164. meltygui/core/runtime/thread_safe_bool.py +24 -0
  165. meltygui/core/runtime/thread_signal.py +30 -0
  166. meltygui/core/runtime/toggles.py +3115 -0
  167. meltygui/core/services/__init__.py +1 -0
  168. meltygui/core/services/account_core.py +10 -0
  169. meltygui/core/services/chat_core.py +19 -0
  170. meltygui/core/services/claude_terminal_core.py +346 -0
  171. meltygui/core/services/terminal_core.py +458 -0
  172. meltygui/core/services/terminal_runtime.py +78 -0
  173. meltygui/core/styling/__init__.py +1 -0
  174. meltygui/core/styling/color_core.py +46 -0
  175. meltygui/core/styling/fonts.py +639 -0
  176. meltygui/core/styling/global_style.py +338 -0
  177. meltygui/core/styling/style.py +198 -0
  178. meltygui/core/styling/style_core.py +522 -0
  179. meltygui/core/styling/warm_start.py +149 -0
  180. meltygui/core/windowing/__init__.py +1 -0
  181. meltygui/core/windowing/backends/PYIMGUI_LICENSE +28 -0
  182. meltygui/core/windowing/backends/__init__.py +1 -0
  183. meltygui/core/windowing/backends/imgui_renderer.py +138 -0
  184. meltygui/core/windowing/backends/native_wayland.py +850 -0
  185. meltygui/core/windowing/backends/protocols/xdg-decoration-unstable-v1.xml +156 -0
  186. meltygui/core/windowing/backends/protocols/xdg-shell.xml +1420 -0
  187. meltygui/core/windowing/backends/wayland_protocol.py +101 -0
  188. meltygui/core/windowing/dock_core.py +182 -0
  189. meltygui/core/windowing/frame_geometry.py +42 -0
  190. meltygui/core/windowing/geometry_feed.py +864 -0
  191. meltygui/core/windowing/glfw_utils.py +1343 -0
  192. meltygui/core/windowing/os_frame.py +1552 -0
  193. meltygui/core/windowing/surface.py +643 -0
  194. meltygui/core/windowing/titlebar.py +1560 -0
  195. meltygui/core/windowing/titlebar_buttons.py +281 -0
  196. meltygui/core/windowing/wayland_move.py +932 -0
  197. meltygui/core/windowing/window_api.py +62 -0
  198. meltygui/core/windowing/window_constants.py +339 -0
  199. meltygui/core/windowing/window_visibility.py +162 -0
  200. meltygui/debug/__init__.py +0 -0
  201. meltygui/debug/app_view_utils.py +9 -0
  202. meltygui/editor/__init__.py +0 -0
  203. meltygui/editor/bash_syntax.py +30 -0
  204. meltygui/editor/code_line_fast.py +152 -0
  205. meltygui/editor/diff.py +139 -0
  206. meltygui/editor/external_changes.py +159 -0
  207. meltygui/editor/file_header.py +41 -0
  208. meltygui/editor/live_usage.py +169 -0
  209. meltygui/editor/live_view_views.py +1803 -0
  210. meltygui/editor/pending_save.py +1351 -0
  211. meltygui/editor/roster_tints.py +547 -0
  212. meltygui/editor/source_preview.py +16 -0
  213. meltygui/editor/source_tools.py +10 -0
  214. meltygui/editor/source_ui.py +70 -0
  215. meltygui/editor/spell_check.py +77 -0
  216. meltygui/editor/text_editor.py +9076 -0
  217. meltygui/editor/usage_picker.py +538 -0
  218. meltygui/events/__init__.py +0 -0
  219. meltygui/events/example.py +118 -0
  220. meltygui/examples/__init__.py +0 -0
  221. meltygui/examples/columns_demo.py +33 -0
  222. meltygui/examples/columns_window_demo.py +60 -0
  223. meltygui/examples/context_menu_demo.py +24 -0
  224. meltygui/examples/context_menu_window_demo.py +49 -0
  225. meltygui/examples/gui_playground.py +127 -0
  226. meltygui/examples/live_view_playground.py +256 -0
  227. meltygui/examples/lora.py +43 -0
  228. meltygui/examples/lora_data.py +59 -0
  229. meltygui/examples/lora_policies.py +67 -0
  230. meltygui/examples/mode_demo.py +49 -0
  231. meltygui/examples/modifies_demo.py +119 -0
  232. meltygui/examples/scalar_policies.py +60 -0
  233. meltygui/examples/style_layouts.py +149 -0
  234. meltygui/examples/tile_manager_demo.py +74 -0
  235. meltygui/examples/tint_demo.py +160 -0
  236. meltygui/examples/tint_functions.py +56 -0
  237. meltygui/examples/trace_demo.py +88 -0
  238. meltygui/examples/two_windows.py +36 -0
  239. meltygui/files/__init__.py +0 -0
  240. meltygui/files/fast_file_explorer.py +439 -0
  241. meltygui/gnome_extension/lsd-window-geometry@latent-descent/extension.js +237 -0
  242. meltygui/gnome_extension/lsd-window-geometry@latent-descent/lsd-window-geometry@latent-descent.iml +9 -0
  243. meltygui/gnome_extension/lsd-window-geometry@latent-descent/metadata.json +7 -0
  244. meltygui/graphics/__init__.py +6 -0
  245. meltygui/graphics/base.py +85 -0
  246. meltygui/graphics/examples.py +507 -0
  247. meltygui/graphics/executor.py +520 -0
  248. meltygui/graphics/filter.py +667 -0
  249. meltygui/graphics/filter.pyi +856 -0
  250. meltygui/graphics/generate_stubs.py +22 -0
  251. meltygui/graphics/registry.py +203 -0
  252. meltygui/graphics/shader_compiler.py +155 -0
  253. meltygui/graphics/shaders.py +1302 -0
  254. meltygui/graphics/stub_generator.py +250 -0
  255. meltygui/graphics/texture_manager.py +170 -0
  256. meltygui/graphics/texture_min_max.py +295 -0
  257. meltygui/hdr_color.py +757 -0
  258. meltygui/image_load.py +308 -0
  259. meltygui/model/__init__.py +1 -0
  260. meltygui/model/account_model.py +142 -0
  261. meltygui/model/camera_model.py +159 -0
  262. meltygui/model/chat_model.py +86 -0
  263. meltygui/model/code_model.py +62 -0
  264. meltygui/model/code_proxy_model.py +876 -0
  265. meltygui/model/collection_model.py +41 -0
  266. meltygui/model/color_model.py +127 -0
  267. meltygui/model/cuda_tensor_model.py +36 -0
  268. meltygui/model/cuda_texture_model.py +149 -0
  269. meltygui/model/dropdown_model.py +137 -0
  270. meltygui/model/file_metadata_model.py +73 -0
  271. meltygui/model/file_model.py +289 -0
  272. meltygui/model/format_model.py +390 -0
  273. meltygui/model/graph_model.py +98 -0
  274. meltygui/model/icon_model.py +1024 -0
  275. meltygui/model/import_graph_model.py +468 -0
  276. meltygui/model/layout_model.py +15 -0
  277. meltygui/model/lut_model.py +266 -0
  278. meltygui/model/search_model.py +173 -0
  279. meltygui/model/tensor_model.py +402 -0
  280. meltygui/model/terminal_model.py +329 -0
  281. meltygui/model/texture_model.py +146 -0
  282. meltygui/model/tile_model.py +24 -0
  283. meltygui/model/trace_model.py +198 -0
  284. meltygui/model/trace_report_model.py +146 -0
  285. meltygui/models/__init__.py +0 -0
  286. meltygui/models/file_meta.py +613 -0
  287. meltygui/models/function_console.py +184 -0
  288. meltygui/models/orchestration.py +48 -0
  289. meltygui/pbr.py +1576 -0
  290. meltygui/png_unfilter.c +49 -0
  291. meltygui/resources/JetBrainsMono-Regular.ttf +0 -0
  292. meltygui/resources/THIRD_PARTY_NOTICES.md +21 -0
  293. meltygui/resources/dejavu/DejaVuSans-Bold.ttf +0 -0
  294. meltygui/resources/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
  295. meltygui/resources/dejavu/DejaVuSans.ttf +0 -0
  296. meltygui/resources/dejavu/LICENSE.txt +78 -0
  297. meltygui/resources/fontawesome-LICENSE.txt +121 -0
  298. meltygui/resources/fontawesome-webfont.ttf +0 -0
  299. meltygui/resources/hdri/studio_small_09_1k.hdr +0 -0
  300. meltygui/resources/jetbrains-weights/JetBrainsMono-Bold.ttf +0 -0
  301. meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraBold.ttf +0 -0
  302. meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraLight.ttf +0 -0
  303. meltygui/resources/jetbrains-weights/JetBrainsMono-Light.ttf +0 -0
  304. meltygui/resources/jetbrains-weights/JetBrainsMono-Medium.ttf +0 -0
  305. meltygui/resources/jetbrains-weights/JetBrainsMono-SemiBold.ttf +0 -0
  306. meltygui/resources/jetbrains-weights/JetBrainsMono-Thin.ttf +0 -0
  307. meltygui/resources/jetbrains-weights/OFL.txt +93 -0
  308. meltygui/resources/jetbrains-weights/README.md +2 -0
  309. meltygui/state/__init__.py +0 -0
  310. meltygui/state/account_state.py +22 -0
  311. meltygui/state/animation_state.py +97 -0
  312. meltygui/state/annotation_state.py +22 -0
  313. meltygui/state/chat_state.py +36 -0
  314. meltygui/state/code_state.py +10 -0
  315. meltygui/state/core_enums.py +59 -0
  316. meltygui/state/core_markers.py +115 -0
  317. meltygui/state/core_undo.py +1075 -0
  318. meltygui/state/file_state.py +75 -0
  319. meltygui/state/graph_state.py +26 -0
  320. meltygui/state/inspection_state.py +47 -0
  321. meltygui/state/menu_state.py +10 -0
  322. meltygui/state/model_enums.py +11 -0
  323. meltygui/state/new_core_model.py +2394 -0
  324. meltygui/state/orchestration_state.py +14 -0
  325. meltygui/state/query_state.py +24 -0
  326. meltygui/state/tensor_state.py +10 -0
  327. meltygui/state/terminal_state.py +23 -0
  328. meltygui/state/trace_state.py +32 -0
  329. meltygui/state/voxel_state.py +12 -0
  330. meltygui/text_index.py +816 -0
  331. meltygui/utils/__init__.py +0 -0
  332. meltygui/utils/jump_to_code.py +344 -0
  333. meltygui/utils/pkl_inspect.py +90 -0
  334. meltygui/utils/render_utils.py +1419 -0
  335. meltygui/view/__init__.py +1 -0
  336. meltygui/view/account_view.py +524 -0
  337. meltygui/view/action_view.py +73 -0
  338. meltygui/view/chat_decoration_view.py +112 -0
  339. meltygui/view/chat_view.py +1703 -0
  340. meltygui/view/code_view.py +2677 -0
  341. meltygui/view/collection_view.py +1185 -0
  342. meltygui/view/color_view.py +824 -0
  343. meltygui/view/control_view.py +559 -0
  344. meltygui/view/decoration_view.py +439 -0
  345. meltygui/view/diagnostic_view.py +177 -0
  346. meltygui/view/dropdown_view.py +1082 -0
  347. meltygui/view/file_view.py +1599 -0
  348. meltygui/view/graph_cuda_view.py +105 -0
  349. meltygui/view/graph_view.py +882 -0
  350. meltygui/view/header_view.py +848 -0
  351. meltygui/view/input_view.py +238 -0
  352. meltygui/view/inspection_view.py +1783 -0
  353. meltygui/view/layout_view.py +596 -0
  354. meltygui/view/lut_view.py +45 -0
  355. meltygui/view/menu_view.py +212 -0
  356. meltygui/view/orchestration_view.py +658 -0
  357. meltygui/view/query_view.py +118 -0
  358. meltygui/view/search_view.py +342 -0
  359. meltygui/view/tab_view.py +258 -0
  360. meltygui/view/tensor_view.py +430 -0
  361. meltygui/view/terminal_view.py +355 -0
  362. meltygui/view/text_view.py +8031 -0
  363. meltygui/view/texture_view.py +480 -0
  364. meltygui/view/tile_view.py +38 -0
  365. meltygui/view/trace_view.py +923 -0
  366. meltygui/view/voxel_cuda_view.py +824 -0
  367. meltygui/view/voxel_view.py +1860 -0
  368. meltygui/view/window_view.py +111 -0
  369. meltygui-0.1.0.dist-info/METADATA +143 -0
  370. meltygui-0.1.0.dist-info/RECORD +372 -0
  371. meltygui-0.1.0.dist-info/WHEEL +4 -0
  372. meltygui-0.1.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,617 @@
1
+ """GitHub Copilot FIM provider — drives the official Copilot Language
2
+ Server (`@github/copilot-language-server`, LSP over stdio) the way
3
+ copilot.vim / the JetBrains plugin do. One `CopilotSession` = one LS
4
+ process = one GitHub account: the LS keeps its token under
5
+ `$XDG_CONFIG_HOME/github-copilot/`, so a profile with a different
6
+ `config_dir` is a different login (work vs home). The default (no
7
+ config_dir) shares `~/.config/github-copilot` with the IDE plugins.
8
+
9
+ Install (one-off, also offered by the Internet Accounts window):
10
+ npm install --prefix ~/.lsd/copilot-ls @github/copilot-language-server
11
+
12
+ Process spawn uses a full node path, `cwd=None`, `close_fds=False` so
13
+ CPython takes the posix_spawn path (a fork of the CUDA/GL address space
14
+ stalls the render thread — project_subprocess_fork_stall).
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import json
19
+ import os
20
+ import shutil
21
+ import subprocess
22
+ import threading
23
+ import time
24
+ from pathlib import Path
25
+
26
+ from meltygui.completion.fim import FimRequest
27
+ from meltygui.completion.fim import FimResult
28
+ from meltygui.completion.fim import FimSession
29
+ from meltygui.completion.fim import fim_provider
30
+
31
+ LS_ROOT = Path.home() / ".lsd" / "copilot-ls"
32
+ LS_ENTRY = LS_ROOT / "node_modules" / "@github" / "copilot-language-server" / "dist" / "language-server.js"
33
+ EDITOR_INFO = {"name": "LatentDescent", "version": "1.0"}
34
+ PLUGIN_INFO = {"name": "meltygui-fim", "version": "1.0"}
35
+
36
+
37
+ def find_node() -> str | None:
38
+ """Full path to a node ≥ 20.8 (PATH, then nvm installs, newest first)."""
39
+ p = shutil.which("node")
40
+ if p:
41
+ return p
42
+ nvm = Path.home() / ".nvm" / "versions" / "node"
43
+ if nvm.is_dir():
44
+ vers = sorted(nvm.iterdir(), key=lambda d: d.name, reverse=True)
45
+ for d in vers:
46
+ cand = d / "bin" / "node"
47
+ if cand.exists():
48
+ return str(cand)
49
+ return None
50
+
51
+
52
+ def server_installed() -> bool:
53
+ return LS_ENTRY.exists()
54
+
55
+
56
+ def cached_login_user(config_dir=None):
57
+ """The signed-in GitHub user from the Copilot token file on disk, or
58
+ None — WITHOUT spawning the language server. Lets the accounts window
59
+ show sign-in state at startup with no process spawn and no web request.
60
+ `config_dir` is the account's XDG_CONFIG_HOME (None = the user default,
61
+ shared with the IDE plugins)."""
62
+ base = Path(os.path.expanduser(config_dir)) if config_dir else (Path.home() / ".config")
63
+ for name in ("apps.json", "hosts.json"):
64
+ f = base / "github-copilot" / name
65
+ try:
66
+ if not f.exists():
67
+ continue
68
+ data = json.loads(f.read_text())
69
+ except Exception:
70
+ continue
71
+ for entry in (data.values() if isinstance(data, dict) else []):
72
+ if isinstance(entry, dict):
73
+ if entry.get("oauth_token") or entry.get("user"):
74
+ return entry.get("user") or "signed in"
75
+ return None
76
+
77
+
78
+ def install_server(log=print) -> bool:
79
+ """`npm install` the language server under LS_ROOT (network). Blocking —
80
+ call from a worker thread."""
81
+ node = find_node()
82
+ if node is None:
83
+ log("copilot: node not found")
84
+ return False
85
+ npm = str(Path(node).parent / "npm")
86
+ if not os.path.exists(npm):
87
+ npm = shutil.which("npm") or npm
88
+ LS_ROOT.mkdir(parents=True, exist_ok=True)
89
+ env = dict(os.environ)
90
+ env["PATH"] = str(Path(node).parent) + os.pathsep + env.get("PATH", "")
91
+ try:
92
+ r = subprocess.run([npm, "install", "--prefix", str(LS_ROOT), "--no-fund", "--no-audit",
93
+ "@github/copilot-language-server"],
94
+ capture_output=True, text=True, env=env, close_fds=False, timeout=600)
95
+ except Exception as e:
96
+ log(f"copilot: npm install failed: {e}")
97
+ return False
98
+ if r.returncode != 0:
99
+ log(f"copilot: npm install failed: {r.stderr[-400:]}")
100
+ return False
101
+ return server_installed()
102
+
103
+
104
+ # ──────────────────────────────────────────────────────────────────────────
105
+ # JSON-RPC over stdio
106
+ # ──────────────────────────────────────────────────────────────────────────
107
+
108
+ class _RpcError(RuntimeError):
109
+ pass
110
+
111
+
112
+ class _Rpc:
113
+ """Minimal LSP transport: Content-Length framing, id-correlated
114
+ requests from any thread, notification handlers, and replies to the
115
+ few server→client requests the LS makes."""
116
+
117
+ def __init__(self, proc, on_notification):
118
+ self.proc = proc
119
+ self._on_notification = on_notification
120
+ self._wlock = threading.Lock()
121
+ self._plock = threading.Lock()
122
+ self._pending = {} # id -> [Event, result, error]
123
+ self._next_id = 1
124
+ self.alive = True
125
+ self._reader = threading.Thread(target=self._read_loop, daemon=True, name="copilot-ls-reader")
126
+ self._reader.start()
127
+
128
+ def _send(self, msg):
129
+ data = json.dumps(msg).encode("utf-8")
130
+ head = f"Content-Length: {len(data)}\r\n\r\n".encode("ascii")
131
+ with self._wlock:
132
+ try:
133
+ self.proc.stdin.write(head + data)
134
+ self.proc.stdin.flush()
135
+ except (BrokenPipeError, OSError, ValueError) as e:
136
+ self.alive = False
137
+ raise _RpcError(f"language server pipe closed: {e}")
138
+
139
+ def notify(self, method, params=None):
140
+ self._send({"jsonrpc": "2.0", "method": method, "params": params or {}})
141
+
142
+ def request(self, method, params=None, timeout=60.0, cancel_event=None):
143
+ with self._plock:
144
+ rid = self._next_id
145
+ self._next_id += 1
146
+ slot = [threading.Event(), None, None]
147
+ self._pending[rid] = slot
148
+ self._send({"jsonrpc": "2.0", "id": rid, "method": method, "params": params or {}})
149
+ deadline = time.monotonic() + timeout if timeout else None
150
+ while not slot[0].wait(0.05):
151
+ if cancel_event is not None and cancel_event.is_set():
152
+ with self._plock:
153
+ self._pending.pop(rid, None)
154
+ try:
155
+ self.notify("$/cancelRequest", {"id": rid})
156
+ except _RpcError:
157
+ pass
158
+ return None
159
+ if not self.alive:
160
+ raise _RpcError("language server exited")
161
+ if deadline is not None and time.monotonic() > deadline:
162
+ with self._plock:
163
+ self._pending.pop(rid, None)
164
+ raise _RpcError(f"{method}: timed out")
165
+ if slot[2] is not None:
166
+ err = slot[2]
167
+ raise _RpcError(f"{method}: {err.get('message') if isinstance(err, dict) else err}")
168
+ return slot[1]
169
+
170
+ def _reply(self, rid, result=None, error=None):
171
+ msg = {"jsonrpc": "2.0", "id": rid}
172
+ if error is not None:
173
+ msg["error"] = error
174
+ else:
175
+ msg["result"] = result
176
+ try:
177
+ self._send(msg)
178
+ except _RpcError:
179
+ pass
180
+
181
+ def _read_loop(self):
182
+ out = self.proc.stdout
183
+ try:
184
+ while True:
185
+ length = None
186
+ while True:
187
+ line = out.readline()
188
+ if not line:
189
+ raise EOFError
190
+ if line in (b"\r\n", b"\n"):
191
+ break
192
+ k, _, v = line.decode("ascii", "replace").partition(":")
193
+ if k.strip().lower() == "content-length":
194
+ length = int(v.strip())
195
+ if length is None:
196
+ continue
197
+ body = out.read(length)
198
+ if len(body) < length:
199
+ raise EOFError
200
+ try:
201
+ msg = json.loads(body.decode("utf-8", "replace"))
202
+ except ValueError:
203
+ continue
204
+ self._dispatch(msg)
205
+ except (EOFError, OSError, ValueError):
206
+ pass
207
+ finally:
208
+ self.alive = False
209
+ with self._plock:
210
+ for slot in self._pending.values():
211
+ slot[2] = {"message": "language server exited"}
212
+ slot[0].set()
213
+ self._pending.clear()
214
+
215
+ def _dispatch(self, msg):
216
+ if "id" in msg and "method" in msg:
217
+ # server → client request: answer what we can, null otherwise
218
+ method = msg["method"]
219
+ params = msg.get("params") or {}
220
+ if method == "workspace/configuration":
221
+ items = params.get("items") or []
222
+ self._reply(msg["id"], [{} for _ in items])
223
+ elif method == "window/showMessageRequest":
224
+ self._reply(msg["id"], None)
225
+ elif method == "window/showDocument":
226
+ uri = params.get("uri")
227
+ if uri:
228
+ open_url(uri)
229
+ self._reply(msg["id"], {"success": bool(uri)})
230
+ else:
231
+ self._reply(msg["id"], None)
232
+ return
233
+ if "id" in msg:
234
+ with self._plock:
235
+ slot = self._pending.pop(msg["id"], None)
236
+ if slot is not None:
237
+ slot[1] = msg.get("result")
238
+ slot[2] = msg.get("error")
239
+ slot[0].set()
240
+ return
241
+ method = msg.get("method")
242
+ if method:
243
+ try:
244
+ self._on_notification(method, msg.get("params") or {})
245
+ except Exception:
246
+ pass
247
+
248
+
249
+ def open_url(url: str):
250
+ """Open a URL in the user's browser without fork()ing the studio:
251
+ full-path xdg-open via posix_spawn (see module docstring)."""
252
+ for cmd in ("xdg-open", "gio", "open"):
253
+ exe = shutil.which(cmd)
254
+ if exe:
255
+ args = [exe, url] if cmd != "gio" else [exe, "open", url]
256
+ try:
257
+ subprocess.Popen(args, close_fds=False, stdin=subprocess.DEVNULL,
258
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
259
+ return True
260
+ except OSError:
261
+ continue
262
+ return False
263
+
264
+
265
+ # ──────────────────────────────────────────────────────────────────────────
266
+ # Session
267
+ # ──────────────────────────────────────────────────────────────────────────
268
+
269
+ class CopilotSession(FimSession):
270
+ """One language-server process / one GitHub login. `account` names an
271
+ Internet Accounts entry of kind "copilot" whose `config_dir` is the
272
+ login's XDG_CONFIG_HOME (empty = the user's default, shared with the
273
+ IDE plugins); an explicit `config_dir` overrides it. `workspace` is
274
+ the folder reported to the LS."""
275
+ KIND = "copilot"
276
+
277
+ def __init__(self, account="default", config_dir=None, workspace=None, node=None):
278
+ self.account = account
279
+ if config_dir is None:
280
+ from meltygui.accounts.internet_accounts import account_field
281
+ config_dir = account_field("copilot", account, "config_dir")
282
+ self.config_dir = os.path.expanduser(config_dir) if config_dir else None
283
+ from meltygui.core.runtime.paths import application_root
284
+ self.workspace = workspace or str(application_root())
285
+ self.node = node or find_node()
286
+ self._lock = threading.RLock()
287
+ self._docs = {} # uri -> (version, text)
288
+ self._status = ("starting",)
289
+ self._kind = "Unknown"
290
+ self._message = ""
291
+ self._busy = False
292
+ self.user = None
293
+ self.login = None # (user_code, verification_uri) while a login flow is open
294
+ self.proc = None
295
+ self.rpc = None
296
+ self._start()
297
+
298
+ # ── lifecycle ───────────────────────────────────────────────────────
299
+ def _start(self):
300
+ if self.node is None:
301
+ raise RuntimeError("node not found (need Node ≥ 20.8 for the Copilot language server)")
302
+ if not server_installed():
303
+ raise RuntimeError(f"Copilot language server not installed under {LS_ROOT} "
304
+ f"(Internet Accounts → Copilot → Install)")
305
+ env = dict(os.environ)
306
+ if self.config_dir:
307
+ env["XDG_CONFIG_HOME"] = self.config_dir
308
+ Path(self.config_dir).mkdir(parents=True, exist_ok=True)
309
+ # --dns-result-order=ipv4first: Node ≥ 17 tries AAAA records first;
310
+ # on a host whose IPv6 route is dead every GitHub access inside the LS
311
+ # ends in an undici timeout (~30 s) before its fallback fetcher kicks
312
+ # in - the first completion took 30 s here. IPv4-first makes it fast.
313
+ self.proc = subprocess.Popen(
314
+ [self.node, "--dns-result-order=ipv4first", str(LS_ENTRY), "--stdio"],
315
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL,
316
+ env=env, close_fds=False)
317
+ self.rpc = _Rpc(self.proc, self._on_notification)
318
+ init = {
319
+ "processId": os.getpid(),
320
+ "rootUri": Path(self.workspace).as_uri(),
321
+ "workspaceFolders": [{"uri": Path(self.workspace).as_uri(), "name": Path(self.workspace).name}],
322
+ "capabilities": {"workspace": {"workspaceFolders": True, "configuration": True},
323
+ "window": {"showDocument": {"support": True}}},
324
+ "initializationOptions": {"editorInfo": EDITOR_INFO, "editorPluginInfo": PLUGIN_INFO},
325
+ }
326
+ self.rpc.request("initialize", init, timeout=60.0)
327
+ self.rpc.notify("initialized", {})
328
+ self.rpc.notify("workspace/didChangeConfiguration",
329
+ {"settings": {"telemetry": {"telemetryLevel": "off"}}})
330
+ self._status = ("ready",)
331
+ # Warm-up: the LS's FIRST completion pays for auth + fallback fetcher
332
+ # negotiation (~30 s on a host whose primary fetcher can't reach
333
+ # GitHub); do it on a throwaway document now so the user's first
334
+ # real request is the ~0.3 s warm-up.
335
+ threading.Thread(target=self._warm, daemon=True, name="copilot-warm").start()
336
+
337
+ def _warm(self):
338
+ try:
339
+ self.check_status()
340
+ if self._kind == "Normal":
341
+ text = "def add(a, b):\n return "
342
+ self.complete("untitled:warmup.py", text, len(text), threading.Event())
343
+ except Exception:
344
+ pass
345
+ _notify_account_change()
346
+
347
+ def close(self):
348
+ rpc, proc = self.rpc, self.proc
349
+ self.rpc = None
350
+ self.proc = None
351
+ if rpc is not None and rpc.alive:
352
+ try:
353
+ rpc.request("shutdown", {}, timeout=5.0)
354
+ rpc.notify("exit", {})
355
+ except Exception:
356
+ pass
357
+ if proc is not None:
358
+ try:
359
+ proc.wait(timeout=3.0)
360
+ except Exception:
361
+ try:
362
+ proc.kill()
363
+ except Exception:
364
+ pass
365
+
366
+ def restart(self):
367
+ self.close()
368
+ self._docs.clear()
369
+ self._start()
370
+
371
+ # ── status & auth ───────────────────────────────────────────────────
372
+ def _on_notification(self, method, params):
373
+ if method == "didChangeStatus":
374
+ self._kind = params.get("kind") or "Normal"
375
+ self._message = params.get("message") or ""
376
+ self._busy = bool(params.get("busy"))
377
+ if self._kind == "Normal" and self.login is not None and not self._busy:
378
+ self.login = None
379
+ _notify_account_change()
380
+ elif method == "window/logMessage":
381
+ pass
382
+
383
+ def alive(self):
384
+ return self.rpc is not None and self.rpc.alive
385
+
386
+ def status(self):
387
+ if self.rpc is None or not self.rpc.alive:
388
+ return ("error", "language server exited")
389
+ if self.login is not None:
390
+ code, uri = self.login
391
+ return ("needs_login", code, uri)
392
+ if self._kind == "Error":
393
+ return ("error", self._message or "not signed in")
394
+ if self._kind == "Warning":
395
+ return ("warning", self._message)
396
+ return ("ready",)
397
+
398
+ def check_status(self):
399
+ """Ask the LS who is signed in (`signIn` answers AlreadySignedIn
400
+ without starting a flow when a token exists). Returns the user
401
+ login or None."""
402
+ try:
403
+ res = self.rpc.request("signIn", {}, timeout=30.0) or {}
404
+ except _RpcError as e:
405
+ self._kind, self._message = "Error", str(e)
406
+ return None
407
+ st = res.get("status")
408
+ if st == "AlreadySignedIn" or res.get("user"):
409
+ self.user = res.get("user") or self.user
410
+ self._kind, self._message = "Normal", ""
411
+ return self.user
412
+ # A device flow was started that we did not ask for: save its
413
+ # details so the UI can show them, but don't block on it.
414
+ if res.get("userCode"):
415
+ self.login = (res.get("userCode"), res.get("verificationUri") or "https://github.com/login/device")
416
+ self._pending_login_cmd = res.get("command")
417
+ self._kind, self._message = "Error", "not signed in"
418
+ return None
419
+
420
+ _pending_login_cmd = None
421
+
422
+ def sign_in(self, open_browser=True):
423
+ """Start (or resume) the device flow. Returns (user_code, uri) or
424
+ the signed-in user. The completion runs on a worker: the LS's
425
+ finish command blocks until the user approves in the browser."""
426
+ if self.login is None:
427
+ res = self.rpc.request("signIn", {}, timeout=30.0) or {}
428
+ if res.get("status") == "AlreadySignedIn" or (res.get("user") and not res.get("userCode")):
429
+ self.user = res.get("user") or self.user
430
+ self._kind, self._message = "Normal", ""
431
+ return self.user
432
+ if not res.get("userCode"):
433
+ raise RuntimeError(f"unexpected signIn reply: {res}")
434
+ self.login = (res["userCode"], res.get("verificationUri") or "https://github.com/login/device")
435
+ self._pending_login_cmd = res.get("command")
436
+ code, uri = self.login
437
+ if open_browser:
438
+ open_url(uri)
439
+ cmd = self._pending_login_cmd
440
+ if cmd:
441
+ self._pending_login_cmd = None
442
+
443
+ def finish():
444
+ try:
445
+ res = self.rpc.request("workspace/executeCommand",
446
+ {"command": cmd.get("command"),
447
+ "arguments": cmd.get("arguments") or []},
448
+ timeout=600.0) or {}
449
+ if isinstance(res, dict):
450
+ self.user = res.get("user") or self.user
451
+ if res.get("status") in ("OK", "AlreadySignedIn") or res.get("user"):
452
+ self._kind, self._message = "Normal", ""
453
+ except Exception as e:
454
+ self._kind, self._message = "Error", str(e)
455
+ self.login = None
456
+ _notify_account_change()
457
+
458
+ threading.Thread(target=finish, daemon=True, name="copilot-signin").start()
459
+ return (code, uri)
460
+
461
+ def sign_out(self):
462
+ try:
463
+ self.rpc.request("signOut", {}, timeout=30.0)
464
+ except _RpcError as e:
465
+ self._kind, self._message = "Error", str(e)
466
+ self.user = None
467
+ self.login = None
468
+ self._kind, self._message = "Error", "not signed in"
469
+ _notify_account_change()
470
+
471
+ # ── documents / completion ──────────────────────────────────────────
472
+ @staticmethod
473
+ def _uri(path):
474
+ try:
475
+ return Path(path).resolve().as_uri()
476
+ except Exception:
477
+ return "untitled:" + str(path).replace(" ", "_")
478
+
479
+ def sync_document(self, path, text, version=None, language="python"):
480
+ uri = self._uri(path)
481
+ with self._lock:
482
+ prev = self._docs.get(uri)
483
+ if prev is not None and prev[1] == text:
484
+ return uri, prev[0]
485
+ v = (prev[0] + 1) if prev is not None else 1
486
+ if prev is None:
487
+ self.rpc.notify("textDocument/didOpen",
488
+ {"textDocument": {"uri": uri, "languageId": language,
489
+ "version": v, "text": text}})
490
+ else:
491
+ self.rpc.notify("textDocument/didChange",
492
+ {"textDocument": {"uri": uri, "version": v},
493
+ "contentChanges": [{"text": text}]})
494
+ self._docs[uri] = (v, text)
495
+ return uri, v
496
+
497
+ def complete(self, path, text, cursor, cancelled, language="python") -> FimResult:
498
+ uri, v = self.sync_document(path, text, language=language)
499
+ line = text.count("\n", 0, cursor)
500
+ ls = text.rfind("\n", 0, cursor) + 1
501
+ char = _utf16_len(text[ls:cursor])
502
+ res = self.rpc.request("textDocument/inlineCompletion",
503
+ {"textDocument": {"uri": uri, "version": v},
504
+ "position": {"line": line, "character": char},
505
+ "context": {"triggerKind": 2},
506
+ "formattingOptions": {"tabSize": 4, "insertSpaces": True}},
507
+ timeout=30.0, cancel_event=cancelled)
508
+ items = (res or {}).get("items") or []
509
+ if not items:
510
+ return FimResult("", provider="copilot")
511
+ texts = []
512
+ for it in items:
513
+ ins = it.get("insertText") or ""
514
+ rng = it.get("range") or {}
515
+ start = rng.get("start") or {}
516
+ end = rng.get("end") or {}
517
+ # The item's range usually starts at the line start and
518
+ # re-states what's already typed: trim what precedes the cursor
519
+ # and count what follows it as a suffix.
520
+ if start.get("line") == line and start.get("character", char) < char:
521
+ already = text[ls + _utf16_to_index(text[ls:], start["character"]):cursor]
522
+ if ins.startswith(already):
523
+ ins = ins[len(already):]
524
+ replace_to = 0
525
+ if end.get("line") == line and end.get("character", char) > char:
526
+ replace_to = _utf16_to_index(text[cursor:], end["character"] - char)
527
+ texts.append((ins, replace_to, it))
528
+ first_ins, first_rep, first_item = texts[0]
529
+ return FimResult(first_ins, replace_to=first_rep,
530
+ alternatives=tuple(t for t, _r, _i in texts[1:] if t),
531
+ token=first_item, provider="copilot")
532
+
533
+ def shown(self, item):
534
+ try:
535
+ self.rpc.notify("textDocument/didShowCompletion", {"item": item})
536
+ except _RpcError:
537
+ pass
538
+
539
+ def accepted(self, item, accepted_length):
540
+ try:
541
+ full = len(item.get("insertText") or "")
542
+ if accepted_length >= full:
543
+ cmd = item.get("command") or {}
544
+ if cmd.get("command"):
545
+ self.rpc.request("workspace/executeCommand",
546
+ {"command": cmd["command"], "arguments": cmd.get("arguments") or []},
547
+ timeout=10.0)
548
+ else:
549
+ self.rpc.notify("textDocument/didPartiallyAcceptCompletion",
550
+ {"item": item, "acceptedLength": accepted_length})
551
+ except _RpcError:
552
+ pass
553
+
554
+
555
+ def _utf16_len(s: str) -> int:
556
+ return sum(2 if ord(c) > 0xFFFF else 1 for c in s)
557
+
558
+
559
+ def _utf16_to_index(s: str, units: int) -> int:
560
+ n = 0
561
+ for i, c in enumerate(s):
562
+ if n >= units:
563
+ return i
564
+ n += 2 if ord(c) > 0xFFFF else 1
565
+ return len(s)
566
+
567
+
568
+ def _notify_account_change():
569
+ """Repaint whoever shows account status (the Internet Accounts window)."""
570
+ try:
571
+ from meltygui.accounts.internet_accounts import accounts_changed
572
+ accounts_changed()
573
+ except Exception:
574
+ pass
575
+
576
+
577
+ # ──────────────────────────────────────────────────────────────────────────
578
+ # Provider
579
+ # ──────────────────────────────────────────────────────────────────────────
580
+
581
+ @fim_provider(name="copilot", session=CopilotSession)
582
+ def copilot_fim(req: FimRequest, session: CopilotSession, neighbors=4) -> FimResult:
583
+ """Copilot completion for the request's file. The LS builds its own
584
+ prompt from the documents it has seen, so the editor's context reaches
585
+ it as neighbor documents: the files of up to `neighbors` referenced
586
+ definitions are opened alongside."""
587
+ st = session.status()
588
+ if st[0] == "needs_login":
589
+ raise RuntimeError(f"Copilot: sign in with code {st[1]} at {st[2]}")
590
+ if st[0] == "error":
591
+ raise RuntimeError(f"Copilot: {st[1]}")
592
+ if req.context is not None and neighbors:
593
+ seen = 0
594
+ for it in req.context.items:
595
+ if it.kind in ("definition", "signature") and it.path and it.path != req.path:
596
+ try:
597
+ from meltygui.code.symbol_roster import file_text
598
+ session.sync_document(it.path, file_text(it.path))
599
+ seen += 1
600
+ except Exception:
601
+ pass
602
+ if seen >= neighbors:
603
+ break
604
+ return session.complete(req.path or "untitled", req.text, req.cursor, req.cancelled,
605
+ language=req.language or "python")
606
+
607
+
608
+ def _copilot_shown(req, result):
609
+ pass
610
+
611
+
612
+ def _copilot_accept(result, accepted_chars):
613
+ pass
614
+
615
+
616
+ copilot_fim._on_shown = _copilot_shown
617
+ copilot_fim._on_accept = _copilot_accept