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,592 @@
|
|
|
1
|
+
"""Codex implements the chat mapping. All protocol interpretation lives here."""
|
|
2
|
+
import time
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from meltygui.chat.codex_settings import ThreadSettingsReader
|
|
6
|
+
from meltygui.chat.codex_settings import effective_settings
|
|
7
|
+
from meltygui.chat.codex_settings import fast_service_tier
|
|
8
|
+
from meltygui.chat.codex_settings import model_service_tiers
|
|
9
|
+
|
|
10
|
+
from meltygui.chat.chat_proxy import Chat
|
|
11
|
+
from meltygui.chat.chat_proxy import ChatProxy
|
|
12
|
+
from meltygui.chat.chat_proxy import epoch_seconds
|
|
13
|
+
from meltygui.chat.messages import from_codex
|
|
14
|
+
from meltygui.chat.messages import upsert
|
|
15
|
+
from meltygui.chat.messages import set_text
|
|
16
|
+
from meltygui.chat.messages import AssistantMessage
|
|
17
|
+
from meltygui.chat.messages import PlanMessage
|
|
18
|
+
from meltygui.chat.messages import CommandExecution
|
|
19
|
+
from meltygui.chat.messages import ToolOutput
|
|
20
|
+
from meltygui.chat.messages import ReasoningMessage
|
|
21
|
+
from meltygui.chat.messages import FileChange
|
|
22
|
+
from meltygui.chat.messages import McpToolCall
|
|
23
|
+
from meltygui.chat.messages import FileTags
|
|
24
|
+
from meltygui.chat.messages import diff_counts
|
|
25
|
+
from meltygui.chat.messages import match_file
|
|
26
|
+
from meltygui.chat.codex_transport import CodexTransport
|
|
27
|
+
from meltygui.chat.writer_locks import codex_writer_locks
|
|
28
|
+
from meltygui.chat.writer_locks import lock_message
|
|
29
|
+
from meltygui.completion.providers.codex_accounts import account_home
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CodexChats(ChatProxy):
|
|
33
|
+
inherits_defaults = True
|
|
34
|
+
|
|
35
|
+
def __init__(self, account_id, metadata=None, wake=None, transport_factory=None):
|
|
36
|
+
from meltygui.chat.activity import UserMessageTimes
|
|
37
|
+
self.user_times = UserMessageTimes("codex")
|
|
38
|
+
self.settings_reader = ThreadSettingsReader()
|
|
39
|
+
self.source_home = account_home(account_id)
|
|
40
|
+
self.transport = None
|
|
41
|
+
self.writers = {}
|
|
42
|
+
self.refresh_pending = False
|
|
43
|
+
self.config_defaults = {}
|
|
44
|
+
self.config_pending = set()
|
|
45
|
+
self.transport_factory = transport_factory or CodexTransport
|
|
46
|
+
super().__init__(account_id, metadata, wake)
|
|
47
|
+
|
|
48
|
+
def connect(self):
|
|
49
|
+
self.transport = self._open_transport()
|
|
50
|
+
account = self.transport.request("account/read", {"refreshToken": False}).get("account")
|
|
51
|
+
if not account:
|
|
52
|
+
raise RuntimeError("Sign in to this account in Internet Accounts first")
|
|
53
|
+
cursor = None
|
|
54
|
+
while not self.closed:
|
|
55
|
+
result = self.transport.request("thread/list", {
|
|
56
|
+
"limit": 100, "cursor": cursor, "sortKey": "updated_at",
|
|
57
|
+
"sourceKinds": ["appServer", "cli", "vscode", "exec"]})
|
|
58
|
+
self.publish("listing", self._listing_sizes(result.get("data", [])))
|
|
59
|
+
cursor = result.get("nextCursor")
|
|
60
|
+
if not cursor:
|
|
61
|
+
break
|
|
62
|
+
models = []
|
|
63
|
+
cursor = None
|
|
64
|
+
try:
|
|
65
|
+
while True:
|
|
66
|
+
result = self.transport.request("model/list", {"limit": 100, "cursor": cursor})
|
|
67
|
+
models.extend(result.get("data", []))
|
|
68
|
+
cursor = result.get("nextCursor")
|
|
69
|
+
if not cursor:
|
|
70
|
+
break
|
|
71
|
+
except Exception:
|
|
72
|
+
pass # Default model remains usable with old servers.
|
|
73
|
+
self.publish("models", models)
|
|
74
|
+
self._read_defaults(self.transport, "")
|
|
75
|
+
self.publish("writer_locks", codex_writer_locks(self.source_home))
|
|
76
|
+
self.publish("ready", None)
|
|
77
|
+
|
|
78
|
+
def defaults_for(self, project):
|
|
79
|
+
"""Queue project-aware config reads without blocking the render thread."""
|
|
80
|
+
# Existing backend instances survive source hotswaps.
|
|
81
|
+
self.__dict__.setdefault("config_defaults", {})
|
|
82
|
+
self.__dict__.setdefault("config_pending", set())
|
|
83
|
+
if project not in self.config_defaults and project not in self.config_pending:
|
|
84
|
+
self.config_pending.add(project)
|
|
85
|
+
self.submit("defaults", project)
|
|
86
|
+
return self.config_defaults.get(project, self.config_defaults.get("", {}))
|
|
87
|
+
|
|
88
|
+
def _read_defaults(self, server, project):
|
|
89
|
+
config = server.request("config/read", {
|
|
90
|
+
"includeLayers": False, **({"cwd": project} if project else {})}).get("config", {})
|
|
91
|
+
self.publish("defaults", (project, config))
|
|
92
|
+
return config
|
|
93
|
+
|
|
94
|
+
def _thread_options(self, server, key, project):
|
|
95
|
+
config = self._read_defaults(server, project)
|
|
96
|
+
settings = self.known[key].metadata
|
|
97
|
+
effective = effective_settings(self.known[key], config, self.default_model)
|
|
98
|
+
options = {"cwd": project}
|
|
99
|
+
# Older Melty versions stamped catalog defaults into settings without
|
|
100
|
+
# an explicit-selection marker. Those should not override config.toml.
|
|
101
|
+
model = effective.get("model")
|
|
102
|
+
if model:
|
|
103
|
+
options["model"] = model
|
|
104
|
+
permissions = settings.get("permissions")
|
|
105
|
+
if permissions in ("ask", "full"):
|
|
106
|
+
options.update(approvalPolicy="never" if permissions == "full" else "on-request",
|
|
107
|
+
sandbox="danger-full-access" if permissions == "full" else "workspace-write")
|
|
108
|
+
else:
|
|
109
|
+
if config.get("approval_policy") is not None:
|
|
110
|
+
options["approvalPolicy"] = config["approval_policy"]
|
|
111
|
+
if config.get("sandbox_mode") is not None:
|
|
112
|
+
options["sandbox"] = config["sandbox_mode"]
|
|
113
|
+
effort = effective.get("effort")
|
|
114
|
+
supported = getattr(self, "model_efforts", {}).get(model)
|
|
115
|
+
if supported is not None and effort not in supported:
|
|
116
|
+
effort = None
|
|
117
|
+
if effort in (None, "", "default"):
|
|
118
|
+
effort = config.get("model_reasoning_effort")
|
|
119
|
+
if effort:
|
|
120
|
+
options["config"] = {"model_reasoning_effort": effort}
|
|
121
|
+
if "service_tier" in effective:
|
|
122
|
+
tier = effective["service_tier"]
|
|
123
|
+
tiers = getattr(self, "model_service_tiers", {}).get(model)
|
|
124
|
+
if tier in ("fast", "priority"):
|
|
125
|
+
tier = fast_service_tier(self, model) or tier
|
|
126
|
+
if tier in ("default", "") or tiers is not None and tier not in {entry["id"] for entry in tiers}:
|
|
127
|
+
tier = None
|
|
128
|
+
options["serviceTier"] = tier
|
|
129
|
+
return options
|
|
130
|
+
|
|
131
|
+
def _listing_sizes(self, threads):
|
|
132
|
+
# Filesystem data is read from the backend worker, never while drawing.
|
|
133
|
+
for thread in threads:
|
|
134
|
+
thread["last_user_at"] = self.user_times.read(thread.get("path"))
|
|
135
|
+
if not hasattr(self, "settings_reader"):
|
|
136
|
+
self.settings_reader = ThreadSettingsReader()
|
|
137
|
+
thread.update(self.settings_reader.read(thread.get("path")))
|
|
138
|
+
try:
|
|
139
|
+
thread["size_bytes"] = Path(thread["path"]).stat().st_size if thread.get("path") else None
|
|
140
|
+
except OSError:
|
|
141
|
+
thread["size_bytes"] = None
|
|
142
|
+
return threads
|
|
143
|
+
|
|
144
|
+
def _open_transport(self, remote_id=None):
|
|
145
|
+
from meltygui.core.runtime.toggles import Toggles
|
|
146
|
+
holder = {}
|
|
147
|
+
def event(value):
|
|
148
|
+
if not holder.get("closing"):
|
|
149
|
+
if value.get("method") == "transport/error" and remote_id:
|
|
150
|
+
value = {**value, "params": {**value.get("params", {}), "threadId": remote_id}}
|
|
151
|
+
if value.get("method") == "thread/settings/updated":
|
|
152
|
+
value = {**value, "settings_observed_at": time.time()}
|
|
153
|
+
self.publish("event", value)
|
|
154
|
+
server = self.transport_factory(self.source_home, executable=Toggles.InternetAccounts.codex_bin,
|
|
155
|
+
timeout=Toggles.InternetAccounts.codex_request_timeout_s, on_event=event)
|
|
156
|
+
server._melty_holder = holder
|
|
157
|
+
return server
|
|
158
|
+
|
|
159
|
+
def _close_transport(self, server):
|
|
160
|
+
server._melty_holder["closing"] = True
|
|
161
|
+
server.close()
|
|
162
|
+
|
|
163
|
+
def disconnect(self):
|
|
164
|
+
for server in list(self.writers.values()):
|
|
165
|
+
self._close_transport(server)
|
|
166
|
+
self.writers.clear()
|
|
167
|
+
if self.transport:
|
|
168
|
+
self._close_transport(self.transport)
|
|
169
|
+
|
|
170
|
+
def refresh(self):
|
|
171
|
+
if not self.refresh_pending:
|
|
172
|
+
self.refresh_pending = True
|
|
173
|
+
self.submit("refresh")
|
|
174
|
+
|
|
175
|
+
def execute(self, operation, *args):
|
|
176
|
+
server = self.transport
|
|
177
|
+
if operation == "defaults":
|
|
178
|
+
self._read_defaults(server, args[0])
|
|
179
|
+
elif operation == "fork":
|
|
180
|
+
key, remote_id, project, title = args
|
|
181
|
+
writer = self._open_transport()
|
|
182
|
+
try:
|
|
183
|
+
result = writer.request("thread/fork", {"threadId": remote_id, "cwd": project})
|
|
184
|
+
fork_id = result["thread"]["id"]
|
|
185
|
+
writer.request("thread/name/set", {"threadId": fork_id, "name": title})
|
|
186
|
+
self.publish("forked", (key, fork_id))
|
|
187
|
+
finally:
|
|
188
|
+
self._close_transport(writer)
|
|
189
|
+
elif operation == "refresh":
|
|
190
|
+
cursor = None
|
|
191
|
+
while not self.closed:
|
|
192
|
+
result = server.request("thread/list", {"limit": 100, "cursor": cursor,
|
|
193
|
+
"sortKey": "updated_at", "sourceKinds": ["appServer", "cli", "vscode", "exec"]})
|
|
194
|
+
self.publish("listing", self._listing_sizes(result.get("data", [])))
|
|
195
|
+
cursor = result.get("nextCursor")
|
|
196
|
+
if not cursor:
|
|
197
|
+
break
|
|
198
|
+
locks = codex_writer_locks(self.source_home)
|
|
199
|
+
self.publish("writer_locks", {key: owner for key, owner in locks.items() if key not in self.writers} if locks is not None else None)
|
|
200
|
+
self.publish("refreshed", None)
|
|
201
|
+
elif operation == "release":
|
|
202
|
+
remote_id, = args
|
|
203
|
+
writer = self.writers.pop(remote_id, None)
|
|
204
|
+
if writer:
|
|
205
|
+
self._close_transport(writer)
|
|
206
|
+
elif operation == "hydrate":
|
|
207
|
+
key, = args
|
|
208
|
+
chat = self.known.get(key) or dict.get(self, key)
|
|
209
|
+
if chat:
|
|
210
|
+
result = server.request("thread/read", {"threadId": chat.remote_id, "includeTurns": True})
|
|
211
|
+
self.publish("hydrated", (key, self._history(result["thread"], chat["project"])))
|
|
212
|
+
elif operation == "create":
|
|
213
|
+
key, project, title = args
|
|
214
|
+
writer = self._open_transport()
|
|
215
|
+
try:
|
|
216
|
+
options = self._thread_options(writer, key, project)
|
|
217
|
+
observed_at = time.time()
|
|
218
|
+
result = writer.request("thread/start", options)
|
|
219
|
+
writer.request("thread/name/set", {"threadId": result["thread"]["id"], "name": title})
|
|
220
|
+
self.writers[result["thread"]["id"]] = writer
|
|
221
|
+
self.publish("settings", (key, result, observed_at))
|
|
222
|
+
self.publish("created", (key, result["thread"]))
|
|
223
|
+
except Exception:
|
|
224
|
+
self._close_transport(writer)
|
|
225
|
+
raise
|
|
226
|
+
elif operation == "archive":
|
|
227
|
+
key, remote_id = args
|
|
228
|
+
try:
|
|
229
|
+
self.writers.get(remote_id, server).request("thread/archive", {"threadId": remote_id})
|
|
230
|
+
except Exception as error:
|
|
231
|
+
# Unsent threads have no rollout file to archive yet.
|
|
232
|
+
chat = self.known.get(key)
|
|
233
|
+
if (chat is None or not chat.loaded or chat["messages"] or chat["running"]
|
|
234
|
+
or "no rollout found for thread id" not in str(error).lower()):
|
|
235
|
+
raise
|
|
236
|
+
writer = self.writers.pop(remote_id, None)
|
|
237
|
+
if writer:
|
|
238
|
+
self._close_transport(writer)
|
|
239
|
+
self.publish("archived", key)
|
|
240
|
+
elif operation == "rename":
|
|
241
|
+
key, remote_id, title = args
|
|
242
|
+
server.request("thread/name/set", {"threadId": remote_id, "name": title})
|
|
243
|
+
self.publish("renamed", (key, title))
|
|
244
|
+
elif operation == "send":
|
|
245
|
+
key, remote_id, message_id, text, resumed = args
|
|
246
|
+
writer = self.writers.get(remote_id) or self._open_transport(remote_id)
|
|
247
|
+
self.writers[remote_id] = writer
|
|
248
|
+
try:
|
|
249
|
+
options = self._thread_options(writer, key, self.known[key]["project"])
|
|
250
|
+
model = options.get("model")
|
|
251
|
+
observed_at = time.time()
|
|
252
|
+
resumed_thread = writer.request("thread/resume", {"threadId": remote_id, **options})
|
|
253
|
+
self.publish("settings", (key, resumed_thread, observed_at))
|
|
254
|
+
result = writer.request("turn/start", {"threadId": remote_id,
|
|
255
|
+
**({"model": model} if model else {}),
|
|
256
|
+
**({"effort": options["config"]["model_reasoning_effort"]} if "config" in options else {}),
|
|
257
|
+
**({"serviceTier": options["serviceTier"]} if "serviceTier" in options else {}),
|
|
258
|
+
"input": [{"type": "text", "text": text}]})
|
|
259
|
+
self.publish("sent", (key, message_id, result["turn"]["id"]))
|
|
260
|
+
except Exception:
|
|
261
|
+
self.writers.pop(remote_id, None)
|
|
262
|
+
self._close_transport(writer)
|
|
263
|
+
raise
|
|
264
|
+
elif operation == "interrupt":
|
|
265
|
+
key, remote_id, turn_id = args
|
|
266
|
+
self.writers.get(remote_id, server).request("turn/interrupt", {"threadId": remote_id, "turnId": turn_id})
|
|
267
|
+
elif operation == "unsupported":
|
|
268
|
+
request_id, message, remote_id = args
|
|
269
|
+
self.writers.get(remote_id, server).answer(request_id, error={"code": -32601, "message": message})
|
|
270
|
+
elif operation == "answer":
|
|
271
|
+
key, request_id, answer = args
|
|
272
|
+
chat = self.known[key]
|
|
273
|
+
self.writers.get(chat.remote_id, server).answer(request_id, answer)
|
|
274
|
+
self.publish("answered", (key, request_id))
|
|
275
|
+
|
|
276
|
+
def receive(self, kind, value):
|
|
277
|
+
if kind == "defaults":
|
|
278
|
+
project, config = value
|
|
279
|
+
self.__dict__.setdefault("config_defaults", {})[project] = config
|
|
280
|
+
self.__dict__.setdefault("config_pending", set()).discard(project)
|
|
281
|
+
if not project and config.get("model"):
|
|
282
|
+
self.default_model = config["model"]
|
|
283
|
+
elif kind == "forked":
|
|
284
|
+
self.finish_fork(*value)
|
|
285
|
+
elif kind == "settings":
|
|
286
|
+
key, settings = value[:2]
|
|
287
|
+
chat = self.known.get(key)
|
|
288
|
+
if chat is not None:
|
|
289
|
+
self._apply_settings(chat, settings, value[2] if len(value) > 2 else None)
|
|
290
|
+
elif kind == "models":
|
|
291
|
+
self.model_service_tiers = {row["model"]: model_service_tiers(row)
|
|
292
|
+
for row in value if row.get("model")}
|
|
293
|
+
self.model_default_service_tiers = {row["model"]: row.get("defaultServiceTier")
|
|
294
|
+
for row in value if row.get("model")}
|
|
295
|
+
self.model_default_efforts = {row["model"]: row.get("defaultReasoningEffort")
|
|
296
|
+
for row in value if row.get("model")}
|
|
297
|
+
self.model_efforts = {row["model"]: tuple(option["reasoningEffort"]
|
|
298
|
+
for option in row.get("supportedReasoningEfforts", []) if option.get("reasoningEffort"))
|
|
299
|
+
for row in value if row.get("model")}
|
|
300
|
+
self.default_model = next((row["model"] for row in value
|
|
301
|
+
if row.get("isDefault") and row.get("model")), None)
|
|
302
|
+
self.models = {row.get("displayName") or row["model"]: row["model"]
|
|
303
|
+
for row in value if row.get("model") and not row.get("hidden")}
|
|
304
|
+
elif kind == "writer_locks":
|
|
305
|
+
if value is not None:
|
|
306
|
+
for chat in self.values():
|
|
307
|
+
locked = chat.remote_id in value
|
|
308
|
+
if (chat.get("locked") and not locked and chat.get("retryable_error")
|
|
309
|
+
and str(chat.error or "").startswith("This conversation is open for writing")):
|
|
310
|
+
chat.error = None
|
|
311
|
+
chat["lock_owner"] = value.get(chat.remote_id) if isinstance(value, dict) else None
|
|
312
|
+
chat["locked"] = locked
|
|
313
|
+
if locked and chat.get("retryable_error"):
|
|
314
|
+
chat.error = lock_message(chat["lock_owner"])
|
|
315
|
+
elif kind == "refreshed":
|
|
316
|
+
self.refresh_pending = False
|
|
317
|
+
elif kind == "listing":
|
|
318
|
+
for thread in value:
|
|
319
|
+
key = self.metadata.local_key(self.account_id, thread["id"])
|
|
320
|
+
if key not in self:
|
|
321
|
+
chat = Chat({"title": thread.get("name") or thread.get("preview") or "Conversation",
|
|
322
|
+
"project": thread.get("cwd") or "",
|
|
323
|
+
"created_at": epoch_seconds(thread.get("createdAt", thread.get("created_at"))),
|
|
324
|
+
"updated": epoch_seconds(thread.get("updatedAt", thread.get("updated_at")))},
|
|
325
|
+
remote_id=thread["id"])
|
|
326
|
+
chat["size_bytes"] = thread.get("size_bytes")
|
|
327
|
+
dict.__setitem__(self, key, chat)
|
|
328
|
+
self.known[key] = chat
|
|
329
|
+
else:
|
|
330
|
+
chat = dict.get(self, key)
|
|
331
|
+
chat["size_bytes"] = thread.get("size_bytes")
|
|
332
|
+
updated = epoch_seconds(thread.get("updatedAt", thread.get("updated_at")))
|
|
333
|
+
if thread.get("name") and chat["title"] == chat.saved_title and "rename" not in chat.inflight:
|
|
334
|
+
chat["title"] = chat.saved_title = thread["name"]
|
|
335
|
+
if updated > chat["updated"] and not chat["running"]:
|
|
336
|
+
chat["updated"] = updated
|
|
337
|
+
self.refresh_history(key)
|
|
338
|
+
chat = dict.get(self, key)
|
|
339
|
+
if thread.get("codex_settings_at", 0) >= chat.get("codex_settings_at", 0):
|
|
340
|
+
chat.update({field: thread[field] for field in ("codex_settings", "codex_settings_at") if field in thread})
|
|
341
|
+
# Session-naming jobs contain real model/assistant messages, so
|
|
342
|
+
# emptiness and a rounded 0.0 MB size cannot identify them.
|
|
343
|
+
preview = str(thread.get("preview") or "").lstrip()
|
|
344
|
+
chat["title_helper"] = preview.startswith("Name a chat session for the request below.")
|
|
345
|
+
chat["last_user_at"] = max(chat.get("last_user_at", 0), thread.get("last_user_at", 0))
|
|
346
|
+
self.metadata.apply(self.account_id, self)
|
|
347
|
+
elif kind == "ready":
|
|
348
|
+
self.loading = False
|
|
349
|
+
elif kind == "created":
|
|
350
|
+
key, thread = value
|
|
351
|
+
chat = self.known.get(key)
|
|
352
|
+
if chat:
|
|
353
|
+
chat.remote_id = thread["id"]
|
|
354
|
+
chat.inflight.discard("create")
|
|
355
|
+
# Persist remote identity even if the row was removed while creation ran.
|
|
356
|
+
self.metadata.conversation(self.account_id, chat["project"], key)["remote_id"] = chat.remote_id
|
|
357
|
+
chat.inflight.discard("archive")
|
|
358
|
+
elif kind == "hydrated":
|
|
359
|
+
key, history = value
|
|
360
|
+
chat = self.known.get(key)
|
|
361
|
+
if chat:
|
|
362
|
+
# A pre-hotswap worker may already have queued an old raw payload.
|
|
363
|
+
if "messages" not in history:
|
|
364
|
+
history = self._history(history, chat["project"])
|
|
365
|
+
# Disk history is authoritative; keep only the unsent local drafts.
|
|
366
|
+
pending = {identifier: message for identifier, message in chat["messages"].items()
|
|
367
|
+
if identifier not in chat.sent and message.get("role") == "user"}
|
|
368
|
+
chat["messages"].clear()
|
|
369
|
+
chat["messages"].update(history["messages"])
|
|
370
|
+
chat.update({field: history[field] for field in ("codex_settings", "codex_settings_at") if field in history
|
|
371
|
+
and history.get("codex_settings_at", 0) >= chat.get("codex_settings_at", 0)})
|
|
372
|
+
chat.command_scripts = getattr(history, "command_scripts", {})
|
|
373
|
+
chat.sent = set(chat["messages"])
|
|
374
|
+
chat["messages"].update(pending)
|
|
375
|
+
chat.loaded, chat.loading, chat.refreshing = True, False, False
|
|
376
|
+
elif kind == "archived":
|
|
377
|
+
self.known.pop(value, None)
|
|
378
|
+
elif kind == "renamed":
|
|
379
|
+
key, title = value
|
|
380
|
+
chat = self.known.get(key)
|
|
381
|
+
if chat:
|
|
382
|
+
chat.saved_title = title
|
|
383
|
+
chat.inflight.discard("rename")
|
|
384
|
+
elif kind == "sent":
|
|
385
|
+
key, message_id, turn_id = value
|
|
386
|
+
chat = self.known.get(key)
|
|
387
|
+
if chat:
|
|
388
|
+
chat["locked"] = False
|
|
389
|
+
chat.sent.add(message_id)
|
|
390
|
+
chat.resumed = False
|
|
391
|
+
chat.inflight.discard("send")
|
|
392
|
+
if chat["running"]:
|
|
393
|
+
chat.turn_id = turn_id
|
|
394
|
+
elif kind == "answered":
|
|
395
|
+
key, request_id = value
|
|
396
|
+
if key in self.known:
|
|
397
|
+
self.known[key]["requests"].pop(request_id, None)
|
|
398
|
+
elif kind == "failure":
|
|
399
|
+
operation, args, error = value
|
|
400
|
+
if operation == "defaults":
|
|
401
|
+
self.config_pending.discard(args[0])
|
|
402
|
+
self.models_error = "Could not read Codex defaults: " + error
|
|
403
|
+
return
|
|
404
|
+
if operation == "refresh":
|
|
405
|
+
self.refresh_pending = False
|
|
406
|
+
return
|
|
407
|
+
chat = self.known.get(args[0]) if args else None
|
|
408
|
+
if operation == "send" and chat is not None and "active writer" in error.lower():
|
|
409
|
+
_, _, message_id, text, _ = args
|
|
410
|
+
chat.error = lock_message(chat.get("lock_owner"))
|
|
411
|
+
chat["recovered_draft"] = text
|
|
412
|
+
chat["retryable_error"] = True
|
|
413
|
+
chat["locked"] = True
|
|
414
|
+
chat["messages"].pop(message_id, None)
|
|
415
|
+
chat.scanned = -1
|
|
416
|
+
chat.inflight.difference_update({"send", "interrupt", "interrupt_requested"})
|
|
417
|
+
chat.turn_id = None
|
|
418
|
+
chat["running"] = False
|
|
419
|
+
chat["updated"] = 0.0
|
|
420
|
+
chat.resumed = False
|
|
421
|
+
return
|
|
422
|
+
if operation != "hydrate":
|
|
423
|
+
self.error = error
|
|
424
|
+
self.loading = False
|
|
425
|
+
if chat:
|
|
426
|
+
chat.error = error
|
|
427
|
+
chat.loading = chat.refreshing = False
|
|
428
|
+
chat["running"] = False
|
|
429
|
+
if operation == "archive":
|
|
430
|
+
dict.__setitem__(self, args[0], chat)
|
|
431
|
+
elif kind == "event":
|
|
432
|
+
self._event(value)
|
|
433
|
+
|
|
434
|
+
def _apply_settings(self, chat, settings, observed_at=None):
|
|
435
|
+
observed_at = time.time() if observed_at is None else observed_at
|
|
436
|
+
if observed_at < chat.get("codex_settings_at", 0):
|
|
437
|
+
return
|
|
438
|
+
values = {target: settings[source] for source, target in
|
|
439
|
+
(("model", "model"), ("reasoningEffort", "effort"), ("effort", "effort"), ("serviceTier", "service_tier"))
|
|
440
|
+
if source in settings}
|
|
441
|
+
if values:
|
|
442
|
+
chat["codex_settings"] = {**chat.get("codex_settings", {}), **values}
|
|
443
|
+
chat["codex_settings_at"] = observed_at
|
|
444
|
+
|
|
445
|
+
def _history(self, thread, project):
|
|
446
|
+
history = Chat({"project": project})
|
|
447
|
+
if not hasattr(self, "settings_reader"):
|
|
448
|
+
self.settings_reader = ThreadSettingsReader()
|
|
449
|
+
history.update(self.settings_reader.read(thread.get("path")))
|
|
450
|
+
for turn in thread.get("turns", []):
|
|
451
|
+
for item in turn.get("items", []):
|
|
452
|
+
self._item(history, item, history=True)
|
|
453
|
+
return history
|
|
454
|
+
|
|
455
|
+
def _item(self, chat, item, history=False):
|
|
456
|
+
identifier = item.get("id")
|
|
457
|
+
if not identifier:
|
|
458
|
+
return
|
|
459
|
+
if item.get("type") == "userMessage" and not history:
|
|
460
|
+
return # the local submitted message already occupies a stable row
|
|
461
|
+
if not hasattr(chat, "command_scripts"):
|
|
462
|
+
chat.command_scripts = {}
|
|
463
|
+
incoming = from_codex(item, scripts=chat.command_scripts)
|
|
464
|
+
incoming["details"].setdefault("cwd", chat["project"])
|
|
465
|
+
old = chat["messages"].get(identifier)
|
|
466
|
+
if isinstance(incoming, CommandExecution) and isinstance(old, CommandExecution):
|
|
467
|
+
if item.get("aggregatedOutput") is None:
|
|
468
|
+
incoming["content"]["output"] = old["content"].get("output", ToolOutput(""))
|
|
469
|
+
# Line counts come from the turn diff, not the item: keep what the
|
|
470
|
+
# started item was stamped with when its completion re-parses the tags.
|
|
471
|
+
for path, entry in incoming.get("summary", {}).items():
|
|
472
|
+
previous = old.get("summary", {}).get(path)
|
|
473
|
+
if previous is not None and entry.get("added") is None and previous.get("added") is not None:
|
|
474
|
+
entry["added"], entry["removed"] = previous["added"], previous["removed"]
|
|
475
|
+
upsert(chat["messages"], identifier, incoming)
|
|
476
|
+
|
|
477
|
+
def _turn_diff(self, chat, diff):
|
|
478
|
+
"""Attribute the turn's cumulative diff growth to the latest command writing each file.
|
|
479
|
+
|
|
480
|
+
Codex reports no per-command diff; `turn/diff/updated` carries the whole
|
|
481
|
+
turn's unified diff after each item. The growth since the previous update
|
|
482
|
+
is what the newest item did, so it lands on the most recent command whose
|
|
483
|
+
parsed tags name that file (a fileChange item already carries its own).
|
|
484
|
+
"""
|
|
485
|
+
counts = diff_counts(diff)
|
|
486
|
+
previous = getattr(chat, "turn_counts", {})
|
|
487
|
+
chat.turn_counts = counts
|
|
488
|
+
for diff_path, (added, removed) in counts.items():
|
|
489
|
+
old_added, old_removed = previous.get(diff_path, (0, 0))
|
|
490
|
+
delta = (max(0, added - old_added), max(0, removed - old_removed))
|
|
491
|
+
for message in reversed(list(chat["messages"].values())):
|
|
492
|
+
if not isinstance(message, CommandExecution) or not isinstance(message.get("summary"), FileTags):
|
|
493
|
+
continue
|
|
494
|
+
entry = next((entry for path, entry in message["summary"].items()
|
|
495
|
+
if entry.get("access", "write") == "write" and match_file(path, diff_path)), None)
|
|
496
|
+
if entry is None:
|
|
497
|
+
continue
|
|
498
|
+
if entry.get("added") is None:
|
|
499
|
+
if delta == (0, 0):
|
|
500
|
+
break # nothing new to count; an unstamped tag stays count-less
|
|
501
|
+
entry["added"], entry["removed"] = delta
|
|
502
|
+
else:
|
|
503
|
+
entry["added"] += delta[0]
|
|
504
|
+
entry["removed"] += delta[1]
|
|
505
|
+
break
|
|
506
|
+
|
|
507
|
+
def _event(self, event):
|
|
508
|
+
method, params = event.get("method", ""), event.get("params") or {}
|
|
509
|
+
if method == "transport/error":
|
|
510
|
+
error = params.get("message")
|
|
511
|
+
remote_id = params.get("threadId")
|
|
512
|
+
if not remote_id:
|
|
513
|
+
self.error = error
|
|
514
|
+
for chat in self.values():
|
|
515
|
+
if not remote_id or chat.remote_id == remote_id:
|
|
516
|
+
chat["running"] = False
|
|
517
|
+
chat.error = error
|
|
518
|
+
return
|
|
519
|
+
remote_id = params.get("threadId") or (params.get("thread") or {}).get("id")
|
|
520
|
+
chat = next((chat for chat in self.known.values() if chat.remote_id == remote_id), None)
|
|
521
|
+
if chat is not None:
|
|
522
|
+
chat["updated"] = time.time() # any event for a thread is activity on it
|
|
523
|
+
if "id" in event:
|
|
524
|
+
if chat is None:
|
|
525
|
+
self.submit("unsupported", event["id"], "Unknown conversation", remote_id)
|
|
526
|
+
return
|
|
527
|
+
if method in ("item/commandExecution/requestApproval", "item/fileChange/requestApproval"):
|
|
528
|
+
chat["requests"][event["id"]] = {"kind": "approval", "data": params,
|
|
529
|
+
"text": params.get("command") or params.get("reason") or "Approve file changes?"}
|
|
530
|
+
elif method == "item/tool/requestUserInput":
|
|
531
|
+
chat["requests"][event["id"]] = {"kind": "input", "data": params}
|
|
532
|
+
else:
|
|
533
|
+
# Unsupported requests fail explicitly; never silently accept permissions.
|
|
534
|
+
self.submit("unsupported", event["id"], "Not supported by Melty chat yet", remote_id)
|
|
535
|
+
chat.error = f"Unsupported request: {method}"
|
|
536
|
+
return
|
|
537
|
+
if chat is None:
|
|
538
|
+
return
|
|
539
|
+
if method == "thread/settings/updated":
|
|
540
|
+
self._apply_settings(chat, params.get("threadSettings") or {}, event.get("settings_observed_at"))
|
|
541
|
+
elif method in ("item/started", "item/completed"):
|
|
542
|
+
self._item(chat, params.get("item") or {})
|
|
543
|
+
elif method in ("item/agentMessage/delta", "item/commandExecution/outputDelta", "item/plan/delta"):
|
|
544
|
+
identifier = params.get("itemId")
|
|
545
|
+
if not identifier:
|
|
546
|
+
return
|
|
547
|
+
cls, kind = ((CommandExecution, "commandExecution") if "commandExecution" in method else
|
|
548
|
+
(PlanMessage, "plan") if "plan" in method else (AssistantMessage, "agentMessage"))
|
|
549
|
+
message = chat["messages"].setdefault(identifier, cls(kind))
|
|
550
|
+
delta = params.get("delta", "")
|
|
551
|
+
if isinstance(message, CommandExecution):
|
|
552
|
+
message["content"]["output"] = ToolOutput(message["content"].get("output", "") + delta)
|
|
553
|
+
else:
|
|
554
|
+
set_text(message, message.source_text + delta)
|
|
555
|
+
elif method in ("item/reasoning/summaryTextDelta", "item/reasoning/textDelta"):
|
|
556
|
+
identifier = params.get("itemId")
|
|
557
|
+
if identifier:
|
|
558
|
+
message = chat["messages"].setdefault(identifier, ReasoningMessage("reasoning"))
|
|
559
|
+
field = "summary" if "summary" in method else "content"
|
|
560
|
+
index = str(params.get("summaryIndex", params.get("contentIndex", 0)))
|
|
561
|
+
parts = message["content"].setdefault(field, {})
|
|
562
|
+
parts[index] = ToolOutput(str(parts.get(index, "")) + params.get("delta", ""))
|
|
563
|
+
elif method in ("item/fileChange/outputDelta", "item/mcpToolCall/progress"):
|
|
564
|
+
identifier = params.get("itemId")
|
|
565
|
+
if identifier:
|
|
566
|
+
is_file = "fileChange" in method
|
|
567
|
+
cls = FileChange if is_file else McpToolCall
|
|
568
|
+
message = chat["messages"].setdefault(identifier, cls("fileChange" if is_file else "mcpToolCall"))
|
|
569
|
+
field = "output" if is_file else "progress"
|
|
570
|
+
delta = params.get("delta", "") if is_file else params.get("message", "") + "\n"
|
|
571
|
+
message["content"][field] = ToolOutput(message["content"].get(field, "") + delta)
|
|
572
|
+
elif method == "turn/diff/updated":
|
|
573
|
+
self._turn_diff(chat, params.get("diff") or params.get("unifiedDiff") or "")
|
|
574
|
+
elif method == "turn/started":
|
|
575
|
+
chat.turn_counts = {}
|
|
576
|
+
chat.turn_id = params["turn"]["id"]
|
|
577
|
+
chat["running"] = "interrupt_requested" not in chat.inflight
|
|
578
|
+
elif method == "turn/completed":
|
|
579
|
+
self.submit("release", chat.remote_id)
|
|
580
|
+
chat.resumed = False
|
|
581
|
+
chat.turn_id = None
|
|
582
|
+
chat["running"] = False
|
|
583
|
+
chat.inflight.discard("interrupt")
|
|
584
|
+
chat.inflight.discard("interrupt_requested")
|
|
585
|
+
chat["requests"].clear()
|
|
586
|
+
error = params.get("turn", {}).get("error")
|
|
587
|
+
if error:
|
|
588
|
+
chat.error = error.get("message", str(error))
|
|
589
|
+
elif method == "error":
|
|
590
|
+
chat.error = params.get("error", {}).get("message", "Codex error")
|
|
591
|
+
elif method == "serverRequest/resolved":
|
|
592
|
+
chat["requests"].pop(params.get("requestId"), None)
|