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,352 @@
1
+ """Dict-first UI contract. Reconcile once AFTER an edit, not inside pop/del.
2
+
3
+ Backends publish inbound changes on the owner thread via drain(). The UI
4
+ only mutates dicts; no RPC or remote lifecycle vocabulary crosses this boundary.
5
+ """
6
+ import queue
7
+ import threading
8
+ import time
9
+
10
+ from meltygui.chat.messages import Message
11
+ from meltygui.chat.messages import UserMessage
12
+ from meltygui.chat.messages import user_message
13
+ from meltygui.chat.messages import input_text
14
+
15
+
16
+ def writer_conflict(error):
17
+ message = str(error or "").lower()
18
+ return ("active writer" in message or
19
+ ("session" in message and "already in use" in message))
20
+
21
+
22
+ def epoch_seconds(value):
23
+ """A provider's timestamp as epoch seconds: seconds or milliseconds
24
+ (anything past 1e11 is milliseconds), an ISO-8601 string, or 0 when
25
+ absent / unreadable."""
26
+ if value is None or value == "":
27
+ return 0.0
28
+ if isinstance(value, str):
29
+ try:
30
+ from datetime import datetime, timezone
31
+ parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
32
+ if parsed.tzinfo is None:
33
+ parsed = parsed.replace(tzinfo=timezone.utc)
34
+ return parsed.timestamp()
35
+ except ValueError:
36
+ return 0.0
37
+ try:
38
+ number = float(value)
39
+ except (TypeError, ValueError):
40
+ return 0.0
41
+ return number / 1000.0 if number > 1e11 else number
42
+
43
+
44
+ def apply_queue_edit(chat, operation, value=None):
45
+ """Queue edits are idempotent so a reconnect can safely replay them."""
46
+ queued = chat.setdefault("queued_messages", {})
47
+ if operation == "queue_message":
48
+ identifier, message, interrupt = value
49
+ if identifier in queued or identifier in chat["messages"] or identifier in chat.sent:
50
+ return
51
+ if interrupt:
52
+ chat["queue_paused"] = False
53
+ busy = chat["running"] or chat.turn_id or "send" in chat.inflight
54
+ if busy:
55
+ chat["queued_messages"] = {identifier: message, **queued}
56
+ chat["running"] = False
57
+ else:
58
+ chat["messages"][identifier] = message
59
+ chat.scanned = -1
60
+ if chat.get("retryable_error"):
61
+ chat.error = None
62
+ chat["retryable_error"] = False
63
+ else:
64
+ queued[identifier] = message
65
+ elif operation == "send_queued":
66
+ message = queued.pop(value, None)
67
+ if message is not None:
68
+ apply_queue_edit(chat, "queue_message", (value, message, True))
69
+ elif operation == "cancel_queued":
70
+ queued.pop(value, None)
71
+ elif operation == "resume_queue":
72
+ chat["queue_paused"] = False
73
+ elif operation == "stop_queue":
74
+ chat["queue_paused"] = True
75
+ chat["running"] = False
76
+
77
+
78
+ class Chat(dict):
79
+ """``updated`` is the conversation's last activity as epoch seconds (0 =
80
+ unknown): the provider's listing stamps it, a backend bumps it as a turn
81
+ streams, the proxy bumps it on send. The sidebar's age filter reads it."""
82
+
83
+ def __init__(self, value=None, remote_id=None):
84
+ super().__init__(title="New conversation", project="", messages={}, requests={}, running=False,
85
+ updated=0.0)
86
+ self.update(value or {})
87
+ self.remote_id = remote_id
88
+ self.applied_tint = None
89
+ self.loaded = remote_id is None
90
+ self.loading = False
91
+ self.refreshing = False # re-reading the session another client is writing
92
+ self.inflight = set()
93
+ self.sent = set()
94
+ self.saved_title = self["title"]
95
+ self.turn_id = None
96
+ self.resumed = remote_id is None
97
+ self.error = None
98
+
99
+
100
+ class ChatProxy(dict):
101
+ def _queue_edit(self, key, operation, value=None):
102
+ apply_queue_edit(self[key], operation, value)
103
+ self.revision += 1
104
+ return True
105
+
106
+ def queue_message(self, key, message, interrupt=False):
107
+ import uuid
108
+ return self._queue_edit(key, "queue_message", (str(uuid.uuid4()), message, interrupt))
109
+
110
+ def stop_chat(self, key):
111
+ self._queue_edit(key, "stop_queue")
112
+
113
+ def cancel_queued(self, key, identifier):
114
+ self._queue_edit(key, "cancel_queued", identifier)
115
+
116
+ def resume_queue(self, key):
117
+ self._queue_edit(key, "resume_queue")
118
+
119
+ def send_queued(self, key, identifier):
120
+ return self._queue_edit(key, "send_queued", identifier)
121
+
122
+ def __init__(self, account_id, metadata=None, wake=None):
123
+ super().__init__()
124
+ self.session_version = 3
125
+ self.account_id = account_id
126
+ from meltygui.chat.metadata import shared_metadata
127
+ self.metadata = metadata if metadata is not None else shared_metadata()
128
+ self.projects = self.metadata.account(account_id)["projects"]
129
+ self.wake = wake or (lambda: None)
130
+ self.events = queue.Queue()
131
+ self.jobs = queue.Queue()
132
+ self.closed = False
133
+ self.loading = True
134
+ self.revision = 0
135
+ self.error = None
136
+ self.known = {}
137
+ self.applied_order = None
138
+ self.refreshed = 0.0 # when the window last asked for `refresh`
139
+ self._applied_revision = -1
140
+ self.worker = threading.Thread(target=self._work, daemon=True, name="chat-backend")
141
+ self.worker.start()
142
+
143
+ def __setitem__(self, key, value):
144
+ chat = value if isinstance(value, Chat) else Chat(value)
145
+ chat.metadata = self.metadata.conversation(self.account_id, chat["project"], key)
146
+ super().__setitem__(key, chat)
147
+
148
+ def __getitem__(self, key):
149
+ chat = super().__getitem__(key)
150
+ self.active_key = key
151
+ self._load_history(key, chat)
152
+ return chat
153
+
154
+ def _load_history(self, key, chat):
155
+ operation = getattr(chat, "pending_history", None)
156
+ if ((not chat.loaded or operation) and not chat.loading and not chat.refreshing
157
+ and not chat["running"] and "send" not in chat.inflight):
158
+ chat.loading = not chat.loaded
159
+ chat.refreshing = chat.loaded
160
+ chat.pending_history = None
161
+ self.submit(operation or "hydrate", key)
162
+
163
+ def refresh_history(self, key, operation="hydrate"):
164
+ """Keep inactive transcripts cached; refresh them when displayed again."""
165
+ chat = dict.get(self, key)
166
+ if chat is not None and chat.loaded:
167
+ chat.pending_history = operation
168
+ if getattr(self, "active_key", None) == key:
169
+ self._load_history(key, chat)
170
+
171
+ def publish(self, kind, value):
172
+ if not self.closed:
173
+ needs_wake = self.events.empty()
174
+ self.events.put((kind, value))
175
+ if needs_wake:
176
+ self.wake()
177
+
178
+ def submit(self, operation, *args):
179
+ if not self.closed:
180
+ self.jobs.put((operation, args))
181
+
182
+ def _work(self):
183
+ try:
184
+ self.connect()
185
+ while not self.closed:
186
+ try:
187
+ job = self.jobs.get(timeout=5.0)
188
+ except queue.Empty:
189
+ self.refresh()
190
+ continue
191
+ if job is None:
192
+ break
193
+ operation, args = job
194
+ try:
195
+ self.execute(operation, *args)
196
+ except Exception as error:
197
+ self.publish("failure", (operation, args, str(error)))
198
+ except Exception as error:
199
+ self.publish("failure", ("connect", (), str(error)))
200
+ finally:
201
+ self.disconnect()
202
+
203
+ def drain(self):
204
+ changed = False
205
+ while True:
206
+ try:
207
+ kind, value = self.events.get_nowait()
208
+ except queue.Empty:
209
+ break
210
+ self.receive(kind, value)
211
+ changed = True
212
+ if changed:
213
+ self.revision += 1
214
+ return changed
215
+
216
+ def reconcile(self):
217
+ """A pop+insert is order-only. A missing key at this boundary is removal."""
218
+ # Normalize dict.update/setdefault insertions before mirroring them.
219
+ for key, value in list(self.items()):
220
+ if not isinstance(value, Chat):
221
+ self[key] = value
222
+ # The metadata passes walk every conversation: only if the set,
223
+ # the order, an event batch or a tint override changed since last time.
224
+ keys = list(self)
225
+ if (keys != self.applied_order or self.revision != self._applied_revision or self._tints_changed()):
226
+ self.metadata.collect(self.account_id, self)
227
+ self.metadata.apply(self.account_id, self)
228
+ self._applied_revision = self.revision
229
+ for key, chat in list(self.known.items()):
230
+ if key not in self and "archive" not in chat.inflight and "create" not in chat.inflight:
231
+ chat.inflight.add("archive")
232
+ if chat.remote_id:
233
+ self.submit("archive", key, chat.remote_id)
234
+ else:
235
+ self.known.pop(key, None)
236
+ for key, value in list(self.items()):
237
+ if not isinstance(value, Chat):
238
+ self[key] = value
239
+ chat = dict.__getitem__(self, key)
240
+ self.known[key] = chat
241
+ if chat.error:
242
+ continue
243
+ if not chat.remote_id:
244
+ if "create" not in chat.inflight:
245
+ chat.inflight.add("create")
246
+ chat["updated"] = chat["updated"] or time.time()
247
+ self.submit("create", key, chat["project"], chat["title"])
248
+ continue
249
+ if chat["title"] != chat.saved_title and "rename" not in chat.inflight:
250
+ chat.inflight.add("rename")
251
+ self.submit("rename", key, chat.remote_id, chat["title"])
252
+ if not chat["running"] and "send" in chat.inflight:
253
+ chat.inflight.add("interrupt_requested")
254
+ if not chat["running"] and chat.turn_id and "interrupt" not in chat.inflight:
255
+ chat.inflight.add("interrupt")
256
+ self.submit("interrupt", key, chat.remote_id, chat.turn_id)
257
+ if (chat.loaded and not chat["running"] and not chat.turn_id and "send" not in chat.inflight
258
+ and not chat.get("queue_paused") and not chat.get("locked") and not chat.get("external_busy")
259
+ and chat.get("queued_messages")
260
+ and not any(identifier not in chat.sent and message.get("role") == "user"
261
+ for identifier, message in chat["messages"].items())):
262
+ identifier = next(iter(chat["queued_messages"]))
263
+ chat["messages"][identifier] = chat["queued_messages"].pop(identifier)
264
+ self.revision += 1
265
+ if (chat.loaded and not chat["running"] and not chat.turn_id and "send" not in chat.inflight
266
+ and (len(chat["messages"]), len(chat.sent)) != getattr(chat, "scanned", None)):
267
+ # A prompt can remain after the previous send was acknowledged,
268
+ # even if the transcript did not grow in the meantime.
269
+ chat.scanned = (len(chat["messages"]), len(chat.sent))
270
+ for message_id, message in chat["messages"].items():
271
+ # Adopt dict-inserted prompts from UI callers at the model boundary.
272
+ if not isinstance(message, Message) and message.get("role") == "user":
273
+ message = chat["messages"][message_id] = (UserMessage("userMessage", content=message["content"])
274
+ if "content" in message else user_message(message.get("text", "")))
275
+ if message_id not in chat.sent and message.get("role") == "user":
276
+ chat.inflight.add("send")
277
+ chat["running"] = True
278
+ chat["updated"] = time.time()
279
+ chat["last_user_at"] = chat["updated"]
280
+ self.submit("send", key, chat.remote_id, message_id, input_text(message), chat.resumed)
281
+ break
282
+ for request_id, request in chat["requests"].items():
283
+ if "answer" in request and not request.get("submitted"):
284
+ request["submitted"] = True
285
+ self.submit("answer", key, request_id, request["answer"])
286
+
287
+ def _tints_changed(self):
288
+ """A tint edited on a chat's metadata entry (the sidebar's chip) or
289
+ pushed through its __overrides__ since the last apply."""
290
+ for chat in self.values():
291
+ applied = getattr(chat, "applied_tint", None)
292
+ if chat.get("__overrides__", {}).get("tint") != applied:
293
+ return True
294
+ entry = getattr(chat, "metadata", None)
295
+ if entry is not None and entry.get("tint") != applied:
296
+ return True
297
+ return False
298
+
299
+ def fork(self, key):
300
+ import uuid
301
+ source = self[key]
302
+ if not source.remote_id:
303
+ return None
304
+ new_key = str(uuid.uuid4())
305
+ self[new_key] = {"title": source["title"] + " (fork)", "project": source["project"],
306
+ "created_at": time.time(), "updated": time.time()}
307
+ chat = dict.__getitem__(self, new_key)
308
+ chat.loaded, chat.loading = False, True
309
+ chat.inflight.add("create")
310
+ self.known[new_key] = chat
311
+ for field in ("model", "permissions", "effort", "model_explicit", "model_selected_at", "permissions_selected_at",
312
+ "effort_selected_at", "service_tier", "service_tier_selected_at"):
313
+ if field in source.metadata:
314
+ chat.metadata[field] = source.metadata[field]
315
+ if "codex_settings" in source:
316
+ chat["codex_settings"] = dict(source["codex_settings"])
317
+ chat["codex_settings_at"] = source.get("codex_settings_at", 0)
318
+ self.submit("fork", new_key, source.remote_id, source["project"], chat["title"])
319
+ return new_key
320
+
321
+ def finish_fork(self, key, remote_id):
322
+ chat = self.known.get(key)
323
+ if chat is not None:
324
+ chat.remote_id = remote_id
325
+ chat.metadata["remote_id"] = remote_id
326
+ chat.inflight.discard("create")
327
+ chat.loading = False
328
+ chat.fresh = False
329
+ chat.resumed = False
330
+
331
+ def refresh(self):
332
+ """Look for activity outside this window — a session another client
333
+ (a terminal) is writing — and publish it: a chat's ``updated`` moves,
334
+ a new session is listed. The window asks every few seconds. Default:
335
+ nothing to look at."""
336
+
337
+ def close(self):
338
+ self.closed = True
339
+ self.jobs.put(None)
340
+ # Transport closes on the worker after its bounded in-flight call.
341
+
342
+ def connect(self):
343
+ raise NotImplementedError
344
+
345
+ def execute(self, operation, *args):
346
+ raise NotImplementedError
347
+
348
+ def receive(self, kind, value):
349
+ raise NotImplementedError
350
+
351
+ def disconnect(self):
352
+ pass