meltygui 0.1.0__py3-none-any.whl → 0.1.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. meltygui/__init__.py +15 -2
  2. meltygui/accounts/internet_accounts.py +27 -17
  3. meltygui/chat/__init__.py +26 -10
  4. meltygui/chat/__main__.py +5 -0
  5. meltygui/chat/assets/source-icons/LICENSE +21 -0
  6. meltygui/chat/assets/source-icons/README.md +4 -0
  7. meltygui/chat/assets/source-icons/claude.png +0 -0
  8. meltygui/chat/assets/source-icons/claude.svg +1 -0
  9. meltygui/chat/assets/source-icons/codex.png +0 -0
  10. meltygui/chat/assets/source-icons/codex.svg +1 -0
  11. meltygui/chat/auto_titles.py +79 -0
  12. meltygui/chat/backends.py +1 -1
  13. meltygui/chat/chat_interface.py +49 -22
  14. meltygui/chat/chat_service.py +517 -0
  15. meltygui/chat/claude_code.py +1054 -0
  16. meltygui/chat/codex_proxy.py +11 -0
  17. meltygui/chat/codex_settings.py +26 -0
  18. meltygui/chat/main.py +202 -0
  19. meltygui/chat/metadata.py +4 -1
  20. meltygui/code/chain_converters.py +2 -2
  21. meltygui/code/codec_registry.py +40 -0
  22. meltygui/code/core_syntax.py +15 -0
  23. meltygui/code/file_converters.py +40 -2
  24. meltygui/code/live_view.py +9 -2
  25. meltygui/code/melty_scan.py +257 -5
  26. meltygui/code/new_codecs.py +69 -32
  27. meltygui/code/new_converters.py +19 -2
  28. meltygui/core/README.md +8 -5
  29. meltygui/core/automation/input_recording_core.py +60 -0
  30. meltygui/core/automation/input_replay.py +180 -0
  31. meltygui/core/automation/mcp_server.py +5 -4
  32. meltygui/core/automation/orchestration_core.py +6 -4
  33. meltygui/core/cache/invalidation_decoration.py +24 -0
  34. meltygui/core/cache/invalidation_tracker.py +39 -0
  35. meltygui/core/cache/mask_batch.py +176 -0
  36. meltygui/core/cache/parameter_dependencies.py +52 -0
  37. meltygui/core/cache/tile_cache.py +535 -442
  38. meltygui/core/cache/tile_marks.py +96 -0
  39. meltygui/core/conversion/dict_conversion.py +3 -0
  40. meltygui/core/conversion/load_save_v2.py +47 -21
  41. meltygui/core/core_render.py +165 -339
  42. meltygui/core/definition_hotswap.py +54 -0
  43. meltygui/core/diagnostics/edge_motion_guard.py +621 -0
  44. meltygui/core/diagnostics/fps_counter.py +13 -27
  45. meltygui/core/diagnostics/geometry_jitter.py +208 -0
  46. meltygui/core/diagnostics/gpu_frame_timer.py +9 -3
  47. meltygui/core/diagnostics/resize_trace.py +4 -2
  48. meltygui/core/files/file_explorer_core.py +45 -0
  49. meltygui/core/files/file_tree_core.py +1 -1
  50. meltygui/core/files/path_icons.py +76 -0
  51. meltygui/core/graphics/cuda_context_core.py +6 -0
  52. meltygui/core/graphics/cuda_interop_core.py +32 -0
  53. meltygui/core/graphics/overlay_renderer.py +31 -7
  54. meltygui/core/input/drag_drop_core.py +16 -6
  55. meltygui/core/input/input_handler.py +71 -23
  56. meltygui/core/input/pynput_backend.py +6 -3
  57. meltygui/core/layout/column_core.py +137 -34
  58. meltygui/core/layout/dropdown_core.py +3 -2
  59. meltygui/core/layout/edge_constraints.py +6 -0
  60. meltygui/core/layout/header_runtime.py +1 -1
  61. meltygui/core/layout/layout_core.py +1 -1
  62. meltygui/core/layout/tile_links.py +180 -0
  63. meltygui/core/layout/tile_manager_core.py +221 -44
  64. meltygui/core/layout/tile_resize.py +54 -0
  65. meltygui/core/melty.py +336 -86
  66. meltygui/core/module_map.json +22 -3
  67. meltygui/core/rendering/fast_view.py +538 -0
  68. meltygui/core/rendering/injected_state.py +62 -0
  69. meltygui/core/rendering/mode.py +305 -259
  70. meltygui/core/rendering/modes.py +0 -5
  71. meltygui/core/rendering/overlay.py +353 -0
  72. meltygui/core/rendering/parameter_core.py +4 -0
  73. meltygui/core/rendering/render_dispatch.py +13 -46
  74. meltygui/core/rendering/render_funcs.py +0 -1
  75. meltygui/core/rendering/view_identity.py +199 -0
  76. meltygui/core/runtime/app.py +211 -30
  77. meltygui/core/runtime/app_settings.py +183 -0
  78. meltygui/core/runtime/background.py +10 -1
  79. meltygui/core/runtime/launch_override.py +223 -0
  80. meltygui/core/runtime/toggles.py +69 -7
  81. meltygui/core/styling/fonts.py +1 -1
  82. meltygui/core/styling/warm_start.py +32 -0
  83. meltygui/core/windowing/dock_core.py +3 -3
  84. meltygui/core/windowing/geometry_feed.py +97 -5
  85. meltygui/core/windowing/glfw_utils.py +113 -27
  86. meltygui/core/windowing/os_frame.py +59 -11
  87. meltygui/core/windowing/surface.py +67 -3
  88. meltygui/core/windowing/titlebar.py +122 -39
  89. meltygui/core/windowing/window_visibility.py +30 -0
  90. meltygui/editor/file_header.py +1 -1
  91. meltygui/editor/live_view_views.py +1 -1
  92. meltygui/editor/pending_save.py +34 -3
  93. meltygui/editor/source_preview.py +17 -1
  94. meltygui/editor/text_editor.py +11 -4
  95. meltygui/editor/usage_picker.py +2 -2
  96. meltygui/examples/settings_demo.py +28 -0
  97. meltygui/examples/tile_manager_demo.py +19 -4
  98. meltygui/examples/voxel_transfer_paths.py +213 -0
  99. meltygui/files/fast_file_explorer.py +4 -6
  100. meltygui/graphics/__init__.py +1 -2
  101. meltygui/model/breakpoint_model.py +67 -0
  102. meltygui/model/chat_folder_model.py +163 -0
  103. meltygui/model/chat_model.py +98 -6
  104. meltygui/model/code_dict_model.py +870 -0
  105. meltygui/model/cuda_buffer_model.py +127 -0
  106. meltygui/model/cuda_tensor_model.py +1 -1
  107. meltygui/model/cuda_texture_model.py +80 -1
  108. meltygui/model/dropdown_model.py +16 -1
  109. meltygui/model/file_location_model.py +118 -0
  110. meltygui/model/folder_icon_model.py +348 -0
  111. meltygui/model/format_model.py +4 -3
  112. meltygui/model/input_recording_model.py +418 -0
  113. meltygui/model/lut_model.py +113 -3
  114. meltygui/model/source_snapshot_model.py +288 -0
  115. meltygui/model/tensor_model.py +12 -1
  116. meltygui/model/texture_model.py +113 -9
  117. meltygui/model/tile_model.py +24 -0
  118. meltygui/model/trace_model.py +29 -10
  119. meltygui/model/trace_report_model.py +38 -1
  120. meltygui/models/file_meta.py +96 -9
  121. meltygui/state/chat_state.py +21 -1
  122. meltygui/state/core_undo.py +0 -4
  123. meltygui/state/file_state.py +6 -2
  124. meltygui/state/new_core_model.py +109 -63
  125. meltygui/state/path_icon_state.py +23 -0
  126. meltygui/state/texture_state.py +21 -0
  127. meltygui/state/trace_state.py +4 -1
  128. meltygui/state/view_reference.py +20 -0
  129. meltygui/view/account_view.py +1 -1
  130. meltygui/view/action_view.py +1 -1
  131. meltygui/view/chat_decoration_view.py +1 -1
  132. meltygui/view/chat_sidebar_view.py +440 -0
  133. meltygui/view/chat_source_icon_view.py +48 -0
  134. meltygui/view/chat_view.py +183 -291
  135. meltygui/view/code_view.py +62 -69
  136. meltygui/view/collection_view.py +524 -197
  137. meltygui/view/color_view.py +218 -3
  138. meltygui/view/control_view.py +64 -16
  139. meltygui/view/decoration_view.py +5 -4
  140. meltygui/view/dropdown_view.py +305 -24
  141. meltygui/view/file_icon_view.py +60 -0
  142. meltygui/view/file_view.py +344 -112
  143. meltygui/view/graph_view.py +3 -0
  144. meltygui/view/header_view.py +78 -64
  145. meltygui/view/input_recording_view.py +50 -0
  146. meltygui/view/inspection_view.py +81 -12
  147. meltygui/view/layout_view.py +3 -3
  148. meltygui/view/menu_view.py +18 -2
  149. meltygui/view/orchestration_view.py +1 -1
  150. meltygui/view/path_icon_view.py +105 -0
  151. meltygui/view/text_view.py +89 -21
  152. meltygui/view/texture_view.py +198 -434
  153. meltygui/view/tile_view.py +299 -21
  154. meltygui/view/trace_view.py +59 -11
  155. meltygui/view/voxel_cuda_view.py +15 -7
  156. meltygui/view/voxel_view.py +18 -14
  157. {meltygui-0.1.0.dist-info → meltygui-0.1.2.dist-info}/METADATA +6 -4
  158. {meltygui-0.1.0.dist-info → meltygui-0.1.2.dist-info}/RECORD +160 -115
  159. meltygui/graphics/texture_manager.py +0 -170
  160. {meltygui-0.1.0.dist-info → meltygui-0.1.2.dist-info}/WHEEL +0 -0
  161. {meltygui-0.1.0.dist-info → meltygui-0.1.2.dist-info}/licenses/LICENSE +0 -0
meltygui/__init__.py CHANGED
@@ -1,7 +1,12 @@
1
1
  """MeltyGUI: immediate-mode apps, live code editing, HDR and tensor views.
2
2
 
3
3
  Install with ``uv pip install meltygui``; define windows with ``@glfw_window``.
4
- Renderers are imported lazily, so importing this package does not start a GUI.
4
+ Importing this package does not start a GUI, and it stays light: the views
5
+ below import on first use, the render libraries (imgui, numpy, OpenGL) and
6
+ the GL side of the runtime load on the boot's import thread while the first
7
+ ``@glfw_window`` opens the display, and the code-editing stack (libcst) loads
8
+ with the first code edit. core/runtime/app.py describes the boot sequence;
9
+ tests/test_startup_imports.py keeps the import graph that shape.
5
10
  """
6
11
  import os
7
12
  import sys
@@ -23,9 +28,13 @@ from meltygui.core.runtime.app import pressed
23
28
  from meltygui.core.runtime.app import content_size
24
29
  from meltygui.core.runtime.app import mark
25
30
  from meltygui.core.runtime.app import persisted
31
+ from meltygui.core.runtime.app import after_first_frame
26
32
 
27
33
  if TYPE_CHECKING: # IDE / type checkers only; never executed
34
+ from meltygui.state.new_core_model import DrawState
28
35
  from meltygui.view.text_view import draw_text
36
+ from meltygui.model.tile_model import multi_instance
37
+ from meltygui.view.chat_view import draw_claude_chat
29
38
  from meltygui.view.texture_view import draw_texture
30
39
  from meltygui.core.rendering.render_dispatch import draw_any
31
40
  from meltygui.view.control_view import draw_button
@@ -50,6 +59,7 @@ if TYPE_CHECKING: # IDE / type checkers only; never executed
50
59
 
51
60
  _NCV = 'meltygui.core.rendering.render_dispatch'
52
61
  _VIEWS = {
62
+ 'DrawState': ('meltygui.state.new_core_model', 'DrawState'),
53
63
  'draw_voxels': ('meltygui.view.voxel_view', 'draw_voxels'),
54
64
  'draw_voxels_opengl': ('meltygui.view.voxel_view', 'draw_voxels_opengl'),
55
65
  'draw_voxels_cuda': ('meltygui.view.voxel_view', 'draw_voxels_cuda'),
@@ -57,6 +67,8 @@ _VIEWS = {
57
67
  'draw_tensor_error': ('meltygui.view.tensor_view', 'draw_tensor_error'),
58
68
  'draw_line_graph': ('meltygui.view.graph_view', 'draw_line_graph'),
59
69
  'render_func': ('meltygui.core.core_render', 'render_func'),
70
+ 'multi_instance': ('meltygui.model.tile_model', 'multi_instance'),
71
+ 'draw_claude_chat': ('meltygui.view.chat_view', 'draw_claude_chat'),
60
72
  'draw_text': ('meltygui.view.text_view', 'draw_text'),
61
73
  'draw_texture': ('meltygui.view.texture_view', 'draw_texture'),
62
74
  'draw_any': (_NCV, 'draw_any'),
@@ -103,5 +115,6 @@ def __getattr__(name):
103
115
  return value
104
116
 
105
117
 
106
- __all__ = ['Style', 'boot', 'glfw_window', 'run', 'pressed', 'content_size', 'mark', 'persisted', 'toggles', 'window_api', 'imgui',
118
+ __all__ = ['Style', 'boot', 'glfw_window', 'run', 'pressed', 'content_size', 'mark', 'persisted', 'after_first_frame',
119
+ 'toggles', 'window_api', 'imgui',
107
120
  *_VIEWS]
@@ -24,8 +24,8 @@ editable fields, its action buttons and any extra rows (a device-code card,
24
24
  the Ollama model list). A kind with a `chat_label` appears in the Chat
25
25
  window's provider dropdown; its conversations come from the ChatProxy
26
26
  factory registered for the kind name with `register_chat_backend`
27
- (chat/backends.py) — the studio registers Codex's below, an external
28
- package (melty_agents) registers Claude Code's against "anthropic".
27
+ (chat/backends.py) — Codex's and Claude Code's are registered below: the
28
+ detached chat service's mirror (chat/chat_service.py).
29
29
 
30
30
  Layout: every row measures its buttons FIRST (`strip_layout`); if the text
31
31
  would be left less than min_text_width, or the strip is wider than the row,
@@ -52,7 +52,7 @@ from meltygui.chat.backends import chat_backend
52
52
  from meltygui.chat.backends import register_chat_backend
53
53
  from meltygui.core.conversion.dict_conversion import DictConversion
54
54
  from meltygui.core.windowing.glfw_utils import request_render
55
- from meltygui.core.cache.tile_cache import add_shadow
55
+ from meltygui.core.cache.tile_marks import add_shadow
56
56
  from meltygui.core.core_render import render_func
57
57
  from meltygui.core.rendering.window_decoration import window
58
58
 
@@ -507,24 +507,23 @@ class AnthropicKind(AccountKind):
507
507
  flow.submit_code(code)
508
508
 
509
509
  def _owns_claude_login(self, account, login):
510
- """One Claude Code login file = one claude.ai account, and every
511
- Anthropic row points at the same default file — so rows sharing a
512
- file must not ALL paint its numbers (work + personal rows showing
513
- one account's bars twice). The file goes to the row whose sign-in
514
- email is the login's account email; when none matches, to the
515
- first row sharing that path (the default account)."""
510
+ """Match known identities even when only one row uses the login file.
511
+
512
+ Rows without a browser identity retain the default-file fallback;
513
+ a known different account must never inherit another account's usage.
514
+ """
516
515
  path = self._claude_code_login_path(account)
517
516
  sharing = [entry for entry in accounts.of_kind(self.name)
518
517
  if self._claude_code_login_path(entry) == path]
519
- if len(sharing) <= 1:
520
- return True
521
- email = (login.get("email") or "").lower()
518
+ email = (login.get("email") or "").strip().lower()
519
+ identities = [((self.login_info(entry) or {}).get("email") or "").strip().lower()
520
+ for entry in sharing]
522
521
  if email:
523
- matching = [entry for entry in sharing
524
- if ((self.login_info(entry) or {}).get("email") or "").lower() == email]
522
+ matching = [entry for entry, identity in zip(sharing, identities) if identity == email]
525
523
  if matching:
526
524
  return matching[0] is account
527
- return sharing[0] is account
525
+ sharing = [entry for entry, identity in zip(sharing, identities) if not identity]
526
+ return bool(sharing) and sharing[0] is account
528
527
 
529
528
  def refresh_all(self, account):
530
529
  """The Refresh button: re-read the logins and, for an open panel,
@@ -830,12 +829,23 @@ class AnthropicKind(AccountKind):
830
829
  return out
831
830
 
832
831
 
832
+ # Both chat kinds mirror the detached chat service (chat/chat_service.py): the
833
+ # service owns the real backends (CodexChats, ClaudeCodeChats), so a turn
834
+ # outlives the window that sent it and every app shows the same conversations.
833
835
  @register_chat_backend("codex")
834
836
  def codex_chats(account, metadata=None, wake=None):
835
837
  if account.get("_codex_signing_in") or account.get("_busy"):
836
838
  return None
837
- from meltygui.chat.codex_proxy import CodexChats
838
- return CodexChats(account["id"], metadata, wake)
839
+ from meltygui.chat.chat_service import PersistentChats
840
+ return PersistentChats("codex", account, metadata, wake)
841
+
842
+
843
+ @register_chat_backend("anthropic")
844
+ def claude_chats(account, metadata=None, wake=None):
845
+ if account.get("_busy"):
846
+ return None
847
+ from meltygui.chat.chat_service import PersistentChats
848
+ return PersistentChats("anthropic", account, metadata, wake)
839
849
 
840
850
 
841
851
  @account_kind
meltygui/chat/__init__.py CHANGED
@@ -1,12 +1,18 @@
1
1
  """meltygui.chat — the code-assistant chat: the window, the data contract a
2
2
  backend implements, and the registry that plugs a backend into a provider.
3
3
 
4
- import meltygui
5
- from meltygui.chat import draw_chat_interface, persistent_metadata
6
- import melty_agents # registers the Claude Code backend
4
+ from meltygui.chat import draw_claude_chat
7
5
 
8
- persistent_metadata('~/.cache/my-app/chat_metadata.json')
9
- meltygui.glfw_window(name='Chat', app_id='my-app')(draw_chat_interface)
6
+ draw_tiles(..., multi_instance_renderers=(draw_claude_chat,)) # a tile, or
7
+ draw_claude_chat(None, width=w, height=h) # in a window body
8
+
9
+ `draw_claude_chat` is the chat ready for a window body or a tile (`python -m
10
+ meltygui.chat [PROJECT]` is it as the melty-claude app): a plain function, one
11
+ call of `draw_chat_interface` (the only render boundary) with folder tints from the file-meta store and its sidebar metadata kept between
12
+ runs. Its input value picks where new conversations start (`chat_project`).
13
+ Conversations run in the detached chat service (`chat_service.py`): a turn
14
+ outlives the window that sent it and every app shows the same live state.
15
+ Claude Code's backend (`claude_code.py`) needs the `meltygui[claude]` extra.
10
16
 
11
17
  The window (`draw_chat_interface`, the studio's Chat playground) lists the
12
18
  account kinds that carry a `chat_label` in a provider dropdown, the accounts
@@ -20,13 +26,15 @@ backend mirrors to its provider from a worker thread. `chat_proxy.py` is
20
26
  the contract; `codex_proxy.py` in the same package is the reference
21
27
  implementation, and its docstrings say what each operation and event means.
22
28
 
23
- A backend registers a factory for an account kind name:
29
+ A backend registers a factory for an account kind name
30
+ (`accounts/internet_accounts.py` registers the chat service's mirror for
31
+ "codex" and "anthropic"; the service builds `CodexChats` / `ClaudeCodeChats`):
24
32
 
25
33
  from meltygui.chat import ChatProxy, Chat, messages, register_chat_backend
26
34
 
27
- @register_chat_backend('anthropic') # the "Claude Code" provider
28
- def claude_chats(account, metadata=None, wake=None):
29
- return ClaudeCodeChats(account['id'], metadata, wake, account=account)
35
+ @register_chat_backend('my-kind')
36
+ def my_chats(account, metadata=None, wake=None):
37
+ return MyChats(account['id'], metadata, wake)
30
38
 
31
39
  Pictures (`images`): an ImageReference in a message — a base64 payload, a
32
40
  path or a data URL — is decoded once and drawn inline by the window, HDR
@@ -57,6 +65,10 @@ if TYPE_CHECKING: # IDE and type checkers only; never executed
57
65
  from meltygui.chat.metadata import persistent_metadata
58
66
  from meltygui.chat.metadata import shared_metadata
59
67
  from meltygui.view.chat_view import draw_chat_interface
68
+ from meltygui.view.chat_view import draw_claude_chat
69
+ from meltygui.chat.chat_interface import stop_running
70
+ from meltygui.chat.chat_interface import disconnect_chats
71
+ from meltygui.model.chat_model import chat_project
60
72
  from meltygui.state.chat_state import ChatInterfaceState
61
73
  from meltygui.accounts.internet_accounts import accounts
62
74
  from meltygui.accounts.internet_accounts import KINDS
@@ -68,7 +80,11 @@ _LAZY = {
68
80
  'shared_metadata': ('meltygui.chat.metadata', 'shared_metadata'),
69
81
  'persistent_metadata': ('meltygui.chat.metadata', 'persistent_metadata'),
70
82
  'draw_chat_interface': ('meltygui.chat.chat_interface', 'draw_chat_interface'),
71
- 'ChatInterfaceState': ('meltygui.chat.chat_interface', 'ChatInterfaceState'),
83
+ 'draw_claude_chat': ('meltygui.view.chat_view', 'draw_claude_chat'),
84
+ 'stop_running': ('meltygui.chat.chat_interface', 'stop_running'),
85
+ 'disconnect_chats': ('meltygui.chat.chat_interface', 'disconnect_chats'),
86
+ 'chat_project': ('meltygui.model.chat_model', 'chat_project'),
87
+ 'ChatInterfaceState': ('meltygui.state.chat_state', 'ChatInterfaceState'),
72
88
  'accounts': ('meltygui.accounts.internet_accounts', 'accounts'),
73
89
  'KINDS': ('meltygui.accounts.internet_accounts', 'KINDS'),
74
90
  'AccountKind': ('meltygui.accounts.internet_accounts', 'AccountKind'),
@@ -0,0 +1,5 @@
1
+ """Run the shared standalone chat app."""
2
+ from meltygui.chat.main import main
3
+
4
+ if __name__ == "__main__":
5
+ raise SystemExit(main())
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023 LobeHub
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ Claude and Codex marks from https://github.com/lobehub/lobe-icons/tree/master/packages/static-svg/icons
2
+ (downloaded 2026-09-14). Upstream MIT license is included in LICENSE.
3
+ The SVG originals are retained; the white 96px PNGs were rendered with CairoSVG
4
+ for tinting by the sidebar. No network access or SVG renderer is needed at runtime.
@@ -0,0 +1 @@
1
+ <svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Claude</title><path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z"></path></svg>
@@ -0,0 +1 @@
1
+ <svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Codex</title><path clip-rule="evenodd" d="M8.086.457a6.105 6.105 0 013.046-.415c1.333.153 2.521.72 3.564 1.7a.117.117 0 00.107.029c1.408-.346 2.762-.224 4.061.366l.063.03.154.076c1.357.703 2.33 1.77 2.918 3.198.278.679.418 1.388.421 2.126a5.655 5.655 0 01-.18 1.631.167.167 0 00.04.155 5.982 5.982 0 011.578 2.891c.385 1.901-.01 3.615-1.183 5.14l-.182.22a6.063 6.063 0 01-2.934 1.851.162.162 0 00-.108.102c-.255.736-.511 1.364-.987 1.992-1.199 1.582-2.962 2.462-4.948 2.451-1.583-.008-2.986-.587-4.21-1.736a.145.145 0 00-.14-.032c-.518.167-1.04.191-1.604.185a5.924 5.924 0 01-2.595-.622 6.058 6.058 0 01-2.146-1.781c-.203-.269-.404-.522-.551-.821a7.74 7.74 0 01-.495-1.283 6.11 6.11 0 01-.017-3.064.166.166 0 00.008-.074.115.115 0 00-.037-.064 5.958 5.958 0 01-1.38-2.202 5.196 5.196 0 01-.333-1.589 6.915 6.915 0 01.188-2.132c.45-1.484 1.309-2.648 2.577-3.493.282-.188.55-.334.802-.438.286-.12.573-.22.861-.304a.129.129 0 00.087-.087A6.016 6.016 0 015.635 2.31C6.315 1.464 7.132.846 8.086.457zm-.804 7.85a.848.848 0 00-1.473.842l1.694 2.965-1.688 2.848a.849.849 0 001.46.864l1.94-3.272a.849.849 0 00.007-.854l-1.94-3.393zm5.446 6.24a.849.849 0 000 1.695h4.848a.849.849 0 000-1.696h-4.848z"></path></svg>
@@ -0,0 +1,79 @@
1
+ """Background conversation names matching ~/bin/claude-d-title."""
2
+ import os
3
+ import re
4
+ import shutil
5
+ import subprocess
6
+ import threading
7
+ from pathlib import Path
8
+
9
+
10
+ TITLE_PROMPT = '''Name a chat session for the request below. Reply with ONLY a title of the form: file name, function or class name, short bug/task name (two-three lowercase words) — comma-separated, e.g. "text_editor.py, draw_text, missing labels". Use only file/function/class names actually mentioned in the request; OMIT any part you cannot identify (keep the others, no placeholders) — worst case just the bug name. No explanation. If the request is too vague to name (e.g. "continue", "yes", "fix it"), reply NONE.
11
+
12
+ Request: '''
13
+
14
+
15
+ def generate_title(prompt):
16
+ executable = shutil.which('claude') or str(Path.home() / '.local/bin/claude')
17
+ env = {k: v for k, v in os.environ.items()
18
+ if k not in ('TMUX', 'TMUX_PANE', 'CLAUDECODE', 'CLAUDE_CODE_ENTRYPOINT')}
19
+ env['CLAUDE_CODE_DISABLE_TERMINAL_TITLE'] = '1'
20
+ try:
21
+ result = subprocess.run(
22
+ [executable, '-p', '--model', 'haiku', '--output-format', 'text',
23
+ '--no-session-persistence', '--tools', '', '--strict-mcp-config',
24
+ '--settings', '{"disableAllHooks":true}'],
25
+ input=TITLE_PROMPT + prompt[:400], cwd='/', env=env,
26
+ capture_output=True, text=True, timeout=60)
27
+ except (OSError, subprocess.TimeoutExpired):
28
+ return None
29
+ lines = result.stdout.strip().splitlines()
30
+ name = ' '.join(lines[-1].split()) if lines else ''
31
+ if (result.returncode == 0 and name != 'NONE' and len(name) <= 64
32
+ and re.fullmatch(r'[A-Za-z0-9_./-]+(?:,? [A-Za-z0-9_./-]+){0,6}', name)):
33
+ return name
34
+ return None
35
+
36
+
37
+ class AutoTitles:
38
+ def __init__(self, lock, generate=generate_title):
39
+ self.lock = lock
40
+ self.generate = generate
41
+ self.pending = set()
42
+
43
+ def install(self, proxy):
44
+ submit = proxy.submit
45
+
46
+ def with_title(operation, *args):
47
+ submit(operation, *args)
48
+ if operation == 'send':
49
+ key, _, _, prompt, *_ = args
50
+ self.request(proxy, key, prompt)
51
+
52
+ proxy.submit = with_title
53
+
54
+ def request(self, proxy, key, prompt):
55
+ chat = dict.get(proxy, key)
56
+ if chat is None or not prompt.strip():
57
+ return
58
+ state = chat.metadata.get('auto_title')
59
+ if state is None and chat['title'] == 'New conversation':
60
+ state = chat.metadata['auto_title'] = 'retry'
61
+ if state != 'retry' or id(chat) in self.pending:
62
+ return
63
+ self.pending.add(id(chat))
64
+ threading.Thread(target=self.finish, args=(proxy, key, chat, prompt),
65
+ name='melty-auto-title', daemon=True).start()
66
+
67
+ def finish(self, proxy, key, chat, prompt):
68
+ try:
69
+ name = self.generate(prompt)
70
+ with self.lock:
71
+ if (name and dict.get(proxy, key) is chat
72
+ and chat.metadata.get('auto_title') == 'retry'):
73
+ chat['title'] = name
74
+ chat.metadata['auto_title'] = 'done'
75
+ proxy.revision += 1
76
+ proxy.reconcile() # The backend persists it as a normal rename.
77
+ finally:
78
+ with self.lock:
79
+ self.pending.discard(id(chat))
meltygui/chat/backends.py CHANGED
@@ -1,7 +1,7 @@
1
1
  """The chat backend registry: account kind name -> ChatProxy factory.
2
2
 
3
3
  A backend is a ChatProxy subclass living anywhere (the studio's Codex one,
4
- an external package such as melty_agents). Registering it against an
4
+ an external package). Registering it against an
5
5
  account kind gives that kind's accounts a conversation list in the Chat
6
6
  window; nothing else in the UI knows which package supplied it.
7
7
 
@@ -48,7 +48,7 @@ from meltygui.core.rendering.window_decoration import window
48
48
  from meltygui.view.collection_view import draw_tuple_fast
49
49
  from meltygui.view.decoration_view import draw_bg
50
50
  from meltygui.view.header_view import flat_button
51
- from meltygui.core.cache.tile_cache import add_shadow
51
+ from meltygui.core.cache.tile_marks import add_shadow
52
52
  from meltygui.view.texture_view import draw_texture
53
53
  import meltygui.accounts.internet_accounts as internet_accounts
54
54
 
@@ -165,7 +165,8 @@ from meltygui.view.chat_view import _scroll_position
165
165
 
166
166
 
167
167
  @contextmanager
168
- def _viewport(draw_state, state, key, width, height, content_height, follow=False):
168
+ def _viewport(draw_state, state, key, width, height, content_height, follow=False,
169
+ scrollbar_overlays=None, stretch_scrollbar=False):
169
170
  """One clipped, independently scrolling region, owned by the window.
170
171
 
171
172
  Only its visible footprint advances layout; content offsets never move
@@ -190,10 +191,16 @@ def _viewport(draw_state, state, key, width, height, content_height, follow=Fals
190
191
  if drag is not None:
191
192
  offset = _scroll_position(view, content_height, height,
192
193
  drag_fraction=offset / maximum + drag.dy / travel)
194
+ if scrollbar_overlays is not None:
195
+ # Keep only prepared geometry; the overlay never lays out messages or
196
+ # handles input. The final pane can grow with the navigation's bottom.
197
+ scrollbar_overlays.append((y - draw_state.abs_top, height, content_height, offset,
198
+ draw_state.abs_left + draw_state.width - x - width,
199
+ draw_state.height if stretch_scrollbar else None))
193
200
  Melty.push_clip(rect)
194
201
  try:
195
202
  yield x, y - offset, Melty.get_clip_rect()
196
- if maximum > 0 and travel > 0:
203
+ if scrollbar_overlays is None and maximum > 0 and travel > 0:
197
204
  thumb_y = y + travel * offset / maximum
198
205
  add_shadow((x + width - bar_width, thumb_y, bar_width, thumb_height),
199
206
  offset=Toggles.Chat.shadow_offset, corner_radius=3)
@@ -361,10 +368,11 @@ def chat_sources(accounts):
361
368
 
362
369
 
363
370
  def conversation_source_tag(kind, chat):
371
+ """A sidebar row's tag text where the provider has no bundled mark: a chat
372
+ glyph, plus a lock while another writer owns the conversation."""
364
373
  from meltygui.chat.chat_proxy import writer_conflict
365
- label = "Claude" if kind.name == "anthropic" else kind.chat_label
366
374
  locked = chat.get("locked", writer_conflict(getattr(chat, "error", None)))
367
- return label + " \uf023" if locked else label
375
+ return "\uf075 \uf023" if locked else "\uf075"
368
376
 
369
377
 
370
378
  def source_initials(label):
@@ -402,16 +410,14 @@ def pick_source(state, account_id, kinds, additive=False):
402
410
 
403
411
 
404
412
  def apply_folder_shortcuts(state, events):
405
- """Set every current and subsequently discovered folder to the requested state."""
406
- changed = False
413
+ """Ctrl+Shift+= / Ctrl+Shift+- fold the hovered sidebar column: queued
414
+ here, applied by the sidebar once it can hit-test this frame's columns."""
415
+ state.hovered_folder_action = None
407
416
  for key, mods in events:
408
417
  if mods & (glfw.MOD_CONTROL | glfw.MOD_SHIFT) == (glfw.MOD_CONTROL | glfw.MOD_SHIFT):
409
418
  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
419
+ state.hovered_folder_action = key in (glfw.KEY_EQUAL, glfw.KEY_KP_ADD)
420
+ return False
415
421
 
416
422
 
417
423
  def _code_background(x, y, width, height, color, shadow=None):
@@ -565,11 +571,13 @@ def transcript_entries(messages, state, key):
565
571
 
566
572
 
567
573
  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"
574
+ """Describe the latest reported work; never infer work from idle history.
575
+ An answered request can remain until the backend acknowledges it."""
576
+ from meltygui.model.chat_model import pending_requests
577
+ if pending_requests(chat):
578
+ return "Answer the request below to continue"
571
579
  if not chat["running"]:
572
- return "Working in another session" if chat.get("external_busy") else "Recent activity"
580
+ return "Working in another session" if chat.get("external_busy") else "Starting the next step…"
573
581
  for message in reversed(chat["messages"].values()):
574
582
  if isinstance(message, UserMessage):
575
583
  break
@@ -591,11 +599,6 @@ def chat_activity(chat, provider):
591
599
  from meltygui.core.services.chat_core import _cleanup_chat
592
600
 
593
601
 
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
602
  def navigation_row_sizes(state, edges, opened, top, height, minimum):
600
603
  """Remember expanded sizes; redistribute space only when a section toggles.
601
604
 
@@ -666,8 +669,9 @@ def is_new_chat(chat):
666
669
 
667
670
  def chat_project_choices(state, proxies, current, *defaults):
668
671
  """Use directory paths as labels so identically named folders stay distinct."""
672
+ from meltygui.model.chat_folder_model import folder_settings
669
673
  projects = {current, *defaults, *state.projects.values(),
670
- *getattr(state, "added_folders", [])}
674
+ *folder_settings()["added_folders"]}
671
675
  for proxy in proxies.values():
672
676
  if proxy is not None:
673
677
  projects.update(chat.get("project") for chat in dict.values(proxy))
@@ -730,3 +734,26 @@ def switch_new_chat_source(state, proxies, kinds, key, account_id, model):
730
734
 
731
735
  from meltygui.view.chat_view import draw_chat_interface
732
736
  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)
737
+
738
+
739
+ def _chat_proxies():
740
+ accounts = internet_accounts.accounts
741
+ for account in accounts.values():
742
+ proxy = account.get("_chat_proxy")
743
+ if proxy is not None and not proxy.closed:
744
+ yield proxy
745
+
746
+
747
+ def stop_running():
748
+ """Interrupt every running turn (an app's Escape)."""
749
+ for proxy in _chat_proxies():
750
+ for key, chat in proxy.items():
751
+ if chat["running"]:
752
+ proxy.stop_chat(key)
753
+
754
+
755
+ def disconnect_chats():
756
+ """Detach this process from the chat service without interrupting its
757
+ conversations (an app's exit)."""
758
+ for proxy in _chat_proxies():
759
+ proxy.close()