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,100 @@
1
+ """Codex catalog capabilities and saved thread settings, shared with the UI."""
2
+ import json
3
+ from pathlib import Path
4
+
5
+ from meltygui.chat.chat_proxy import epoch_seconds
6
+
7
+
8
+ def model_service_tiers(row):
9
+ if "serviceTiers" in row:
10
+ return tuple(tier for tier in row["serviceTiers"] if tier.get("id"))
11
+ return tuple({"id": tier, "name": "Fast" if tier in ("fast", "priority") else tier}
12
+ for tier in row.get("additionalSpeedTiers", []))
13
+
14
+
15
+ def fast_service_tier(proxy, model):
16
+ return next((tier["id"] for tier in getattr(proxy, "model_service_tiers", {}).get(model, ())
17
+ if tier["id"] in ("priority", "fast") or tier.get("name", "").lower() == "fast"), None)
18
+
19
+
20
+ def effective_settings(chat, defaults, default_model=None):
21
+ saved = chat.get("codex_settings", {})
22
+ saved_at = chat.get("codex_settings_at", 0)
23
+ metadata = chat.metadata
24
+ result = {}
25
+ for field, config_field in (("model", "model"), ("effort", "model_reasoning_effort"),
26
+ ("service_tier", "service_tier")):
27
+ explicit = field in metadata and (field != "model" or metadata.get("model_explicit"))
28
+ if explicit and metadata.get(field + "_selected_at", 0) >= saved_at:
29
+ value = metadata[field]
30
+ if field != "service_tier" and value in (None, "", "default"):
31
+ value = defaults.get(config_field)
32
+ result[field] = value
33
+ elif field in saved:
34
+ result[field] = saved[field]
35
+ elif config_field in defaults and (field != "service_tier" or defaults[config_field] is not None):
36
+ result[field] = defaults[config_field]
37
+ result["model"] = result.get("model") or default_model
38
+ return result
39
+
40
+
41
+ class ThreadSettingsReader:
42
+ """Worker-only incremental log reader; initial reads scan backwards."""
43
+ def __init__(self):
44
+ self.files = {}
45
+
46
+ @staticmethod
47
+ def parse(line):
48
+ if b'"thread_settings_applied"' not in line:
49
+ return None
50
+ try:
51
+ row = json.loads(line)
52
+ except (ValueError, UnicodeError):
53
+ return None
54
+ payload = row.get("payload") or {}
55
+ if row.get("type") != "event_msg" or payload.get("type") != "thread_settings_applied":
56
+ return None
57
+ values = payload.get("thread_settings") or {}
58
+ return {"codex_settings": {target: values[source] for source, target in
59
+ (("model", "model"), ("reasoning_effort", "effort"), ("service_tier", "service_tier"))
60
+ if source in values}, "codex_settings_at": epoch_seconds(row.get("timestamp"))}
61
+
62
+ def read(self, path):
63
+ if not path:
64
+ return {}
65
+ path = Path(path)
66
+ previous = self.files.get(path)
67
+ try:
68
+ stat = path.stat()
69
+ signature = (stat.st_ino, stat.st_size, stat.st_mtime_ns)
70
+ if previous and previous[0] == signature:
71
+ return previous[2]
72
+ with path.open("rb") as handle:
73
+ if previous and previous[0][0] == stat.st_ino and stat.st_size > previous[0][1]:
74
+ offset, settings = previous[1:]
75
+ handle.seek(offset)
76
+ while line := handle.readline():
77
+ if not line.endswith(b'\n'):
78
+ break
79
+ settings = self.parse(line) or settings
80
+ offset = handle.tell()
81
+ else:
82
+ offset, settings = stat.st_size, {}
83
+ position, tail, first = stat.st_size, b"", True
84
+ while position and not settings:
85
+ count = min(position, 65536)
86
+ position -= count
87
+ handle.seek(position)
88
+ parts = (handle.read(count) + tail).split(b"\n")
89
+ tail = parts.pop(0) if position else b""
90
+ if first and parts:
91
+ offset -= len(parts.pop()) # exclude the partial trailing record
92
+ first = False
93
+ for line in reversed(parts):
94
+ settings = self.parse(line) or {}
95
+ if settings:
96
+ break
97
+ self.files[path] = (signature, offset, settings)
98
+ return settings
99
+ except OSError:
100
+ return previous[2] if previous else {}
@@ -0,0 +1,60 @@
1
+ """Multiplexed, persistent version of the account-only stdio transport."""
2
+ import json
3
+ import queue
4
+ import threading
5
+
6
+ from meltygui.completion.providers.codex_accounts import AppServer
7
+
8
+
9
+ class CodexTransport(AppServer):
10
+ def __init__(self, home, executable="", timeout=30, on_event=None):
11
+ self.pending = {}
12
+ self.write_lock = threading.Lock()
13
+ self.pending_lock = threading.Lock()
14
+ self.on_event = on_event or (lambda event: None)
15
+ super().__init__(home, executable, timeout)
16
+
17
+ def _send(self, message):
18
+ with self.write_lock:
19
+ super()._send(message)
20
+
21
+ def _read(self):
22
+ try:
23
+ for line in self.process.stdout:
24
+ message = json.loads(line)
25
+ if "method" in message:
26
+ self.on_event(message)
27
+ else:
28
+ with self.pending_lock:
29
+ response = self.pending.get(message.get("id"))
30
+ if response is not None:
31
+ response.put(message)
32
+ except (ValueError, OSError) as error:
33
+ self.on_event({"method": "transport/error", "params": {"message": str(error)}})
34
+ finally:
35
+ with self.pending_lock:
36
+ for response in self.pending.values():
37
+ response.put({"error": {"message": "Codex disconnected"}})
38
+ self.on_event({"method": "transport/error", "params": {"message": "Codex disconnected"}})
39
+
40
+ def request(self, method, params=None):
41
+ response = queue.Queue()
42
+ with self.pending_lock:
43
+ self.sequence += 1
44
+ identifier = self.sequence
45
+ self.pending[identifier] = response
46
+ try:
47
+ self._send({"method": method, "id": identifier, "params": params or {}})
48
+ try:
49
+ message = response.get(timeout=self.timeout)
50
+ except queue.Empty:
51
+ raise TimeoutError(f"Codex timed out: {method}") from None
52
+ if "error" in message:
53
+ raise RuntimeError(message["error"].get("message", "Codex request failed"))
54
+ return message.get("result") or {}
55
+ finally:
56
+ with self.pending_lock:
57
+ self.pending.pop(identifier, None)
58
+
59
+ def answer(self, request_id, result=None, error=None):
60
+ self._send({"id": request_id, **({"error": error} if error else {"result": result})})
@@ -0,0 +1,204 @@
1
+ """Static command inspection. Never execute commands, import code, or read files."""
2
+ import ast
3
+ import posixpath
4
+ import re
5
+ import shlex
6
+
7
+
8
+ def shell_body(command):
9
+ """Remove a literal shell -c wrapper, retaining the original script text."""
10
+ for _ in range(3):
11
+ try:
12
+ words = shlex.split(command)
13
+ except ValueError:
14
+ break
15
+ if len(words) >= 3 and posixpath.basename(words[0]) in ('bash', 'sh', 'zsh'):
16
+ flag = next((i for i, word in enumerate(words[1:], 1)
17
+ if word.startswith('-') and 'c' in word), None)
18
+ if flag is not None and flag + 1 < len(words):
19
+ command = words[flag + 1]
20
+ continue
21
+ break
22
+ return command
23
+
24
+
25
+ def parse_command(command, actions=(), scripts=None, cwd=""):
26
+ """Return literal file references and captured script bodies.
27
+
28
+ `access` is an operation requested by the command, not proof it succeeded.
29
+ Unknown variables, substitutions, imports and function calls stay unknown.
30
+ """
31
+ files = {}
32
+ scripts = scripts if scripts is not None else {}
33
+
34
+ def add(path, access='read', explicit=False):
35
+ if not isinstance(path, str) or not path or path.startswith('-'):
36
+ return
37
+ if any(char in path for char in '\n\r$*?{}|') or '://' in path:
38
+ return
39
+ if not explicit and not re.fullmatch(r'[\w./@ +\-]+\.[\w]+', path):
40
+ return
41
+ path = posixpath.normpath(posixpath.join(cwd, path))
42
+ old = files.get(path)
43
+ if old is None or access == 'write':
44
+ files[path] = access
45
+
46
+ def python(source):
47
+ try:
48
+ tree = ast.parse(source)
49
+ except (SyntaxError, ValueError):
50
+ return
51
+ env = {}
52
+
53
+ def resolve(node):
54
+ if isinstance(node, ast.Constant) and isinstance(node.value, str):
55
+ return node.value
56
+ if isinstance(node, ast.Name):
57
+ return env.get(node.id)
58
+ if isinstance(node, ast.BinOp):
59
+ left, right = resolve(node.left), resolve(node.right)
60
+ if left is not None and right is not None:
61
+ if isinstance(node.op, ast.Div):
62
+ return posixpath.join(left, right)
63
+ if isinstance(node.op, ast.Add):
64
+ return left + right
65
+ if isinstance(node, ast.Call):
66
+ name = getattr(node.func, 'id', getattr(node.func, 'attr', ''))
67
+ if name in ('Path', 'PurePath', 'PurePosixPath') and node.args:
68
+ parts = [resolve(arg) for arg in node.args]
69
+ if all(part is not None for part in parts):
70
+ return posixpath.join(*parts)
71
+ if name == 'open' and node.args:
72
+ return resolve(node.args[0])
73
+ if isinstance(node.func, ast.Attribute) and name == 'joinpath':
74
+ base = resolve(node.func.value)
75
+ parts = [resolve(arg) for arg in node.args]
76
+ if base is not None and all(part is not None for part in parts):
77
+ return posixpath.join(base, *parts)
78
+ return None
79
+
80
+ def visit(node):
81
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
82
+ return # a definition is not evidence that its body ran
83
+ if isinstance(node, ast.Assign):
84
+ value = resolve(node.value)
85
+ for target in node.targets:
86
+ if isinstance(target, ast.Name):
87
+ env[target.id] = value
88
+ if isinstance(node, ast.With):
89
+ for item in node.items:
90
+ if isinstance(item.optional_vars, ast.Name):
91
+ env[item.optional_vars.id] = resolve(item.context_expr)
92
+ if isinstance(node, ast.For) and isinstance(node.target, ast.Name) and isinstance(node.iter, (ast.List, ast.Tuple)):
93
+ before = dict(env)
94
+ for value in node.iter.elts[:100]:
95
+ env[node.target.id] = resolve(value)
96
+ for child in node.body:
97
+ visit(child)
98
+ env.clear()
99
+ env.update(before)
100
+ return
101
+ if isinstance(node, ast.Call):
102
+ name = getattr(node.func, 'id', getattr(node.func, 'attr', ''))
103
+ if name == 'open' and node.args:
104
+ mode = resolve(node.args[1]) if len(node.args) > 1 else 'r'
105
+ mode = next((resolve(kw.value) for kw in node.keywords if kw.arg == 'mode'), mode)
106
+ add(resolve(node.args[0]), 'write' if mode and any(c in mode for c in 'wax+') else 'read', True)
107
+ if isinstance(node.func, ast.Attribute):
108
+ path = resolve(node.func.value)
109
+ if name in ('write_text', 'write_bytes', 'write', 'writelines', 'unlink', 'touch'):
110
+ add(path, 'write', True)
111
+ elif name in ('read_text', 'read_bytes', 'read', 'readlines'):
112
+ add(path, 'read', True)
113
+ elif name in ('rename', 'replace') and path is not None and node.args:
114
+ add(path, 'write', True)
115
+ add(resolve(node.args[0]), 'write', True)
116
+ for child in ast.iter_child_nodes(node):
117
+ visit(child)
118
+ visit(tree)
119
+
120
+ source = shell_body(str(command))
121
+ # Heredocs expose Python directly, or the body of a script written
122
+ # in one history item and run by a later item. Remember only the
123
+ # literal scripts; never consult the filesystem for an indirect script.
124
+ pattern = re.compile(r"^([^\n]*?)<<-?\s*(['\"]?)([A-Za-z_]\w*)\2([^\n]*)\n(.*?)^\3[ \t]*(?:\n|$)", re.M | re.S)
125
+ segments = []
126
+ last = 0
127
+ for match in pattern.finditer(source):
128
+ segments.append(source[last:match.start()])
129
+ head, body = match[1] + match[4], match[5]
130
+ try:
131
+ words = shlex.split(head)
132
+ except ValueError:
133
+ words = []
134
+ if any(re.fullmatch(r'python(?:\d+(?:\.\d+)?)?', posixpath.basename(w)) for w in words[:2]):
135
+ python(body)
136
+ else:
137
+ target = re.search(r'>\s*([^\s]+)', head)
138
+ if target:
139
+ path = target[1].strip("'\"")
140
+ add(path, 'write', True)
141
+ if path.endswith('.py'):
142
+ scripts[posixpath.normpath(path)] = body
143
+ if 'apply_patch' in head:
144
+ for path in re.findall(r'^\*\*\* (?:Update|Add|Delete) File: (.+)$', body, re.M):
145
+ add(path, 'write', True)
146
+ segments.append(head + '\n')
147
+ last = match.end()
148
+ segments.append(source[last:])
149
+ source_without_bodies = ''.join(segments)
150
+ try:
151
+ lexer = shlex.shlex(source_without_bodies, posix=True, punctuation_chars=';&|<>\n')
152
+ lexer.whitespace_split = True
153
+ lexer.whitespace = ' \t\r'
154
+ words = list(lexer)
155
+ except ValueError:
156
+ words = []
157
+ groups, group = [], []
158
+ for word in words + [';']:
159
+ if word and all(char in ';|&\n' for char in word):
160
+ if group:
161
+ groups.append(group)
162
+ group = []
163
+ else:
164
+ group.append(word)
165
+ for group in groups:
166
+ executable = posixpath.basename(group[0])
167
+ if re.fullmatch(r'python(?:\d+(?:\.\d+)?)?', executable):
168
+ if '-c' in group and group.index('-c') + 1 < len(group):
169
+ python(group[group.index('-c') + 1])
170
+ else:
171
+ for word in group[1:]:
172
+ if word.endswith('.py'):
173
+ add(word)
174
+ captured = scripts.get(posixpath.normpath(word))
175
+ if captured is not None:
176
+ python(captured)
177
+ for index, word in enumerate(group):
178
+ if word in ('>', '>>') and index + 1 < len(group):
179
+ add(group[index + 1], 'write', True)
180
+ elif index > 0:
181
+ access = 'write' if executable in ('tee', 'touch', 'rm', 'mv') or executable == 'sed' and any(w.startswith('-i') for w in group) else 'read'
182
+ if executable in ('cp', 'install') and index == len(group) - 1:
183
+ access = 'write'
184
+ add(word, access)
185
+ def add_action(path):
186
+ if not isinstance(path, str) or not path:
187
+ return
188
+ canonical = posixpath.normpath(posixpath.join(cwd, path))
189
+ if canonical in files:
190
+ return
191
+ # Providers might report only a suffix for a fully parsed operand.
192
+ # Resolve that suffix only when it identifies exactly one file.
193
+ matches = [known for known in files if known.endswith('/' + posixpath.normpath(path))]
194
+ if not posixpath.isabs(path) and len(matches) == 1:
195
+ return
196
+ add(path, 'read', True)
197
+
198
+ for action in actions or ():
199
+ if isinstance(action, dict):
200
+ if action.get('path'):
201
+ add_action(action['path'])
202
+ for path in action.get('paths') or []:
203
+ add_action(path)
204
+ return source, files
@@ -0,0 +1,227 @@
1
+ """Inline images for the chat transcript: decode off the render thread, upload
2
+ on it, draw fitted, HDR intact.
3
+
4
+ An `ImageReference` in a message names its picture one of three ways —
5
+ ``path`` (a file), ``data`` + ``media_type`` (a base64 payload, the Claude API
6
+ shape, also accepted nested under ``source``), or ``url`` (a data URL) — and
7
+ `image_key` reduces any of them to one cache key. `ImageCache.entry(ref)`
8
+ returns the picture's state: it queues a decode the first time (a worker
9
+ thread running meltygui's `image_load`, hdr-viewer's decoder: PQ PNGs and PQ
10
+ ICC profiles come out as linear scRGB above 1.0, everything else as sRGB8),
11
+ uploads the decoded pixels the first time the render thread asks (GL is
12
+ current inside a window body only), and is drawn with `draw_image` through
13
+ imgui's draw list — an RGB16F texture in meltygui's fp16 scene, so on an HDR
14
+ desktop the highlights present as HDR with nothing more to do, and on an
15
+ SDR desktop they clip at white like everything else meltygui draws.
16
+
17
+ Textures are few and small compared to the payloads: the cache keeps
18
+ ``keep`` of them and drops the least recently drawn beyond that.
19
+ """
20
+ import base64
21
+ import hashlib
22
+ import os
23
+ import queue
24
+ import threading
25
+ import time
26
+ import weakref
27
+
28
+ from meltygui.chat.messages import ImageReference
29
+
30
+ # The SDR reference white a PQ file is authored against (BT.2408: 203 nits);
31
+ # dividing by it makes 1.0 = the file's SDR white = what meltygui maps to the
32
+ # desktop's SDR white (the same mapping Chrome applies).
33
+ PQ_SDR_WHITE = 203.0
34
+ MEDIA_TYPES = {"image/png": ".png", "image/jpeg": ".jpg", "image/webp": ".webp", "image/gif": ".gif"}
35
+
36
+
37
+ def _source(ref):
38
+ """(path, bytes-or-None) of an ImageReference, or (None, None) when it
39
+ carries nothing decodable. Data URLs and base64 payloads are decoded here."""
40
+ source = ref.get("source") if isinstance(ref.get("source"), dict) else ref
41
+ path = source.get("path") or ref.get("path") or source.get("savedPath")
42
+ if path and not str(source.get("data") or "").strip():
43
+ return str(path), None
44
+ data = source.get("data")
45
+ url = source.get("url") or ref.get("url") or ref.get("image_url")
46
+ if isinstance(url, dict):
47
+ url = url.get("url")
48
+ if not data and isinstance(url, str) and url.startswith("data:"):
49
+ data = url.split(",", 1)[1] if "," in url else ""
50
+ if isinstance(data, str) and data.strip():
51
+ try:
52
+ return None, base64.b64decode(data, validate=False)
53
+ except (ValueError, TypeError):
54
+ return None, None
55
+ if isinstance(url, str) and url and not url.startswith("data:") and os.path.exists(url):
56
+ return url, None
57
+ return None, None
58
+
59
+
60
+ def image_key(ref):
61
+ """One stable key per picture, kept on the reference after the first call
62
+ (a payload is hashed once, never per frame). None: nothing to show."""
63
+ key = getattr(ref, "_image_key", False)
64
+ if key is not False:
65
+ return key
66
+ path, data = _source(ref)
67
+ if data is not None:
68
+ key = "data:" + hashlib.sha1(data).hexdigest()
69
+ elif path is not None:
70
+ try:
71
+ stat = os.stat(path)
72
+ key = f"path:{path}:{stat.st_mtime_ns}:{stat.st_size}"
73
+ except OSError:
74
+ key = "path:" + path
75
+ else:
76
+ key = None
77
+ try:
78
+ ref._image_key = key
79
+ except AttributeError:
80
+ pass
81
+ return key
82
+
83
+
84
+ class Entry:
85
+ """One picture. ``status``: loading → ready | failed. ``size`` (w, h)
86
+ arrives with the decode (the layout can fit the row before the upload),
87
+ ``texture`` with the first draw. ``hdr``: the source was PQ."""
88
+
89
+ def __init__(self, key):
90
+ self.key = key
91
+ self.status = "loading"
92
+ self.size = None
93
+ self.texture = None
94
+ self.hdr = False
95
+ self.peak_nits = 0.0
96
+ self.error = None
97
+ self.decoded = None # image_load.Loaded, until uploaded
98
+ self.last_drawn = time.monotonic()
99
+
100
+
101
+ class ImageCache:
102
+ def __init__(self, keep=48, wake=None):
103
+ self.entries = {}
104
+ self.keep = keep
105
+ self.wake = wake # called on the worker thread every decode: ask for a frame
106
+ self.jobs = queue.Queue()
107
+ self.worker = None
108
+ self.lock = threading.Lock()
109
+ self.views = weakref.WeakValueDictionary()
110
+ self.generation = 0 # bumped when a decode finishes, layouts keyed on it re-fit
111
+
112
+ def watch(self, draw_state):
113
+ if callable(getattr(draw_state, "invalidate_up", None)):
114
+ self.views[id(draw_state)] = draw_state
115
+
116
+ def decoded_changed(self):
117
+ self.generation += 1
118
+ for view in list(self.views.values()):
119
+ view.invalidate_up()
120
+ if self.wake is not None:
121
+ self.wake()
122
+
123
+ def entry(self, ref):
124
+ key = image_key(ref)
125
+ if key is None:
126
+ return None
127
+ entry = self.entries.get(key)
128
+ if entry is None:
129
+ entry = self.entries[key] = Entry(key)
130
+ self.jobs.put((entry, ref))
131
+ self._ensure_worker()
132
+ return entry
133
+
134
+ def _ensure_worker(self):
135
+ if self.worker is None or not self.worker.is_alive():
136
+ self.worker = threading.Thread(target=self._work, daemon=True, name="chat-images")
137
+ self.worker.start()
138
+
139
+ def _work(self):
140
+ while True:
141
+ try:
142
+ entry, ref = self.jobs.get(timeout=30)
143
+ except queue.Empty:
144
+ return
145
+ try:
146
+ import meltygui.image_load as image_load
147
+ path, data = _source(ref)
148
+ loaded = (image_load.load_bytes(data, PQ_SDR_WHITE) if data is not None
149
+ else image_load.load(path, PQ_SDR_WHITE))
150
+ with self.lock:
151
+ entry.decoded, entry.size = loaded, loaded.size
152
+ entry.hdr, entry.peak_nits = loaded.hdr, loaded.peak_nits
153
+ entry.status = "ready"
154
+ except Exception as error:
155
+ entry.error = f"{type(error).__name__}: {error}"
156
+ entry.status = "failed"
157
+ self.decoded_changed()
158
+
159
+ # -- render-only -------------------------------------------------------
160
+
161
+ def texture(self, entry):
162
+ """The GL texture of a decoded entry, uploaded on first use (call
163
+ inside a window body: GL is current there). None until decoded."""
164
+ if entry.texture is None and entry.decoded is not None:
165
+ with self.lock:
166
+ loaded, entry.decoded = entry.decoded, None
167
+ entry.texture = upload_texture(loaded)
168
+ self._evict()
169
+ entry.last_drawn = time.monotonic()
170
+ return entry.texture
171
+
172
+ def _evict(self):
173
+ uploaded = [e for e in self.entries.values() if e.texture is not None]
174
+ if len(uploaded) <= self.keep:
175
+ return
176
+ import OpenGL.GL as gl
177
+ for entry in sorted(uploaded, key=lambda e: e.last_drawn)[:len(uploaded) - self.keep]:
178
+ gl.glDeleteTextures(1, [entry.texture])
179
+ del self.entries[entry.key]
180
+
181
+
182
+ def upload_texture(image):
183
+ """image_load.Loaded -> GL texture id, row 0 of the array at v = 0 (draw
184
+ with uv (0, 0) → (1, 1) for the top of the picture at the top). Linear
185
+ float goes up as RGB16F, an opaque 8-bit sRGB source as SRGB8 (the GPU
186
+ linearises it on sample)."""
187
+ import numpy as np
188
+ import OpenGL.GL as gl
189
+ w, h = image.size
190
+ tex = int(gl.glGenTextures(1))
191
+ previous = gl.glGetIntegerv(gl.GL_TEXTURE_BINDING_2D)
192
+ gl.glBindTexture(gl.GL_TEXTURE_2D, tex)
193
+ gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1)
194
+ if image.srgb8 is not None:
195
+ gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_SRGB8, w, h, 0, gl.GL_RGB, gl.GL_UNSIGNED_BYTE,
196
+ np.ascontiguousarray(image.srgb8))
197
+ else:
198
+ gl.glTexImage2D(gl.GL_TEXTURE_2D, 0, gl.GL_RGB16F, w, h, 0, gl.GL_RGB, gl.GL_FLOAT,
199
+ np.ascontiguousarray(image.rgb, dtype=np.float32))
200
+ for p, v in ((gl.GL_TEXTURE_MIN_FILTER, gl.GL_LINEAR), (gl.GL_TEXTURE_MAG_FILTER, gl.GL_LINEAR),
201
+ (gl.GL_TEXTURE_WRAP_S, gl.GL_CLAMP_TO_EDGE), (gl.GL_TEXTURE_WRAP_T, gl.GL_CLAMP_TO_EDGE)):
202
+ gl.glTexParameteri(gl.GL_TEXTURE_2D, p, v)
203
+ gl.glBindTexture(gl.GL_TEXTURE_2D, int(previous))
204
+ return tex
205
+
206
+
207
+ def fitted_size(size, max_width, max_height):
208
+ """The picture's (w, h) inside the box, never upscaled."""
209
+ w, h = size
210
+ if w <= 0 or h <= 0:
211
+ return max_width, max_height
212
+ scale = min(1.0, max_width / w, max_height / h)
213
+ return max(1.0, w * scale), max(1.0, h * scale)
214
+
215
+
216
+ def image_label(ref, entry=None):
217
+ """The caption under a picture: its name, size and HDR peak when known."""
218
+ name = ref.get("name") or ref.get("path") or ""
219
+ if isinstance(ref.get("source"), dict):
220
+ name = name or ref["source"].get("path") or ""
221
+ name = os.path.basename(str(name)) if name else ""
222
+ parts = [part for part in (name,) if part]
223
+ if entry is not None and entry.size:
224
+ parts.append("%d×%d" % entry.size)
225
+ if entry.hdr:
226
+ parts.append(f"HDR, peak {entry.peak_nits:.0f} nits")
227
+ return " · ".join(parts)