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,417 @@
|
|
|
1
|
+
"""Provider-neutral message values. Types select views; no rendering or file I/O."""
|
|
2
|
+
import re
|
|
3
|
+
from meltygui.chat.command_parser import parse_command
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TextString(str):
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class MarkdownString(TextString):
|
|
11
|
+
pass
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class CodeString(TextString):
|
|
15
|
+
def __new__(cls, value="", language=""):
|
|
16
|
+
obj = super().__new__(cls, value)
|
|
17
|
+
obj.language = language
|
|
18
|
+
return obj
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PythonString(CodeString):
|
|
22
|
+
def __new__(cls, value="", language="python"):
|
|
23
|
+
return super().__new__(cls, value, language)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ShellString(CodeString):
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BashString(ShellString):
|
|
31
|
+
def __new__(cls, value="", language="bash"):
|
|
32
|
+
return super().__new__(cls, value, language)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class FileTags(dict):
|
|
36
|
+
"""Ordered file references with access intent and optional reported diff counts."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ToolOutput(TextString):
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class DiffString(CodeString):
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class JsonData(dict):
|
|
48
|
+
"""Structured tool arguments/results; keys and numeric/bool values stay native."""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Reference(dict):
|
|
52
|
+
label = "Reference"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class ImageReference(Reference):
|
|
56
|
+
label = "Image"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class AudioReference(Reference):
|
|
60
|
+
label = "Audio"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class FileReference(Reference):
|
|
64
|
+
label = "File"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class SkillReference(Reference):
|
|
68
|
+
label = "Skill"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class MentionReference(Reference):
|
|
72
|
+
label = "Mention"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class Message(dict):
|
|
76
|
+
role = "activity"
|
|
77
|
+
label = "Activity"
|
|
78
|
+
|
|
79
|
+
def __init__(self, kind="", content=None, status="", details=None):
|
|
80
|
+
super().__init__(kind=kind, role=self.role, status=status,
|
|
81
|
+
content={} if content is None else content,
|
|
82
|
+
details=JsonData() if details is None else details)
|
|
83
|
+
self.source_text = ""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class UserMessage(Message):
|
|
87
|
+
role, label = "user", "You"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class AssistantMessage(Message):
|
|
91
|
+
role, label = "assistant", "Assistant"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class PlanMessage(AssistantMessage):
|
|
95
|
+
label = "Plan"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class ReasoningMessage(Message):
|
|
99
|
+
label = "Reasoning summary"
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class ToolCall(Message):
|
|
103
|
+
label = "Tool call"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class CommandExecution(ToolCall):
|
|
107
|
+
label = "Command"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class FileChange(ToolCall):
|
|
111
|
+
label = "File changes"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class McpToolCall(ToolCall):
|
|
115
|
+
label = "MCP tool"
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class DynamicToolCall(ToolCall):
|
|
119
|
+
label = "Tool"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class CollabAgentToolCall(ToolCall):
|
|
123
|
+
label = "Agent tool"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class FunctionCallOutput(ToolCall):
|
|
127
|
+
label = "Tool result"
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class WebSearch(ToolCall):
|
|
131
|
+
label = "Web search"
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class HookPrompt(Message):
|
|
135
|
+
label = "Hook"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class SubAgentActivity(Message):
|
|
139
|
+
label = "Agent activity"
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class ImageView(Message):
|
|
143
|
+
label = "Image viewed"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class ImageGeneration(Message):
|
|
147
|
+
label = "Image generated"
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
class Sleep(Message):
|
|
151
|
+
label = "Waiting"
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class EnteredReviewMode(Message):
|
|
155
|
+
label = "Review started"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
class ExitedReviewMode(Message):
|
|
159
|
+
label = "Review completed"
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class ContextCompaction(Message):
|
|
163
|
+
label = "Context compacted"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class UnknownMessage(Message):
|
|
167
|
+
label = "Other activity"
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
ITEM_TYPES = {
|
|
171
|
+
"userMessage": UserMessage, "agentMessage": AssistantMessage, "plan": PlanMessage,
|
|
172
|
+
"reasoning": ReasoningMessage, "commandExecution": CommandExecution, "fileChange": FileChange,
|
|
173
|
+
"mcpToolCall": McpToolCall, "dynamicToolCall": DynamicToolCall,
|
|
174
|
+
"collabAgentToolCall": CollabAgentToolCall, "functionCallOutput": FunctionCallOutput,
|
|
175
|
+
"webSearch": WebSearch, "hookPrompt": HookPrompt, "subAgentActivity": SubAgentActivity,
|
|
176
|
+
"imageView": ImageView, "imageGeneration": ImageGeneration, "sleep": Sleep,
|
|
177
|
+
"enteredReviewMode": EnteredReviewMode, "exitedReviewMode": ExitedReviewMode,
|
|
178
|
+
"contextCompaction": ContextCompaction,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def tagged(value):
|
|
183
|
+
if isinstance(value, str):
|
|
184
|
+
return TextString(value)
|
|
185
|
+
if isinstance(value, dict):
|
|
186
|
+
kind = value.get("type")
|
|
187
|
+
cls = (ImageReference if kind in ("image", "localImage", "image_url", "inputImage") else
|
|
188
|
+
AudioReference if kind in ("audio", "localAudio", "inputAudio") else
|
|
189
|
+
FileReference if kind in ("resource", "resource_link", "file") else JsonData)
|
|
190
|
+
result = cls((key, tagged(val)) for key, val in value.items())
|
|
191
|
+
language = str(value.get("language", "")).lower()
|
|
192
|
+
if language in ("python", "python3", "py") and isinstance(value.get("code"), str):
|
|
193
|
+
result["code"] = PythonString(value["code"])
|
|
194
|
+
elif language in ("bash", "sh", "shell", "zsh") and isinstance(value.get("code"), str):
|
|
195
|
+
result["code"] = BashString(value["code"], language)
|
|
196
|
+
return result
|
|
197
|
+
if isinstance(value, list):
|
|
198
|
+
return [tagged(val) for val in value]
|
|
199
|
+
return value
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def text_blocks(text):
|
|
203
|
+
"""Split fenced code from prose, including the open fence of a streamed reply.
|
|
204
|
+
|
|
205
|
+
Explicit Python/py/python3 fences become PythonString. Other languages
|
|
206
|
+
retain their language tag but do not pretend to be Python.
|
|
207
|
+
"""
|
|
208
|
+
result = {}
|
|
209
|
+
lines = str(text).splitlines(keepends=True)
|
|
210
|
+
buffer, fence, language = [], None, ""
|
|
211
|
+
|
|
212
|
+
def flush(code=False):
|
|
213
|
+
value = "".join(buffer)
|
|
214
|
+
if value or code:
|
|
215
|
+
cls = PythonString if language in ("python", "py", "python3") else BashString if language in ("bash", "sh", "shell", "zsh") else CodeString
|
|
216
|
+
result[str(len(result))] = cls(value, language) if code else MarkdownString(value)
|
|
217
|
+
buffer.clear()
|
|
218
|
+
|
|
219
|
+
for line in lines:
|
|
220
|
+
if fence is None:
|
|
221
|
+
match = re.match(r"^ {0,3}(`{3,}|~{3,})([^\r\n]*)\r?\n?$", line)
|
|
222
|
+
if match:
|
|
223
|
+
flush()
|
|
224
|
+
fence = match[1]
|
|
225
|
+
language = match[2].strip().split()[0].lower() if match[2].strip() else ""
|
|
226
|
+
else:
|
|
227
|
+
buffer.append(line)
|
|
228
|
+
elif re.fullmatch(r" {0,3}" + re.escape(fence[0]) + "{" + str(len(fence)) + r",}\s*", line):
|
|
229
|
+
flush(code=True)
|
|
230
|
+
fence = None
|
|
231
|
+
else:
|
|
232
|
+
buffer.append(line)
|
|
233
|
+
flush(code=fence is not None)
|
|
234
|
+
if not result:
|
|
235
|
+
result["0"] = MarkdownString("")
|
|
236
|
+
return result
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _reuse(target, incoming):
|
|
240
|
+
"""Retain unchanged leaves by identity so sibling text tiles stay cached."""
|
|
241
|
+
for key in list(target):
|
|
242
|
+
if key not in incoming:
|
|
243
|
+
del target[key]
|
|
244
|
+
for key, value in incoming.items():
|
|
245
|
+
old = target.get(key)
|
|
246
|
+
if type(old) is type(value) and isinstance(value, dict):
|
|
247
|
+
_reuse(old, value)
|
|
248
|
+
elif type(old) is not type(value) or old != value or getattr(old, "language", None) != getattr(value, "language", None):
|
|
249
|
+
target[key] = value
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def set_text(message, text):
|
|
253
|
+
message.source_text = text
|
|
254
|
+
_reuse(message["content"], text_blocks(text))
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def user_message(text):
|
|
258
|
+
message = UserMessage("userMessage")
|
|
259
|
+
set_text(message, text)
|
|
260
|
+
return message
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def input_text(message):
|
|
264
|
+
def render(value):
|
|
265
|
+
if isinstance(value, Reference):
|
|
266
|
+
return "" # a picture / file / skill is not prose (only its payload)
|
|
267
|
+
if isinstance(value, CodeString):
|
|
268
|
+
return "```" + value.language + "\n" + str(value) + ("" if value.endswith("\n") else "\n") + "```\n"
|
|
269
|
+
if isinstance(value, str):
|
|
270
|
+
return str(value)
|
|
271
|
+
if isinstance(value, dict):
|
|
272
|
+
return "".join(render(v) for v in value.values())
|
|
273
|
+
return ""
|
|
274
|
+
return render(message["content"])
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _input(part):
|
|
278
|
+
kind = part.get("type")
|
|
279
|
+
if kind == "text":
|
|
280
|
+
return text_blocks(part.get("text", ""))
|
|
281
|
+
cls = {"image": ImageReference, "localImage": ImageReference,
|
|
282
|
+
"audio": AudioReference, "localAudio": AudioReference,
|
|
283
|
+
"skill": SkillReference, "mention": MentionReference}.get(kind, Reference)
|
|
284
|
+
return cls(tagged(part))
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def from_codex(item, scripts=None):
|
|
288
|
+
kind = item.get("type", "unknown")
|
|
289
|
+
cls = ITEM_TYPES.get(kind, UnknownMessage)
|
|
290
|
+
message = cls(kind, status=item.get("status", ""))
|
|
291
|
+
content = message["content"]
|
|
292
|
+
consumed = {"id", "type", "status"}
|
|
293
|
+
if cls in (AssistantMessage, PlanMessage):
|
|
294
|
+
set_text(message, item.get("text", ""))
|
|
295
|
+
consumed.add("text")
|
|
296
|
+
elif cls is UserMessage:
|
|
297
|
+
content.update((str(i), _input(part)) for i, part in enumerate(item.get("content", [])))
|
|
298
|
+
consumed.add("content")
|
|
299
|
+
elif cls is CommandExecution:
|
|
300
|
+
source, files = parse_command(item.get("command", ""), item.get("commandActions"), scripts, cwd=item.get("cwd") or "")
|
|
301
|
+
message["summary"] = FileTags((path, {"file": FileReference(path=TextString(path)),
|
|
302
|
+
"access": access, "added": None, "removed": None}) for path, access in files.items())
|
|
303
|
+
content.update(command=BashString(source),
|
|
304
|
+
output=ToolOutput(item.get("aggregatedOutput") or ""))
|
|
305
|
+
consumed.update(("command", "aggregatedOutput"))
|
|
306
|
+
elif cls is FileChange:
|
|
307
|
+
for i, change in enumerate(item.get("changes", [])):
|
|
308
|
+
content[str(i)] = {"file": FileReference(path=TextString(change.get("path", ""))),
|
|
309
|
+
"diff": DiffString(change.get("diff", ""), "diff"),
|
|
310
|
+
"kind": tagged(change.get("kind"))}
|
|
311
|
+
consumed.add("changes")
|
|
312
|
+
elif cls is ReasoningMessage:
|
|
313
|
+
content["summary"] = {str(i): text_blocks(t) for i, t in enumerate(item.get("summary", []))}
|
|
314
|
+
content["content"] = {str(i): text_blocks(t) for i, t in enumerate(item.get("content", []))}
|
|
315
|
+
consumed.update(("summary", "content"))
|
|
316
|
+
elif cls in (ImageView, ImageGeneration):
|
|
317
|
+
content["image"] = ImageReference(path=TextString(item.get("savedPath") or item.get("path") or ""),
|
|
318
|
+
result=TextString(item.get("result") or ""))
|
|
319
|
+
consumed.update(("savedPath", "path", "result"))
|
|
320
|
+
else:
|
|
321
|
+
# Tool arguments/results and log-compatible events keep their
|
|
322
|
+
# structure rather than being flattened into a lossy log string.
|
|
323
|
+
for field in ("arguments", "result", "output", "contentItems", "query", "results",
|
|
324
|
+
"prompt", "review", "fragments", "agentsStates", "durationMs"):
|
|
325
|
+
if field in item:
|
|
326
|
+
content[field] = tagged(item[field])
|
|
327
|
+
consumed.add(field)
|
|
328
|
+
message["details"].update((k, tagged(v)) for k, v in item.items() if k not in consumed)
|
|
329
|
+
if cls is FileChange:
|
|
330
|
+
message["summary"] = changed_files(message)
|
|
331
|
+
return message
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def upsert(messages, identifier, incoming):
|
|
335
|
+
old = messages.get(identifier)
|
|
336
|
+
if type(old) is type(incoming):
|
|
337
|
+
_reuse(old, incoming)
|
|
338
|
+
old.source_text = incoming.source_text
|
|
339
|
+
return old
|
|
340
|
+
messages[identifier] = incoming
|
|
341
|
+
return incoming
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def count_diff_lines(diff):
|
|
345
|
+
"""(added, removed) of one file's diff; a bare +/- listing counts without hunks."""
|
|
346
|
+
added = removed = 0
|
|
347
|
+
has_hunks = "@@" in diff
|
|
348
|
+
in_hunk = not has_hunks
|
|
349
|
+
for line in diff.splitlines():
|
|
350
|
+
if line.startswith("@@"):
|
|
351
|
+
in_hunk = True
|
|
352
|
+
elif line.startswith("diff --git"):
|
|
353
|
+
in_hunk = False
|
|
354
|
+
elif not has_hunks and line.startswith(("+++ ", "--- ")):
|
|
355
|
+
continue
|
|
356
|
+
elif in_hunk and line.startswith("+"):
|
|
357
|
+
added += 1
|
|
358
|
+
elif in_hunk and line.startswith("-"):
|
|
359
|
+
removed += 1
|
|
360
|
+
return added, removed
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def diff_counts(diff):
|
|
364
|
+
"""Per-file (added, removed) of a multi-file unified diff, keyed by the new path.
|
|
365
|
+
|
|
366
|
+
Codex's turn diff names files as `diff --git a/<path> b/<path>` (deleted
|
|
367
|
+
files keep the `a/` side); paths stay as written, callers match them.
|
|
368
|
+
"""
|
|
369
|
+
counts = {}
|
|
370
|
+
path, lines = None, []
|
|
371
|
+
def flush():
|
|
372
|
+
if path is not None:
|
|
373
|
+
counts[path] = count_diff_lines("\n".join(lines))
|
|
374
|
+
for line in str(diff).splitlines():
|
|
375
|
+
if line.startswith("diff --git "):
|
|
376
|
+
flush()
|
|
377
|
+
sides = line[len("diff --git "):].split(" b/", 1)
|
|
378
|
+
path = sides[1] if len(sides) == 2 else sides[0].removeprefix("a/")
|
|
379
|
+
lines = []
|
|
380
|
+
else:
|
|
381
|
+
lines.append(line)
|
|
382
|
+
flush()
|
|
383
|
+
return counts
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def match_file(tag_path, diff_path):
|
|
387
|
+
"""A tag holds the command's absolute path; a diff names it relative or absolute."""
|
|
388
|
+
tag_path, diff_path = str(tag_path), str(diff_path)
|
|
389
|
+
return (tag_path == diff_path or tag_path.endswith("/" + diff_path)
|
|
390
|
+
or diff_path.endswith("/" + tag_path))
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def changed_files(message):
|
|
394
|
+
"""Normalize provider-reported diffs; command summaries are parsed on ingestion."""
|
|
395
|
+
if not isinstance(message, FileChange):
|
|
396
|
+
return message.get("summary", FileTags())
|
|
397
|
+
signature = tuple((key, change.get("diff"), str(change.get("file", {}).get("path", "")))
|
|
398
|
+
for key, change in message["content"].items() if isinstance(change, dict))
|
|
399
|
+
cached = getattr(message, "_changed_files", None)
|
|
400
|
+
if (cached is not None and len(cached[0]) == len(signature)
|
|
401
|
+
and all(a[0] == b[0] and a[1] is b[1] and a[2] == b[2]
|
|
402
|
+
for a, b in zip(cached[0], signature))):
|
|
403
|
+
return cached[1]
|
|
404
|
+
files = FileTags()
|
|
405
|
+
for change in message["content"].values():
|
|
406
|
+
if not isinstance(change, dict) or not isinstance(change.get("file"), FileReference):
|
|
407
|
+
continue
|
|
408
|
+
path = str(change["file"].get("path", ""))
|
|
409
|
+
if not path:
|
|
410
|
+
continue
|
|
411
|
+
added, removed = count_diff_lines(change.get("diff", ""))
|
|
412
|
+
entry = files.setdefault(path, {"added": 0, "removed": 0, "change": change,
|
|
413
|
+
"file": change["file"], "access": "write"})
|
|
414
|
+
entry["added"] += added
|
|
415
|
+
entry["removed"] += removed
|
|
416
|
+
message._changed_files = (signature, files)
|
|
417
|
+
return files
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""The chat equivalent of FileMetaCollection: account → project → chat.
|
|
2
|
+
|
|
3
|
+
No colour is assigned here: a project (a directory) wears the tint painted
|
|
4
|
+
on it in the shared file-meta store, a conversation the tint the user
|
|
5
|
+
painted on its row (``entry["tint"]``, absent until then) — the file
|
|
6
|
+
browser's rule, brush then picker.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from meltygui.core.conversion.dict_conversion import DictConversion
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ChatMetadata(DictConversion):
|
|
13
|
+
def __init__(self):
|
|
14
|
+
super().__init__()
|
|
15
|
+
self.name = "Chat Metadata"
|
|
16
|
+
self.accounts = {}
|
|
17
|
+
|
|
18
|
+
def account(self, account_id):
|
|
19
|
+
return self.accounts.setdefault(account_id, {"projects": {}})
|
|
20
|
+
|
|
21
|
+
def project(self, account_id, project):
|
|
22
|
+
projects = self.account(account_id)["projects"]
|
|
23
|
+
if project not in projects:
|
|
24
|
+
projects[project] = {"expanded": True, "order": len(projects), "chats": {}}
|
|
25
|
+
return projects[project]
|
|
26
|
+
|
|
27
|
+
def conversation(self, account_id, project, key):
|
|
28
|
+
# Moving a conversation between projects moves its metadata entry too.
|
|
29
|
+
target = self.project(account_id, project)
|
|
30
|
+
if key not in target["chats"]:
|
|
31
|
+
for entry in self.account(account_id)["projects"].values():
|
|
32
|
+
if key in entry["chats"]:
|
|
33
|
+
target["chats"][key] = entry["chats"].pop(key)
|
|
34
|
+
break
|
|
35
|
+
else:
|
|
36
|
+
target["chats"][key] = {"order": len(target["chats"])}
|
|
37
|
+
return target["chats"][key]
|
|
38
|
+
|
|
39
|
+
def apply(self, account_id, chats):
|
|
40
|
+
orders = {}
|
|
41
|
+
for key, chat in chats.items():
|
|
42
|
+
project = self.project(account_id, chat["project"])
|
|
43
|
+
entry = self.conversation(account_id, chat["project"], key)
|
|
44
|
+
# These are local overrides, not remotely editable chat fields.
|
|
45
|
+
chat["__overrides__"] = {"tint": entry.get("tint")}
|
|
46
|
+
if hasattr(chat, "applied_tint"):
|
|
47
|
+
chat.applied_tint = entry.get("tint")
|
|
48
|
+
chat.metadata = entry
|
|
49
|
+
orders[key] = (project["order"], entry["order"])
|
|
50
|
+
ordered = sorted(chats, key=lambda key: orders[key])
|
|
51
|
+
if ordered != list(chats): # re-insert only when the order moved (this runs every frame)
|
|
52
|
+
for key in ordered:
|
|
53
|
+
dict.__setitem__(chats, key, dict.pop(chats, key))
|
|
54
|
+
if hasattr(chats, "applied_order"):
|
|
55
|
+
chats.applied_order = list(chats)
|
|
56
|
+
|
|
57
|
+
def collect(self, account_id, chats):
|
|
58
|
+
positions = {}
|
|
59
|
+
order_changed = list(chats) != getattr(chats, "applied_order", None)
|
|
60
|
+
for key, chat in chats.items():
|
|
61
|
+
project = chat["project"]
|
|
62
|
+
entry = self.conversation(account_id, project, key)
|
|
63
|
+
position = positions.get(project, 0)
|
|
64
|
+
if order_changed:
|
|
65
|
+
entry["order"] = position
|
|
66
|
+
positions[project] = position + 1
|
|
67
|
+
tint = chat.get("__overrides__", {}).get("tint")
|
|
68
|
+
if tint is not None and tint != getattr(chat, "applied_tint", None):
|
|
69
|
+
entry["tint"] = tuple(tint)
|
|
70
|
+
if getattr(chat, "remote_id", None):
|
|
71
|
+
entry["remote_id"] = chat.remote_id
|
|
72
|
+
|
|
73
|
+
def local_key(self, account_id, remote_id):
|
|
74
|
+
for project in self.account(account_id)["projects"].values():
|
|
75
|
+
for key, entry in project["chats"].items():
|
|
76
|
+
if entry.get("remote_id") == remote_id:
|
|
77
|
+
return key
|
|
78
|
+
return remote_id
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
_app_metadata = None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def shared_metadata():
|
|
85
|
+
"""Resolve the persistent mirror at the model boundary, never in a view.
|
|
86
|
+
|
|
87
|
+
In the studio it lives on the root model and is saved with the session.
|
|
88
|
+
An app on meltygui has no root: every proxy then shares one process-wide
|
|
89
|
+
instance, persisted to a JSON file when the app asked for it with
|
|
90
|
+
`persistent_metadata(path)`."""
|
|
91
|
+
global _app_metadata
|
|
92
|
+
from meltygui.core.melty import Melty
|
|
93
|
+
root = getattr(getattr(Melty, "vis", None), "root", None)
|
|
94
|
+
if root is None:
|
|
95
|
+
if _app_metadata is None:
|
|
96
|
+
_app_metadata = ChatMetadata()
|
|
97
|
+
return _app_metadata
|
|
98
|
+
if not hasattr(root, "chat_metadata"):
|
|
99
|
+
root.chat_metadata = ChatMetadata()
|
|
100
|
+
return root.chat_metadata
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def persistent_metadata(path):
|
|
104
|
+
"""The app-wide ChatMetadata, loaded from ``path`` (a JSON file; missing
|
|
105
|
+
or unreadable starts empty) and written back at interpreter exit. Call
|
|
106
|
+
it once before the first chat proxy is created."""
|
|
107
|
+
import atexit
|
|
108
|
+
import json
|
|
109
|
+
import os
|
|
110
|
+
import pathlib
|
|
111
|
+
global _app_metadata
|
|
112
|
+
path = pathlib.Path(path).expanduser()
|
|
113
|
+
metadata = shared_metadata()
|
|
114
|
+
try:
|
|
115
|
+
accounts = json.loads(path.read_text()).get("accounts", {})
|
|
116
|
+
except (OSError, ValueError):
|
|
117
|
+
accounts = {}
|
|
118
|
+
for account in accounts.values():
|
|
119
|
+
for project in account.get("projects", {}).values():
|
|
120
|
+
project.pop("tint", None) # a project's tint is the folder's, in the project-meta store
|
|
121
|
+
for chat in project.get("chats", {}).values():
|
|
122
|
+
if chat.get("tint") is not None:
|
|
123
|
+
chat["tint"] = tuple(chat["tint"])
|
|
124
|
+
else:
|
|
125
|
+
chat.pop("tint", None)
|
|
126
|
+
metadata.accounts.update(accounts)
|
|
127
|
+
|
|
128
|
+
def save():
|
|
129
|
+
try:
|
|
130
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
131
|
+
tmp = path.with_suffix(path.suffix + ".tmp")
|
|
132
|
+
tmp.write_text(json.dumps({"accounts": metadata.accounts}, indent=1))
|
|
133
|
+
os.replace(tmp, path)
|
|
134
|
+
except (OSError, TypeError, ValueError) as error:
|
|
135
|
+
print(f"chat metadata: cannot write {path}: {error}")
|
|
136
|
+
|
|
137
|
+
atexit.register(save)
|
|
138
|
+
metadata.save = save
|
|
139
|
+
return metadata
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Read Linux's held file locks without acquiring or creating session locks."""
|
|
2
|
+
import os
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def codex_writer_locks(home, locks_path='/proc/locks'):
|
|
7
|
+
try:
|
|
8
|
+
held = {}
|
|
9
|
+
for line in Path(locks_path).read_text().splitlines():
|
|
10
|
+
fields = line.split()
|
|
11
|
+
# Lock requests have a '->' marker; only held WRITE locks
|
|
12
|
+
# mean the session has a writer. File existence alone is not a lock.
|
|
13
|
+
if len(fields) < 6 or fields[1] == '->' or fields[3] != 'WRITE':
|
|
14
|
+
continue
|
|
15
|
+
major, minor, inode = fields[5].split(':')
|
|
16
|
+
held[(int(major, 16), int(minor, 16), int(inode))] = int(fields[4])
|
|
17
|
+
locked = {}
|
|
18
|
+
for path in (Path(home) / 'thread-writer-locks').glob('*.lock'):
|
|
19
|
+
if path.name.startswith('.'):
|
|
20
|
+
continue
|
|
21
|
+
try:
|
|
22
|
+
stat = path.stat()
|
|
23
|
+
except OSError:
|
|
24
|
+
continue
|
|
25
|
+
if (os.major(stat.st_dev), os.minor(stat.st_dev), stat.st_ino) in held:
|
|
26
|
+
locked[path.stem] = lock_owner(held[(os.major(stat.st_dev), os.minor(stat.st_dev), stat.st_ino)])
|
|
27
|
+
return locked
|
|
28
|
+
except (OSError, ValueError):
|
|
29
|
+
return None # unavailable is not evidence that previous locks released
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def lock_owner(pid):
|
|
33
|
+
info = {"pid": pid, "client": "Unknown client", "location": "local machine"}
|
|
34
|
+
try:
|
|
35
|
+
executable = os.readlink(f"/proc/{pid}/exe")
|
|
36
|
+
name = Path(f"/proc/{pid}/comm").read_text().strip()
|
|
37
|
+
info["client"] = "Codex desktop app" if "/chatgpt/" in executable else name
|
|
38
|
+
# Read only process names/parent IDs; never arguments or environment
|
|
39
|
+
# variables, which may contain credentials or conversation contents.
|
|
40
|
+
current = pid
|
|
41
|
+
for _ in range(12):
|
|
42
|
+
status = Path(f"/proc/{current}/status").read_text().splitlines()
|
|
43
|
+
parent = int(next(line.split()[1] for line in status if line.startswith("PPid:")))
|
|
44
|
+
if not parent:
|
|
45
|
+
break
|
|
46
|
+
parent_name = Path(f"/proc/{parent}/comm").read_text().strip()
|
|
47
|
+
if parent_name in ("sshd", "sshd-session"):
|
|
48
|
+
info["location"] = "local machine, launched through SSH"
|
|
49
|
+
break
|
|
50
|
+
current = parent
|
|
51
|
+
except (OSError, ValueError, StopIteration):
|
|
52
|
+
pass
|
|
53
|
+
return info
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def lock_message(owner=None):
|
|
57
|
+
owner = owner or {}
|
|
58
|
+
identity = owner.get("client", "another client")
|
|
59
|
+
if owner.get("pid", -1) > 0:
|
|
60
|
+
identity += f" (PID {owner['pid']}, {owner.get('location', 'local machine')})"
|
|
61
|
+
return (f"This conversation is open for writing in {identity}.\n"
|
|
62
|
+
"To release it, finish or stop its active turn, then close the owning app or CLI session. "
|
|
63
|
+
"Stopping a turn alone may leave the session locked. You can still try Send if this status looks stale.\n"
|
|
64
|
+
"Your unsent message stays in the composer. A remotely connected client cannot be identified from this local lock.")
|
|
File without changes
|