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.
- {dulus-0.2.50/dulus.egg-info → dulus-0.2.51}/PKG-INFO +1 -1
- {dulus-0.2.50 → dulus-0.2.51}/data/context.json +13 -18
- {dulus-0.2.50 → dulus-0.2.51/dulus.egg-info}/PKG-INFO +1 -1
- {dulus-0.2.50 → dulus-0.2.51}/dulus.py +15 -22
- {dulus-0.2.50 → dulus-0.2.51}/dulus_gui.py +54 -21
- {dulus-0.2.50 → dulus-0.2.51}/gui/agent_bridge.py +2 -2
- {dulus-0.2.50 → dulus-0.2.51}/gui/session_utils.py +58 -51
- {dulus-0.2.50 → dulus-0.2.51}/gui/sidebar.py +79 -53
- {dulus-0.2.50 → dulus-0.2.51}/pyproject.toml +1 -1
- {dulus-0.2.50 → dulus-0.2.51}/webchat_server.py +15 -1
- {dulus-0.2.50 → dulus-0.2.51}/LICENSE +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/MANIFEST.in +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/README.md +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/agent.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/backend/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/backend/agents_bridge.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/backend/compressor.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/backend/context.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/backend/githook.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/backend/marketplace.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/backend/mempalace_bridge.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/backend/personas.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/backend/plugins.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/backend/server.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/backend/tasks.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/batch_api.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/checkpoint/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/checkpoint/hooks.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/checkpoint/store.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/checkpoint/types.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/claude_code_watcher.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/clipboard_utils.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/cloudsave.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/common.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/compaction.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/config.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/context.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/data/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/data/active_persona.json +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/data/marketplace.json +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/data/personas.json +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/data/plugins/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/data/plugins/composio/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/data/plugins/composio/composio_plugin/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/data/plugins/composio/composio_plugin/session_manager.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/data/plugins/composio/composio_plugin/tool_generator.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/data/plugins/composio/plugin.json +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/data/plugins/composio/plugin_tool.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/data/tasks.json +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/README.md +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/api.html +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/architecture.md +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/azure-speech-template.json +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/dashboard/index.html +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/divider.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/generate.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/hero.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/index.html +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/news.md +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/nvidia-models.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/particle-playground.html +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/personas/index.html +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/poetry-banner.png +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/preview.html +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/sec-agents.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/sec-brainstorm.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/sec-bridges.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/sec-features.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/sec-freetier.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/sec-memory.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/sec-models.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/sec-perms.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/sec-plugins.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/sec-quickstart.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/sec-ssj.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/spinners.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/split-pane.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/terminal-boot.svg +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/docs/uploads/particle-playground.html +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/dulus.egg-info/SOURCES.txt +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/dulus.egg-info/dependency_links.txt +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/dulus.egg-info/entry_points.txt +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/dulus.egg-info/requires.txt +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/dulus.egg-info/top_level.txt +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/dulus_mcp/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/dulus_mcp/client.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/dulus_mcp/config.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/dulus_mcp/tools.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/dulus_mcp/types.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/gui/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/gui/chat_widget.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/gui/main_window.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/gui/personas.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/gui/settings_dialog.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/gui/tasks_view.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/gui/themes.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/gui/tool_panel.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/input.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/license_manager.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/memory/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/memory/audit.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/memory/consolidator.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/memory/context.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/memory/offload.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/memory/palace.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/memory/scan.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/memory/sessions.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/memory/store.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/memory/tools.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/memory/types.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/memory/vector_search.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/multi_agent/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/multi_agent/subagent.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/multi_agent/tools.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/offload_helper.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/plugin/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/plugin/autoadapter.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/plugin/loader.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/plugin/recommend.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/plugin/store.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/plugin/types.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/providers.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/README.md +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/components.json +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/dist/assets/index-DE51D6wI.css +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/dist/assets/index-DMCCNE9Y.js +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/dist/index.html +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/dist/wallpaper-default.jpg +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/dist/wallpapers/default.jpeg +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/dist/wallpapers/light.jpeg +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/dist/wallpapers/nature.jpeg +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/dist/wallpapers/tech.jpeg +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/eslint.config.js +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/index.html +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/info.md +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/package-lock.json +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/package.json +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/postcss.config.js +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/public/wallpaper-default.jpg +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/public/wallpapers/default.jpeg +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/public/wallpapers/light.jpeg +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/public/wallpapers/nature.jpeg +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/public/wallpapers/tech.jpeg +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/App.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/AgentMonitor.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/ApiTester.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/AppRouter.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/ArchiveManager.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/AsciiArt.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Base64Tool.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Browser.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Calculator.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Calendar.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Chat.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Chess.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Clock.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/CodeEditor.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/ColorPalette.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/ColorPicker.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Contacts.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/DocumentViewer.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Drawing.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Email.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/FileManager.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/FlappyBird.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/FtpClient.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Game2048.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/GitClient.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/ImageGallery.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/ImageViewer.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/JsonFormatter.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/MarkdownPreview.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/MatrixRain.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/MediaConverter.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Memory.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/MemoryManager.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Minesweeper.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/MusicPlayer.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/NetworkTools.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Notes.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/PasswordManager.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/PhotoEditor.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Pong.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/RegexTester.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Reminders.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/RssReader.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/ScreenRecorder.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Settings.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/SkillsLauncher.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Snake.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Solitaire.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Spreadsheet.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Sudoku.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/SystemMonitor.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/TaskManager.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Terminal.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Tetris.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/TextEditor.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/TicTacToe.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Todo.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/VideoPlayer.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/VoiceRecorder.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Weather.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/Whiteboard.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/apps/registry.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/AppContainer.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/AppLauncher.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/BootSequence.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ContextMenu.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/Desktop.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/Dock.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/LoginScreen.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/NotImplemented.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/NotificationCenter.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/NotificationSystem.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/TopPanel.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/WindowFrame.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/WindowManager.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/accordion.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/alert-dialog.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/alert.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/aspect-ratio.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/avatar.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/badge.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/breadcrumb.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/button-group.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/button.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/calendar.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/card.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/carousel.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/chart.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/checkbox.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/collapsible.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/command.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/context-menu.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/dialog.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/drawer.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/dropdown-menu.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/empty.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/field.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/form.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/hover-card.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/input-group.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/input-otp.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/input.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/item.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/kbd.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/label.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/menubar.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/navigation-menu.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/pagination.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/popover.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/progress.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/radio-group.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/resizable.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/scroll-area.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/select.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/separator.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/sheet.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/sidebar.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/skeleton.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/slider.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/sonner.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/spinner.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/switch.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/table.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/tabs.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/textarea.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/toggle-group.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/toggle.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/components/ui/tooltip.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/index.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/use-mobile.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/useAutoOpenChat.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/useDulusAgents.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/useDulusChat.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/useDulusEvents.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/useDulusHealth.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/useDulusMemory.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/useDulusSkills.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/useDulusTasks.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/useFileSystem.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/useOSStore.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/useSkillBridge.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/useSystemBattery.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/useSystemNetwork.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/hooks/useSystemVolume.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/index.css +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/lib/dulus-api.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/lib/utils.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/main.tsx +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/types/index.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/src/utils/assets.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/tailwind.config.js +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/tsconfig.app.json +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/tsconfig.json +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/tsconfig.node.json +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/sandbox/vite.config.ts +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/setup.cfg +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/skill/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/skill/builtin.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/skill/clawhub.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/skill/executor.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/skill/loader.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/skill/tools.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/skills.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/spinner.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/string_utils.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/subagent.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/task/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/task/store.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/task/tools.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/task/types.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_afk_yolo.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_approval_runtime.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_background_task_tools.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_background_tasks.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_checkpoint.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_clipboard_utils.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_compaction.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_diff_view.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_diff_visualization.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_display_blocks.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_export_import.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_hook_engine.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_injection_fix.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_license.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_mcp.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_memory.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_notification_manager.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_plugin.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_session_fork.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_shell_mode.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_skills.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_steer_input.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_subagent.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_task.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_telegram_buffer.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_think_tool.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_todo_tool.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_todo_visualization.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_tool_registry.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_voice.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tests/test_wire_events.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tmux_offloader.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tmux_tools.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tool_registry.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/tools.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/ui/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/ui/input.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/ui/render.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/voice/__init__.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/voice/keyterms.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/voice/recorder.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/voice/stt.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/voice/tts.py +0 -0
- {dulus-0.2.50 → dulus-0.2.51}/webchat.py +0 -0
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
"project": {
|
|
10
10
|
"name": "Dulus Command Center",
|
|
11
11
|
"repo_stats": {
|
|
12
|
-
"files":
|
|
13
|
-
"lines":
|
|
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":
|
|
25
|
-
".whl":
|
|
26
|
-
".gz":
|
|
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-
|
|
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"
|
|
@@ -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.
|
|
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
|
|
1170
|
-
|
|
1171
|
-
save_path =
|
|
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
|
|
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("
|
|
1217
|
-
history_limit = cfg.get("
|
|
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
|
-
|
|
1225
|
-
latest_path =
|
|
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,
|
|
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
|
|
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 [
|
|
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
|
|
1418
|
+
from config import SESSIONS_DIR
|
|
1426
1419
|
|
|
1427
1420
|
if not args.strip():
|
|
1428
|
-
path =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
195
|
-
|
|
225
|
+
_refresh_sidebar_async()
|
|
226
|
+
|
|
196
227
|
app.hide_thinking()
|
|
197
|
-
|
|
198
|
-
# 1.
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
237
|
+
session_path = s.get("path")
|
|
205
238
|
break
|
|
206
|
-
|
|
207
|
-
if not
|
|
239
|
+
|
|
240
|
+
if not session_path:
|
|
208
241
|
return
|
|
209
242
|
|
|
210
|
-
# 2.
|
|
211
|
-
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
105
|
-
(
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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
|
-
#
|
|
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,
|
|
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
|
|
259
|
-
"""
|
|
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.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
)
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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:
|