meltygui 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- meltygui/__init__.py +107 -0
- meltygui/accounts/__init__.py +0 -0
- meltygui/accounts/internet_accounts.py +1355 -0
- meltygui/chat/__init__.py +91 -0
- meltygui/chat/activity.py +75 -0
- meltygui/chat/backends.py +36 -0
- meltygui/chat/chat_interface.py +732 -0
- meltygui/chat/chat_proxy.py +352 -0
- meltygui/chat/codex_proxy.py +592 -0
- meltygui/chat/codex_settings.py +100 -0
- meltygui/chat/codex_transport.py +60 -0
- meltygui/chat/command_parser.py +204 -0
- meltygui/chat/images.py +227 -0
- meltygui/chat/messages.py +417 -0
- meltygui/chat/metadata.py +139 -0
- meltygui/chat/writer_locks.py +64 -0
- meltygui/code/__init__.py +0 -0
- meltygui/code/basic_converters.py +533 -0
- meltygui/code/chain_converters.py +2111 -0
- meltygui/code/code_checks.py +2209 -0
- meltygui/code/core_syntax.py +1430 -0
- meltygui/code/file_converters.py +1933 -0
- meltygui/code/fileref.py +702 -0
- meltygui/code/hotswap_guard.py +144 -0
- meltygui/code/libcst_conversion.py +9724 -0
- meltygui/code/live_instrument.py +392 -0
- meltygui/code/live_view.py +2490 -0
- meltygui/code/melty_scan.py +2684 -0
- meltygui/code/new_codecs.py +1255 -0
- meltygui/code/new_converters.py +3017 -0
- meltygui/code/project_code.py +278 -0
- meltygui/code/source_context.py +63 -0
- meltygui/code/symbol_roster.py +1588 -0
- meltygui/code/syntax_check.py +34 -0
- meltygui/code/syntax_check_worker.py +114 -0
- meltygui/completion/__init__.py +0 -0
- meltygui/completion/fim.py +1232 -0
- meltygui/completion/fim_context.py +481 -0
- meltygui/completion/providers/__init__.py +0 -0
- meltygui/completion/providers/anthropic_oauth.py +446 -0
- meltygui/completion/providers/anthropic_requests.py +69 -0
- meltygui/completion/providers/claude.py +203 -0
- meltygui/completion/providers/claude_usage.py +499 -0
- meltygui/completion/providers/codex_accounts.py +180 -0
- meltygui/completion/providers/copilot.py +617 -0
- meltygui/completion/providers/oauth_popup.py +220 -0
- meltygui/completion/providers/ollama.py +320 -0
- meltygui/completion/providers/profiles.py +23 -0
- meltygui/core/README.md +88 -0
- meltygui/core/__init__.py +1 -0
- meltygui/core/automation/__init__.py +1 -0
- meltygui/core/automation/action_core.py +153 -0
- meltygui/core/automation/collection_action.py +45 -0
- meltygui/core/automation/mcp_eval.py +167 -0
- meltygui/core/automation/mcp_hotswap.py +107 -0
- meltygui/core/automation/mcp_query.py +552 -0
- meltygui/core/automation/mcp_server.py +657 -0
- meltygui/core/automation/orchestration_core.py +2636 -0
- meltygui/core/automation/query_core.py +124 -0
- meltygui/core/automation/search_core.py +26 -0
- meltygui/core/automation/selector_core.py +340 -0
- meltygui/core/automation/value_core.py +1752 -0
- meltygui/core/cache/__init__.py +1 -0
- meltygui/core/cache/cache_diagnostics.py +0 -0
- meltygui/core/cache/invalidation_decoration.py +153 -0
- meltygui/core/cache/invalidation_tracker.py +43 -0
- meltygui/core/cache/tile_cache.py +5968 -0
- meltygui/core/conversion/__init__.py +1 -0
- meltygui/core/conversion/bubbling.py +599 -0
- meltygui/core/conversion/cache_tree.py +188 -0
- meltygui/core/conversion/chain.py +113 -0
- meltygui/core/conversion/converter_register.py +145 -0
- meltygui/core/conversion/data_decoration.py +67 -0
- meltygui/core/conversion/dict_conversion.py +1815 -0
- meltygui/core/conversion/dict_conversion_util.py +177 -0
- meltygui/core/conversion/dynamic_obj.py +89 -0
- meltygui/core/conversion/graph_compare.py +183 -0
- meltygui/core/conversion/load_save_v2.py +1032 -0
- meltygui/core/conversion/missing_saved_class.py +39 -0
- meltygui/core/conversion/path_finder.py +606 -0
- meltygui/core/conversion/render_host.py +1083 -0
- meltygui/core/core_render.py +6537 -0
- meltygui/core/definition_hotswap.py +231 -0
- meltygui/core/diagnostics/__init__.py +1 -0
- meltygui/core/diagnostics/attribute_churn.py +38 -0
- meltygui/core/diagnostics/fps_counter.py +39 -0
- meltygui/core/diagnostics/gpu_frame_timer.py +103 -0
- meltygui/core/diagnostics/inspection_core.py +169 -0
- meltygui/core/diagnostics/monitor_core.py +105 -0
- meltygui/core/diagnostics/notifications.py +706 -0
- meltygui/core/diagnostics/perf_trace.py +281 -0
- meltygui/core/diagnostics/profile_decoration.py +88 -0
- meltygui/core/diagnostics/resize_trace.py +62 -0
- meltygui/core/diagnostics/screenshot_core.py +247 -0
- meltygui/core/diagnostics/session_status.py +98 -0
- meltygui/core/diagnostics/trace_core.py +445 -0
- meltygui/core/files/__init__.py +1 -0
- meltygui/core/files/file_core.py +208 -0
- meltygui/core/files/file_explorer_core.py +104 -0
- meltygui/core/files/file_tree_core.py +198 -0
- meltygui/core/files/file_watch_core.py +43 -0
- meltygui/core/files/import_graph_core.py +43 -0
- meltygui/core/files/metadata_core.py +51 -0
- meltygui/core/graphics/__init__.py +1 -0
- meltygui/core/graphics/cuda_context_core.py +166 -0
- meltygui/core/graphics/cuda_interop_core.py +136 -0
- meltygui/core/graphics/cuda_kernel_core.py +91 -0
- meltygui/core/graphics/framebuffer_recorder.py +337 -0
- meltygui/core/graphics/gl_state.py +658 -0
- meltygui/core/graphics/lut_core.py +52 -0
- meltygui/core/graphics/overlay_renderer.py +984 -0
- meltygui/core/graphics/scene_target.py +180 -0
- meltygui/core/graphics/screenshot.py +439 -0
- meltygui/core/graphics/shader_func.py +478 -0
- meltygui/core/graphics/tensor_core.py +45 -0
- meltygui/core/graphics/text_texture.py +329 -0
- meltygui/core/graphics/wayland_color.py +635 -0
- meltygui/core/input/__init__.py +1 -0
- meltygui/core/input/collision.py +165 -0
- meltygui/core/input/drag_drop_core.py +1525 -0
- meltygui/core/input/hypr_left_drag.py +323 -0
- meltygui/core/input/input_core.py +245 -0
- meltygui/core/input/input_handler.py +1101 -0
- meltygui/core/input/mouse_cursor.py +355 -0
- meltygui/core/input/pynput_backend.py +1054 -0
- meltygui/core/input/space_mouse.py +338 -0
- meltygui/core/input/touchpad_backend.py +393 -0
- meltygui/core/input/view_selection.py +177 -0
- meltygui/core/layout/__init__.py +1 -0
- meltygui/core/layout/column_core.py +2153 -0
- meltygui/core/layout/cursor_core.py +161 -0
- meltygui/core/layout/dropdown_core.py +278 -0
- meltygui/core/layout/edge_constraints.py +155 -0
- meltygui/core/layout/grid_core.py +117 -0
- meltygui/core/layout/header_core.py +23 -0
- meltygui/core/layout/header_runtime.py +67 -0
- meltygui/core/layout/layout_core.py +87 -0
- meltygui/core/layout/tile_manager_core.py +591 -0
- meltygui/core/melty.py +6726 -0
- meltygui/core/module_map.json +896 -0
- meltygui/core/module_names.py +19 -0
- meltygui/core/rendering/__init__.py +1 -0
- meltygui/core/rendering/core_decoration.py +431 -0
- meltygui/core/rendering/core_render_helpers.py +328 -0
- meltygui/core/rendering/func_metadata.py +398 -0
- meltygui/core/rendering/mode.py +818 -0
- meltygui/core/rendering/mode_defaults.py +41 -0
- meltygui/core/rendering/modes.py +136 -0
- meltygui/core/rendering/parameter_core.py +1665 -0
- meltygui/core/rendering/render_dispatch.py +1891 -0
- meltygui/core/rendering/render_funcs.py +273 -0
- meltygui/core/rendering/shaped.py +312 -0
- meltygui/core/rendering/window_decoration.py +25 -0
- meltygui/core/runtime/__init__.py +1 -0
- meltygui/core/runtime/app.py +735 -0
- meltygui/core/runtime/app_session.py +140 -0
- meltygui/core/runtime/background.py +564 -0
- meltygui/core/runtime/extensions.py +53 -0
- meltygui/core/runtime/gc_manager.py +1163 -0
- meltygui/core/runtime/lifecycle.py +21 -0
- meltygui/core/runtime/paths.py +27 -0
- meltygui/core/runtime/settings.py +14 -0
- meltygui/core/runtime/singleton.py +16 -0
- meltygui/core/runtime/thread_safe_bool.py +24 -0
- meltygui/core/runtime/thread_signal.py +30 -0
- meltygui/core/runtime/toggles.py +3115 -0
- meltygui/core/services/__init__.py +1 -0
- meltygui/core/services/account_core.py +10 -0
- meltygui/core/services/chat_core.py +19 -0
- meltygui/core/services/claude_terminal_core.py +346 -0
- meltygui/core/services/terminal_core.py +458 -0
- meltygui/core/services/terminal_runtime.py +78 -0
- meltygui/core/styling/__init__.py +1 -0
- meltygui/core/styling/color_core.py +46 -0
- meltygui/core/styling/fonts.py +639 -0
- meltygui/core/styling/global_style.py +338 -0
- meltygui/core/styling/style.py +198 -0
- meltygui/core/styling/style_core.py +522 -0
- meltygui/core/styling/warm_start.py +149 -0
- meltygui/core/windowing/__init__.py +1 -0
- meltygui/core/windowing/backends/PYIMGUI_LICENSE +28 -0
- meltygui/core/windowing/backends/__init__.py +1 -0
- meltygui/core/windowing/backends/imgui_renderer.py +138 -0
- meltygui/core/windowing/backends/native_wayland.py +850 -0
- meltygui/core/windowing/backends/protocols/xdg-decoration-unstable-v1.xml +156 -0
- meltygui/core/windowing/backends/protocols/xdg-shell.xml +1420 -0
- meltygui/core/windowing/backends/wayland_protocol.py +101 -0
- meltygui/core/windowing/dock_core.py +182 -0
- meltygui/core/windowing/frame_geometry.py +42 -0
- meltygui/core/windowing/geometry_feed.py +864 -0
- meltygui/core/windowing/glfw_utils.py +1343 -0
- meltygui/core/windowing/os_frame.py +1552 -0
- meltygui/core/windowing/surface.py +643 -0
- meltygui/core/windowing/titlebar.py +1560 -0
- meltygui/core/windowing/titlebar_buttons.py +281 -0
- meltygui/core/windowing/wayland_move.py +932 -0
- meltygui/core/windowing/window_api.py +62 -0
- meltygui/core/windowing/window_constants.py +339 -0
- meltygui/core/windowing/window_visibility.py +162 -0
- meltygui/debug/__init__.py +0 -0
- meltygui/debug/app_view_utils.py +9 -0
- meltygui/editor/__init__.py +0 -0
- meltygui/editor/bash_syntax.py +30 -0
- meltygui/editor/code_line_fast.py +152 -0
- meltygui/editor/diff.py +139 -0
- meltygui/editor/external_changes.py +159 -0
- meltygui/editor/file_header.py +41 -0
- meltygui/editor/live_usage.py +169 -0
- meltygui/editor/live_view_views.py +1803 -0
- meltygui/editor/pending_save.py +1351 -0
- meltygui/editor/roster_tints.py +547 -0
- meltygui/editor/source_preview.py +16 -0
- meltygui/editor/source_tools.py +10 -0
- meltygui/editor/source_ui.py +70 -0
- meltygui/editor/spell_check.py +77 -0
- meltygui/editor/text_editor.py +9076 -0
- meltygui/editor/usage_picker.py +538 -0
- meltygui/events/__init__.py +0 -0
- meltygui/events/example.py +118 -0
- meltygui/examples/__init__.py +0 -0
- meltygui/examples/columns_demo.py +33 -0
- meltygui/examples/columns_window_demo.py +60 -0
- meltygui/examples/context_menu_demo.py +24 -0
- meltygui/examples/context_menu_window_demo.py +49 -0
- meltygui/examples/gui_playground.py +127 -0
- meltygui/examples/live_view_playground.py +256 -0
- meltygui/examples/lora.py +43 -0
- meltygui/examples/lora_data.py +59 -0
- meltygui/examples/lora_policies.py +67 -0
- meltygui/examples/mode_demo.py +49 -0
- meltygui/examples/modifies_demo.py +119 -0
- meltygui/examples/scalar_policies.py +60 -0
- meltygui/examples/style_layouts.py +149 -0
- meltygui/examples/tile_manager_demo.py +74 -0
- meltygui/examples/tint_demo.py +160 -0
- meltygui/examples/tint_functions.py +56 -0
- meltygui/examples/trace_demo.py +88 -0
- meltygui/examples/two_windows.py +36 -0
- meltygui/files/__init__.py +0 -0
- meltygui/files/fast_file_explorer.py +439 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/extension.js +237 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/lsd-window-geometry@latent-descent.iml +9 -0
- meltygui/gnome_extension/lsd-window-geometry@latent-descent/metadata.json +7 -0
- meltygui/graphics/__init__.py +6 -0
- meltygui/graphics/base.py +85 -0
- meltygui/graphics/examples.py +507 -0
- meltygui/graphics/executor.py +520 -0
- meltygui/graphics/filter.py +667 -0
- meltygui/graphics/filter.pyi +856 -0
- meltygui/graphics/generate_stubs.py +22 -0
- meltygui/graphics/registry.py +203 -0
- meltygui/graphics/shader_compiler.py +155 -0
- meltygui/graphics/shaders.py +1302 -0
- meltygui/graphics/stub_generator.py +250 -0
- meltygui/graphics/texture_manager.py +170 -0
- meltygui/graphics/texture_min_max.py +295 -0
- meltygui/hdr_color.py +757 -0
- meltygui/image_load.py +308 -0
- meltygui/model/__init__.py +1 -0
- meltygui/model/account_model.py +142 -0
- meltygui/model/camera_model.py +159 -0
- meltygui/model/chat_model.py +86 -0
- meltygui/model/code_model.py +62 -0
- meltygui/model/code_proxy_model.py +876 -0
- meltygui/model/collection_model.py +41 -0
- meltygui/model/color_model.py +127 -0
- meltygui/model/cuda_tensor_model.py +36 -0
- meltygui/model/cuda_texture_model.py +149 -0
- meltygui/model/dropdown_model.py +137 -0
- meltygui/model/file_metadata_model.py +73 -0
- meltygui/model/file_model.py +289 -0
- meltygui/model/format_model.py +390 -0
- meltygui/model/graph_model.py +98 -0
- meltygui/model/icon_model.py +1024 -0
- meltygui/model/import_graph_model.py +468 -0
- meltygui/model/layout_model.py +15 -0
- meltygui/model/lut_model.py +266 -0
- meltygui/model/search_model.py +173 -0
- meltygui/model/tensor_model.py +402 -0
- meltygui/model/terminal_model.py +329 -0
- meltygui/model/texture_model.py +146 -0
- meltygui/model/tile_model.py +24 -0
- meltygui/model/trace_model.py +198 -0
- meltygui/model/trace_report_model.py +146 -0
- meltygui/models/__init__.py +0 -0
- meltygui/models/file_meta.py +613 -0
- meltygui/models/function_console.py +184 -0
- meltygui/models/orchestration.py +48 -0
- meltygui/pbr.py +1576 -0
- meltygui/png_unfilter.c +49 -0
- meltygui/resources/JetBrainsMono-Regular.ttf +0 -0
- meltygui/resources/THIRD_PARTY_NOTICES.md +21 -0
- meltygui/resources/dejavu/DejaVuSans-Bold.ttf +0 -0
- meltygui/resources/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
- meltygui/resources/dejavu/DejaVuSans.ttf +0 -0
- meltygui/resources/dejavu/LICENSE.txt +78 -0
- meltygui/resources/fontawesome-LICENSE.txt +121 -0
- meltygui/resources/fontawesome-webfont.ttf +0 -0
- meltygui/resources/hdri/studio_small_09_1k.hdr +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Bold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraBold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraLight.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Light.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Medium.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-SemiBold.ttf +0 -0
- meltygui/resources/jetbrains-weights/JetBrainsMono-Thin.ttf +0 -0
- meltygui/resources/jetbrains-weights/OFL.txt +93 -0
- meltygui/resources/jetbrains-weights/README.md +2 -0
- meltygui/state/__init__.py +0 -0
- meltygui/state/account_state.py +22 -0
- meltygui/state/animation_state.py +97 -0
- meltygui/state/annotation_state.py +22 -0
- meltygui/state/chat_state.py +36 -0
- meltygui/state/code_state.py +10 -0
- meltygui/state/core_enums.py +59 -0
- meltygui/state/core_markers.py +115 -0
- meltygui/state/core_undo.py +1075 -0
- meltygui/state/file_state.py +75 -0
- meltygui/state/graph_state.py +26 -0
- meltygui/state/inspection_state.py +47 -0
- meltygui/state/menu_state.py +10 -0
- meltygui/state/model_enums.py +11 -0
- meltygui/state/new_core_model.py +2394 -0
- meltygui/state/orchestration_state.py +14 -0
- meltygui/state/query_state.py +24 -0
- meltygui/state/tensor_state.py +10 -0
- meltygui/state/terminal_state.py +23 -0
- meltygui/state/trace_state.py +32 -0
- meltygui/state/voxel_state.py +12 -0
- meltygui/text_index.py +816 -0
- meltygui/utils/__init__.py +0 -0
- meltygui/utils/jump_to_code.py +344 -0
- meltygui/utils/pkl_inspect.py +90 -0
- meltygui/utils/render_utils.py +1419 -0
- meltygui/view/__init__.py +1 -0
- meltygui/view/account_view.py +524 -0
- meltygui/view/action_view.py +73 -0
- meltygui/view/chat_decoration_view.py +112 -0
- meltygui/view/chat_view.py +1703 -0
- meltygui/view/code_view.py +2677 -0
- meltygui/view/collection_view.py +1185 -0
- meltygui/view/color_view.py +824 -0
- meltygui/view/control_view.py +559 -0
- meltygui/view/decoration_view.py +439 -0
- meltygui/view/diagnostic_view.py +177 -0
- meltygui/view/dropdown_view.py +1082 -0
- meltygui/view/file_view.py +1599 -0
- meltygui/view/graph_cuda_view.py +105 -0
- meltygui/view/graph_view.py +882 -0
- meltygui/view/header_view.py +848 -0
- meltygui/view/input_view.py +238 -0
- meltygui/view/inspection_view.py +1783 -0
- meltygui/view/layout_view.py +596 -0
- meltygui/view/lut_view.py +45 -0
- meltygui/view/menu_view.py +212 -0
- meltygui/view/orchestration_view.py +658 -0
- meltygui/view/query_view.py +118 -0
- meltygui/view/search_view.py +342 -0
- meltygui/view/tab_view.py +258 -0
- meltygui/view/tensor_view.py +430 -0
- meltygui/view/terminal_view.py +355 -0
- meltygui/view/text_view.py +8031 -0
- meltygui/view/texture_view.py +480 -0
- meltygui/view/tile_view.py +38 -0
- meltygui/view/trace_view.py +923 -0
- meltygui/view/voxel_cuda_view.py +824 -0
- meltygui/view/voxel_view.py +1860 -0
- meltygui/view/window_view.py +111 -0
- meltygui-0.1.0.dist-info/METADATA +143 -0
- meltygui-0.1.0.dist-info/RECORD +372 -0
- meltygui-0.1.0.dist-info/WHEEL +4 -0
- meltygui-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,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)
|