dulus 0.2.50__tar.gz → 0.2.51__tar.gz

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 (359) hide show
  1. {dulus-0.2.50/dulus.egg-info → dulus-0.2.51}/PKG-INFO +1 -1
  2. {dulus-0.2.50 → dulus-0.2.51}/data/context.json +13 -18
  3. {dulus-0.2.50 → dulus-0.2.51/dulus.egg-info}/PKG-INFO +1 -1
  4. {dulus-0.2.50 → dulus-0.2.51}/dulus.py +15 -22
  5. {dulus-0.2.50 → dulus-0.2.51}/dulus_gui.py +54 -21
  6. {dulus-0.2.50 → dulus-0.2.51}/gui/agent_bridge.py +2 -2
  7. {dulus-0.2.50 → dulus-0.2.51}/gui/session_utils.py +58 -51
  8. {dulus-0.2.50 → dulus-0.2.51}/gui/sidebar.py +79 -53
  9. {dulus-0.2.50 → dulus-0.2.51}/pyproject.toml +1 -1
  10. {dulus-0.2.50 → dulus-0.2.51}/webchat_server.py +15 -1
  11. {dulus-0.2.50 → dulus-0.2.51}/LICENSE +0 -0
  12. {dulus-0.2.50 → dulus-0.2.51}/MANIFEST.in +0 -0
  13. {dulus-0.2.50 → dulus-0.2.51}/README.md +0 -0
  14. {dulus-0.2.50 → dulus-0.2.51}/agent.py +0 -0
  15. {dulus-0.2.50 → dulus-0.2.51}/backend/__init__.py +0 -0
  16. {dulus-0.2.50 → dulus-0.2.51}/backend/agents_bridge.py +0 -0
  17. {dulus-0.2.50 → dulus-0.2.51}/backend/compressor.py +0 -0
  18. {dulus-0.2.50 → dulus-0.2.51}/backend/context.py +0 -0
  19. {dulus-0.2.50 → dulus-0.2.51}/backend/githook.py +0 -0
  20. {dulus-0.2.50 → dulus-0.2.51}/backend/marketplace.py +0 -0
  21. {dulus-0.2.50 → dulus-0.2.51}/backend/mempalace_bridge.py +0 -0
  22. {dulus-0.2.50 → dulus-0.2.51}/backend/personas.py +0 -0
  23. {dulus-0.2.50 → dulus-0.2.51}/backend/plugins.py +0 -0
  24. {dulus-0.2.50 → dulus-0.2.51}/backend/server.py +0 -0
  25. {dulus-0.2.50 → dulus-0.2.51}/backend/tasks.py +0 -0
  26. {dulus-0.2.50 → dulus-0.2.51}/batch_api.py +0 -0
  27. {dulus-0.2.50 → dulus-0.2.51}/checkpoint/__init__.py +0 -0
  28. {dulus-0.2.50 → dulus-0.2.51}/checkpoint/hooks.py +0 -0
  29. {dulus-0.2.50 → dulus-0.2.51}/checkpoint/store.py +0 -0
  30. {dulus-0.2.50 → dulus-0.2.51}/checkpoint/types.py +0 -0
  31. {dulus-0.2.50 → dulus-0.2.51}/claude_code_watcher.py +0 -0
  32. {dulus-0.2.50 → dulus-0.2.51}/clipboard_utils.py +0 -0
  33. {dulus-0.2.50 → dulus-0.2.51}/cloudsave.py +0 -0
  34. {dulus-0.2.50 → dulus-0.2.51}/common.py +0 -0
  35. {dulus-0.2.50 → dulus-0.2.51}/compaction.py +0 -0
  36. {dulus-0.2.50 → dulus-0.2.51}/config.py +0 -0
  37. {dulus-0.2.50 → dulus-0.2.51}/context.py +0 -0
  38. {dulus-0.2.50 → dulus-0.2.51}/data/__init__.py +0 -0
  39. {dulus-0.2.50 → dulus-0.2.51}/data/active_persona.json +0 -0
  40. {dulus-0.2.50 → dulus-0.2.51}/data/marketplace.json +0 -0
  41. {dulus-0.2.50 → dulus-0.2.51}/data/personas.json +0 -0
  42. {dulus-0.2.50 → dulus-0.2.51}/data/plugins/__init__.py +0 -0
  43. {dulus-0.2.50 → dulus-0.2.51}/data/plugins/composio/__init__.py +0 -0
  44. {dulus-0.2.50 → dulus-0.2.51}/data/plugins/composio/composio_plugin/__init__.py +0 -0
  45. {dulus-0.2.50 → dulus-0.2.51}/data/plugins/composio/composio_plugin/session_manager.py +0 -0
  46. {dulus-0.2.50 → dulus-0.2.51}/data/plugins/composio/composio_plugin/tool_generator.py +0 -0
  47. {dulus-0.2.50 → dulus-0.2.51}/data/plugins/composio/plugin.json +0 -0
  48. {dulus-0.2.50 → dulus-0.2.51}/data/plugins/composio/plugin_tool.py +0 -0
  49. {dulus-0.2.50 → dulus-0.2.51}/data/tasks.json +0 -0
  50. {dulus-0.2.50 → dulus-0.2.51}/docs/README.md +0 -0
  51. {dulus-0.2.50 → dulus-0.2.51}/docs/__init__.py +0 -0
  52. {dulus-0.2.50 → dulus-0.2.51}/docs/api.html +0 -0
  53. {dulus-0.2.50 → dulus-0.2.51}/docs/architecture.md +0 -0
  54. {dulus-0.2.50 → dulus-0.2.51}/docs/azure-speech-template.json +0 -0
  55. {dulus-0.2.50 → dulus-0.2.51}/docs/dashboard/index.html +0 -0
  56. {dulus-0.2.50 → dulus-0.2.51}/docs/divider.svg +0 -0
  57. {dulus-0.2.50 → dulus-0.2.51}/docs/generate.py +0 -0
  58. {dulus-0.2.50 → dulus-0.2.51}/docs/hero.svg +0 -0
  59. {dulus-0.2.50 → dulus-0.2.51}/docs/index.html +0 -0
  60. {dulus-0.2.50 → dulus-0.2.51}/docs/news.md +0 -0
  61. {dulus-0.2.50 → dulus-0.2.51}/docs/nvidia-models.svg +0 -0
  62. {dulus-0.2.50 → dulus-0.2.51}/docs/particle-playground.html +0 -0
  63. {dulus-0.2.50 → dulus-0.2.51}/docs/personas/index.html +0 -0
  64. {dulus-0.2.50 → dulus-0.2.51}/docs/poetry-banner.png +0 -0
  65. {dulus-0.2.50 → dulus-0.2.51}/docs/preview.html +0 -0
  66. {dulus-0.2.50 → dulus-0.2.51}/docs/sec-agents.svg +0 -0
  67. {dulus-0.2.50 → dulus-0.2.51}/docs/sec-brainstorm.svg +0 -0
  68. {dulus-0.2.50 → dulus-0.2.51}/docs/sec-bridges.svg +0 -0
  69. {dulus-0.2.50 → dulus-0.2.51}/docs/sec-features.svg +0 -0
  70. {dulus-0.2.50 → dulus-0.2.51}/docs/sec-freetier.svg +0 -0
  71. {dulus-0.2.50 → dulus-0.2.51}/docs/sec-memory.svg +0 -0
  72. {dulus-0.2.50 → dulus-0.2.51}/docs/sec-models.svg +0 -0
  73. {dulus-0.2.50 → dulus-0.2.51}/docs/sec-perms.svg +0 -0
  74. {dulus-0.2.50 → dulus-0.2.51}/docs/sec-plugins.svg +0 -0
  75. {dulus-0.2.50 → dulus-0.2.51}/docs/sec-quickstart.svg +0 -0
  76. {dulus-0.2.50 → dulus-0.2.51}/docs/sec-ssj.svg +0 -0
  77. {dulus-0.2.50 → dulus-0.2.51}/docs/spinners.svg +0 -0
  78. {dulus-0.2.50 → dulus-0.2.51}/docs/split-pane.svg +0 -0
  79. {dulus-0.2.50 → dulus-0.2.51}/docs/terminal-boot.svg +0 -0
  80. {dulus-0.2.50 → dulus-0.2.51}/docs/uploads/particle-playground.html +0 -0
  81. {dulus-0.2.50 → dulus-0.2.51}/dulus.egg-info/SOURCES.txt +0 -0
  82. {dulus-0.2.50 → dulus-0.2.51}/dulus.egg-info/dependency_links.txt +0 -0
  83. {dulus-0.2.50 → dulus-0.2.51}/dulus.egg-info/entry_points.txt +0 -0
  84. {dulus-0.2.50 → dulus-0.2.51}/dulus.egg-info/requires.txt +0 -0
  85. {dulus-0.2.50 → dulus-0.2.51}/dulus.egg-info/top_level.txt +0 -0
  86. {dulus-0.2.50 → dulus-0.2.51}/dulus_mcp/__init__.py +0 -0
  87. {dulus-0.2.50 → dulus-0.2.51}/dulus_mcp/client.py +0 -0
  88. {dulus-0.2.50 → dulus-0.2.51}/dulus_mcp/config.py +0 -0
  89. {dulus-0.2.50 → dulus-0.2.51}/dulus_mcp/tools.py +0 -0
  90. {dulus-0.2.50 → dulus-0.2.51}/dulus_mcp/types.py +0 -0
  91. {dulus-0.2.50 → dulus-0.2.51}/gui/__init__.py +0 -0
  92. {dulus-0.2.50 → dulus-0.2.51}/gui/chat_widget.py +0 -0
  93. {dulus-0.2.50 → dulus-0.2.51}/gui/main_window.py +0 -0
  94. {dulus-0.2.50 → dulus-0.2.51}/gui/personas.py +0 -0
  95. {dulus-0.2.50 → dulus-0.2.51}/gui/settings_dialog.py +0 -0
  96. {dulus-0.2.50 → dulus-0.2.51}/gui/tasks_view.py +0 -0
  97. {dulus-0.2.50 → dulus-0.2.51}/gui/themes.py +0 -0
  98. {dulus-0.2.50 → dulus-0.2.51}/gui/tool_panel.py +0 -0
  99. {dulus-0.2.50 → dulus-0.2.51}/input.py +0 -0
  100. {dulus-0.2.50 → dulus-0.2.51}/license_manager.py +0 -0
  101. {dulus-0.2.50 → dulus-0.2.51}/memory/__init__.py +0 -0
  102. {dulus-0.2.50 → dulus-0.2.51}/memory/audit.py +0 -0
  103. {dulus-0.2.50 → dulus-0.2.51}/memory/consolidator.py +0 -0
  104. {dulus-0.2.50 → dulus-0.2.51}/memory/context.py +0 -0
  105. {dulus-0.2.50 → dulus-0.2.51}/memory/offload.py +0 -0
  106. {dulus-0.2.50 → dulus-0.2.51}/memory/palace.py +0 -0
  107. {dulus-0.2.50 → dulus-0.2.51}/memory/scan.py +0 -0
  108. {dulus-0.2.50 → dulus-0.2.51}/memory/sessions.py +0 -0
  109. {dulus-0.2.50 → dulus-0.2.51}/memory/store.py +0 -0
  110. {dulus-0.2.50 → dulus-0.2.51}/memory/tools.py +0 -0
  111. {dulus-0.2.50 → dulus-0.2.51}/memory/types.py +0 -0
  112. {dulus-0.2.50 → dulus-0.2.51}/memory/vector_search.py +0 -0
  113. {dulus-0.2.50 → dulus-0.2.51}/multi_agent/__init__.py +0 -0
  114. {dulus-0.2.50 → dulus-0.2.51}/multi_agent/subagent.py +0 -0
  115. {dulus-0.2.50 → dulus-0.2.51}/multi_agent/tools.py +0 -0
  116. {dulus-0.2.50 → dulus-0.2.51}/offload_helper.py +0 -0
  117. {dulus-0.2.50 → dulus-0.2.51}/plugin/__init__.py +0 -0
  118. {dulus-0.2.50 → dulus-0.2.51}/plugin/autoadapter.py +0 -0
  119. {dulus-0.2.50 → dulus-0.2.51}/plugin/loader.py +0 -0
  120. {dulus-0.2.50 → dulus-0.2.51}/plugin/recommend.py +0 -0
  121. {dulus-0.2.50 → dulus-0.2.51}/plugin/store.py +0 -0
  122. {dulus-0.2.50 → dulus-0.2.51}/plugin/types.py +0 -0
  123. {dulus-0.2.50 → dulus-0.2.51}/providers.py +0 -0
  124. {dulus-0.2.50 → dulus-0.2.51}/sandbox/README.md +0 -0
  125. {dulus-0.2.50 → dulus-0.2.51}/sandbox/__init__.py +0 -0
  126. {dulus-0.2.50 → dulus-0.2.51}/sandbox/components.json +0 -0
  127. {dulus-0.2.50 → dulus-0.2.51}/sandbox/dist/assets/index-DE51D6wI.css +0 -0
  128. {dulus-0.2.50 → dulus-0.2.51}/sandbox/dist/assets/index-DMCCNE9Y.js +0 -0
  129. {dulus-0.2.50 → dulus-0.2.51}/sandbox/dist/index.html +0 -0
  130. {dulus-0.2.50 → dulus-0.2.51}/sandbox/dist/wallpaper-default.jpg +0 -0
  131. {dulus-0.2.50 → dulus-0.2.51}/sandbox/dist/wallpapers/default.jpeg +0 -0
  132. {dulus-0.2.50 → dulus-0.2.51}/sandbox/dist/wallpapers/light.jpeg +0 -0
  133. {dulus-0.2.50 → dulus-0.2.51}/sandbox/dist/wallpapers/nature.jpeg +0 -0
  134. {dulus-0.2.50 → dulus-0.2.51}/sandbox/dist/wallpapers/tech.jpeg +0 -0
  135. {dulus-0.2.50 → dulus-0.2.51}/sandbox/eslint.config.js +0 -0
  136. {dulus-0.2.50 → dulus-0.2.51}/sandbox/index.html +0 -0
  137. {dulus-0.2.50 → dulus-0.2.51}/sandbox/info.md +0 -0
  138. {dulus-0.2.50 → dulus-0.2.51}/sandbox/package-lock.json +0 -0
  139. {dulus-0.2.50 → dulus-0.2.51}/sandbox/package.json +0 -0
  140. {dulus-0.2.50 → dulus-0.2.51}/sandbox/postcss.config.js +0 -0
  141. {dulus-0.2.50 → dulus-0.2.51}/sandbox/public/wallpaper-default.jpg +0 -0
  142. {dulus-0.2.50 → dulus-0.2.51}/sandbox/public/wallpapers/default.jpeg +0 -0
  143. {dulus-0.2.50 → dulus-0.2.51}/sandbox/public/wallpapers/light.jpeg +0 -0
  144. {dulus-0.2.50 → dulus-0.2.51}/sandbox/public/wallpapers/nature.jpeg +0 -0
  145. {dulus-0.2.50 → dulus-0.2.51}/sandbox/public/wallpapers/tech.jpeg +0 -0
  146. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/App.tsx +0 -0
  147. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/AgentMonitor.tsx +0 -0
  148. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/ApiTester.tsx +0 -0
  149. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/AppRouter.tsx +0 -0
  150. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/ArchiveManager.tsx +0 -0
  151. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/AsciiArt.tsx +0 -0
  152. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Base64Tool.tsx +0 -0
  153. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Browser.tsx +0 -0
  154. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Calculator.tsx +0 -0
  155. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Calendar.tsx +0 -0
  156. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Chat.tsx +0 -0
  157. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Chess.tsx +0 -0
  158. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Clock.tsx +0 -0
  159. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/CodeEditor.tsx +0 -0
  160. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/ColorPalette.tsx +0 -0
  161. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/ColorPicker.tsx +0 -0
  162. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Contacts.tsx +0 -0
  163. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/DocumentViewer.tsx +0 -0
  164. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Drawing.tsx +0 -0
  165. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Email.tsx +0 -0
  166. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/FileManager.tsx +0 -0
  167. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/FlappyBird.tsx +0 -0
  168. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/FtpClient.tsx +0 -0
  169. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Game2048.tsx +0 -0
  170. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/GitClient.tsx +0 -0
  171. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/ImageGallery.tsx +0 -0
  172. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/ImageViewer.tsx +0 -0
  173. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/JsonFormatter.tsx +0 -0
  174. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/MarkdownPreview.tsx +0 -0
  175. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/MatrixRain.tsx +0 -0
  176. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/MediaConverter.tsx +0 -0
  177. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Memory.tsx +0 -0
  178. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/MemoryManager.tsx +0 -0
  179. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Minesweeper.tsx +0 -0
  180. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/MusicPlayer.tsx +0 -0
  181. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/NetworkTools.tsx +0 -0
  182. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Notes.tsx +0 -0
  183. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/PasswordManager.tsx +0 -0
  184. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/PhotoEditor.tsx +0 -0
  185. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Pong.tsx +0 -0
  186. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/RegexTester.tsx +0 -0
  187. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Reminders.tsx +0 -0
  188. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/RssReader.tsx +0 -0
  189. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/ScreenRecorder.tsx +0 -0
  190. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Settings.tsx +0 -0
  191. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/SkillsLauncher.tsx +0 -0
  192. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Snake.tsx +0 -0
  193. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Solitaire.tsx +0 -0
  194. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Spreadsheet.tsx +0 -0
  195. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Sudoku.tsx +0 -0
  196. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/SystemMonitor.tsx +0 -0
  197. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/TaskManager.tsx +0 -0
  198. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Terminal.tsx +0 -0
  199. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Tetris.tsx +0 -0
  200. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/TextEditor.tsx +0 -0
  201. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/TicTacToe.tsx +0 -0
  202. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Todo.tsx +0 -0
  203. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/VideoPlayer.tsx +0 -0
  204. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/VoiceRecorder.tsx +0 -0
  205. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Weather.tsx +0 -0
  206. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Whiteboard.tsx +0 -0
  207. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/registry.ts +0 -0
  208. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/AppContainer.tsx +0 -0
  209. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/AppLauncher.tsx +0 -0
  210. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/BootSequence.tsx +0 -0
  211. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ContextMenu.tsx +0 -0
  212. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/Desktop.tsx +0 -0
  213. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/Dock.tsx +0 -0
  214. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/LoginScreen.tsx +0 -0
  215. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/NotImplemented.tsx +0 -0
  216. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/NotificationCenter.tsx +0 -0
  217. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/NotificationSystem.tsx +0 -0
  218. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/TopPanel.tsx +0 -0
  219. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/WindowFrame.tsx +0 -0
  220. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/WindowManager.tsx +0 -0
  221. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/accordion.tsx +0 -0
  222. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/alert-dialog.tsx +0 -0
  223. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/alert.tsx +0 -0
  224. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/aspect-ratio.tsx +0 -0
  225. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/avatar.tsx +0 -0
  226. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/badge.tsx +0 -0
  227. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/breadcrumb.tsx +0 -0
  228. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/button-group.tsx +0 -0
  229. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/button.tsx +0 -0
  230. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/calendar.tsx +0 -0
  231. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/card.tsx +0 -0
  232. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/carousel.tsx +0 -0
  233. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/chart.tsx +0 -0
  234. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/checkbox.tsx +0 -0
  235. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/collapsible.tsx +0 -0
  236. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/command.tsx +0 -0
  237. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/context-menu.tsx +0 -0
  238. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/dialog.tsx +0 -0
  239. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/drawer.tsx +0 -0
  240. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/dropdown-menu.tsx +0 -0
  241. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/empty.tsx +0 -0
  242. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/field.tsx +0 -0
  243. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/form.tsx +0 -0
  244. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/hover-card.tsx +0 -0
  245. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/input-group.tsx +0 -0
  246. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/input-otp.tsx +0 -0
  247. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/input.tsx +0 -0
  248. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/item.tsx +0 -0
  249. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/kbd.tsx +0 -0
  250. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/label.tsx +0 -0
  251. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/menubar.tsx +0 -0
  252. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/navigation-menu.tsx +0 -0
  253. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/pagination.tsx +0 -0
  254. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/popover.tsx +0 -0
  255. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/progress.tsx +0 -0
  256. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/radio-group.tsx +0 -0
  257. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/resizable.tsx +0 -0
  258. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/scroll-area.tsx +0 -0
  259. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/select.tsx +0 -0
  260. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/separator.tsx +0 -0
  261. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/sheet.tsx +0 -0
  262. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/sidebar.tsx +0 -0
  263. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/skeleton.tsx +0 -0
  264. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/slider.tsx +0 -0
  265. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/sonner.tsx +0 -0
  266. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/spinner.tsx +0 -0
  267. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/switch.tsx +0 -0
  268. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/table.tsx +0 -0
  269. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/tabs.tsx +0 -0
  270. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/textarea.tsx +0 -0
  271. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/toggle-group.tsx +0 -0
  272. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/toggle.tsx +0 -0
  273. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/tooltip.tsx +0 -0
  274. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/index.ts +0 -0
  275. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/use-mobile.ts +0 -0
  276. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/useAutoOpenChat.ts +0 -0
  277. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/useDulusAgents.ts +0 -0
  278. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/useDulusChat.ts +0 -0
  279. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/useDulusEvents.ts +0 -0
  280. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/useDulusHealth.ts +0 -0
  281. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/useDulusMemory.ts +0 -0
  282. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/useDulusSkills.ts +0 -0
  283. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/useDulusTasks.ts +0 -0
  284. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/useFileSystem.ts +0 -0
  285. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/useOSStore.tsx +0 -0
  286. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/useSkillBridge.ts +0 -0
  287. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/useSystemBattery.ts +0 -0
  288. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/useSystemNetwork.ts +0 -0
  289. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/useSystemVolume.ts +0 -0
  290. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/index.css +0 -0
  291. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/lib/dulus-api.ts +0 -0
  292. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/lib/utils.ts +0 -0
  293. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/main.tsx +0 -0
  294. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/types/index.ts +0 -0
  295. {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/utils/assets.ts +0 -0
  296. {dulus-0.2.50 → dulus-0.2.51}/sandbox/tailwind.config.js +0 -0
  297. {dulus-0.2.50 → dulus-0.2.51}/sandbox/tsconfig.app.json +0 -0
  298. {dulus-0.2.50 → dulus-0.2.51}/sandbox/tsconfig.json +0 -0
  299. {dulus-0.2.50 → dulus-0.2.51}/sandbox/tsconfig.node.json +0 -0
  300. {dulus-0.2.50 → dulus-0.2.51}/sandbox/vite.config.ts +0 -0
  301. {dulus-0.2.50 → dulus-0.2.51}/setup.cfg +0 -0
  302. {dulus-0.2.50 → dulus-0.2.51}/skill/__init__.py +0 -0
  303. {dulus-0.2.50 → dulus-0.2.51}/skill/builtin.py +0 -0
  304. {dulus-0.2.50 → dulus-0.2.51}/skill/clawhub.py +0 -0
  305. {dulus-0.2.50 → dulus-0.2.51}/skill/executor.py +0 -0
  306. {dulus-0.2.50 → dulus-0.2.51}/skill/loader.py +0 -0
  307. {dulus-0.2.50 → dulus-0.2.51}/skill/tools.py +0 -0
  308. {dulus-0.2.50 → dulus-0.2.51}/skills.py +0 -0
  309. {dulus-0.2.50 → dulus-0.2.51}/spinner.py +0 -0
  310. {dulus-0.2.50 → dulus-0.2.51}/string_utils.py +0 -0
  311. {dulus-0.2.50 → dulus-0.2.51}/subagent.py +0 -0
  312. {dulus-0.2.50 → dulus-0.2.51}/task/__init__.py +0 -0
  313. {dulus-0.2.50 → dulus-0.2.51}/task/store.py +0 -0
  314. {dulus-0.2.50 → dulus-0.2.51}/task/tools.py +0 -0
  315. {dulus-0.2.50 → dulus-0.2.51}/task/types.py +0 -0
  316. {dulus-0.2.50 → dulus-0.2.51}/tests/test_afk_yolo.py +0 -0
  317. {dulus-0.2.50 → dulus-0.2.51}/tests/test_approval_runtime.py +0 -0
  318. {dulus-0.2.50 → dulus-0.2.51}/tests/test_background_task_tools.py +0 -0
  319. {dulus-0.2.50 → dulus-0.2.51}/tests/test_background_tasks.py +0 -0
  320. {dulus-0.2.50 → dulus-0.2.51}/tests/test_checkpoint.py +0 -0
  321. {dulus-0.2.50 → dulus-0.2.51}/tests/test_clipboard_utils.py +0 -0
  322. {dulus-0.2.50 → dulus-0.2.51}/tests/test_compaction.py +0 -0
  323. {dulus-0.2.50 → dulus-0.2.51}/tests/test_diff_view.py +0 -0
  324. {dulus-0.2.50 → dulus-0.2.51}/tests/test_diff_visualization.py +0 -0
  325. {dulus-0.2.50 → dulus-0.2.51}/tests/test_display_blocks.py +0 -0
  326. {dulus-0.2.50 → dulus-0.2.51}/tests/test_export_import.py +0 -0
  327. {dulus-0.2.50 → dulus-0.2.51}/tests/test_hook_engine.py +0 -0
  328. {dulus-0.2.50 → dulus-0.2.51}/tests/test_injection_fix.py +0 -0
  329. {dulus-0.2.50 → dulus-0.2.51}/tests/test_license.py +0 -0
  330. {dulus-0.2.50 → dulus-0.2.51}/tests/test_mcp.py +0 -0
  331. {dulus-0.2.50 → dulus-0.2.51}/tests/test_memory.py +0 -0
  332. {dulus-0.2.50 → dulus-0.2.51}/tests/test_notification_manager.py +0 -0
  333. {dulus-0.2.50 → dulus-0.2.51}/tests/test_plugin.py +0 -0
  334. {dulus-0.2.50 → dulus-0.2.51}/tests/test_session_fork.py +0 -0
  335. {dulus-0.2.50 → dulus-0.2.51}/tests/test_shell_mode.py +0 -0
  336. {dulus-0.2.50 → dulus-0.2.51}/tests/test_skills.py +0 -0
  337. {dulus-0.2.50 → dulus-0.2.51}/tests/test_steer_input.py +0 -0
  338. {dulus-0.2.50 → dulus-0.2.51}/tests/test_subagent.py +0 -0
  339. {dulus-0.2.50 → dulus-0.2.51}/tests/test_task.py +0 -0
  340. {dulus-0.2.50 → dulus-0.2.51}/tests/test_telegram_buffer.py +0 -0
  341. {dulus-0.2.50 → dulus-0.2.51}/tests/test_think_tool.py +0 -0
  342. {dulus-0.2.50 → dulus-0.2.51}/tests/test_todo_tool.py +0 -0
  343. {dulus-0.2.50 → dulus-0.2.51}/tests/test_todo_visualization.py +0 -0
  344. {dulus-0.2.50 → dulus-0.2.51}/tests/test_tool_registry.py +0 -0
  345. {dulus-0.2.50 → dulus-0.2.51}/tests/test_voice.py +0 -0
  346. {dulus-0.2.50 → dulus-0.2.51}/tests/test_wire_events.py +0 -0
  347. {dulus-0.2.50 → dulus-0.2.51}/tmux_offloader.py +0 -0
  348. {dulus-0.2.50 → dulus-0.2.51}/tmux_tools.py +0 -0
  349. {dulus-0.2.50 → dulus-0.2.51}/tool_registry.py +0 -0
  350. {dulus-0.2.50 → dulus-0.2.51}/tools.py +0 -0
  351. {dulus-0.2.50 → dulus-0.2.51}/ui/__init__.py +0 -0
  352. {dulus-0.2.50 → dulus-0.2.51}/ui/input.py +0 -0
  353. {dulus-0.2.50 → dulus-0.2.51}/ui/render.py +0 -0
  354. {dulus-0.2.50 → dulus-0.2.51}/voice/__init__.py +0 -0
  355. {dulus-0.2.50 → dulus-0.2.51}/voice/keyterms.py +0 -0
  356. {dulus-0.2.50 → dulus-0.2.51}/voice/recorder.py +0 -0
  357. {dulus-0.2.50 → dulus-0.2.51}/voice/stt.py +0 -0
  358. {dulus-0.2.50 → dulus-0.2.51}/voice/tts.py +0 -0
  359. {dulus-0.2.50 → dulus-0.2.51}/webchat.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dulus
3
- Version: 0.2.50
3
+ Version: 0.2.51
4
4
  Summary: Spanish-first multi-provider AI CLI — 14 NVIDIA models free, Mesa Redonda, voice, TTS, RTK token reducer, MemPalace
5
5
  Author: KevRojo
6
6
  License: GPL-3.0
@@ -9,8 +9,8 @@
9
9
  "project": {
10
10
  "name": "Dulus Command Center",
11
11
  "repo_stats": {
12
- "files": 433,
13
- "lines": 205053,
12
+ "files": 432,
13
+ "lines": 204971,
14
14
  "languages": {
15
15
  ".example": 5,
16
16
  "no_ext": 1424,
@@ -21,9 +21,9 @@
21
21
  ".md": 2161,
22
22
  ".txt": 463,
23
23
  ".lock": 1,
24
- ".json": 9532,
25
- ".whl": 21581,
26
- ".gz": 20971,
24
+ ".json": 9521,
25
+ ".whl": 21605,
26
+ ".gz": 20876,
27
27
  ".svg": 1123,
28
28
  ".png": 31639,
29
29
  ".sh": 144,
@@ -37,6 +37,12 @@
37
37
  }
38
38
  },
39
39
  "recent_commits": [
40
+ {
41
+ "hash": "fae4455",
42
+ "subject": "feat: MemoryManager overhaul, skill bridge polish, server sync",
43
+ "author": "KevRojo",
44
+ "date": "2026-05-12"
45
+ },
40
46
  {
41
47
  "hash": "bf6455f",
42
48
  "subject": "feat: skill-to-chat bridge, MemPalace search API, sandbox UX polish",
@@ -60,29 +66,18 @@
60
66
  "subject": "bump: 0.2.45 → 0.2.46",
61
67
  "author": "KevRojo",
62
68
  "date": "2026-05-12"
63
- },
64
- {
65
- "hash": "bc4cf5c",
66
- "subject": "chore: pre-release sync — backend agents, sandbox updates, explorer HTML",
67
- "author": "KevRojo",
68
- "date": "2026-05-12"
69
69
  }
70
70
  ],
71
71
  "recent_changes": [
72
- ".gitignore",
73
72
  "backend/server.py",
74
73
  "data/context.json",
75
74
  "pyproject.toml",
76
75
  "sandbox/dist/assets/index-CsIO61nW.css",
77
- "sandbox/dist/assets/index-nQkFq-oq.js",
76
+ "sandbox/dist/assets/index-DE51D6wI.css",
77
+ "sandbox/dist/assets/index-DMCCNE9Y.js",
78
78
  "sandbox/dist/index.html",
79
- "sandbox/src/apps/AppRouter.tsx",
80
79
  "sandbox/src/apps/Chat.tsx",
81
80
  "sandbox/src/apps/MemoryManager.tsx",
82
- "sandbox/src/apps/SkillsLauncher.tsx",
83
- "sandbox/src/hooks/index.ts",
84
- "sandbox/src/hooks/useAutoOpenChat.ts",
85
- "sandbox/src/hooks/useOSStore.tsx",
86
81
  "sandbox/src/hooks/useSkillBridge.ts",
87
82
  "sandbox/src/lib/dulus-api.ts",
88
83
  "webchat_server.py"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dulus
3
- Version: 0.2.50
3
+ Version: 0.2.51
4
4
  Summary: Spanish-first multi-provider AI CLI — 14 NVIDIA models free, Mesa Redonda, voice, TTS, RTK token reducer, MemPalace
5
5
  Author: KevRojo
6
6
  License: GPL-3.0
@@ -1158,7 +1158,7 @@ def _atomic_write_json(path: Path, data) -> None:
1158
1158
  def _save_roundtable_session(log: list, save_path=None):
1159
1159
  """Save the full roundtable session log to a JSON file.
1160
1160
 
1161
- Sessions go under config.MR_SESSION_DIR (~/.dulus/sessions/mr_sessions/),
1161
+ Sessions go under config.SESSIONS_DIR (~/.dulus/sessions/),
1162
1162
  consistent with /save and other session artifacts. Pass an explicit
1163
1163
  save_path to override (used to keep all turns of one debate in one file).
1164
1164
  """
@@ -1166,9 +1166,9 @@ def _save_roundtable_session(log: list, save_path=None):
1166
1166
  return
1167
1167
  if save_path is None:
1168
1168
  from datetime import datetime as _dt
1169
- from config import MR_SESSION_DIR
1170
- MR_SESSION_DIR.mkdir(parents=True, exist_ok=True)
1171
- save_path = MR_SESSION_DIR / f"round_table_{_dt.now().strftime('%Y%m%d_%H%M%S')}.json"
1169
+ from config import SESSIONS_DIR
1170
+ SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
1171
+ save_path = SESSIONS_DIR / f"round_table_{_dt.now().strftime('%Y%m%d_%H%M%S')}.json"
1172
1172
  try:
1173
1173
  _atomic_write_json(save_path, log)
1174
1174
  ok(f"Sesión de Mesa Redonda guardada en: {save_path}")
@@ -1193,7 +1193,7 @@ def save_latest(args: str, state, config=None, mode: str = "full") -> bool:
1193
1193
  mode="full" → session_latest.json + daily/ copy + append to history.json (REPL default)
1194
1194
  mode="daemon"→ only overwrite SESSIONS_DIR/session_<sid>.json, skip latest/history/daily.
1195
1195
  """
1196
- from config import MR_SESSION_DIR, DAILY_DIR, SESSION_HIST_FILE, SESSIONS_DIR
1196
+ from config import DAILY_DIR, SESSION_HIST_FILE, SESSIONS_DIR
1197
1197
  if not state.messages:
1198
1198
  return True
1199
1199
 
@@ -1213,16 +1213,16 @@ def save_latest(args: str, state, config=None, mode: str = "full") -> bool:
1213
1213
  return True
1214
1214
 
1215
1215
  # ── Full mode (REPL exit) ──
1216
- daily_limit = cfg.get("session_daily_limit", 5)
1217
- history_limit = cfg.get("session_history_limit", 100)
1216
+ daily_limit = cfg.get("session_limit_daily", 10)
1217
+ history_limit = cfg.get("session_limit_history", 200)
1218
1218
 
1219
1219
  now = datetime.now()
1220
1220
  ts = now.strftime("%H%M%S")
1221
1221
  date_str = now.strftime("%Y-%m-%d")
1222
1222
 
1223
1223
  # 1. session_latest.json — always overwrite for quick /resume
1224
- MR_SESSION_DIR.mkdir(parents=True, exist_ok=True)
1225
- latest_path = MR_SESSION_DIR / "session_latest.json"
1224
+ SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
1225
+ latest_path = SESSIONS_DIR / "session_latest.json"
1226
1226
  latest_path.write_text(payload)
1227
1227
 
1228
1228
  # 2. daily/YYYY-MM-DD/session_HHMMSS_sid.json
@@ -1267,22 +1267,16 @@ def save_latest(args: str, state, config=None, mode: str = "full") -> bool:
1267
1267
  ok(f" → {SESSION_HIST_FILE} ({len(hist['sessions'])} sessions / {hist['total_turns']} total turns)")
1268
1268
  return True
1269
1269
  def cmd_load(args: str, state, config) -> bool:
1270
- from config import SESSIONS_DIR, MR_SESSION_DIR, DAILY_DIR
1270
+ from config import SESSIONS_DIR, DAILY_DIR
1271
1271
 
1272
1272
  path = None
1273
1273
  if not args.strip():
1274
- # Collect sessions from daily/ folders, newest first
1274
+ # Collect sessions from daily/ folders only (single source of truth for listing)
1275
1275
  sessions: list[Path] = []
1276
1276
  if DAILY_DIR.exists():
1277
1277
  for day_dir in sorted(DAILY_DIR.iterdir(), reverse=True):
1278
1278
  if day_dir.is_dir():
1279
1279
  sessions.extend(sorted(day_dir.glob("session_*.json"), reverse=True))
1280
- # Fall back to legacy mr_sessions/ if daily/ is empty
1281
- if not sessions and MR_SESSION_DIR.exists():
1282
- sessions = [s for s in sorted(MR_SESSION_DIR.glob("*.json"), reverse=True)
1283
- if s.name != "session_latest.json"]
1284
- # Also include manually /save'd sessions from SESSIONS_DIR root
1285
- sessions.extend(sorted(SESSIONS_DIR.glob("session_*.json"), reverse=True))
1286
1280
 
1287
1281
  if not sessions:
1288
1282
  info("No saved sessions found.")
@@ -1403,8 +1397,7 @@ def cmd_load(args: str, state, config) -> bool:
1403
1397
  fname = args.strip()
1404
1398
  path = Path(fname) if "/" in fname or "\\" in fname else SESSIONS_DIR / fname
1405
1399
  if not path.exists() and ("/" not in fname and "\\" not in fname):
1406
- for alt in [MR_SESSION_DIR / fname,
1407
- *(d / fname for d in DAILY_DIR.iterdir()
1400
+ for alt in [*(d / fname for d in DAILY_DIR.iterdir()
1408
1401
  if DAILY_DIR.exists() and d.is_dir())]:
1409
1402
  if alt.exists():
1410
1403
  path = alt
@@ -1422,16 +1415,16 @@ def cmd_load(args: str, state, config) -> bool:
1422
1415
  return True
1423
1416
 
1424
1417
  def cmd_resume(args: str, state, config) -> bool:
1425
- from config import MR_SESSION_DIR
1418
+ from config import SESSIONS_DIR
1426
1419
 
1427
1420
  if not args.strip():
1428
- path = MR_SESSION_DIR / "session_latest.json"
1421
+ path = SESSIONS_DIR / "session_latest.json"
1429
1422
  if not path.exists():
1430
1423
  info("No auto-saved sessions found.")
1431
1424
  return True
1432
1425
  else:
1433
1426
  fname = args.strip()
1434
- path = Path(fname) if "/" in fname else MR_SESSION_DIR / fname
1427
+ path = Path(fname) if "/" in fname else SESSIONS_DIR / fname
1435
1428
 
1436
1429
  if not path.exists():
1437
1430
  err(f"File not found: {path}")
@@ -7,8 +7,10 @@ Usage:
7
7
  from __future__ import annotations
8
8
 
9
9
  import datetime
10
+ import json
10
11
  import queue
11
12
  import sys
13
+ import threading
12
14
  import traceback
13
15
  from pathlib import Path
14
16
  from typing import Callable
@@ -27,7 +29,7 @@ from gui.themes import get_theme, set_theme
27
29
  from gui.session_utils import scan_sessions
28
30
 
29
31
  # Session directories
30
- from config import SESSIONS_DIR, DAILY_DIR, MR_SESSION_DIR
32
+ from config import SESSIONS_DIR, DAILY_DIR
31
33
 
32
34
 
33
35
  # ── Helpers ───────────────────────────────────────────────────────────────────
@@ -163,6 +165,35 @@ def launch_gui(config: dict | None = None, initial_prompt: str | None = None) ->
163
165
  # Wire bridge into sidebar so context bar / model list work
164
166
  app.sidebar.bridge = bridge
165
167
 
168
+ # ── Sidebar refresh (non-blocking) ────────────────────────────────────────
169
+ _sidebar_refresh_pending = False
170
+
171
+ def _refresh_sidebar_async() -> None:
172
+ """Run scan_sessions in a background thread so the UI never freezes."""
173
+ nonlocal _sidebar_refresh_pending
174
+ if _sidebar_refresh_pending:
175
+ return
176
+ _sidebar_refresh_pending = True
177
+
178
+ def _do_scan():
179
+ try:
180
+ data = scan_sessions()
181
+ # Update UI from main thread
182
+ app.after(0, lambda: app.set_sessions(data))
183
+ finally:
184
+ nonlocal _sidebar_refresh_pending
185
+ _sidebar_refresh_pending = False
186
+
187
+ threading.Thread(target=_do_scan, daemon=True).start()
188
+
189
+ def _load_session_messages(path: str) -> list[dict]:
190
+ """Load messages directly from a session file."""
191
+ try:
192
+ data = json.loads(Path(path).read_text(encoding="utf-8", errors="replace"))
193
+ return data.get("messages", [])
194
+ except Exception:
195
+ return []
196
+
166
197
  # ── Wire callbacks ────────────────────────────────────────────────────────
167
198
 
168
199
  def _on_send(text: str) -> None:
@@ -176,8 +207,8 @@ def launch_gui(config: dict | None = None, initial_prompt: str | None = None) ->
176
207
  sid = bridge.save_current_session()
177
208
  if sid:
178
209
  # If a new session was created, refresh sidebar to show it
179
- app.set_sessions(scan_sessions())
180
-
210
+ _refresh_sidebar_async()
211
+
181
212
  app.hide_thinking()
182
213
  app.chat.clear_chat()
183
214
  bridge.clear_session()
@@ -188,36 +219,38 @@ def launch_gui(config: dict | None = None, initial_prompt: str | None = None) ->
188
219
  def _on_session_select(session_id: str) -> None:
189
220
  # Save current session before switching to ensure no loss
190
221
  sid = bridge.save_current_session()
191
-
222
+
192
223
  # If we were in a new chat that just got saved, refresh sidebar to show it
193
224
  if sid:
194
- app.set_sessions(scan_sessions())
195
-
225
+ _refresh_sidebar_async()
226
+
196
227
  app.hide_thinking()
197
-
198
- # 1. Use cached data from sidebar for instant switching
199
- session_data = app.sidebar._session_cache.get(session_id)
200
- if not session_data:
201
- # Fallback to scanning if cache missed (rare)
228
+
229
+ # 1. Find the session file path (from cache or scan)
230
+ session_path = None
231
+ cached = app.sidebar._session_cache.get(session_id)
232
+ if cached:
233
+ session_path = cached.get("path")
234
+ if not session_path:
202
235
  for s in scan_sessions():
203
236
  if s["id"] == session_id:
204
- session_data = s
237
+ session_path = s.get("path")
205
238
  break
206
-
207
- if not session_data:
239
+
240
+ if not session_path:
208
241
  return
209
242
 
210
- # 2. Update UI instantly (fluid)
211
- messages = session_data.get("messages", [])
243
+ # 2. Load messages directly from disk (avoids keeping all messages in memory)
244
+ messages = _load_session_messages(session_path)
212
245
  app.chat.load_messages(messages)
213
-
246
+
214
247
  # 3. Defer bridge loading until first message (user request)
215
248
  bridge.pending_history = messages
216
249
  bridge.session_id = session_id
217
250
  # Important: clear actual AI state so it's fresh until sync
218
251
  from agent import AgentState
219
252
  bridge.state = AgentState()
220
-
253
+
221
254
  app.set_active_session(session_id)
222
255
  app.sidebar.update_context_bar()
223
256
  app.set_status("Sesión lista (Contexto diferido)", t["success"])
@@ -236,8 +269,8 @@ def launch_gui(config: dict | None = None, initial_prompt: str | None = None) ->
236
269
  app.on_model_change = _on_model_change
237
270
  app.on_session_select = _on_session_select
238
271
 
239
- # Load existing sessions into sidebar
240
- app.set_sessions(scan_sessions())
272
+ # Load existing sessions into sidebar (async so GUI shows immediately)
273
+ _refresh_sidebar_async()
241
274
  app.sidebar._refresh_model_list()
242
275
  app.sidebar.update_context_bar()
243
276
 
@@ -291,7 +324,7 @@ def launch_gui(config: dict | None = None, initial_prompt: str | None = None) ->
291
324
  # Rebuilding after every message causes annoying flicker.
292
325
  sid = event.get("session_id")
293
326
  if sid and sid not in app.sidebar._session_buttons:
294
- app.set_sessions(scan_sessions())
327
+ _refresh_sidebar_async()
295
328
  if sid:
296
329
  app.set_active_session(sid)
297
330
 
@@ -162,9 +162,9 @@ class DulusBridge:
162
162
 
163
163
  def _process_turn(self, user_message: str) -> None:
164
164
  # Assign session_id immediately to prevent UI duplication during turn
165
+ # Use "default" as fallback so New Chat doesn't spawn random UUIDs
165
166
  if not self.session_id:
166
- import uuid
167
- self.session_id = uuid.uuid4().hex[:8]
167
+ self.session_id = "default"
168
168
 
169
169
  # ── Skill inject (one-shot) ────────────────────────────────────────
170
170
  skill_body = self._skill_inject
@@ -3,7 +3,11 @@ import json
3
3
  import datetime
4
4
  import uuid
5
5
  from pathlib import Path
6
- from config import SESSIONS_DIR, DAILY_DIR, MR_SESSION_DIR, SESSION_HIST_FILE
6
+ from config import SESSIONS_DIR, DAILY_DIR, SESSION_HIST_FILE
7
+
8
+ # File-mtime cache: path -> (mtime, result) to avoid re-reading unchanged files
9
+ _scan_cache: dict[str, tuple[float, dict]] = {}
10
+
7
11
 
8
12
  def build_title(messages: list[dict]) -> str:
9
13
  """Generate a descriptive title from the first user message."""
@@ -15,60 +19,71 @@ def build_title(messages: list[dict]) -> str:
15
19
  text = " ".join(part.get("text", "") for part in content if isinstance(part, dict))
16
20
  else:
17
21
  text = str(content)
18
-
22
+
19
23
  if text.strip():
20
24
  clean = text.strip().replace("\n", " ")
21
25
  return clean[:40] + ("..." if len(clean) > 40 else "")
22
26
  return "Nueva conversación"
23
27
 
28
+
29
+ def _read_session_meta(path: Path) -> dict | None:
30
+ """Read session metadata with mtime caching."""
31
+ global _scan_cache
32
+ try:
33
+ mtime = path.stat().st_mtime
34
+ key = str(path)
35
+ if key in _scan_cache:
36
+ cached_mtime, cached = _scan_cache[key]
37
+ if cached_mtime == mtime:
38
+ return cached
39
+
40
+ data = json.loads(path.read_text(encoding="utf-8", errors="replace"))
41
+ sid = data.get("session_id", path.stem)
42
+ messages = data.get("messages", [])
43
+ title = build_title(messages)
44
+ saved_at = data.get("saved_at", "")
45
+ if saved_at and len(saved_at) >= 19:
46
+ title = f"{saved_at[11:16]} {title}"
47
+
48
+ result = {
49
+ "id": sid,
50
+ "title": title,
51
+ "path": str(path),
52
+ "saved_at": saved_at,
53
+ "turn_count": data.get("turn_count", len(messages) // 2),
54
+ }
55
+ _scan_cache[key] = (mtime, result)
56
+ return result
57
+ except Exception:
58
+ return None
59
+
60
+
24
61
  def scan_sessions() -> list[dict]:
25
- """Scan session directories and return sorted list of metadata."""
62
+ """Scan daily session directories and return sorted list of metadata.
63
+
64
+ Single source of truth for listing: only daily/ folder is scanned.
65
+ Other locations (root sessions/, checkpoints) continue to exist for
66
+ internal use but are not listed to avoid duplicates.
67
+ """
26
68
  sessions: list[dict] = []
27
69
  seen: set[str] = set()
28
70
  files: list[Path] = []
29
71
 
30
- # Daily sessions (newest first)
72
+ # Daily sessions only (newest first)
31
73
  if DAILY_DIR.exists():
32
74
  for day_dir in sorted(DAILY_DIR.iterdir(), reverse=True):
33
75
  if day_dir.is_dir():
34
76
  files.extend(sorted(day_dir.glob("session_*.json"), reverse=True))
35
77
 
36
- # MR sessions
37
- if MR_SESSION_DIR.exists():
38
- files.extend(
39
- s for s in sorted(MR_SESSION_DIR.glob("*.json"), reverse=True)
40
- if s.name != "session_latest.json"
41
- )
42
-
43
- # Root sessions
44
- if SESSIONS_DIR.exists():
45
- files.extend(sorted(SESSIONS_DIR.glob("session_*.json"), reverse=True))
46
-
47
78
  for path in files:
48
- try:
49
- data = json.loads(path.read_text(encoding="utf-8", errors="replace"))
50
- sid = data.get("session_id", path.stem)
51
- if sid in seen:
52
- continue
53
- seen.add(sid)
54
-
55
- messages = data.get("messages", [])
56
- title = build_title(messages)
57
-
58
- saved_at = data.get("saved_at", "")
59
- if saved_at and len(saved_at) >= 19:
60
- # Add time prefix: "HH:MM Title"
61
- title = f"{saved_at[11:16]} {title}"
62
-
63
- sessions.append({
64
- "id": sid,
65
- "title": title,
66
- "path": str(path),
67
- "messages": messages,
68
- "saved_at": saved_at
69
- })
70
- except Exception:
79
+ meta = _read_session_meta(path)
80
+ if not meta:
81
+ continue
82
+ sid = meta["id"]
83
+ if sid in seen:
71
84
  continue
85
+ seen.add(sid)
86
+ sessions.append(meta)
72
87
 
73
88
  # Sort all found sessions by saved_at DESC
74
89
  sessions.sort(key=lambda x: x.get("saved_at", ""), reverse=True)
@@ -101,8 +116,8 @@ def save_session(state, config: dict, session_id: str | None = None) -> str:
101
116
  payload = json.dumps(data, indent=2, default=str)
102
117
 
103
118
  # 2. Save latest for /resume
104
- MR_SESSION_DIR.mkdir(parents=True, exist_ok=True)
105
- (MR_SESSION_DIR / "session_latest.json").write_text(payload, encoding="utf-8")
119
+ SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
120
+ (SESSIONS_DIR / "session_latest.json").write_text(payload, encoding="utf-8")
106
121
 
107
122
  # 3. Save to daily folder
108
123
  day_dir = DAILY_DIR / date_str
@@ -155,16 +170,8 @@ def delete_session(session_id: str) -> bool:
155
170
  return False
156
171
 
157
172
  deleted = False
158
-
159
- # 1. Scan and delete in MR_SESSION_DIR (except latest maybe?)
160
- if MR_SESSION_DIR.exists():
161
- for p in MR_SESSION_DIR.glob(f"*{session_id}*"):
162
- try:
163
- p.unlink()
164
- deleted = True
165
- except: pass
166
173
 
167
- # 2. Daily sessions
174
+ # 1. Daily sessions
168
175
  if DAILY_DIR.exists():
169
176
  for d in DAILY_DIR.iterdir():
170
177
  if d.is_dir():
@@ -174,7 +181,7 @@ def delete_session(session_id: str) -> bool:
174
181
  deleted = True
175
182
  except: pass
176
183
 
177
- # 3. Root sessions
184
+ # 2. Root sessions (includes session_latest.json and manual /save files)
178
185
  if SESSIONS_DIR.exists():
179
186
  for p in SESSIONS_DIR.glob(f"*{session_id}*"):
180
187
  try:
@@ -182,7 +189,7 @@ def delete_session(session_id: str) -> bool:
182
189
  deleted = True
183
190
  except: pass
184
191
 
185
- # 4. Update history.json
192
+ # 3. Update history.json
186
193
  if SESSION_HIST_FILE.exists():
187
194
  try:
188
195
  hist = json.loads(SESSION_HIST_FILE.read_text())
@@ -18,7 +18,7 @@ except ImportError:
18
18
  from tkinter import ttk
19
19
  HAS_CTK = False
20
20
 
21
- from config import CONFIG_DIR, SESSIONS_DIR, DAILY_DIR, MR_SESSION_DIR, load_config
21
+ from config import CONFIG_DIR, SESSIONS_DIR, DAILY_DIR, load_config
22
22
  from tool_registry import get_all_tools
23
23
  from providers import PROVIDERS, list_ollama_models
24
24
  from gui.themes import get_theme, CURATED_MODELS
@@ -255,63 +255,89 @@ class DulusSidebar(ctk.CTkFrame if HAS_CTK else ctk.Frame):
255
255
 
256
256
  # ── Refresh helpers ───────────────────────────────────────────────────────
257
257
 
258
- def set_sessions(self, sessions: list[dict]) -> None:
259
- """Update the session history list in the sidebar."""
260
- # Clear existing buttons
261
- for widget in getattr(self.session_frame, "winfo_children", lambda: [])():
262
- widget.destroy()
263
- self._session_buttons.clear()
264
- self._sessions = []
265
-
266
- if not sessions:
267
- lbl = ctk.CTkLabel if HAS_CTK else ctk.Label
268
- lbl(self.session_frame, text="(sin sesiones)", font=FONT_SMALL,
269
- text_color=TEXT_DIM if HAS_CTK else TEXT_DIM).pack(anchor="w")
270
- return
271
-
258
+ def _create_session_row(self, sid: str, title: str) -> None:
259
+ """Create a single session row (button + delete) in the sidebar."""
272
260
  btn_cls = ctk.CTkButton if HAS_CTK else ctk.Button
273
261
  frm_cls = ctk.CTkFrame if HAS_CTK else ctk.Frame
274
-
262
+
263
+ row = frm_cls(self.session_frame, fg_color="transparent" if HAS_CTK else CARD_COLOR)
264
+ row.pack(fill="x", pady=1)
265
+ row.grid_columnconfigure(0, weight=1)
266
+
267
+ btn = btn_cls(
268
+ row,
269
+ text=title,
270
+ font=FONT_SMALL,
271
+ fg_color="transparent" if HAS_CTK else CARD_COLOR,
272
+ hover_color=BORDER_COLOR if HAS_CTK else BORDER_COLOR,
273
+ text_color=TEXT_DIM if HAS_CTK else TEXT_DIM,
274
+ **({"bg": CARD_COLOR} if not HAS_CTK else {}),
275
+ anchor="w",
276
+ height=28,
277
+ command=lambda s=sid: self._on_session_click(s),
278
+ )
279
+ btn.grid(row=0, column=0, sticky="ew")
280
+ self._session_buttons[sid] = btn
281
+
282
+ del_btn = btn_cls(
283
+ row,
284
+ text=" \u2715 ",
285
+ width=24,
286
+ height=24,
287
+ font=(FONT_FAMILY, 10, "bold"),
288
+ fg_color="transparent" if HAS_CTK else CARD_COLOR,
289
+ hover_color="#aa3333",
290
+ text_color=TEXT_DIM if HAS_CTK else TEXT_DIM,
291
+ command=lambda s=sid: self._on_delete_click(s),
292
+ )
293
+ del_btn.grid(row=0, column=1, padx=(0, 2))
294
+
295
+ def set_sessions(self, sessions: list[dict]) -> None:
296
+ """Update the session history list in the sidebar (incremental diff)."""
297
+ new_ids = {s.get("id", "") for s in sessions}
298
+ old_ids = set(self._sessions)
299
+
300
+ # Remove rows that no longer exist
301
+ for sid in old_ids - new_ids:
302
+ btn = self._session_buttons.pop(sid, None)
303
+ if btn:
304
+ # Destroy parent row frame (button is inside a frame)
305
+ try:
306
+ btn.master.destroy()
307
+ except Exception:
308
+ pass
309
+ if sid in self._sessions:
310
+ self._sessions.remove(sid)
311
+
312
+ # Add or update rows
275
313
  for sess in sessions:
276
314
  sid = sess.get("id", "")
277
315
  title = sess.get("title", "Untitled")
278
- self._sessions.append(sid)
279
-
280
- # Row container for button + delete
281
- row = frm_cls(self.session_frame, fg_color="transparent" if HAS_CTK else CARD_COLOR)
282
- row.pack(fill="x", pady=1)
283
- row.grid_columnconfigure(0, weight=1)
284
-
285
- # Main session button
286
- btn = btn_cls(
287
- row,
288
- text=title,
289
- font=FONT_SMALL,
290
- fg_color="transparent" if HAS_CTK else CARD_COLOR,
291
- hover_color=BORDER_COLOR if HAS_CTK else BORDER_COLOR,
292
- text_color=TEXT_DIM if HAS_CTK else TEXT_DIM,
293
- **({"bg": CARD_COLOR} if not HAS_CTK else {}),
294
- anchor="w",
295
- height=28,
296
- command=lambda s=sid: self._on_session_click(s),
297
- )
298
- btn.grid(row=0, column=0, sticky="ew")
299
- self._session_buttons[sid] = btn
300
-
301
- # Delete button (X)
302
- del_btn = btn_cls(
303
- row,
304
- text=" \u2715 ", # Unicode X
305
- width=24,
306
- height=24,
307
- font=(FONT_FAMILY, 10, "bold"),
308
- fg_color="transparent" if HAS_CTK else CARD_COLOR,
309
- hover_color="#aa3333", # Reddish on hover
310
- text_color=TEXT_DIM if HAS_CTK else TEXT_DIM,
311
- command=lambda s=sid: self._on_delete_click(s),
312
- )
313
- del_btn.grid(row=0, column=1, padx=(0, 2))
314
-
316
+ if sid in self._session_buttons:
317
+ # Update text if changed
318
+ try:
319
+ current = self._session_buttons[sid].cget("text")
320
+ if current != title:
321
+ self._session_buttons[sid].configure(text=title)
322
+ except Exception:
323
+ pass
324
+ else:
325
+ self._sessions.append(sid)
326
+ self._create_session_row(sid, title)
327
+
328
+ # Show placeholder if empty
329
+ if not sessions and not any(
330
+ isinstance(w, (ctk.CTkLabel if HAS_CTK else ctk.Label)) and w.cget("text") == "(sin sesiones)"
331
+ for w in getattr(self.session_frame, "winfo_children", lambda: [])()
332
+ ):
333
+ for widget in getattr(self.session_frame, "winfo_children", lambda: [])():
334
+ widget.destroy()
335
+ self._session_buttons.clear()
336
+ self._sessions.clear()
337
+ lbl = ctk.CTkLabel if HAS_CTK else ctk.Label
338
+ lbl(self.session_frame, text="(sin sesiones)", font=FONT_SMALL,
339
+ text_color=TEXT_DIM if HAS_CTK else TEXT_DIM).pack(anchor="w")
340
+
315
341
  self._highlight_active_session()
316
342
 
317
343
  def _on_delete_click(self, session_id: str) -> None: