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,732 @@
1
+ """Chat studio: draw-list navigation/transcript over provider-owned dictionaries."""
2
+ from meltygui.view.chat_decoration_view import _color
3
+ import colorsys
4
+ from bisect import bisect_right
5
+ import dataclasses
6
+ import math
7
+ import time
8
+ from functools import lru_cache
9
+ from contextlib import contextmanager
10
+ from pathlib import Path
11
+ import uuid
12
+
13
+ import meltygui_imgui as imgui
14
+ import numpy as np
15
+ from meltygui.hdr_color import pack_color
16
+ import meltygui.core.windowing.window_api as glfw
17
+
18
+ from meltygui.core.melty import Melty
19
+ from meltygui.chat.messages import Message
20
+ from meltygui.chat.messages import AssistantMessage
21
+ from meltygui.chat.messages import UserMessage
22
+ from meltygui.chat.messages import ToolCall
23
+ from meltygui.chat.messages import ReasoningMessage
24
+ from meltygui.chat.messages import PythonString
25
+ from meltygui.chat.messages import CodeString
26
+ from meltygui.chat.messages import Reference
27
+ from meltygui.chat.messages import ImageReference
28
+ from meltygui.chat.messages import user_message
29
+ from meltygui.chat.messages import BashString
30
+ from meltygui.chat.messages import ToolOutput
31
+ from meltygui.chat.messages import FileTags
32
+ from meltygui.chat.messages import CommandExecution
33
+ from meltygui.chat.messages import input_text
34
+ import meltygui.chat.images as chat_images
35
+ from meltygui.models.file_meta import FileMeta
36
+ from meltygui.models.file_meta import file_meta_store
37
+ from meltygui.model.file_metadata_model import set_row_tint
38
+ from meltygui.core.runtime.toggles import Tint
39
+ from meltygui.core.runtime.toggles import Toggles
40
+ from meltygui.core.styling.fonts import Font
41
+ from meltygui.core.conversion.dict_conversion import DictConversion
42
+ from meltygui.core.layout.column_core import ColumnLayout
43
+ from meltygui.core.layout.column_core import RowLayout
44
+ from meltygui.core.input.drag_drop_core import DragDrop
45
+ from meltygui.core.core_render import render_func
46
+ from meltygui.core.rendering.core_decoration import no_save
47
+ from meltygui.core.rendering.window_decoration import window
48
+ from meltygui.view.collection_view import draw_tuple_fast
49
+ from meltygui.view.decoration_view import draw_bg
50
+ from meltygui.view.header_view import flat_button
51
+ from meltygui.core.cache.tile_cache import add_shadow
52
+ from meltygui.view.texture_view import draw_texture
53
+ import meltygui.accounts.internet_accounts as internet_accounts
54
+
55
+
56
+ TRASH_ICON = "" # FontAwesome trash-alt
57
+
58
+ def image_cache():
59
+ """The transcript's pictures, decoded once per process (chat/images.py)."""
60
+ cache = getattr(Melty, "chat_image_cache", None)
61
+ if cache is None:
62
+ from meltygui.core.windowing.glfw_utils import request_render
63
+ cache = Melty.chat_image_cache = chat_images.ImageCache(wake=request_render)
64
+ return cache
65
+
66
+
67
+ def image_box(entry, max_width, size=None):
68
+ """The (w, h) a picture takes in the transcript: fitted into the span
69
+ and Toggles.Chat.image_max_height, a placeholder while it decodes."""
70
+ max_height = Melty.px(Toggles.Chat.image_max_height)
71
+ if entry is None or entry.status == "failed":
72
+ return max_width, 0
73
+ if entry.size is None:
74
+ return min(max_width, Melty.px(240)), Melty.px(120)
75
+ if size is not None:
76
+ return min(max_width, size[0]), size[1]
77
+ return chat_images.fitted_size(entry.size, max_width, max_height)
78
+
79
+
80
+ # A conversation counts as ACTIVE (the live dot) while a turn runs here or
81
+ # its session was written within this many seconds by anyone - a provider's
82
+ # Claude Code or another window; the window asks each backend to look every
83
+ # REFRESH_S seconds.
84
+ REFRESH_S = 5
85
+
86
+
87
+ # The sidebar's age filter chips: label → hours (0 = every conversation).
88
+ AGE_FILTERS = (("1h", 1), ("2h", 2), ("Day", 24), ("2 days", 48), ("All", 0))
89
+
90
+
91
+ def _tint_chip(meta, draw_state, key, x, y):
92
+ changed, tint = draw_tuple_fast(tuple(meta["tint"]), draw_state, key,
93
+ x=x, y=y, size=Melty.px(17), outline=True, priority_delta=6,
94
+ setter=lambda value: meta.__setitem__("tint", value))
95
+ if changed:
96
+ meta["tint"] = tint
97
+ return changed
98
+
99
+
100
+
101
+
102
+
103
+
104
+
105
+
106
+ def _tint_slot(draw_state, key, tint, x, y, hovered, default_tint, setter, show_brush=True, brush_tint=None):
107
+ """A row's tint control, the file browser's: the colour chip when the row
108
+ is painted, a faint paint-brush when not (a click stamps `default_tint`
109
+ in and opens the picker; `show_brush` False draws none — the selected
110
+ conversation and a hovered heading show theirs, the rest stay tidy).
111
+ Returns True when a tint was written."""
112
+ from meltygui.files.fast_file_explorer import tint_control
113
+ size = Melty.px(13)
114
+ text_y = y + max(0.0, (size - imgui.get_text_line_height()) / 2)
115
+ return tint_control(draw_state, key, tuple(tint) if tint else None, x, y, size, text_y, hovered,
116
+ tuple(default_tint), setter=setter, show_brush=show_brush,
117
+ brush_color=tuple(c * 0.55 * float(Toggles.Melty.arrow_brightness) for c in
118
+ _tint_style(tuple(brush_tint or default_tint)).make_color_style_value(input={
119
+ "value": 7.788, "saturation": 1.559, "max_value": 1.601})[:3]))
120
+
121
+
122
+ from meltygui.view.chat_view import _soft_wrap
123
+
124
+
125
+ def _text_layout(state, key, text, prefix="", font=Font.FONTAWESOME_MONO_19, wrap_width=None, keep=False):
126
+ """Stable display buffer and height; no per-frame string/kwargs churn.
127
+
128
+ Measure in the same font as draw_text. Explicit line breaks and a fixed
129
+ height keep offscreen layout identical to onscreen layout, like the
130
+ measured-pane skips in draw_stack_trace. `wrap_width` (px) soft-wraps the
131
+ DISPLAY buffer here — draw_text itself never reflows — so the height is
132
+ the wrapped height and the memo re-runs when the width changes.
133
+ """
134
+ font = Melty.font_mgr.get(font) if Melty.font_mgr else None
135
+ signature = (text, type(text), getattr(text, "language", None), Melty.ui_scale, font, prefix, "trimmed", wrap_width)
136
+ memo = state.text_layouts.get(key)
137
+ if memo is not None and memo[0][0] is text and (memo[0][1:] == signature[1:]
138
+ or keep and memo[0][1:-1] == signature[1:-1]):
139
+ # `keep`: an off-screen leaf mid-resize keeps its last wrap, whatever width it was in.
140
+ return memo[1], memo[2]
141
+ if font is not None:
142
+ imgui.push_font(font)
143
+ try:
144
+ line_px = imgui.get_text_line_height() * 1.2
145
+ display = prefix + text if prefix else text
146
+ if wrap_width is not None:
147
+ # The editor's text is a little in on its left edge; leave that slack.
148
+ columns = int((wrap_width - Melty.px(16)) / max(1.0, imgui.calc_text_size("M").x))
149
+ display = _soft_wrap(str(display), columns)
150
+ if display.endswith(("\n", "\r")) or display is not text:
151
+ trimmed = display.rstrip("\r\n")
152
+ display = type(text)(trimmed, text.language) if isinstance(text, CodeString) else type(text)(trimmed)
153
+ # str(): an EMPTY string subclass splits to [itself], and imgui's typed
154
+ # `text` argument refuses subclasses (MarkdownString) — the 07:49 crash.
155
+ text_width = max((imgui.calc_text_size(line).x for line in str(display).split("\n")), default=0)
156
+ memo = (signature, display, (display.count("\n") + 1) * line_px, text_width)
157
+ state.text_layouts[key] = memo
158
+ return memo[1], memo[2]
159
+ finally:
160
+ if font is not None:
161
+ imgui.pop_font()
162
+
163
+
164
+ from meltygui.view.chat_view import _scroll_position
165
+
166
+
167
+ @contextmanager
168
+ def _viewport(draw_state, state, key, width, height, content_height, follow=False):
169
+ """One clipped, independently scrolling region, owned by the window.
170
+
171
+ Only its visible footprint advances layout; content offsets never move
172
+ the composer or change the window's own scroll geometry.
173
+ """
174
+ view = state.viewports.setdefault(key, {"offset": 0.0, "follow": follow})
175
+ x, y = imgui.get_cursor_screen_pos()
176
+ rect = (x, y, x + width, y + height)
177
+ wheel = draw_state.on_action("scroll_y_changed", view_id=key + ":wheel",
178
+ rect=rect, priority_delta=40)
179
+ offset = _scroll_position(view, content_height, height, wheel.value if wheel is not None else 0)
180
+ maximum = max(0, content_height - height)
181
+ bar_width = Melty.px(7)
182
+ thumb_height = min(height, max(Melty.px(24), height * height / max(1.0, height, content_height)))
183
+ travel = height - thumb_height
184
+ if maximum > 0 and travel > 0:
185
+ thumb_y = y + travel * offset / maximum
186
+ grab = (x + width - bar_width, thumb_y, x + width, thumb_y + thumb_height)
187
+ for event in ("left_mouse_down", "left_mouse_held", "left_mouse_clicked"):
188
+ draw_state.on_action(event, view_id=key + ":grab", rect=grab, priority_delta=50)
189
+ drag = draw_state.on_action("left_mouse_drag", view_id=key + ":grab", rect=grab, priority_delta=50)
190
+ if drag is not None:
191
+ offset = _scroll_position(view, content_height, height,
192
+ drag_fraction=offset / maximum + drag.dy / travel)
193
+ Melty.push_clip(rect)
194
+ try:
195
+ yield x, y - offset, Melty.get_clip_rect()
196
+ if maximum > 0 and travel > 0:
197
+ thumb_y = y + travel * offset / maximum
198
+ add_shadow((x + width - bar_width, thumb_y, bar_width, thumb_height),
199
+ offset=Toggles.Chat.shadow_offset, corner_radius=3)
200
+ imgui.get_window_draw_list().add_rect_filled(
201
+ x + width - bar_width, thumb_y, x + width, thumb_y + thumb_height,
202
+ _color((0.55, 0.65, 0.65)), rounding=3)
203
+ finally:
204
+ Melty.pop_clip()
205
+ imgui.set_cursor_screen_pos((x, y))
206
+ imgui.dummy(width, height)
207
+
208
+
209
+ from meltygui.view.chat_view import _visible
210
+
211
+
212
+ from meltygui.view.chat_decoration_view import _tint_style
213
+
214
+
215
+ from meltygui.view.chat_decoration_view import _text_tint
216
+
217
+
218
+ _LABEL_WIDTHS = {}
219
+
220
+
221
+ def _label_width(text):
222
+ """Width of `text` in the editor font, memoised: the same tool labels,
223
+ captions and titles are measured every frame."""
224
+ key = (text, Melty.ui_scale, Melty.font_mgr is not None)
225
+ width = _LABEL_WIDTHS.get(key)
226
+ if width is not None:
227
+ return width
228
+ if len(_LABEL_WIDTHS) > 8192:
229
+ _LABEL_WIDTHS.clear()
230
+ font = Melty.font_mgr.get(Font.FONTAWESOME_MONO_19) if Melty.font_mgr else None
231
+ if font is not None:
232
+ imgui.push_font(font)
233
+ try:
234
+ width = _LABEL_WIDTHS[key] = imgui.calc_text_size(text).x
235
+ return width
236
+ finally:
237
+ if font is not None:
238
+ imgui.pop_font()
239
+
240
+
241
+ def _prose_metrics():
242
+ """(line height, character width) of prose in the editor's mono font."""
243
+ font = Melty.font_mgr.get(Font.FONTAWESOME_MONO_19) if Melty.font_mgr else None
244
+ if font is not None:
245
+ imgui.push_font(font)
246
+ try:
247
+ return imgui.get_text_line_height() * 1.2, max(1.0, imgui.calc_text_size("M").x)
248
+ finally:
249
+ if font is not None:
250
+ imgui.pop_font()
251
+
252
+
253
+ def _draw_prose(text, x, y, width, height, tint, clip=None, selected=None, **_):
254
+ """User / assistant prose straight to the draw list: pre-wrapped lines, editor
255
+ font, no render wrapper, no child tile. Rows outside `clip` are skipped.
256
+ ``selected`` = (lo, hi) paints the selection plate behind those characters
257
+ (draw_messages tracks the drag; Ctrl+C copies)."""
258
+ draw_list = imgui.get_window_draw_list()
259
+ if Melty.channels_split:
260
+ draw_list.channels_set_current(Melty.get_channel())
261
+ font = Melty.font_mgr.get(Font.FONTAWESOME_MONO_19) if Melty.font_mgr else None
262
+ if font is not None:
263
+ imgui.push_font(font)
264
+ Melty.push_clip((x, y, x + width, y + height))
265
+ try:
266
+ line_px = imgui.get_text_line_height() * 1.2
267
+ color = _color(_text_tint(tuple(tint)))
268
+ if selected is not None:
269
+ lo, hi = selected
270
+ char_w = max(1.0, imgui.calc_text_size("M").x)
271
+ plate = _color(_text_tint(tuple(tint)), 0.22)
272
+ offset = 0
273
+ for index, line in enumerate(str(text).split("\n")):
274
+ top = y + index * line_px
275
+ c0, c1 = max(lo - offset, 0), min(hi - offset, len(line))
276
+ if c1 > c0 or (hi > offset + len(line) and lo <= offset + len(line) and c0 <= len(line)):
277
+ # a line inside the span; one that the span runs past gets a half-cell tail
278
+ tail = 0.5 if hi > offset + len(line) else 0.0
279
+ if _visible(top, line_px, clip):
280
+ draw_list.add_rect_filled(x + Melty.px(4) + c0 * char_w, top,
281
+ x + Melty.px(4) + (max(c1, c0) + tail) * char_w, top + line_px,
282
+ plate, rounding=Melty.px(2))
283
+ offset += len(line) + 1
284
+ for index, line in enumerate(str(text).split("\n")):
285
+ top = y + index * line_px
286
+ if _visible(top, line_px, clip) and line:
287
+ draw_list.add_text(x + Melty.px(4), top, color, line)
288
+ finally:
289
+ Melty.pop_clip()
290
+ if font is not None:
291
+ imgui.pop_font()
292
+
293
+
294
+ def _icon_chip(icon, x, y, width, height, color):
295
+ """One fixed-size flat rounded chip per header row, the glyph centred in it.
296
+
297
+ Every row's chip is the same box whatever glyph it carries, so a column of
298
+ rows reads as a column of tiles. Returns the x the glyph should be drawn at.
299
+ """
300
+ draw_list = imgui.get_window_draw_list()
301
+ if Melty.channels_split:
302
+ draw_list.channels_set_current(Melty.get_channel()) # body channel, see _card
303
+ draw_list.add_rect_filled(x, y + Melty.px(1), x + width, y + height - Melty.px(1),
304
+ _color(color), rounding=Melty.px(4))
305
+ return x + max(0, (width - _label_width(icon)) / 2)
306
+
307
+
308
+ _ELLIPSIS_MEMO = {}
309
+
310
+
311
+ def _ellipsize(text, max_width):
312
+ """`text` cut to `max_width` px with a trailing ellipsis, measured in the
313
+ font pushed by the caller; memoized per (text, width, scale, font)."""
314
+ key = (text, max_width, Melty.ui_scale)
315
+ hit = _ELLIPSIS_MEMO.get(key)
316
+ if hit is not None:
317
+ return hit
318
+ result = text
319
+ if max_width > 0 and imgui.calc_text_size(text).x > max_width:
320
+ low, high = 0, len(text)
321
+ while low < high:
322
+ mid = (low + high + 1) // 2
323
+ if imgui.calc_text_size(text[:mid] + "…").x <= max_width:
324
+ low = mid
325
+ else:
326
+ high = mid - 1
327
+ result = text[:low].rstrip() + "…"
328
+ if len(_ELLIPSIS_MEMO) > 4096:
329
+ _ELLIPSIS_MEMO.clear()
330
+ _ELLIPSIS_MEMO[key] = result
331
+ return result
332
+
333
+
334
+ def _title(text, x, y, width, height, tint, brightness=1.0, ellipsis=False, text_color=None):
335
+ """Navigation labels have no render wrapper or child tile; `brightness` dims
336
+ the text, `ellipsis` trims it with a … instead of clipping."""
337
+ draw_list = imgui.get_window_draw_list()
338
+ if Melty.channels_split:
339
+ draw_list.channels_set_current(Melty.get_channel())
340
+ font = Melty.font_mgr.get(Font.FONTAWESOME_MONO_19) if Melty.font_mgr else None
341
+ if font is not None:
342
+ imgui.push_font(font)
343
+ Melty.push_clip((x, y, x + width, y + height))
344
+ try:
345
+ if ellipsis:
346
+ text = _ellipsize(text, width)
347
+ draw_list.add_text(x, y + max(0, (height - imgui.get_text_line_height()) / 2),
348
+ _color(tuple(c * brightness for c in (text_color or _text_tint(tuple(tint))))), text)
349
+ finally:
350
+ Melty.pop_clip()
351
+ if font is not None:
352
+ imgui.pop_font()
353
+
354
+
355
+ def chat_sources(accounts):
356
+ """Every account of a chat kind, in provider order: [(account_id, kind)].
357
+ One tab each in the sidebar's source bar."""
358
+ return [(entry["id"], kind)
359
+ for kind in internet_accounts.KINDS.values() if kind.chat_label
360
+ for entry in accounts.of_kind(kind.name)]
361
+
362
+
363
+ def conversation_source_tag(kind, chat):
364
+ from meltygui.chat.chat_proxy import writer_conflict
365
+ label = "Claude" if kind.name == "anthropic" else kind.chat_label
366
+ locked = chat.get("locked", writer_conflict(getattr(chat, "error", None)))
367
+ return label + " \uf023" if locked else label
368
+
369
+
370
+ def source_initials(label):
371
+ """A provider's two-letter tag for a mixed list: "Claude Code" → "CC",
372
+ "Codex" → "Co"."""
373
+ words = [word for word in str(label).split() if word]
374
+ if len(words) >= 2:
375
+ return "".join(word[0] for word in words[:2]).upper()
376
+ return (words[0][:2] if words else "?").capitalize()
377
+
378
+
379
+ def source_label(account_id, kind, accounts):
380
+ """A tab's text: the provider, plus the account when the kind has several."""
381
+ if len(accounts.of_kind(kind.name)) > 1:
382
+ return f'{kind.chat_label} · {accounts[account_id]["label"]}'
383
+ return kind.chat_label
384
+
385
+
386
+ def pick_source(state, account_id, kinds, additive=False):
387
+ """A source tab click. Plain: that source alone. ``additive`` (Shift held):
388
+ toggle it in or out of the shown set, never down to none. The primary
389
+ account — the transcript's, where New conversation goes — follows the
390
+ click, or falls back to the first shown source when it was toggled out."""
391
+ shown = list(getattr(state, "sources", None) or [state.account])
392
+ if not additive:
393
+ shown = [account_id]
394
+ elif account_id in shown:
395
+ if len(shown) > 1:
396
+ shown.remove(account_id)
397
+ else:
398
+ shown.append(account_id)
399
+ state.sources = shown
400
+ state.account = account_id if account_id in shown else shown[0]
401
+ state.provider = kinds[state.account]
402
+
403
+
404
+ def apply_folder_shortcuts(state, events):
405
+ """Set every current and subsequently discovered folder to the requested state."""
406
+ changed = False
407
+ for key, mods in events:
408
+ if mods & (glfw.MOD_CONTROL | glfw.MOD_SHIFT) == (glfw.MOD_CONTROL | glfw.MOD_SHIFT):
409
+ if key in (glfw.KEY_EQUAL, glfw.KEY_KP_ADD, glfw.KEY_MINUS, glfw.KEY_KP_SUBTRACT):
410
+ state.folders_default_expanded = key in (glfw.KEY_EQUAL, glfw.KEY_KP_ADD)
411
+ state.folder_expanded = {}
412
+ state.revision += 1
413
+ changed = True
414
+ return changed
415
+
416
+
417
+ def _code_background(x, y, width, height, color, shadow=None):
418
+ draw_list = imgui.get_window_draw_list()
419
+ if Melty.channels_split:
420
+ draw_list.channels_set_current(Melty.get_channel()) # body channel, see _chat
421
+ shadow = Toggles.Chat.shadow_offset if shadow is None else shadow
422
+ if shadow:
423
+ add_shadow((x, y, width, height), offset=shadow, corner_radius=Melty.px(6))
424
+ draw_list.add_rect_filled(x, y, x + width, y + height, _color(color), rounding=Melty.px(6))
425
+
426
+
427
+ def _terminal_layout(state, key, message, width):
428
+ """Cache a passive VT screen by input identity; never start a shell."""
429
+ import pyte
430
+ import re
431
+ command = message["content"]["command"]
432
+ output = message["content"].get("output", "")
433
+ cwd = message["details"].get("cwd", "")
434
+ font = Melty.font_mgr.get(Font.FONTAWESOME_MONO_19) if Melty.font_mgr else None
435
+ if font is not None:
436
+ imgui.push_font(font)
437
+ try:
438
+ char_width = imgui.calc_text_size("M").x
439
+ line_height = imgui.get_text_line_height() * 1.2
440
+ finally:
441
+ if font is not None:
442
+ imgui.pop_font()
443
+ show_all = state.output_expanded.get(key, False)
444
+ signature = (command, output, cwd, line_height, font, Melty.ui_scale, "preview", show_all)
445
+ memo = state.text_layouts.get((key, "terminal"))
446
+ if (memo is not None and all(a is b for a, b in zip(memo[0][:3], signature[:3]))
447
+ and memo[0][3:] == signature[3:]):
448
+ return memo[1], memo[2]
449
+ parts = output.split("\n", 5)
450
+ has_more = len(parts) > 5 and bool(parts[5].rstrip("\r\n"))
451
+ shown_output = output if show_all or not has_more else "\n".join(parts[:5])
452
+ prompt = ("\x1b[1;32m" + str(cwd) + "\x1b[0m" if cwd else "") + "\x1b[1;32m$\x1b[0m "
453
+ text = prompt + str(command).rstrip("\r\n") + ("\n" + str(shown_output) if shown_output else "")
454
+ # The captured terminal has a maximum content width, independent of the view.
455
+ # Allow two cells per code point for wide glyphs; the sparse grid trims the rest.
456
+ plain = re.sub(r"\x1b\[[0-?]*[ -/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)", "", text)
457
+ columns = max(1, max(len(line.expandtabs(8)) for line in plain.split("\n")) * 2)
458
+ screen = pyte.Screen(columns, text.count("\n") + 2)
459
+ screen.set_mode(20) # LF also returns to column zero, as captured shell output expects.
460
+ pyte.Stream(screen).feed(text)
461
+ last = max((index for index, row in screen.buffer.items() if row), default=0)
462
+ grid = tuple(tuple(screen.buffer[row][col] for col in range(max(screen.buffer[row], default=-1) + 1))
463
+ for row in range(last + 1))
464
+ value = (grid, char_width, line_height)
465
+ height = len(grid) * line_height + Melty.px(8)
466
+ state.text_layouts[(key, "terminal")] = (signature, value, height, has_more,
467
+ max((len(row) for row in grid), default=0) * char_width + Melty.px(8))
468
+ return value, height
469
+
470
+
471
+ from meltygui.view.chat_view import _message_preview
472
+
473
+
474
+ from meltygui.view.chat_view import _message_failed
475
+
476
+
477
+ def _failure_badge(x, y, width, height):
478
+ icon = f""
479
+ cursor = imgui.get_cursor_screen_pos()
480
+ draw_list = imgui.get_window_draw_list()
481
+ if Melty.channels_split:
482
+ draw_list.channels_set_current(Melty.get_channel()) # body channel, see _card
483
+ # Fixed design colours are scaled by Toggles.Chat.failed_badge_brightness so the
484
+ # badge reads as a status indicator rather than an alarm.
485
+ dim = Toggles.Chat.failed_badge_brightness
486
+ draw_list.add_rect_filled(x, y, x + width, y + height,
487
+ _color(tuple(c * dim for c in (0.30, 0.055, 0.075))), rounding=Melty.px(4))
488
+ try:
489
+ imgui.set_cursor_screen_pos((x, y))
490
+ flat_button(icon + " Failed", None, "chat-failed", width=width, height=height,
491
+ color=tuple(c * dim for c in (0.9, 0.16, 0.22)),
492
+ text_color=tuple(c * dim for c in (1.0, 0.76, 0.77)),
493
+ alpha=0, hovered=False, layout=False)
494
+ finally:
495
+ imgui.set_cursor_screen_pos(cursor)
496
+
497
+
498
+ def _draw_image(ref, x, y, max_width, box_height, caption_height, tint, *, name="image", size=None):
499
+ """An interactive texture with a caption, or a decode placeholder/error."""
500
+ cache = image_cache()
501
+ entry = cache.entry(ref)
502
+ draw_list = imgui.get_window_draw_list()
503
+ if Melty.channels_split:
504
+ draw_list.channels_set_current(Melty.get_channel()) # body channel, see _card
505
+ width, height = image_box(entry, max_width, size)
506
+ rendered_size = None
507
+ # Decode can finish between measuring this row and painting it. Keep this
508
+ # frame inside the reserved box; the decode generation reflows the next one.
509
+ if height > box_height:
510
+ scale = max(0, box_height) / height
511
+ width, height = width * scale, height * scale
512
+ caption = chat_images.image_label(ref, entry)
513
+ if entry is not None and entry.status == "ready":
514
+ texture = cache.texture(entry)
515
+ if texture is not None:
516
+ imgui.set_cursor_screen_pos((x, y))
517
+ # Seed the fitted box once; setting width/height each frame would
518
+ # disable the texture view's built-in right-drag resize.
519
+ _, _, image_state = draw_texture(
520
+ np.uint32(texture), name=name, initial={"width": width, "height": height},
521
+ auto_resize=False, fill_height=False, max_width=max_width, return_extras=True,
522
+ min_width=Melty.px(35), min_height=Melty.px(35),
523
+ show_header=False, show_bg=False, with_header=None, show_footer=False, show_info=False, flip_y=True)
524
+ rendered_size = (image_state.width, image_state.height)
525
+ width, height = rendered_size
526
+ elif entry is not None and entry.status == "failed":
527
+ caption = (caption + " · " if caption else "") + "could not decode: " + str(entry.error)
528
+ else:
529
+ draw_list.add_rect_filled(x, y, x + width, y + height, _color(tint, 0.12), rounding=Melty.px(6))
530
+ caption = (caption + " · " if caption else "") + "decoding…"
531
+ _title(caption or ref.label, x, y + height, max_width, caption_height, tint, brightness=0.7, ellipsis=True)
532
+ return rendered_size
533
+
534
+
535
+ def transcript_entries(messages, state, key):
536
+ """Group uninterrupted actions without hiding the prose around them."""
537
+ if not getattr(state, "concise", False):
538
+ yield from messages.items()
539
+ return
540
+ from itertools import groupby
541
+ for actions, entries in groupby(messages.items(),
542
+ key=lambda entry: isinstance(entry[1], Message) and not isinstance(entry[1], (UserMessage, AssistantMessage))):
543
+ if not actions:
544
+ yield from entries
545
+ continue
546
+ entries = list(entries)
547
+ identifier = "actions:" + entries[0][0]
548
+ counts = {}
549
+ for _, message in entries:
550
+ label = message.label
551
+ counts[label] = counts.get(label, 0) + 1
552
+ summary = ", ".join(f"{count} {label.lower()}{'s' if count > 1 and not label.endswith('s') else ''}"
553
+ for label, count in counts.items())
554
+ if len(entries) == 1:
555
+ preview = _message_preview(entries[0][1])
556
+ if preview:
557
+ summary += " · " + preview
558
+ failures = sum(_message_failed(message) for _, message in entries)
559
+ if failures:
560
+ summary += f" · {failures} failed"
561
+ group = Message("actionGroup", details={"label": summary})
562
+ yield identifier, group
563
+ if getattr(state, "action_groups", {}).get(key + ":" + identifier, False):
564
+ yield from entries
565
+
566
+
567
+ def chat_activity(chat, provider):
568
+ """Describe the latest reported work; never infer work from idle history."""
569
+ if chat["requests"]:
570
+ return "Waiting for your approval or input"
571
+ if not chat["running"]:
572
+ return "Working in another session" if chat.get("external_busy") else "Recent activity"
573
+ for message in reversed(chat["messages"].values()):
574
+ if isinstance(message, UserMessage):
575
+ break
576
+ if isinstance(message, ToolCall) and message.get("status") not in ("completed", "failed", "declined"):
577
+ progress = message["content"].get("progress")
578
+ detail = str(progress).strip().splitlines()[-1] if progress else _message_preview(message)
579
+ if not detail:
580
+ detail = ", ".join(message.get("summary", {}))
581
+ tool = message["details"].get("tool") or message["details"].get("name") or message.label
582
+ return f"{tool}: {' '.join(detail.split())[:160]}" if detail else str(tool)
583
+ if isinstance(message, ReasoningMessage) and message.get("status") != "completed":
584
+ summary = _message_preview(message)
585
+ return "Thinking: " + " ".join(summary.split())[:160] if summary else "Thinking…"
586
+ if isinstance(message, AssistantMessage):
587
+ return "Writing a response…" if message.get("status") != "completed" else "Working on the next step…"
588
+ return f"{provider} is starting the next step…"
589
+
590
+
591
+ from meltygui.core.services.chat_core import _cleanup_chat
592
+
593
+
594
+ def navigation_heading_control(pane, draw_state, state, x, y, width, height):
595
+ """App extension: draw a trailing heading control; return changed, width used."""
596
+ return False, 0
597
+
598
+
599
+ def navigation_row_sizes(state, edges, opened, top, height, minimum):
600
+ """Remember expanded sizes; redistribute space only when a section toggles.
601
+
602
+ Keep edge identities for resize captures and the collision graph.
603
+ RowLayout enforces the floors and caps afterwards.
604
+ """
605
+ previous = getattr(state, "navigation_opened", tuple(opened))
606
+ sizes = list(getattr(state, "navigation_sizes", [height / len(opened)] * len(opened)))
607
+ valid = (isinstance(edges, list) and len(edges) == len(opened) + 1
608
+ and all(isinstance(edge, dict) and "y" in edge for edge in edges))
609
+ if valid:
610
+ for index, expanded in enumerate(previous):
611
+ if expanded:
612
+ sizes[index] = max(minimum, edges[index + 1]["y"] - edges[index]["y"])
613
+ state.navigation_sizes = sizes
614
+ state.navigation_opened = tuple(opened)
615
+ available = max(0, height - minimum * len(opened))
616
+ weights = [max(0, size - minimum) if expanded else 0
617
+ for size, expanded in zip(sizes, opened)]
618
+ total = sum(weights)
619
+ fitted = [minimum + (available * (weight / total if total else 1 / sum(opened))
620
+ if expanded else 0)
621
+ for weight, expanded in zip(weights, opened)]
622
+ if valid and tuple(previous) != tuple(opened):
623
+ cursor = top
624
+ for index, size in enumerate(fitted[:-1]):
625
+ cursor += size
626
+ edges[index + 1]["y"] = cursor
627
+ return fitted
628
+
629
+
630
+ def chat_context_menu_items(state, sources):
631
+ """The wrapper owns right-release routing; rows only identify its target."""
632
+ def action(operation):
633
+ target = getattr(state, "chat_menu", None)
634
+ if not target:
635
+ return
636
+ account_id, key = target["account"], target["key"]
637
+ proxy = next((proxy for account, proxy, *_ in sources if account == account_id), None)
638
+ if proxy is None or key not in proxy:
639
+ return
640
+ if operation == "rename":
641
+ state.rename = {"account": account_id, "key": key, "pane": target.get("pane", "all"),
642
+ "draft": proxy.get(key)["title"], "focus": True}
643
+ elif operation == "fork":
644
+ new_key = proxy.fork(key)
645
+ if new_key:
646
+ state.account = account_id
647
+ state.selected[account_id] = new_key
648
+ else:
649
+ del proxy[key]
650
+ state.revision += 1
651
+ return {"Rename chat": lambda: action("rename"),
652
+ "Fork chat": lambda: action("fork"),
653
+ "Delete chat": lambda: action("delete")}
654
+
655
+
656
+ def chat_models(kind, proxy, selected_model=""):
657
+ models = dict(getattr(proxy, "models", {}))
658
+ if selected_model and selected_model != "default" and selected_model not in models.values():
659
+ models[selected_model] = selected_model
660
+ return models
661
+
662
+
663
+ def is_new_chat(chat):
664
+ return chat.loaded and not chat["messages"] and not chat["running"] and not chat.get("queued_messages")
665
+
666
+
667
+ def chat_project_choices(state, proxies, current, *defaults):
668
+ """Use directory paths as labels so identically named folders stay distinct."""
669
+ projects = {current, *defaults, *state.projects.values(),
670
+ *getattr(state, "added_folders", [])}
671
+ for proxy in proxies.values():
672
+ if proxy is not None:
673
+ projects.update(chat.get("project") for chat in dict.values(proxy))
674
+ return {project: project for project in sorted(filter(None, projects), key=str.casefold)}
675
+
676
+
677
+ def switch_new_chat_project(state, proxies, project):
678
+ """Replace an empty session: providers bind the cwd when creating it."""
679
+ proxy = proxies[state.account]
680
+ key = state.selected[state.account]
681
+ chat = proxy[key]
682
+ if not is_new_chat(chat) or chat.get("external_busy") or chat.get("locked") or chat["project"] == project:
683
+ return False
684
+ new_key = str(uuid.uuid4())
685
+ proxy[new_key] = {"title": chat["title"], "project": project,
686
+ "created_at": chat.get("created_at", 0), "updated": chat.get("updated", 0)}
687
+ proxy[new_key].metadata.update({field: value for field, value in chat.metadata.items()
688
+ if field in ("permissions", "model", "effort", "model_explicit", "model_selected_at",
689
+ "permissions_selected_at", "effort_selected_at", "service_tier", "service_tier_selected_at")})
690
+ state.drafts[state.account + ":" + new_key] = state.drafts.pop(state.account + ":" + key, "")
691
+ state.selected[state.account] = new_key
692
+ state.projects[state.account] = project
693
+ del proxy[key]
694
+ return True
695
+
696
+
697
+ def chat_effort_levels(kind, proxy, model):
698
+ levels = getattr(proxy, "model_efforts", {}).get(model)
699
+ if levels is None:
700
+ levels = ("low", "medium", "high") if kind.name == "anthropic" else ()
701
+ return tuple(levels)
702
+
703
+
704
+ def switch_new_chat_source(state, proxies, kinds, key, account_id, model):
705
+ """Move an unsent draft; never move a provider's existing transcript."""
706
+ previous = state.account
707
+ chat = proxies[previous][key]
708
+ if not is_new_chat(chat):
709
+ return False
710
+ if account_id != previous:
711
+ target = proxies[account_id]
712
+ metadata = {field: chat.metadata[field] for field in ("permissions", "tint", "model", "effort")
713
+ if field in chat.metadata}
714
+ target[key] = {"title": chat["title"], "project": chat["project"],
715
+ "created_at": chat.get("created_at", 0), "updated": chat.get("updated", 0)}
716
+ target[key].metadata.update(metadata)
717
+ del proxies[previous][key]
718
+ state.selected.pop(previous, None)
719
+ state.drafts[account_id + ":" + key] = state.drafts.pop(previous + ":" + key, "")
720
+ state.account = account_id
721
+ state.provider = kinds[account_id].name
722
+ if account_id not in state.sources:
723
+ state.sources = [*state.sources, account_id]
724
+ state.selected[account_id] = key
725
+ proxies[account_id][key].metadata["model"] = model
726
+ proxies[account_id][key].metadata["model_explicit"] = True
727
+ proxies[account_id][key].metadata["model_selected_at"] = time.time()
728
+ return True
729
+
730
+
731
+ from meltygui.view.chat_view import draw_chat_interface
732
+ draw_chat_interface = window(tint=(1.34, 1.62, 1.76), display_name='Chat', icon=f'\uf27a', initial={'width': 1100, 'height': 760})(draw_chat_interface)