iCode-TUI 0.29.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chrys/__init__.py +16 -0
- chrys/app/__init__.py +3 -0
- chrys/app/acp/__init__.py +3 -0
- chrys/app/acp/bridge.py +664 -0
- chrys/app/acp/content.py +132 -0
- chrys/app/acp/doc/frontend-api.md +425 -0
- chrys/app/acp/history.py +585 -0
- chrys/app/acp/server.py +1796 -0
- chrys/app/acp/session_manager.py +1815 -0
- chrys/app/acp/tool_status.py +45 -0
- chrys/app/cli/__init__.py +3 -0
- chrys/app/cli/acp.py +178 -0
- chrys/app/cli/app.py +165 -0
- chrys/app/cli/approvals.py +101 -0
- chrys/app/cli/headless.py +223 -0
- chrys/app/cli/launch_cwd.py +42 -0
- chrys/app/cli/profiles.py +298 -0
- chrys/app/cli/progress.py +857 -0
- chrys/app/cli/run.py +425 -0
- chrys/app/cli/serve.py +1593 -0
- chrys/app/cli/tool_summary.py +225 -0
- chrys/app/cli/trajectory.py +201 -0
- chrys/app/cli/workflow.py +601 -0
- chrys/app/features/__init__.py +3 -0
- chrys/app/features/buddy/__init__.py +3 -0
- chrys/app/features/buddy/actions.py +68 -0
- chrys/app/features/buddy/animation.py +76 -0
- chrys/app/features/buddy/commands.py +196 -0
- chrys/app/features/buddy/docs/design.md +187 -0
- chrys/app/features/buddy/hatchery.py +80 -0
- chrys/app/features/buddy/lifecycle.py +41 -0
- chrys/app/features/buddy/model.py +265 -0
- chrys/app/features/buddy/pixel_renderer.py +113 -0
- chrys/app/features/buddy/pixel_sprites.py +279 -0
- chrys/app/features/buddy/portrait.py +76 -0
- chrys/app/features/buddy/progression.py +58 -0
- chrys/app/features/buddy/replies.py +191 -0
- chrys/app/features/buddy/sprites/alien.toml +70 -0
- chrys/app/features/buddy/sprites/axolotl.toml +71 -0
- chrys/app/features/buddy/sprites/bat.toml +69 -0
- chrys/app/features/buddy/sprites/bee.toml +69 -0
- chrys/app/features/buddy/sprites/cactus.toml +73 -0
- chrys/app/features/buddy/sprites/capybara.toml +70 -0
- chrys/app/features/buddy/sprites/cat.toml +69 -0
- chrys/app/features/buddy/sprites/crab.toml +70 -0
- chrys/app/features/buddy/sprites/dragon.toml +71 -0
- chrys/app/features/buddy/sprites/duck.toml +69 -0
- chrys/app/features/buddy/sprites/fox.toml +69 -0
- chrys/app/features/buddy/sprites/frog.toml +70 -0
- chrys/app/features/buddy/sprites/ghost.toml +70 -0
- chrys/app/features/buddy/sprites/goose.toml +69 -0
- chrys/app/features/buddy/sprites/jelly.toml +70 -0
- chrys/app/features/buddy/sprites/llama.toml +70 -0
- chrys/app/features/buddy/sprites/mushroom.toml +69 -0
- chrys/app/features/buddy/sprites/octopus.toml +70 -0
- chrys/app/features/buddy/sprites/owl.toml +70 -0
- chrys/app/features/buddy/sprites/panda.toml +71 -0
- chrys/app/features/buddy/sprites/penguin.toml +69 -0
- chrys/app/features/buddy/sprites/rabbit.toml +70 -0
- chrys/app/features/buddy/sprites/robot.toml +72 -0
- chrys/app/features/buddy/sprites/shark.toml +70 -0
- chrys/app/features/buddy/sprites/snail.toml +69 -0
- chrys/app/features/buddy/sprites/snake.toml +69 -0
- chrys/app/features/buddy/sprites/tabby.toml +69 -0
- chrys/app/features/buddy/sprites/turtle.toml +69 -0
- chrys/app/features/buddy/store.py +143 -0
- chrys/app/features/session_title/__init__.py +7 -0
- chrys/app/features/session_title/updater.py +582 -0
- chrys/app/installer.py +651 -0
- chrys/app/parsing.py +17 -0
- chrys/app/tui/__init__.py +3 -0
- chrys/app/tui/app.py +1287 -0
- chrys/app/tui/behaviors/__init__.py +3 -0
- chrys/app/tui/behaviors/click_outside_dismiss.py +90 -0
- chrys/app/tui/behaviors/insert_clipboard.py +73 -0
- chrys/app/tui/behaviors/right_click_copy.py +146 -0
- chrys/app/tui/binding_display.py +139 -0
- chrys/app/tui/buddy_messages.py +17 -0
- chrys/app/tui/buddy_reply.py +86 -0
- chrys/app/tui/chrys.tcss +549 -0
- chrys/app/tui/clipboard.py +109 -0
- chrys/app/tui/copy_messages.py +10 -0
- chrys/app/tui/i18n.py +230 -0
- chrys/app/tui/language.py +58 -0
- chrys/app/tui/notifications/__init__.py +12 -0
- chrys/app/tui/notifications/drivers.py +180 -0
- chrys/app/tui/notifications/service.py +126 -0
- chrys/app/tui/notifications/settings.py +141 -0
- chrys/app/tui/screens/__init__.py +3 -0
- chrys/app/tui/screens/agents/__init__.py +8 -0
- chrys/app/tui/screens/agents/agent_draft_store.py +937 -0
- chrys/app/tui/screens/agents/config.py +2142 -0
- chrys/app/tui/screens/agents/config.tcss +169 -0
- chrys/app/tui/screens/agents/panels/__init__.py +3 -0
- chrys/app/tui/screens/agents/panels/acp.py +1176 -0
- chrys/app/tui/screens/agents/panels/basic.py +497 -0
- chrys/app/tui/screens/agents/panels/compaction.py +231 -0
- chrys/app/tui/screens/agents/panels/config_card.py +218 -0
- chrys/app/tui/screens/agents/panels/instructions.py +91 -0
- chrys/app/tui/screens/agents/panels/markdown_report.py +76 -0
- chrys/app/tui/screens/agents/panels/mcp.py +2262 -0
- chrys/app/tui/screens/agents/panels/memory.py +684 -0
- chrys/app/tui/screens/agents/panels/path_entry.py +227 -0
- chrys/app/tui/screens/agents/panels/skills.py +806 -0
- chrys/app/tui/screens/agents/panels/subagents.py +652 -0
- chrys/app/tui/screens/agents/panels/tools.py +205 -0
- chrys/app/tui/screens/agents/picker.py +103 -0
- chrys/app/tui/screens/agents/skill_paths.py +38 -0
- chrys/app/tui/screens/agents/validation_messages.py +452 -0
- chrys/app/tui/screens/dialogs/__init__.py +35 -0
- chrys/app/tui/screens/dialogs/agent_load.py +834 -0
- chrys/app/tui/screens/dialogs/agent_load.tcss +78 -0
- chrys/app/tui/screens/dialogs/app_mode.py +55 -0
- chrys/app/tui/screens/dialogs/approval/__init__.py +15 -0
- chrys/app/tui/screens/dialogs/approval/approval.tcss +311 -0
- chrys/app/tui/screens/dialogs/approval/bodies/__init__.py +3 -0
- chrys/app/tui/screens/dialogs/approval/bodies/file_edit.py +210 -0
- chrys/app/tui/screens/dialogs/approval/bodies/sub_agent.py +82 -0
- chrys/app/tui/screens/dialogs/approval/body.py +110 -0
- chrys/app/tui/screens/dialogs/approval/dialog.py +565 -0
- chrys/app/tui/screens/dialogs/approval/mode.py +105 -0
- chrys/app/tui/screens/dialogs/ask_user.py +148 -0
- chrys/app/tui/screens/dialogs/ask_user.tcss +62 -0
- chrys/app/tui/screens/dialogs/base.py +84 -0
- chrys/app/tui/screens/dialogs/confirm.py +184 -0
- chrys/app/tui/screens/dialogs/confirm.tcss +46 -0
- chrys/app/tui/screens/dialogs/connection_test.py +183 -0
- chrys/app/tui/screens/dialogs/connection_test.tcss +92 -0
- chrys/app/tui/screens/dialogs/diagram.py +102 -0
- chrys/app/tui/screens/dialogs/diagram.tcss +24 -0
- chrys/app/tui/screens/dialogs/editor.py +378 -0
- chrys/app/tui/screens/dialogs/editor.tcss +132 -0
- chrys/app/tui/screens/dialogs/file_picker.py +582 -0
- chrys/app/tui/screens/dialogs/file_picker.tcss +78 -0
- chrys/app/tui/screens/dialogs/fork_session.py +219 -0
- chrys/app/tui/screens/dialogs/fork_session.tcss +75 -0
- chrys/app/tui/screens/dialogs/image_compression.py +102 -0
- chrys/app/tui/screens/dialogs/image_compression.tcss +28 -0
- chrys/app/tui/screens/dialogs/man_page.py +172 -0
- chrys/app/tui/screens/dialogs/man_page.tcss +39 -0
- chrys/app/tui/screens/dialogs/new_folder.py +277 -0
- chrys/app/tui/screens/dialogs/new_folder.tcss +51 -0
- chrys/app/tui/screens/dialogs/notice.tcss +14 -0
- chrys/app/tui/screens/dialogs/prompt_cache.py +144 -0
- chrys/app/tui/screens/dialogs/prompt_cache.tcss +38 -0
- chrys/app/tui/screens/dialogs/runtime_details.py +371 -0
- chrys/app/tui/screens/dialogs/runtime_details.tcss +49 -0
- chrys/app/tui/screens/dialogs/session_title.py +110 -0
- chrys/app/tui/screens/dialogs/session_title.tcss +43 -0
- chrys/app/tui/screens/dialogs/tool_view.py +207 -0
- chrys/app/tui/screens/dialogs/tool_view.tcss +43 -0
- chrys/app/tui/screens/dialogs/vision_unsupported.py +122 -0
- chrys/app/tui/screens/dialogs/vision_unsupported.tcss +35 -0
- chrys/app/tui/screens/dialogs/workflow_confirm.py +91 -0
- chrys/app/tui/screens/dialogs/workflow_confirm.tcss +17 -0
- chrys/app/tui/screens/dialogs/workflow_input.py +258 -0
- chrys/app/tui/screens/dialogs/workflow_input.tcss +73 -0
- chrys/app/tui/screens/dialogs/workflow_node.py +462 -0
- chrys/app/tui/screens/dialogs/workflow_node.tcss +41 -0
- chrys/app/tui/screens/dialogs/workflow_picker.py +46 -0
- chrys/app/tui/screens/dialogs/workflow_result.py +136 -0
- chrys/app/tui/screens/dialogs/workflow_result.tcss +14 -0
- chrys/app/tui/screens/diff/__init__.py +22 -0
- chrys/app/tui/screens/diff/rollback_modal.py +1381 -0
- chrys/app/tui/screens/diff/rollback_modal.tcss +248 -0
- chrys/app/tui/screens/diff/screen.py +1532 -0
- chrys/app/tui/screens/diff/screen.tcss +104 -0
- chrys/app/tui/screens/guides/__init__.py +7 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/agents.md +178 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/approval.md +117 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/chat-options.md +276 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/compaction.md +42 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/memory.md +48 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/models.md +158 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/settings.md +210 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/themes.md +61 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/tools.md +60 -0
- chrys/app/tui/screens/guides/_docs/en/guides/configuration/web-tools.md +286 -0
- chrys/app/tui/screens/guides/_docs/en/guides/daily-use/sessions.md +77 -0
- chrys/app/tui/screens/guides/_docs/en/guides/daily-use/workspaces.md +51 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/external-acp-agents.md +126 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/hooks.md +167 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/mcp.md +95 -0
- chrys/app/tui/screens/guides/_docs/en/guides/extensions/skills.md +175 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/icode-acp.md +61 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/icode-run.md +190 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/icode-serve.md +239 -0
- chrys/app/tui/screens/guides/_docs/en/guides/running/workflows.md +664 -0
- chrys/app/tui/screens/guides/_docs/en/reference/agent-profile.md +548 -0
- chrys/app/tui/screens/guides/_docs/en/reference/hooks.md +602 -0
- chrys/app/tui/screens/guides/_docs/en/reference/opentelemetry.md +177 -0
- chrys/app/tui/screens/guides/_docs/en/reference/settings.md +245 -0
- chrys/app/tui/screens/guides/_docs/en/reference/themes.md +149 -0
- chrys/app/tui/screens/guides/_docs/en/reference/tool-kinds-and-names.md +58 -0
- chrys/app/tui/screens/guides/_docs/en/reference/tui-slash-commands.md +76 -0
- chrys/app/tui/screens/guides/_docs/en/reference/workflows.md +1006 -0
- chrys/app/tui/screens/guides/_docs/en/start/getting-started.md +170 -0
- chrys/app/tui/screens/guides/_docs/en/start/what-is-icode.md +34 -0
- chrys/app/tui/screens/guides/_docs/index.yaml +100 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/agents.md +178 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/approval.md +117 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/chat-options.md +276 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/compaction.md +42 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/memory.md +48 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/models.md +158 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/settings.md +210 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/themes.md +61 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/tools.md +60 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/configuration/web-tools.md +286 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/sessions.md +77 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/daily-use/workspaces.md +51 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/external-acp-agents.md +126 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/hooks.md +167 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/mcp.md +95 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/extensions/skills.md +175 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-acp.md +61 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-run.md +190 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/icode-serve.md +239 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/guides/running/workflows.md +664 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/agent-profile.md +548 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/hooks.md +602 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/opentelemetry.md +177 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/settings.md +245 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/themes.md +149 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tool-kinds-and-names.md +58 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/tui-slash-commands.md +76 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/reference/workflows.md +1005 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/start/getting-started.md +170 -0
- chrys/app/tui/screens/guides/_docs/zh-Hans/start/what-is-icode.md +34 -0
- chrys/app/tui/screens/guides/index.py +215 -0
- chrys/app/tui/screens/guides/reader.py +173 -0
- chrys/app/tui/screens/guides/screen.py +442 -0
- chrys/app/tui/screens/guides/screen.tcss +52 -0
- chrys/app/tui/screens/logs/__init__.py +7 -0
- chrys/app/tui/screens/logs/screen.py +504 -0
- chrys/app/tui/screens/logs/screen.tcss +45 -0
- chrys/app/tui/screens/main/__init__.py +7 -0
- chrys/app/tui/screens/main/action_availability.py +30 -0
- chrys/app/tui/screens/main/buddy_command.py +143 -0
- chrys/app/tui/screens/main/commands.py +1073 -0
- chrys/app/tui/screens/main/config_actions.py +464 -0
- chrys/app/tui/screens/main/copy_actions.py +117 -0
- chrys/app/tui/screens/main/dialog_controllers.py +826 -0
- chrys/app/tui/screens/main/dialog_gateway.py +166 -0
- chrys/app/tui/screens/main/diff_controller.py +478 -0
- chrys/app/tui/screens/main/engine_read_model.py +184 -0
- chrys/app/tui/screens/main/event_handlers.py +2014 -0
- chrys/app/tui/screens/main/input_flow.py +241 -0
- chrys/app/tui/screens/main/live_diff.py +244 -0
- chrys/app/tui/screens/main/main_screen_presenter.py +295 -0
- chrys/app/tui/screens/main/model_indicator.py +217 -0
- chrys/app/tui/screens/main/navigation.py +343 -0
- chrys/app/tui/screens/main/ports.py +571 -0
- chrys/app/tui/screens/main/recent_dirs.py +123 -0
- chrys/app/tui/screens/main/rollback_controller.py +586 -0
- chrys/app/tui/screens/main/runtime_info.py +71 -0
- chrys/app/tui/screens/main/screen.py +2265 -0
- chrys/app/tui/screens/main/screen.tcss +21 -0
- chrys/app/tui/screens/main/session_handlers.py +1127 -0
- chrys/app/tui/screens/main/session_title.py +239 -0
- chrys/app/tui/screens/main/settings_coordinator.py +365 -0
- chrys/app/tui/screens/main/settings_persistence.py +184 -0
- chrys/app/tui/screens/main/shell_mode.py +112 -0
- chrys/app/tui/screens/main/state.py +289 -0
- chrys/app/tui/screens/main/submit_state.py +11 -0
- chrys/app/tui/screens/main/subscriptions.py +177 -0
- chrys/app/tui/screens/main/suggestions.py +982 -0
- chrys/app/tui/screens/main/tool_action_bridge.py +75 -0
- chrys/app/tui/screens/main/view_adapter.py +1567 -0
- chrys/app/tui/screens/main/workflow_browser.py +747 -0
- chrys/app/tui/screens/main/workflow_content.py +248 -0
- chrys/app/tui/screens/main/workflow_controller.py +240 -0
- chrys/app/tui/screens/main/workflow_feedback.py +137 -0
- chrys/app/tui/screens/main/workflow_flow.py +49 -0
- chrys/app/tui/screens/main/workflow_run_control.py +456 -0
- chrys/app/tui/screens/main/workflow_session_view.py +316 -0
- chrys/app/tui/screens/main/workspace_actions.py +272 -0
- chrys/app/tui/screens/main/workspace_branch.py +194 -0
- chrys/app/tui/screens/menus/__init__.py +8 -0
- chrys/app/tui/screens/menus/languages.py +114 -0
- chrys/app/tui/screens/models/__init__.py +7 -0
- chrys/app/tui/screens/models/picker.py +147 -0
- chrys/app/tui/screens/models/screen.py +2043 -0
- chrys/app/tui/screens/models/screen.tcss +255 -0
- chrys/app/tui/screens/sessions/__init__.py +8 -0
- chrys/app/tui/screens/sessions/presenter.py +586 -0
- chrys/app/tui/screens/sessions/screen.py +779 -0
- chrys/app/tui/screens/sessions/screen.tcss +127 -0
- chrys/app/tui/screens/settings/__init__.py +29 -0
- chrys/app/tui/screens/settings/danger.py +26 -0
- chrys/app/tui/screens/settings/dialog.py +281 -0
- chrys/app/tui/screens/settings/layout.py +519 -0
- chrys/app/tui/screens/settings/migrate_dialog.py +253 -0
- chrys/app/tui/screens/settings/panes/__init__.py +3 -0
- chrys/app/tui/screens/settings/panes/notifications.py +248 -0
- chrys/app/tui/screens/settings/panes/sessions.py +148 -0
- chrys/app/tui/screens/settings/ports.py +55 -0
- chrys/app/tui/screens/settings/provenance.py +102 -0
- chrys/app/tui/screens/settings/rows.py +546 -0
- chrys/app/tui/screens/settings/settings.tcss +406 -0
- chrys/app/tui/screens/themes/__init__.py +8 -0
- chrys/app/tui/screens/themes/dialogs.py +330 -0
- chrys/app/tui/screens/themes/editor.py +632 -0
- chrys/app/tui/screens/themes/editor.tcss +268 -0
- chrys/app/tui/screens/themes/messages.py +95 -0
- chrys/app/tui/screens/themes/palette.py +694 -0
- chrys/app/tui/screens/themes/panel.py +113 -0
- chrys/app/tui/screens/themes/picker.py +203 -0
- chrys/app/tui/screens/themes/save.py +191 -0
- chrys/app/tui/support/__init__.py +3 -0
- chrys/app/tui/support/file_manager.py +64 -0
- chrys/app/tui/support/gc_freeze.py +1114 -0
- chrys/app/tui/support/path_shortcuts.py +50 -0
- chrys/app/tui/support/prompt_history.py +298 -0
- chrys/app/tui/support/workspace_mru.py +476 -0
- chrys/app/tui/terminal/__init__.py +6 -0
- chrys/app/tui/terminal/_pty_backend.py +114 -0
- chrys/app/tui/terminal/emulator/__init__.py +24 -0
- chrys/app/tui/terminal/emulator/buffer.py +456 -0
- chrys/app/tui/terminal/emulator/charsets.py +113 -0
- chrys/app/tui/terminal/emulator/core.py +963 -0
- chrys/app/tui/terminal/emulator/input.py +342 -0
- chrys/app/tui/terminal/emulator/parser.py +227 -0
- chrys/app/tui/terminal/emulator/pen.py +158 -0
- chrys/app/tui/terminal/launcher.py +455 -0
- chrys/app/tui/terminal/leaked_reports.py +44 -0
- chrys/app/tui/terminal/panel.py +323 -0
- chrys/app/tui/terminal/pty_process.py +405 -0
- chrys/app/tui/terminal/rendering.py +151 -0
- chrys/app/tui/terminal/shell.py +273 -0
- chrys/app/tui/terminal/shell_integration.py +199 -0
- chrys/app/tui/terminal/title.py +224 -0
- chrys/app/tui/terminal/widget.py +654 -0
- chrys/app/tui/theme.py +422 -0
- chrys/app/tui/theme_loader.py +201 -0
- chrys/app/tui/theme_stylesheet.py +49 -0
- chrys/app/tui/themes/__init__.py +3 -0
- chrys/app/tui/themes/document.py +120 -0
- chrys/app/tui/themes/preview.py +170 -0
- chrys/app/tui/themes/store.py +176 -0
- chrys/app/tui/toasts.py +69 -0
- chrys/app/tui/util/__init__.py +3 -0
- chrys/app/tui/util/context_pressure.py +146 -0
- chrys/app/tui/util/debug_log.py +292 -0
- chrys/app/tui/util/diff_entries.py +64 -0
- chrys/app/tui/util/formatting.py +53 -0
- chrys/app/tui/util/git_branch.py +326 -0
- chrys/app/tui/util/invocation_progress.py +57 -0
- chrys/app/tui/util/logo.py +32 -0
- chrys/app/tui/util/message_gate.py +35 -0
- chrys/app/tui/util/mutation_sources.py +14 -0
- chrys/app/tui/util/picker_text.py +27 -0
- chrys/app/tui/util/removal.py +47 -0
- chrys/app/tui/util/rich_style.py +36 -0
- chrys/app/tui/util/source_text.py +12 -0
- chrys/app/tui/util/static_update.py +88 -0
- chrys/app/tui/util/todo_format.py +51 -0
- chrys/app/tui/util/visibility.py +214 -0
- chrys/app/tui/widgets/__init__.py +60 -0
- chrys/app/tui/widgets/ask_user_controls.py +652 -0
- chrys/app/tui/widgets/ask_user_markdown.py +17 -0
- chrys/app/tui/widgets/ask_user_prompt.py +606 -0
- chrys/app/tui/widgets/buttons.py +52 -0
- chrys/app/tui/widgets/chat/__init__.py +3 -0
- chrys/app/tui/widgets/chat/agent_transcript_surface.py +818 -0
- chrys/app/tui/widgets/chat/chrome.py +296 -0
- chrys/app/tui/widgets/chat/compaction_card.py +353 -0
- chrys/app/tui/widgets/chat/context_fold.py +77 -0
- chrys/app/tui/widgets/chat/file_snapshot.py +91 -0
- chrys/app/tui/widgets/chat/image_preview.py +214 -0
- chrys/app/tui/widgets/chat/live_renderer.py +383 -0
- chrys/app/tui/widgets/chat/messages.py +1039 -0
- chrys/app/tui/widgets/chat/panel.py +1769 -0
- chrys/app/tui/widgets/chat/ports.py +245 -0
- chrys/app/tui/widgets/chat/renderers/__init__.py +7 -0
- chrys/app/tui/widgets/chat/renderers/ask_user.py +747 -0
- chrys/app/tui/widgets/chat/renderers/execute.py +431 -0
- chrys/app/tui/widgets/chat/renderers/file_edit.py +925 -0
- chrys/app/tui/widgets/chat/renderers/hosted_code.py +83 -0
- chrys/app/tui/widgets/chat/renderers/hosted_generic.py +330 -0
- chrys/app/tui/widgets/chat/renderers/hosted_image.py +107 -0
- chrys/app/tui/widgets/chat/renderers/hosted_mcp.py +43 -0
- chrys/app/tui/widgets/chat/renderers/hosted_search.py +167 -0
- chrys/app/tui/widgets/chat/renderers/hosted_shell.py +132 -0
- chrys/app/tui/widgets/chat/renderers/read_file.py +399 -0
- chrys/app/tui/widgets/chat/renderers/search.py +348 -0
- chrys/app/tui/widgets/chat/renderers/skill.py +564 -0
- chrys/app/tui/widgets/chat/renderers/sleep.py +287 -0
- chrys/app/tui/widgets/chat/renderers/sub_agent.py +1385 -0
- chrys/app/tui/widgets/chat/renderers/todo.py +114 -0
- chrys/app/tui/widgets/chat/renderers/web.py +69 -0
- chrys/app/tui/widgets/chat/replay.py +1605 -0
- chrys/app/tui/widgets/chat/replay_mount.py +322 -0
- chrys/app/tui/widgets/chat/scroll_controller.py +579 -0
- chrys/app/tui/widgets/chat/selection_controller.py +435 -0
- chrys/app/tui/widgets/chat/session_json.py +508 -0
- chrys/app/tui/widgets/chat/toc_model.py +156 -0
- chrys/app/tui/widgets/chat/tool_call.py +2478 -0
- chrys/app/tui/widgets/chat/tool_registry.py +753 -0
- chrys/app/tui/widgets/chat/tool_renderers.py +247 -0
- chrys/app/tui/widgets/chat/tool_view_builders.py +260 -0
- chrys/app/tui/widgets/chat/transcript_reader.py +57 -0
- chrys/app/tui/widgets/checkbox.py +24 -0
- chrys/app/tui/widgets/chrome/__init__.py +3 -0
- chrys/app/tui/widgets/chrome/app_header.py +283 -0
- chrys/app/tui/widgets/chrome/commands.py +95 -0
- chrys/app/tui/widgets/chrome/file_index.py +68 -0
- chrys/app/tui/widgets/chrome/file_scanner.py +670 -0
- chrys/app/tui/widgets/chrome/footer.py +284 -0
- chrys/app/tui/widgets/chrome/image_paste.py +351 -0
- chrys/app/tui/widgets/chrome/input_bar.py +1299 -0
- chrys/app/tui/widgets/chrome/status_bar.py +911 -0
- chrys/app/tui/widgets/chrome/suggestion_list.py +652 -0
- chrys/app/tui/widgets/click_affordance.py +24 -0
- chrys/app/tui/widgets/color_picker/__init__.py +254 -0
- chrys/app/tui/widgets/color_picker/controls.py +171 -0
- chrys/app/tui/widgets/color_picker/model.py +89 -0
- chrys/app/tui/widgets/dialog_buttons.py +58 -0
- chrys/app/tui/widgets/dialog_scroll.py +24 -0
- chrys/app/tui/widgets/diff_view/__init__.py +20 -0
- chrys/app/tui/widgets/diff_view/cells.py +59 -0
- chrys/app/tui/widgets/diff_view/code.py +280 -0
- chrys/app/tui/widgets/diff_view/compute.py +286 -0
- chrys/app/tui/widgets/diff_view/gutter.py +161 -0
- chrys/app/tui/widgets/diff_view/inline.py +77 -0
- chrys/app/tui/widgets/diff_view/palette.py +87 -0
- chrys/app/tui/widgets/diff_view/protocols.py +24 -0
- chrys/app/tui/widgets/diff_view/rows.py +138 -0
- chrys/app/tui/widgets/diff_view/unified.py +267 -0
- chrys/app/tui/widgets/diff_view/widget.py +209 -0
- chrys/app/tui/widgets/editor/__init__.py +79 -0
- chrys/app/tui/widgets/editor/coordinates.py +30 -0
- chrys/app/tui/widgets/editor/highlighter.py +139 -0
- chrys/app/tui/widgets/editor/keymaps.py +357 -0
- chrys/app/tui/widgets/editor/types.py +216 -0
- chrys/app/tui/widgets/editor/vim.py +1195 -0
- chrys/app/tui/widgets/editor/widget.py +401 -0
- chrys/app/tui/widgets/file_preview.py +58 -0
- chrys/app/tui/widgets/hatch.py +138 -0
- chrys/app/tui/widgets/input.py +66 -0
- chrys/app/tui/widgets/loading.py +125 -0
- chrys/app/tui/widgets/markdown/__init__.py +25 -0
- chrys/app/tui/widgets/markdown/_utils.py +21 -0
- chrys/app/tui/widgets/markdown/anchors.py +71 -0
- chrys/app/tui/widgets/markdown/blocks.py +115 -0
- chrys/app/tui/widgets/markdown/copyable.py +163 -0
- chrys/app/tui/widgets/markdown/diagram/__init__.py +35 -0
- chrys/app/tui/widgets/markdown/diagram/canvas.py +409 -0
- chrys/app/tui/widgets/markdown/diagram/charts.py +52 -0
- chrys/app/tui/widgets/markdown/diagram/diagnostics.py +101 -0
- chrys/app/tui/widgets/markdown/diagram/geometry.py +46 -0
- chrys/app/tui/widgets/markdown/diagram/layout.py +494 -0
- chrys/app/tui/widgets/markdown/diagram/messages.py +18 -0
- chrys/app/tui/widgets/markdown/diagram/model.py +275 -0
- chrys/app/tui/widgets/markdown/diagram/parser.py +173 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/__init__.py +3 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/charts.py +349 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/common.py +403 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/graphs.py +1072 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/planning.py +317 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/schedule.py +298 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/schemas.py +241 -0
- chrys/app/tui/widgets/markdown/diagram/parsers/structure.py +221 -0
- chrys/app/tui/widgets/markdown/diagram/presentation.py +32 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/__init__.py +3 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/charts.py +275 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/common.py +73 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/planning.py +157 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/schedule.py +202 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/structure.py +77 -0
- chrys/app/tui/widgets/markdown/diagram/renderers/xy.py +385 -0
- chrys/app/tui/widgets/markdown/diagram/router.py +651 -0
- chrys/app/tui/widgets/markdown/diagram/specs/__init__.py +3 -0
- chrys/app/tui/widgets/markdown/diagram/specs/charts.py +94 -0
- chrys/app/tui/widgets/markdown/diagram/specs/planning.py +68 -0
- chrys/app/tui/widgets/markdown/diagram/specs/schedule.py +65 -0
- chrys/app/tui/widgets/markdown/diagram/specs/structure.py +23 -0
- chrys/app/tui/widgets/markdown/diagram/viewer.py +92 -0
- chrys/app/tui/widgets/markdown/links.py +47 -0
- chrys/app/tui/widgets/markdown/math/__init__.py +36 -0
- chrys/app/tui/widgets/markdown/math/layout.py +739 -0
- chrys/app/tui/widgets/markdown/math/markdown.py +291 -0
- chrys/app/tui/widgets/markdown/math/parser.py +325 -0
- chrys/app/tui/widgets/markdown/math/symbols.py +325 -0
- chrys/app/tui/widgets/markdown/navigator.py +80 -0
- chrys/app/tui/widgets/markdown/parser.py +1057 -0
- chrys/app/tui/widgets/markdown/stream.py +73 -0
- chrys/app/tui/widgets/markdown/toc.py +98 -0
- chrys/app/tui/widgets/markdown/viewer.py +146 -0
- chrys/app/tui/widgets/markdown/widget.py +2046 -0
- chrys/app/tui/widgets/marquee.py +101 -0
- chrys/app/tui/widgets/option_menu.py +82 -0
- chrys/app/tui/widgets/page_navigator.py +118 -0
- chrys/app/tui/widgets/select.py +196 -0
- chrys/app/tui/widgets/selection.py +59 -0
- chrys/app/tui/widgets/sidebar/__init__.py +3 -0
- chrys/app/tui/widgets/sidebar/buddy.py +367 -0
- chrys/app/tui/widgets/sidebar/context.py +482 -0
- chrys/app/tui/widgets/sidebar/debug.py +195 -0
- chrys/app/tui/widgets/sidebar/empty_state.py +11 -0
- chrys/app/tui/widgets/sidebar/panel.py +198 -0
- chrys/app/tui/widgets/sidebar/tasks.py +198 -0
- chrys/app/tui/widgets/sidebar/toc.py +166 -0
- chrys/app/tui/widgets/sidebar/toc_types.py +30 -0
- chrys/app/tui/widgets/syntax_theme.py +85 -0
- chrys/app/tui/widgets/text_area.py +258 -0
- chrys/app/tui/widgets/trajectory/__init__.py +8 -0
- chrys/app/tui/widgets/trajectory/chartkit.py +264 -0
- chrys/app/tui/widgets/trajectory/insights.py +1266 -0
- chrys/app/tui/widgets/trajectory/overview.py +1101 -0
- chrys/app/tui/widgets/trajectory/panel.py +983 -0
- chrys/app/tui/widgets/trajectory/presentation.py +311 -0
- chrys/app/tui/widgets/trajectory/session_info.py +124 -0
- chrys/app/tui/widgets/trajectory/text_view.py +124 -0
- chrys/app/tui/widgets/trajectory/timeline.py +380 -0
- chrys/app/tui/widgets/welcome.py +126 -0
- chrys/app/tui/widgets/workflow/__init__.py +5 -0
- chrys/app/tui/widgets/workflow/edges.py +89 -0
- chrys/app/tui/widgets/workflow/graph.py +869 -0
- chrys/app/tui/widgets/workflow/info.py +310 -0
- chrys/app/tui/widgets/workflow/node_view.py +27 -0
- chrys/app/tui/widgets/workflow/output.py +225 -0
- chrys/app/tui/widgets/workflow/panel.py +658 -0
- chrys/app/tui/widgets/workflow/projector.py +324 -0
- chrys/app/tui/widgets/workflow/records.py +69 -0
- chrys/app/tui/widgets/workflow/scrollbar.py +82 -0
- chrys/app/tui/widgets/workflow/selection.py +151 -0
- chrys/app/tui/widgets/workflow/source.py +65 -0
- chrys/app/tui/widgets/workflow/text.py +257 -0
- chrys/app/tui/widgets/workflow/values.py +317 -0
- chrys/foundation/__init__.py +0 -0
- chrys/foundation/branding.py +15 -0
- chrys/foundation/config/__init__.py +7 -0
- chrys/foundation/config/coercion.py +374 -0
- chrys/foundation/config/context.py +34 -0
- chrys/foundation/config/env_file.py +141 -0
- chrys/foundation/config/env_layers.py +370 -0
- chrys/foundation/config/migrations.py +521 -0
- chrys/foundation/config/notification_events.py +22 -0
- chrys/foundation/config/process_settings.py +332 -0
- chrys/foundation/config/runtime_pointer.py +133 -0
- chrys/foundation/config/settings.py +1952 -0
- chrys/foundation/config/settings_store.py +996 -0
- chrys/foundation/config/spec.py +396 -0
- chrys/foundation/config/user_settings.py +140 -0
- chrys/foundation/config/warnings.py +262 -0
- chrys/foundation/config/web_values.py +135 -0
- chrys/foundation/config/yaml_store.py +487 -0
- chrys/foundation/errors/__init__.py +68 -0
- chrys/foundation/errors/_legacy.py +117 -0
- chrys/foundation/errors/_text.py +13 -0
- chrys/foundation/errors/_walk.py +85 -0
- chrys/foundation/errors/classify.py +278 -0
- chrys/foundation/errors/display.py +274 -0
- chrys/foundation/errors/formatting.py +283 -0
- chrys/foundation/errors/kinds.py +67 -0
- chrys/foundation/errors/network.py +497 -0
- chrys/foundation/errors/provider.py +445 -0
- chrys/foundation/errors/route.py +85 -0
- chrys/foundation/events/__init__.py +8 -0
- chrys/foundation/events/bus.py +143 -0
- chrys/foundation/events/types.py +1629 -0
- chrys/foundation/events/workflow.py +38 -0
- chrys/foundation/hosted_tools.py +142 -0
- chrys/foundation/i18n/__init__.py +32 -0
- chrys/foundation/i18n/_catalogs/zh-Hans/LC_MESSAGES/chrys.mo +0 -0
- chrys/foundation/i18n/catalogs.py +42 -0
- chrys/foundation/i18n/formatting.py +202 -0
- chrys/foundation/i18n/locale.py +92 -0
- chrys/foundation/i18n/localizer.py +187 -0
- chrys/foundation/i18n/messages.py +268 -0
- chrys/foundation/io/__init__.py +3 -0
- chrys/foundation/io/result_content.py +237 -0
- chrys/foundation/models/__init__.py +3 -0
- chrys/foundation/models/approval_reuse.py +23 -0
- chrys/foundation/models/ask_user.py +468 -0
- chrys/foundation/models/execution.py +18 -0
- chrys/foundation/models/history_markers.py +115 -0
- chrys/foundation/models/invocations.py +63 -0
- chrys/foundation/models/mutation_scope.py +28 -0
- chrys/foundation/models/session_env.py +87 -0
- chrys/foundation/models/session_surface.py +29 -0
- chrys/foundation/models/todos.py +74 -0
- chrys/foundation/models/turns.py +125 -0
- chrys/foundation/models/workflow_session.py +89 -0
- chrys/foundation/models/workspace.py +59 -0
- chrys/foundation/net/__init__.py +3 -0
- chrys/foundation/net/address_class.py +57 -0
- chrys/foundation/net/route_probe.py +94 -0
- chrys/foundation/net/url.py +107 -0
- chrys/foundation/observability/__init__.py +3 -0
- chrys/foundation/observability/exporters.py +174 -0
- chrys/foundation/observability/gate.py +31 -0
- chrys/foundation/observability/setup.py +497 -0
- chrys/foundation/observability/sink.py +228 -0
- chrys/foundation/patches/__init__.py +19 -0
- chrys/foundation/patches/mdurl_cache.py +59 -0
- chrys/foundation/patches/patcher.py +287 -0
- chrys/foundation/patches/staged_members.py +205 -0
- chrys/foundation/patches/textual_block_border.py +105 -0
- chrys/foundation/patches/textual_button_ansi.py +65 -0
- chrys/foundation/patches/textual_button_ansi_old.txt +68 -0
- chrys/foundation/patches/textual_callback_cache.py +248 -0
- chrys/foundation/patches/textual_callback_dispatch.py +109 -0
- chrys/foundation/patches/textual_compositor_cjk.py +566 -0
- chrys/foundation/patches/textual_dispatch_cache.py +150 -0
- chrys/foundation/patches/textual_ime_cursor_anchor.py +92 -0
- chrys/foundation/patches/textual_kitty_keyboard.py +268 -0
- chrys/foundation/patches/textual_lru_acyclic.py +241 -0
- chrys/foundation/patches/textual_message_pump.py +136 -0
- chrys/foundation/patches/textual_node_diet.py +875 -0
- chrys/foundation/patches/textual_one_shot_timer.py +48 -0
- chrys/foundation/patches/textual_option_list.py +90 -0
- chrys/foundation/patches/textual_precompose.py +89 -0
- chrys/foundation/patches/textual_pruned_tabs.py +69 -0
- chrys/foundation/patches/textual_reflow_reuse.py +1753 -0
- chrys/foundation/patches/textual_removed_node_caches.py +86 -0
- chrys/foundation/patches/textual_removed_screen_callbacks.py +55 -0
- chrys/foundation/patches/textual_selection_extract.py +48 -0
- chrys/foundation/patches/textual_strip_cycles.py +119 -0
- chrys/foundation/patches/textual_tab_selection.py +1611 -0
- chrys/foundation/patches/textual_timer_skip.py +70 -0
- chrys/foundation/patches/textual_utf8_decoder.py +106 -0
- chrys/foundation/patches/textual_win_sleep.py +167 -0
- chrys/foundation/patches/watchdog_fsevents.py +70 -0
- chrys/foundation/patches/watchdog_windows.py +52 -0
- chrys/foundation/platform/__init__.py +556 -0
- chrys/foundation/platform/c_api.py +32 -0
- chrys/foundation/platform/child_reap.py +107 -0
- chrys/foundation/platform/command_line.py +95 -0
- chrys/foundation/platform/files.py +1470 -0
- chrys/foundation/platform/output_capture.py +113 -0
- chrys/foundation/platform/paths.py +193 -0
- chrys/foundation/platform/process.py +1468 -0
- chrys/foundation/platform/pty_output.py +19 -0
- chrys/foundation/platform/runtime_paths.py +281 -0
- chrys/foundation/platform/win_console.py +129 -0
- chrys/foundation/reasoning_origin.py +67 -0
- chrys/foundation/recovery.py +16 -0
- chrys/foundation/retry.py +278 -0
- chrys/foundation/text/__init__.py +3 -0
- chrys/foundation/text/encoding.py +603 -0
- chrys/foundation/text/images.py +304 -0
- chrys/foundation/text/lines.py +24 -0
- chrys/foundation/text/mentions.py +90 -0
- chrys/foundation/text/model_json.py +25 -0
- chrys/foundation/text/reading_order.py +20 -0
- chrys/foundation/text/tokenizer.py +25 -0
- chrys/foundation/text/tool_output.py +287 -0
- chrys/foundation/text/yaml_io.py +62 -0
- chrys/foundation/tool_call_context.py +202 -0
- chrys/foundation/tool_execution_stamp.py +184 -0
- chrys/foundation/tool_invocation_order.py +48 -0
- chrys/foundation/tool_kinds.py +102 -0
- chrys/foundation/tool_result_metadata.py +287 -0
- chrys/foundation/trajectory/__init__.py +10 -0
- chrys/foundation/trajectory/context.py +481 -0
- chrys/foundation/trajectory/envelope.py +571 -0
- chrys/foundation/trajectory/event_types.py +294 -0
- chrys/foundation/trajectory/finalizers.py +46 -0
- chrys/foundation/trajectory/fingerprint.py +62 -0
- chrys/foundation/trajectory/ids.py +155 -0
- chrys/foundation/trajectory/keys.py +99 -0
- chrys/foundation/trajectory/lease.py +173 -0
- chrys/foundation/trajectory/metadata.py +72 -0
- chrys/foundation/trajectory/reader.py +271 -0
- chrys/foundation/trajectory/recovery.py +225 -0
- chrys/foundation/trajectory/revisions.py +152 -0
- chrys/foundation/trajectory/segments.py +225 -0
- chrys/foundation/trajectory/tools.py +117 -0
- chrys/foundation/trajectory/usage.py +151 -0
- chrys/foundation/trajectory/writer.py +972 -0
- chrys/foundation/trajectory_timing.py +97 -0
- chrys/foundation/util/__init__.py +3 -0
- chrys/foundation/util/async_tasks.py +29 -0
- chrys/foundation/util/chrys_headers.py +23 -0
- chrys/foundation/util/env_templates.py +104 -0
- chrys/foundation/util/filenames.py +96 -0
- chrys/foundation/util/header_charset.py +138 -0
- chrys/foundation/util/httpx_helpers.py +12 -0
- chrys/foundation/util/lock.py +236 -0
- chrys/foundation/util/once_close.py +46 -0
- chrys/foundation/util/session_ids.py +29 -0
- chrys/foundation/util/shell_tokens.py +88 -0
- chrys/foundation/util/sub_agent_context.py +38 -0
- chrys/foundation/util/time.py +35 -0
- chrys/foundation/util/unicode_scalars.py +36 -0
- chrys/foundation/vendor/__init__.py +43 -0
- chrys/foundation/vendor/ripgrep/rg-aarch64-apple-darwin +0 -0
- chrys/foundation/vendor/ripgrep/rg-aarch64-pc-windows-msvc.exe +0 -0
- chrys/foundation/vendor/ripgrep/rg-aarch64-unknown-linux-gnu +0 -0
- chrys/foundation/vendor/ripgrep/rg-x86_64-apple-darwin +0 -0
- chrys/foundation/vendor/ripgrep/rg-x86_64-pc-windows-msvc.exe +0 -0
- chrys/foundation/vendor/ripgrep/rg-x86_64-unknown-linux-musl +0 -0
- chrys/kernel/__init__.py +256 -0
- chrys/kernel/_content.py +1866 -0
- chrys/kernel/_loop_recorder.py +631 -0
- chrys/kernel/_null_overlay.py +1026 -0
- chrys/kernel/_result_ceiling.py +216 -0
- chrys/kernel/_serialization.py +431 -0
- chrys/kernel/_tool_arg_errors.py +1629 -0
- chrys/kernel/_tool_execution.py +1172 -0
- chrys/kernel/_tool_expansion.py +26 -0
- chrys/kernel/_types.py +2598 -0
- chrys/kernel/agent.py +994 -0
- chrys/kernel/client.py +1084 -0
- chrys/kernel/compaction.py +1315 -0
- chrys/kernel/exceptions.py +211 -0
- chrys/kernel/exchanges.py +683 -0
- chrys/kernel/identity.py +95 -0
- chrys/kernel/images.py +36 -0
- chrys/kernel/instrumentation.py +1618 -0
- chrys/kernel/loop.py +2308 -0
- chrys/kernel/middleware.py +616 -0
- chrys/kernel/sessions.py +604 -0
- chrys/kernel/tools.py +1061 -0
- chrys/kernel/types.py +126 -0
- chrys/orchestration/__init__.py +3 -0
- chrys/orchestration/engine/__init__.py +3 -0
- chrys/orchestration/engine/assembly.py +236 -0
- chrys/orchestration/engine/build/__init__.py +3 -0
- chrys/orchestration/engine/build/builder.py +1061 -0
- chrys/orchestration/engine/build/construction.py +408 -0
- chrys/orchestration/engine/build/loaded.py +139 -0
- chrys/orchestration/engine/engine.py +789 -0
- chrys/orchestration/engine/execution.py +771 -0
- chrys/orchestration/engine/loader.py +865 -0
- chrys/orchestration/engine/rollback.py +918 -0
- chrys/orchestration/engine/run/__init__.py +3 -0
- chrys/orchestration/engine/run/active_injection.py +419 -0
- chrys/orchestration/engine/run/attachments.py +519 -0
- chrys/orchestration/engine/run/bindings.py +1095 -0
- chrys/orchestration/engine/run/coordinator.py +1062 -0
- chrys/orchestration/engine/run/finalizer.py +417 -0
- chrys/orchestration/engine/run/input_refs.py +60 -0
- chrys/orchestration/engine/run/prompt_content.py +457 -0
- chrys/orchestration/engine/run/resume.py +299 -0
- chrys/orchestration/engine/run/retry.py +948 -0
- chrys/orchestration/engine/run/runner.py +625 -0
- chrys/orchestration/engine/run/runtime_skills.py +146 -0
- chrys/orchestration/engine/run/sub_agent_coordination.py +126 -0
- chrys/orchestration/engine/run/turn_hooks.py +258 -0
- chrys/orchestration/engine/run/turn_state.py +149 -0
- chrys/orchestration/engine/run/working_dir.py +50 -0
- chrys/orchestration/engine/session_lifecycle.py +2013 -0
- chrys/orchestration/engine/state/__init__.py +3 -0
- chrys/orchestration/engine/state/active_session.py +183 -0
- chrys/orchestration/engine/state/controls.py +639 -0
- chrys/orchestration/engine/state/current_agent.py +26 -0
- chrys/orchestration/engine/state/lifecycle_permits.py +395 -0
- chrys/orchestration/engine/state/machine.py +193 -0
- chrys/orchestration/engine/state/session_writer.py +434 -0
- chrys/orchestration/engine/trajectory.py +714 -0
- chrys/orchestration/engine/usage.py +118 -0
- chrys/orchestration/invoker/__init__.py +3 -0
- chrys/orchestration/invoker/acp.py +727 -0
- chrys/orchestration/invoker/acp_protocol.py +1115 -0
- chrys/orchestration/invoker/acp_spec.py +79 -0
- chrys/orchestration/invoker/attempts.py +994 -0
- chrys/orchestration/invoker/child_compaction.py +131 -0
- chrys/orchestration/invoker/child_history.py +99 -0
- chrys/orchestration/invoker/contracts.py +253 -0
- chrys/orchestration/invoker/evidence.py +94 -0
- chrys/orchestration/invoker/kernel.py +343 -0
- chrys/orchestration/invoker/origin.py +79 -0
- chrys/orchestration/invoker/resources.py +343 -0
- chrys/orchestration/invoker/runtime.py +299 -0
- chrys/orchestration/invoker/usage.py +80 -0
- chrys/orchestration/session_hooks.py +117 -0
- chrys/orchestration/session_host.py +1023 -0
- chrys/orchestration/session_resources.py +95 -0
- chrys/orchestration/session_usage.py +219 -0
- chrys/orchestration/startup.py +255 -0
- chrys/orchestration/sub_agents/__init__.py +3 -0
- chrys/orchestration/sub_agents/acp_policy.py +269 -0
- chrys/orchestration/sub_agents/kernel_policy.py +848 -0
- chrys/orchestration/sub_agents/registration.py +44 -0
- chrys/orchestration/sub_agents/shell.py +337 -0
- chrys/orchestration/sub_agents/tools.py +2228 -0
- chrys/orchestration/workflows/__init__.py +3 -0
- chrys/orchestration/workflows/agent_archive.py +156 -0
- chrys/orchestration/workflows/agent_node.py +894 -0
- chrys/orchestration/workflows/agent_node_build.py +687 -0
- chrys/orchestration/workflows/catalog.py +332 -0
- chrys/orchestration/workflows/coordinator.py +771 -0
- chrys/orchestration/workflows/hooks.py +176 -0
- chrys/orchestration/workflows/preview.py +322 -0
- chrys/orchestration/workflows/rollback.py +151 -0
- chrys/orchestration/workflows/runner.py +986 -0
- chrys/orchestration/workflows/session.py +390 -0
- chrys/orchestration/workflows/settings.py +97 -0
- chrys/orchestration/workflows/validation.py +674 -0
- chrys/orchestration/workflows/worker_client.py +1002 -0
- chrys/service/__init__.py +3 -0
- chrys/service/acp_client/__init__.py +65 -0
- chrys/service/acp_client/_transport.py +922 -0
- chrys/service/acp_client/client.py +1311 -0
- chrys/service/acp_client/errors.py +224 -0
- chrys/service/acp_client/protocol.py +220 -0
- chrys/service/acp_client/spawn.py +239 -0
- chrys/service/acp_client/spec.py +100 -0
- chrys/service/agent_middleware/__init__.py +60 -0
- chrys/service/agent_middleware/_metadata_keys.py +52 -0
- chrys/service/agent_middleware/control/__init__.py +3 -0
- chrys/service/agent_middleware/control/approval.py +769 -0
- chrys/service/agent_middleware/control/approval_hooks.py +62 -0
- chrys/service/agent_middleware/control/ask_user.py +207 -0
- chrys/service/agent_middleware/control/interrupt.py +50 -0
- chrys/service/agent_middleware/control/sleep.py +118 -0
- chrys/service/agent_middleware/control/todo.py +117 -0
- chrys/service/agent_middleware/events/__init__.py +3 -0
- chrys/service/agent_middleware/events/hook_dispatch.py +250 -0
- chrys/service/agent_middleware/events/hosted_tools.py +1762 -0
- chrys/service/agent_middleware/events/intermediate_text.py +128 -0
- chrys/service/agent_middleware/events/rejection_metadata.py +50 -0
- chrys/service/agent_middleware/events/result_persistence.py +98 -0
- chrys/service/agent_middleware/events/sub_agent_events.py +951 -0
- chrys/service/agent_middleware/events/tool_error_text.py +22 -0
- chrys/service/agent_middleware/events/tool_events.py +779 -0
- chrys/service/agent_middleware/injection.py +510 -0
- chrys/service/agent_middleware/reminders/__init__.py +91 -0
- chrys/service/agent_middleware/reminders/archive_pointer.py +154 -0
- chrys/service/agent_middleware/reminders/base.py +25 -0
- chrys/service/agent_middleware/reminders/context_usage.py +74 -0
- chrys/service/agent_middleware/reminders/file_change.py +63 -0
- chrys/service/agent_middleware/reminders/mcp.py +39 -0
- chrys/service/agent_middleware/reminders/profile_switch.py +106 -0
- chrys/service/agent_middleware/reminders/runtime_env.py +190 -0
- chrys/service/agent_middleware/reminders/skills.py +44 -0
- chrys/service/agent_middleware/reminders/sub_agents.py +35 -0
- chrys/service/agent_middleware/reminders/todo.py +35 -0
- chrys/service/agent_middleware/reminders/turn_line.py +48 -0
- chrys/service/agent_middleware/response_validation.py +1380 -0
- chrys/service/agent_middleware/system_reminder.py +1107 -0
- chrys/service/agent_middleware/validators.py +396 -0
- chrys/service/analytics/__init__.py +124 -0
- chrys/service/analytics/_actions.py +896 -0
- chrys/service/analytics/_context_evidence.py +357 -0
- chrys/service/analytics/_critical_path.py +179 -0
- chrys/service/analytics/_facts.py +919 -0
- chrys/service/analytics/_insights.py +769 -0
- chrys/service/analytics/_metric_ops.py +83 -0
- chrys/service/analytics/_session_projection.py +460 -0
- chrys/service/analytics/_timeline.py +434 -0
- chrys/service/analytics/_turn_graph.py +1237 -0
- chrys/service/analytics/_turns.py +1377 -0
- chrys/service/analytics/_workflows.py +171 -0
- chrys/service/analytics/aggregation.py +1474 -0
- chrys/service/analytics/classification.py +128 -0
- chrys/service/analytics/export.py +803 -0
- chrys/service/analytics/findings.py +282 -0
- chrys/service/analytics/math.py +79 -0
- chrys/service/analytics/model.py +886 -0
- chrys/service/analytics/reader.py +321 -0
- chrys/service/approval/__init__.py +3 -0
- chrys/service/approval/arbitration.py +102 -0
- chrys/service/approval/command_identity.py +109 -0
- chrys/service/approval/correlation.py +57 -0
- chrys/service/approval/grant_store.py +108 -0
- chrys/service/approval/judge.py +589 -0
- chrys/service/approval/level.py +15 -0
- chrys/service/approval/policy.py +209 -0
- chrys/service/approval/reuse.py +223 -0
- chrys/service/approval/reuse_binding.py +133 -0
- chrys/service/approval/safety_classifier.py +536 -0
- chrys/service/approval/turn_context.py +49 -0
- chrys/service/context/__init__.py +3 -0
- chrys/service/context/compaction/__init__.py +38 -0
- chrys/service/context/compaction/breaker.py +156 -0
- chrys/service/context/compaction/budgets.py +93 -0
- chrys/service/context/compaction/calibration.py +77 -0
- chrys/service/context/compaction/compression.py +690 -0
- chrys/service/context/compaction/current_turn_drop.py +458 -0
- chrys/service/context/compaction/events.py +201 -0
- chrys/service/context/compaction/exclusions.py +261 -0
- chrys/service/context/compaction/groups.py +361 -0
- chrys/service/context/compaction/last_words.py +1996 -0
- chrys/service/context/compaction/last_words_state.py +752 -0
- chrys/service/context/compaction/scoped.py +472 -0
- chrys/service/context/compaction/spill.py +1947 -0
- chrys/service/context/compaction/strategy.py +1116 -0
- chrys/service/context/compaction/summaries.py +319 -0
- chrys/service/context/compaction/turns.py +375 -0
- chrys/service/context/manager.py +174 -0
- chrys/service/context/memory_loader.py +792 -0
- chrys/service/context/middleware/__init__.py +3 -0
- chrys/service/context/middleware/usage.py +332 -0
- chrys/service/context/providers/__init__.py +3 -0
- chrys/service/context/providers/context_management.py +432 -0
- chrys/service/context/providers/history.py +647 -0
- chrys/service/context/providers/memory.py +50 -0
- chrys/service/context/tool_content.py +22 -0
- chrys/service/hooks/__init__.py +37 -0
- chrys/service/hooks/detached_worker.py +118 -0
- chrys/service/hooks/docs/README.md +14 -0
- chrys/service/hooks/docs/authoring.md +684 -0
- chrys/service/hooks/docs/configuration.md +499 -0
- chrys/service/hooks/docs/design.md +359 -0
- chrys/service/hooks/docs/project-hooks.md +348 -0
- chrys/service/hooks/events.py +157 -0
- chrys/service/hooks/loader.py +665 -0
- chrys/service/hooks/manager.py +807 -0
- chrys/service/hooks/matcher.py +111 -0
- chrys/service/hooks/outbox.py +294 -0
- chrys/service/hooks/runner.py +572 -0
- chrys/service/hooks/schema.py +407 -0
- chrys/service/llm/__init__.py +7 -0
- chrys/service/llm/_structured_outputs.py +192 -0
- chrys/service/llm/anthropic_messages/__init__.py +9 -0
- chrys/service/llm/anthropic_messages/client.py +254 -0
- chrys/service/llm/anthropic_messages/decode.py +331 -0
- chrys/service/llm/anthropic_messages/history.py +279 -0
- chrys/service/llm/anthropic_messages/request.py +323 -0
- chrys/service/llm/anthropic_messages/server_tools.py +319 -0
- chrys/service/llm/anthropic_messages/stream.py +297 -0
- chrys/service/llm/anthropic_messages/thinking_binding.py +156 -0
- chrys/service/llm/chat_completions/__init__.py +9 -0
- chrys/service/llm/chat_completions/client.py +369 -0
- chrys/service/llm/chat_completions/decode.py +338 -0
- chrys/service/llm/chat_completions/history.py +657 -0
- chrys/service/llm/chat_completions/reasoning.py +244 -0
- chrys/service/llm/chat_completions/request.py +192 -0
- chrys/service/llm/chat_completions/stream.py +546 -0
- chrys/service/llm/chat_completions/validation.py +308 -0
- chrys/service/llm/clients.py +613 -0
- chrys/service/llm/images.py +65 -0
- chrys/service/llm/mock.py +413 -0
- chrys/service/llm/observer.py +446 -0
- chrys/service/llm/one_shot.py +106 -0
- chrys/service/llm/openai_exceptions.py +93 -0
- chrys/service/llm/openai_responses/__init__.py +10 -0
- chrys/service/llm/openai_responses/client.py +323 -0
- chrys/service/llm/openai_responses/decode.py +432 -0
- chrys/service/llm/openai_responses/history.py +519 -0
- chrys/service/llm/openai_responses/hosted.py +625 -0
- chrys/service/llm/openai_responses/replay.py +629 -0
- chrys/service/llm/openai_responses/request.py +342 -0
- chrys/service/llm/openai_responses/stream.py +847 -0
- chrys/service/llm/openai_timestamps.py +57 -0
- chrys/service/llm/providers.py +107 -0
- chrys/service/llm/proxy_route.py +54 -0
- chrys/service/llm/raw_http_log.py +309 -0
- chrys/service/llm/route_facts.py +63 -0
- chrys/service/llm/route_sessions.py +49 -0
- chrys/service/llm/sdk_anthropic.py +18 -0
- chrys/service/llm/sdk_headers.py +47 -0
- chrys/service/llm/sdk_openai.py +18 -0
- chrys/service/llm/sdk_retry.py +59 -0
- chrys/service/llm/wire_client.py +246 -0
- chrys/service/mcp/__init__.py +3 -0
- chrys/service/mcp/_connection.py +235 -0
- chrys/service/mcp/_http_transport.py +350 -0
- chrys/service/mcp/_progressive.py +459 -0
- chrys/service/mcp/_stdio_transport.py +614 -0
- chrys/service/mcp/_tool_mixins.py +96 -0
- chrys/service/mcp/adapter.py +1031 -0
- chrys/service/mcp/cache.py +476 -0
- chrys/service/mcp/content.py +32 -0
- chrys/service/mcp/errors.py +169 -0
- chrys/service/mcp/owned.py +1330 -0
- chrys/service/mcp/result_limits.py +168 -0
- chrys/service/mcp/thinking_warning.py +95 -0
- chrys/service/mcp/validation.py +74 -0
- chrys/service/mutations/__init__.py +84 -0
- chrys/service/mutations/coordination.py +894 -0
- chrys/service/mutations/detector.py +458 -0
- chrys/service/mutations/git_calibrator.py +397 -0
- chrys/service/mutations/git_state.py +647 -0
- chrys/service/mutations/pipeline.py +779 -0
- chrys/service/mutations/scanner.py +466 -0
- chrys/service/mutations/snapshot_files.py +233 -0
- chrys/service/mutations/store.py +625 -0
- chrys/service/mutations/tool_names.py +15 -0
- chrys/service/mutations/trace.py +246 -0
- chrys/service/mutations/tracker.py +1289 -0
- chrys/service/mutations/types.py +559 -0
- chrys/service/mutations/workspace_changes.py +1583 -0
- chrys/service/profiles/__init__.py +2 -0
- chrys/service/profiles/agents/__init__.py +3 -0
- chrys/service/profiles/agents/builtins/Code.yaml +199 -0
- chrys/service/profiles/agents/builtins/Explore.yaml +62 -0
- chrys/service/profiles/agents/builtins/General.yaml +114 -0
- chrys/service/profiles/agents/builtins/QA.yaml +128 -0
- chrys/service/profiles/agents/loader.py +808 -0
- chrys/service/profiles/agents/registry.py +727 -0
- chrys/service/profiles/agents/schema.py +371 -0
- chrys/service/profiles/agents/serializer.py +196 -0
- chrys/service/profiles/coercion.py +22 -0
- chrys/service/profiles/models/__init__.py +3 -0
- chrys/service/profiles/models/env_bridge.py +157 -0
- chrys/service/profiles/models/loader.py +282 -0
- chrys/service/profiles/models/options.py +438 -0
- chrys/service/profiles/models/registry.py +104 -0
- chrys/service/profiles/models/resolver.py +286 -0
- chrys/service/profiles/models/schema.py +91 -0
- chrys/service/profiles/models/serializer.py +76 -0
- chrys/service/session/__init__.py +3 -0
- chrys/service/session/checkpoint.py +156 -0
- chrys/service/session/history.py +1616 -0
- chrys/service/session/message_metadata.py +151 -0
- chrys/service/session/persistence.py +435 -0
- chrys/service/session/runtime_metadata.py +210 -0
- chrys/service/session/sub_agent_logs.py +1069 -0
- chrys/service/session/sub_agent_transcript.py +654 -0
- chrys/service/skills/__init__.py +3 -0
- chrys/service/skills/adapter.py +408 -0
- chrys/service/skills/constants.py +8 -0
- chrys/service/skills/loader.py +575 -0
- chrys/service/skills/model.py +130 -0
- chrys/service/skills/provider.py +841 -0
- chrys/service/skills/runner.py +390 -0
- chrys/service/state/__init__.py +3 -0
- chrys/service/state/_fork.py +710 -0
- chrys/service/state/_session_files.py +175 -0
- chrys/service/state/_session_meta.py +552 -0
- chrys/service/state/locks.py +154 -0
- chrys/service/state/serializers.py +156 -0
- chrys/service/state/session_catalog.py +361 -0
- chrys/service/state/session_listing.py +84 -0
- chrys/service/state/session_migration.py +446 -0
- chrys/service/state/session_mru.py +315 -0
- chrys/service/state/store.py +2132 -0
- chrys/service/state/workflow.py +158 -0
- chrys/service/text_blocks.py +47 -0
- chrys/service/todos/__init__.py +3 -0
- chrys/service/todos/reminder.py +31 -0
- chrys/service/todos/tracker.py +58 -0
- chrys/service/tools/__init__.py +3 -0
- chrys/service/tools/approval_targets.py +92 -0
- chrys/service/tools/builtins/__init__.py +3 -0
- chrys/service/tools/builtins/ask_user.py +326 -0
- chrys/service/tools/builtins/doc_converter/__init__.py +494 -0
- chrys/service/tools/builtins/doc_converter/artifacts.py +392 -0
- chrys/service/tools/builtins/doc_converter/parsers/__init__.py +3 -0
- chrys/service/tools/builtins/doc_converter/parsers/_table.py +34 -0
- chrys/service/tools/builtins/doc_converter/parsers/base.py +101 -0
- chrys/service/tools/builtins/doc_converter/parsers/docx.py +88 -0
- chrys/service/tools/builtins/doc_converter/parsers/pdf.py +144 -0
- chrys/service/tools/builtins/doc_converter/parsers/pptx.py +103 -0
- chrys/service/tools/builtins/doc_converter/parsers/xls.py +32 -0
- chrys/service/tools/builtins/doc_converter/parsers/xlsx.py +51 -0
- chrys/service/tools/builtins/doc_converter/registry.py +55 -0
- chrys/service/tools/builtins/filesystem.py +1071 -0
- chrys/service/tools/builtins/search.py +1265 -0
- chrys/service/tools/builtins/shell.py +967 -0
- chrys/service/tools/builtins/shell_command_filtering/__init__.py +5 -0
- chrys/service/tools/builtins/shell_command_filtering/policy.py +60 -0
- chrys/service/tools/builtins/shell_command_filtering/presets.py +152 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/__init__.py +358 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/commands.py +416 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/git.py +408 -0
- chrys/service/tools/builtins/shell_command_filtering/readonly/syntax.py +87 -0
- chrys/service/tools/builtins/shell_command_filtering/tokenizer.py +214 -0
- chrys/service/tools/builtins/shell_filter.py +24 -0
- chrys/service/tools/builtins/sleep.py +91 -0
- chrys/service/tools/builtins/todo.py +64 -0
- chrys/service/tools/builtins/web/__init__.py +6 -0
- chrys/service/tools/builtins/web/build.py +268 -0
- chrys/service/tools/builtins/web/config.py +338 -0
- chrys/service/tools/builtins/web/credentials.py +37 -0
- chrys/service/tools/builtins/web/fetch/__init__.py +3 -0
- chrys/service/tools/builtins/web/fetch/cache.py +68 -0
- chrys/service/tools/builtins/web/fetch/convert.py +56 -0
- chrys/service/tools/builtins/web/fetch/tool.py +223 -0
- chrys/service/tools/builtins/web/hosted.py +61 -0
- chrys/service/tools/builtins/web/http.py +330 -0
- chrys/service/tools/builtins/web/progress.py +18 -0
- chrys/service/tools/builtins/web/search/__init__.py +3 -0
- chrys/service/tools/builtins/web/search/exa_mcp.py +146 -0
- chrys/service/tools/builtins/web/search/html.py +152 -0
- chrys/service/tools/builtins/web/search/json_api.py +227 -0
- chrys/service/tools/builtins/web/search/providers.py +77 -0
- chrys/service/tools/builtins/web/search/tool.py +218 -0
- chrys/service/tools/builtins/web/search/types.py +28 -0
- chrys/service/tools/builtins/web/tls_identity.py +66 -0
- chrys/service/tools/kinds.py +115 -0
- chrys/service/tools/names.py +47 -0
- chrys/service/tools/registry.py +242 -0
- chrys/service/tools/result_metadata.py +103 -0
- chrys/service/tools/session_artifacts.py +196 -0
- chrys/service/tools/spill.py +218 -0
- chrys/service/tools/workspace_paths.py +81 -0
- chrys/service/trajectory/__init__.py +11 -0
- chrys/service/trajectory/approvals.py +201 -0
- chrys/service/trajectory/compaction.py +420 -0
- chrys/service/trajectory/fork.py +75 -0
- chrys/service/trajectory/hooks.py +217 -0
- chrys/service/trajectory/items.py +41 -0
- chrys/service/trajectory/preparation.py +374 -0
- chrys/service/trajectory/retries.py +95 -0
- chrys/service/trajectory/revisions.py +129 -0
- chrys/service/trajectory/segmented.py +163 -0
- chrys/service/trajectory/session.py +762 -0
- chrys/service/trajectory/state.py +83 -0
- chrys/service/trajectory/sub_agents.py +169 -0
- chrys/service/trajectory/tombstone.py +262 -0
- chrys/service/trajectory/tools.py +323 -0
- chrys/service/trajectory/validation.py +130 -0
- chrys/service/trajectory/waits.py +161 -0
- chrys/service/trajectory/workflow.py +148 -0
- chrys/service/vision.py +128 -0
- chrys/service/workflows/__init__.py +3 -0
- chrys/service/workflows/admission.py +151 -0
- chrys/service/workflows/artifacts.py +109 -0
- chrys/service/workflows/asks.py +130 -0
- chrys/service/workflows/builtins/__init__.py +12 -0
- chrys/service/workflows/builtins/demo-workflow.manifest.json +635 -0
- chrys/service/workflows/builtins/demo-workflow.py +756 -0
- chrys/service/workflows/discovery.py +554 -0
- chrys/service/workflows/environment.py +284 -0
- chrys/service/workflows/graph.py +430 -0
- chrys/service/workflows/history.py +82 -0
- chrys/service/workflows/interpreter.py +107 -0
- chrys/service/workflows/journal.py +334 -0
- chrys/service/workflows/layout.py +56 -0
- chrys/service/workflows/ledger.py +149 -0
- chrys/service/workflows/model_selection.py +18 -0
- chrys/service/workflows/orphans.py +209 -0
- chrys/service/workflows/outcomes.py +46 -0
- chrys/service/workflows/protocol.py +210 -0
- chrys/service/workflows/records.py +142 -0
- chrys/service/workflows/scheduler.py +770 -0
- chrys/service/workflows/sdk/__init__.py +51 -0
- chrys/service/workflows/sdk/_ask.py +167 -0
- chrys/service/workflows/sdk/_builder.py +781 -0
- chrys/service/workflows/sdk/_values.py +92 -0
- chrys/service/workflows/sdk_artifact.py +74 -0
- chrys/service/workflows/store.py +494 -0
- chrys/service/workflows/transcript.py +102 -0
- chrys/service/workflows/values.py +73 -0
- chrys/service/workflows/worker_host.py +1820 -0
- chrys/workflows.py +38 -0
- icode_tui-0.29.1.dist-info/METADATA +104 -0
- icode_tui-0.29.1.dist-info/RECORD +1138 -0
- icode_tui-0.29.1.dist-info/WHEEL +4 -0
- icode_tui-0.29.1.dist-info/entry_points.txt +2 -0
- icode_tui-0.29.1.dist-info/licenses/LICENSE +202 -0
- icode_tui-0.29.1.dist-info/licenses/NOTICE +1955 -0
|
@@ -0,0 +1,2142 @@
|
|
|
1
|
+
# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""AgentsConfigScreen — unified agent configuration modal with sidebar and tabbed config."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import contextlib
|
|
8
|
+
import copy
|
|
9
|
+
import logging
|
|
10
|
+
import re
|
|
11
|
+
import uuid
|
|
12
|
+
from collections.abc import Callable, Collection
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from enum import StrEnum
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import TYPE_CHECKING, Any, ClassVar
|
|
17
|
+
|
|
18
|
+
from rich.text import Text
|
|
19
|
+
from textual import on
|
|
20
|
+
from textual.containers import Horizontal, Vertical
|
|
21
|
+
from textual.content import Content
|
|
22
|
+
from textual.css.query import NoMatches
|
|
23
|
+
from textual.widgets import Button, OptionList, Select, Static, TabbedContent, TabPane, TextArea
|
|
24
|
+
from textual.widgets.option_list import Option, OptionDoesNotExist
|
|
25
|
+
|
|
26
|
+
from chrys.app.tui.binding_display import CANCEL_BINDING, localized_binding
|
|
27
|
+
from chrys.app.tui.i18n import render_str, widget_locale_controller, widget_localizer
|
|
28
|
+
from chrys.app.tui.screens.agents.agent_draft_store import AgentDraftStore, mcp_draft_errors_by_server
|
|
29
|
+
from chrys.app.tui.screens.agents.panels.config_card import CardListChanged
|
|
30
|
+
from chrys.app.tui.screens.agents.validation_messages import FIX_VALIDATION_ERRORS
|
|
31
|
+
from chrys.app.tui.screens.dialogs.base import BaseDialog
|
|
32
|
+
from chrys.app.tui.widgets import Checkbox
|
|
33
|
+
from chrys.app.tui.widgets import EnhancedInput as Input
|
|
34
|
+
from chrys.foundation.i18n import DisplayBlock, MessageDef, MessageRef, msg
|
|
35
|
+
|
|
36
|
+
if TYPE_CHECKING:
|
|
37
|
+
from textual.app import ComposeResult
|
|
38
|
+
from textual.widget import Widget
|
|
39
|
+
|
|
40
|
+
from chrys.service.profiles.agents.registry import AgentProfileRegistry, AgentProfileRegistrySnapshot
|
|
41
|
+
from chrys.service.profiles.agents.schema import AgentProfile
|
|
42
|
+
from chrys.service.profiles.models.registry import ModelProfileRegistry
|
|
43
|
+
|
|
44
|
+
logger = logging.getLogger(__name__)
|
|
45
|
+
|
|
46
|
+
_READ_ONLY_NOTICE = msg(
|
|
47
|
+
"tui.agent_config.read_only_notice",
|
|
48
|
+
fallback="• Agent is running. This page is read-only.",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
_CONFIGURATION_TITLE = msg("tui.agent_config.title.configuration", fallback="Agent Configuration")
|
|
52
|
+
_TAB_BASIC = msg("tui.agent_config.tab.basic", fallback="Basic")
|
|
53
|
+
_TAB_INSTRUCTIONS = msg("tui.agent_config.tab.instructions", fallback="Instructions")
|
|
54
|
+
_TAB_TOOLS = msg("tui.agent_config.tab.tools", fallback="Tools")
|
|
55
|
+
_TAB_SUB_AGENTS = msg("tui.agent_config.tab.sub_agents", fallback="Sub-Agents")
|
|
56
|
+
_TAB_SKILLS = msg("tui.agent_config.tab.skills", fallback="Skills")
|
|
57
|
+
_TAB_MCP = msg("tui.agent_config.tab.mcp", fallback="MCP")
|
|
58
|
+
_TAB_MEMORY = msg("tui.agent_config.tab.memory", fallback="Memory")
|
|
59
|
+
_TAB_COMPACTION = msg("tui.agent_config.tab.compaction", fallback="Compaction")
|
|
60
|
+
_TAB_ACP_SETTINGS = msg("tui.agent_config.tab.acp_settings", fallback="ACP Settings")
|
|
61
|
+
_TAB_ACP_SHORT = msg("tui.agent_config.tab.acp_short", fallback="ACP")
|
|
62
|
+
_NEW = msg("tui.agent_config.button.new", fallback="New")
|
|
63
|
+
_CLONE = msg("tui.agent_config.button.clone", fallback="Clone")
|
|
64
|
+
_DELETE = msg("tui.agent_config.button.delete", fallback="Delete")
|
|
65
|
+
_RESET = msg("tui.agent_config.button.reset", fallback="Reset")
|
|
66
|
+
_SAVE = msg("tui.agent_config.button.save", fallback="Save")
|
|
67
|
+
_CLOSE = msg("tui.agent_config.button.close", fallback="Close")
|
|
68
|
+
_MODIFIED_LABEL = msg("tui.agent_config.profile.modified", fallback="{name} (modified)")
|
|
69
|
+
_RESET_AGENT_TITLE = msg("tui.agent_config.confirm.reset.title", fallback="Reset Agent")
|
|
70
|
+
_DELETE_AGENT_TITLE = msg("tui.agent_config.confirm.delete.title", fallback="Delete Agent")
|
|
71
|
+
_RESET_AGENT_MESSAGE = msg(
|
|
72
|
+
"tui.agent_config.confirm.reset.message",
|
|
73
|
+
fallback=(
|
|
74
|
+
'Reset built-in agent\n"{display}" to system defaults?\n\n'
|
|
75
|
+
"Skills, MCP, and Memory settings will be preserved. All other changes will be discarded."
|
|
76
|
+
),
|
|
77
|
+
multiline=True,
|
|
78
|
+
)
|
|
79
|
+
_DELETE_AGENT_MESSAGE = msg(
|
|
80
|
+
"tui.agent_config.confirm.delete.message",
|
|
81
|
+
fallback='Delete agent profile\n"{display}"?',
|
|
82
|
+
multiline=True,
|
|
83
|
+
)
|
|
84
|
+
_TAB_ERROR = msg("tui.agent_config.panel_error.detail", fallback="{tab}: {error}", multiline=True)
|
|
85
|
+
_PANELS_STILL_MOUNTING = msg(
|
|
86
|
+
"tui.agent_config.panel_error.mounting",
|
|
87
|
+
fallback="Agent config panels are still mounting:\n{details}",
|
|
88
|
+
multiline=True,
|
|
89
|
+
)
|
|
90
|
+
_PANELS_READ_ERROR = msg(
|
|
91
|
+
"tui.agent_config.panel_error.read",
|
|
92
|
+
fallback="Could not read tab configs:\n{details}",
|
|
93
|
+
multiline=True,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
_AGENT_TITLE = msg("tui.agent_config.title.agent", fallback="Agent")
|
|
97
|
+
_AGENT_SETTINGS_TITLE = msg("tui.agent_config.title.settings", fallback="Agent Settings")
|
|
98
|
+
_VALIDATION_ERROR_TITLE = msg("tui.agent_config.title.validation_error", fallback="Validation Error")
|
|
99
|
+
_SAVE_ERROR_TITLE = msg("tui.agent_config.title.save_error", fallback="Save Error")
|
|
100
|
+
_LAST_AGENT = msg(
|
|
101
|
+
"tui.agent_config.last_agent",
|
|
102
|
+
fallback="Cannot remove the last agent profile — at least one must remain.",
|
|
103
|
+
)
|
|
104
|
+
_LAST_MAIN_AGENT = msg(
|
|
105
|
+
"tui.agent_config.last_main_agent",
|
|
106
|
+
fallback="Cannot remove the last main agent profile — add another first.",
|
|
107
|
+
)
|
|
108
|
+
_AGENT_STILL_REFERENCED = msg(
|
|
109
|
+
"tui.agent_config.still_referenced",
|
|
110
|
+
fallback=(
|
|
111
|
+
"Cannot remove '{name}' — still used as a sub-agent by: {preview}{suffix}. Unlink it from those agents first."
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
_AGENT_MORE_SUFFIX = msg("tui.agent_config.more_suffix", fallback=" (+{extra_count} more)")
|
|
115
|
+
_AGENT_REMOVED = msg("tui.agent_config.removed", fallback="Agent removed")
|
|
116
|
+
_AGENT_DELETED = msg("tui.agent_config.deleted", fallback="Agent deleted")
|
|
117
|
+
_AGENT_RESET_PENDING = msg(
|
|
118
|
+
"tui.agent_config.reset_pending",
|
|
119
|
+
fallback="Agent reset prepared — save to apply",
|
|
120
|
+
)
|
|
121
|
+
_AGENT_RESET_COMPLETE = msg(
|
|
122
|
+
"tui.agent_config.reset_complete",
|
|
123
|
+
fallback="Agent reset to system defaults",
|
|
124
|
+
)
|
|
125
|
+
_AGENT_ALREADY_DEFAULT = msg(
|
|
126
|
+
"tui.agent_config.already_default",
|
|
127
|
+
fallback="Agent already uses the system defaults",
|
|
128
|
+
)
|
|
129
|
+
_AGENT_PROFILE_SAVED = msg("tui.agent_config.saved", fallback="Agent profile saved")
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class _DismissIntent(StrEnum):
|
|
133
|
+
"""Modal result to return when the user closes the config screen."""
|
|
134
|
+
|
|
135
|
+
NONE = ""
|
|
136
|
+
UPDATED = "updated"
|
|
137
|
+
SWITCHED = "switched"
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@dataclass
|
|
141
|
+
class _AgentDraft:
|
|
142
|
+
"""Modal-local staged state for one agent profile."""
|
|
143
|
+
|
|
144
|
+
key: str
|
|
145
|
+
original_name: str | None
|
|
146
|
+
profile: AgentProfile
|
|
147
|
+
original_profile: AgentProfile | None
|
|
148
|
+
is_builtin: bool
|
|
149
|
+
dirty: bool = False
|
|
150
|
+
reset_to_builtin: bool = False
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@dataclass
|
|
154
|
+
class _AgentDraftSnapshot:
|
|
155
|
+
"""Restorable staged draft state for failed save attempts."""
|
|
156
|
+
|
|
157
|
+
profile: AgentProfile
|
|
158
|
+
dirty: bool
|
|
159
|
+
reset_to_builtin: bool
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass
|
|
163
|
+
class _AgentSaveSnapshot:
|
|
164
|
+
"""State needed to roll back a multi-profile save."""
|
|
165
|
+
|
|
166
|
+
files: dict[Path, bytes | None]
|
|
167
|
+
registry: AgentProfileRegistrySnapshot
|
|
168
|
+
current_profile: str
|
|
169
|
+
active_profile_name: str
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class _AgentConfigPanelsNotReady(RuntimeError):
|
|
173
|
+
"""Raised when a panel exists but its child widgets have not mounted yet."""
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
class AgentsConfigScreen(BaseDialog[str]):
|
|
177
|
+
"""Unified agent configuration modal.
|
|
178
|
+
|
|
179
|
+
Left sidebar lists all agent profiles; selecting one loads its
|
|
180
|
+
configuration in the tabbed right panel.
|
|
181
|
+
|
|
182
|
+
Tabs: Basic, Instructions, Tools, Sub-Agents, Skills, MCP, Compaction.
|
|
183
|
+
|
|
184
|
+
Dismiss result:
|
|
185
|
+
- ``""`` — user cancelled, nothing to reload.
|
|
186
|
+
- ``"updated"`` — user saved; caller should reload the registry.
|
|
187
|
+
- ``"switched"`` — active agent profile changed (e.g. deleted), caller
|
|
188
|
+
should reload and switch the engine to the new active profile.
|
|
189
|
+
"""
|
|
190
|
+
|
|
191
|
+
CSS_PATH = "config.tcss"
|
|
192
|
+
_HYDRATION_RETRY_SECONDS: ClassVar[float] = 0.02
|
|
193
|
+
_HYDRATION_MAX_ATTEMPTS: ClassVar[int] = 25
|
|
194
|
+
_TAB_IDS: ClassVar[tuple[str, ...]] = (
|
|
195
|
+
"basic",
|
|
196
|
+
"instructions",
|
|
197
|
+
"tools",
|
|
198
|
+
"sub-agents",
|
|
199
|
+
"skills",
|
|
200
|
+
"mcp",
|
|
201
|
+
"memory",
|
|
202
|
+
"compaction",
|
|
203
|
+
"acp",
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
BINDINGS: ClassVar[list] = [
|
|
207
|
+
localized_binding("escape", "cancel", CANCEL_BINDING, show=False, priority=True),
|
|
208
|
+
]
|
|
209
|
+
|
|
210
|
+
def __init__(
|
|
211
|
+
self,
|
|
212
|
+
registry: AgentProfileRegistry,
|
|
213
|
+
current_profile: str,
|
|
214
|
+
initial_profile: str = "",
|
|
215
|
+
initial_tab: str = "basic",
|
|
216
|
+
*,
|
|
217
|
+
active_profile_name: str = "",
|
|
218
|
+
model_registry: ModelProfileRegistry | None = None,
|
|
219
|
+
active_model_profile_id: str = "",
|
|
220
|
+
workspace_cwd: str | None = None,
|
|
221
|
+
workspace_roots: list[str] | None = None,
|
|
222
|
+
on_saved: Callable[[str | None, str | None], None] | None = None,
|
|
223
|
+
read_only: bool = False,
|
|
224
|
+
) -> None:
|
|
225
|
+
self._registry = registry
|
|
226
|
+
self._model_registry = model_registry
|
|
227
|
+
self._active_model_profile_id = active_model_profile_id
|
|
228
|
+
self._workspace_cwd = workspace_cwd
|
|
229
|
+
self._workspace_roots = [root for root in workspace_roots or [] if root]
|
|
230
|
+
if not self._workspace_roots and workspace_cwd:
|
|
231
|
+
self._workspace_roots = [workspace_cwd]
|
|
232
|
+
self._read_only = read_only
|
|
233
|
+
self._current_profile = current_profile
|
|
234
|
+
self._initial_profile = initial_profile or current_profile
|
|
235
|
+
self._initial_tab = initial_tab
|
|
236
|
+
self._selected_profile_name: str = ""
|
|
237
|
+
self._selected_draft_key: str = ""
|
|
238
|
+
self._drafts: dict[str, _AgentDraft] = {}
|
|
239
|
+
self._mounted_tabs: set[str] = set()
|
|
240
|
+
# Per draft key: its MCP server cards' folds when it was last shown. The panel is
|
|
241
|
+
# rebuilt from the draft, which carries no UI state, each time the agent loads.
|
|
242
|
+
self._mcp_card_folds: dict[str, list[bool]] = {}
|
|
243
|
+
self._hydrating: bool = False
|
|
244
|
+
self._hydration_preserve_dirty: bool = False
|
|
245
|
+
self._hydrating_generation: int = 0
|
|
246
|
+
self._pending_agent_type_change: tuple[str, str] | None = None
|
|
247
|
+
self._dismiss_intent = _DismissIntent.NONE
|
|
248
|
+
# Resolve the session-active profile's *registry name* (stable
|
|
249
|
+
# identifier — ``display_name`` is freely editable and ``name``
|
|
250
|
+
# is editable for user profiles). Prefer the caller-supplied
|
|
251
|
+
# ``active_profile_name`` when provided (unambiguous); otherwise
|
|
252
|
+
# fall back to matching ``current_profile`` against ``.name``
|
|
253
|
+
# first and ``display_name`` second so a profile whose display
|
|
254
|
+
# label collides with another profile's registry name still
|
|
255
|
+
# resolves correctly.
|
|
256
|
+
self._original_active_profile_name: str = ""
|
|
257
|
+
if active_profile_name and registry.get(active_profile_name) is not None:
|
|
258
|
+
self._original_active_profile_name = active_profile_name
|
|
259
|
+
else:
|
|
260
|
+
profiles = registry.list_profiles(include_sub_agent_only=True)
|
|
261
|
+
for p in profiles:
|
|
262
|
+
if p.name == current_profile:
|
|
263
|
+
self._original_active_profile_name = p.name
|
|
264
|
+
break
|
|
265
|
+
else:
|
|
266
|
+
for p in profiles:
|
|
267
|
+
if (p.display_name or p.name) == current_profile:
|
|
268
|
+
self._original_active_profile_name = p.name
|
|
269
|
+
break
|
|
270
|
+
self._active_profile_name = self._original_active_profile_name
|
|
271
|
+
# Sync callback fired after each successful Save (no dismiss).
|
|
272
|
+
# Args: (new_display_label, new_registry_name).
|
|
273
|
+
# ``new_display_label`` is non-None when the active profile's
|
|
274
|
+
# display label changed (caller refreshes main-screen widgets).
|
|
275
|
+
# ``new_registry_name`` is non-None when the active profile's
|
|
276
|
+
# registry name changed (caller queues an engine switch for
|
|
277
|
+
# modal Close).
|
|
278
|
+
self._on_saved = on_saved
|
|
279
|
+
super().__init__()
|
|
280
|
+
|
|
281
|
+
# ── Compose ──────────────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
def compose(self) -> ComposeResult:
|
|
284
|
+
localizer = widget_localizer(self)
|
|
285
|
+
with Vertical(id="ac-container") as container:
|
|
286
|
+
container.border_title = Text(render_str(localizer, _CONFIGURATION_TITLE.bind()))
|
|
287
|
+
|
|
288
|
+
with Horizontal(id="ac-main"):
|
|
289
|
+
# Left sidebar
|
|
290
|
+
with Vertical(id="ac-sidebar"):
|
|
291
|
+
yield OptionList(id="ac-list")
|
|
292
|
+
|
|
293
|
+
# Right panel
|
|
294
|
+
with Vertical(id="ac-right"), TabbedContent(initial=self._initial_tab, id="ac-tabs"):
|
|
295
|
+
with TabPane(render_str(localizer, _TAB_BASIC.bind()), id="basic"):
|
|
296
|
+
pass
|
|
297
|
+
with TabPane(render_str(localizer, _TAB_INSTRUCTIONS.bind()), id="instructions"):
|
|
298
|
+
pass
|
|
299
|
+
with TabPane(render_str(localizer, _TAB_TOOLS.bind()), id="tools"):
|
|
300
|
+
pass
|
|
301
|
+
with TabPane(render_str(localizer, _TAB_SUB_AGENTS.bind()), id="sub-agents"):
|
|
302
|
+
pass
|
|
303
|
+
with TabPane(render_str(localizer, _TAB_SKILLS.bind()), id="skills"):
|
|
304
|
+
pass
|
|
305
|
+
with TabPane(render_str(localizer, _TAB_MCP.bind()), id="mcp"):
|
|
306
|
+
pass
|
|
307
|
+
with TabPane(render_str(localizer, _TAB_MEMORY.bind()), id="memory"):
|
|
308
|
+
pass
|
|
309
|
+
with TabPane(render_str(localizer, _TAB_COMPACTION.bind()), id="compaction"):
|
|
310
|
+
pass
|
|
311
|
+
with TabPane(render_str(localizer, _TAB_ACP_SETTINGS.bind()), id="acp"):
|
|
312
|
+
pass
|
|
313
|
+
|
|
314
|
+
# Footer buttons
|
|
315
|
+
with Vertical(id="ac-footer") as footer:
|
|
316
|
+
if self._read_only:
|
|
317
|
+
footer.add_class("-read-only")
|
|
318
|
+
with Horizontal(id="ac-buttons"):
|
|
319
|
+
yield Button(render_str(localizer, _NEW.bind()), variant="primary", id="ac-new", flat=True)
|
|
320
|
+
yield Button(render_str(localizer, _CLONE.bind()), variant="primary", id="ac-clone", flat=True)
|
|
321
|
+
yield Button(render_str(localizer, _RESET.bind()), variant="warning", id="ac-reset", flat=True)
|
|
322
|
+
yield Button(render_str(localizer, _DELETE.bind()), variant="error", id="ac-delete", flat=True)
|
|
323
|
+
yield Static(id="ac-buttons-spacer")
|
|
324
|
+
save_button = Button(
|
|
325
|
+
render_str(localizer, _SAVE.bind()), variant="primary", id="ac-save", flat=True
|
|
326
|
+
)
|
|
327
|
+
save_button.disabled = True
|
|
328
|
+
yield save_button
|
|
329
|
+
yield Button(render_str(localizer, _CLOSE.bind()), variant="warning", id="ac-cancel", flat=True)
|
|
330
|
+
notice = Static(Text(render_str(localizer, _READ_ONLY_NOTICE.bind())), id="ac-read-only-notice")
|
|
331
|
+
notice.display = self._read_only
|
|
332
|
+
yield notice
|
|
333
|
+
|
|
334
|
+
# ── Mount ────────────────────────────────────────────────────────
|
|
335
|
+
|
|
336
|
+
def on_mount(self) -> None:
|
|
337
|
+
self._initialize_drafts()
|
|
338
|
+
self._populate_sidebar()
|
|
339
|
+
self._select_initial_profile()
|
|
340
|
+
self._update_save_button_state()
|
|
341
|
+
self._apply_read_only_state()
|
|
342
|
+
|
|
343
|
+
def on_unmount(self) -> None:
|
|
344
|
+
self._hydrating_generation += 1
|
|
345
|
+
self._hydrating = False
|
|
346
|
+
self._hydration_preserve_dirty = False
|
|
347
|
+
self._pending_agent_type_change = None
|
|
348
|
+
|
|
349
|
+
@staticmethod
|
|
350
|
+
def _existing_draft_key(profile_name: str) -> str:
|
|
351
|
+
return f"profile:{profile_name}"
|
|
352
|
+
|
|
353
|
+
@staticmethod
|
|
354
|
+
def _new_draft_key() -> str:
|
|
355
|
+
return f"draft:{uuid.uuid4().hex}"
|
|
356
|
+
|
|
357
|
+
def _initialize_drafts(self) -> None:
|
|
358
|
+
"""Seed modal-local drafts from the current registry state."""
|
|
359
|
+
self._drafts.clear()
|
|
360
|
+
self._mcp_card_folds.clear()
|
|
361
|
+
profiles = self._registry.list_profiles(include_sub_agent_only=True)
|
|
362
|
+
for profile in profiles:
|
|
363
|
+
key = self._existing_draft_key(profile.name)
|
|
364
|
+
original = copy.deepcopy(profile)
|
|
365
|
+
self._canonicalize_profile_for_ui(original)
|
|
366
|
+
draft_profile = copy.deepcopy(profile)
|
|
367
|
+
self._canonicalize_profile_for_ui(draft_profile)
|
|
368
|
+
self._drafts[key] = _AgentDraft(
|
|
369
|
+
key=key,
|
|
370
|
+
original_name=profile.name,
|
|
371
|
+
profile=draft_profile,
|
|
372
|
+
original_profile=original,
|
|
373
|
+
is_builtin=self._registry.is_builtin(profile.name),
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
@staticmethod
|
|
377
|
+
def _canonicalize_profile_for_ui(profile) -> None:
|
|
378
|
+
"""Match harmless normalizations done by tab panels (ordering)."""
|
|
379
|
+
from chrys.app.tui.screens.agents.panels.tools import _ALL_CATEGORIES
|
|
380
|
+
|
|
381
|
+
enabled = set(profile.tools.builtins)
|
|
382
|
+
known = {cat for cat, _label, _desc, _enabled in _ALL_CATEGORIES}
|
|
383
|
+
ordered = [cat for cat, _label, _desc, _enabled in _ALL_CATEGORIES if cat in enabled]
|
|
384
|
+
ordered.extend(cat for cat in profile.tools.builtins if cat not in known)
|
|
385
|
+
profile.tools.builtins = ordered
|
|
386
|
+
profile.skills.script_extensions = sorted(profile.skills.script_extensions)
|
|
387
|
+
profile.name = profile.name.strip()
|
|
388
|
+
profile.display_name = profile.display_name.strip()
|
|
389
|
+
profile.description = profile.description.strip()
|
|
390
|
+
for ref in profile.sub_agents.agents:
|
|
391
|
+
ref.profile = ref.profile.strip()
|
|
392
|
+
ref.tool_name = ref.tool_name.strip()
|
|
393
|
+
ref.tool_description = ref.tool_description.strip()
|
|
394
|
+
profile.memory.files = [path.strip() for path in profile.memory.files if path.strip()]
|
|
395
|
+
profile.memory.folders = [path.strip() for path in profile.memory.folders if path.strip()]
|
|
396
|
+
profile.skills.paths = [path.strip() for path in profile.skills.paths if path.strip()]
|
|
397
|
+
for server in profile.tools.mcp:
|
|
398
|
+
server.name = server.name.strip()
|
|
399
|
+
server.description = server.description.strip()
|
|
400
|
+
server.tool_name_prefix = server.tool_name_prefix.strip()
|
|
401
|
+
server.allowed_tools = (
|
|
402
|
+
[tool.strip() for tool in server.allowed_tools if tool.strip()]
|
|
403
|
+
if server.allowed_tools is not None
|
|
404
|
+
else None
|
|
405
|
+
)
|
|
406
|
+
server.always_load = [tool.strip() for tool in server.always_load if tool.strip()]
|
|
407
|
+
if not server.use_progressive_disclosure or server.allowed_tools == []:
|
|
408
|
+
server.use_progressive_disclosure = False
|
|
409
|
+
server.always_load = []
|
|
410
|
+
# Mirror AcpConfigPanel.get_config exactly (command/cwd/mode/model
|
|
411
|
+
# stripped, mapping keys stripped with empty keys dropped, argument
|
|
412
|
+
# values untouched) so hydration settles and dirty stays honest.
|
|
413
|
+
if profile.acp is not None:
|
|
414
|
+
acp = profile.acp
|
|
415
|
+
acp.command = acp.command.strip()
|
|
416
|
+
acp.cwd = acp.cwd.strip()
|
|
417
|
+
acp.session_mode = acp.session_mode.strip()
|
|
418
|
+
acp.model_id = acp.model_id.strip()
|
|
419
|
+
acp.env = {key.strip(): value for key, value in acp.env.items() if key.strip()}
|
|
420
|
+
acp.config_options = {key.strip(): value for key, value in acp.config_options.items() if key.strip()}
|
|
421
|
+
|
|
422
|
+
def _visible_drafts(self) -> list[_AgentDraft]:
|
|
423
|
+
return list(self._drafts.values())
|
|
424
|
+
|
|
425
|
+
def _find_visible_draft_key(self, profile_name: str) -> str | None:
|
|
426
|
+
for draft in self._visible_drafts():
|
|
427
|
+
if draft.profile.name == profile_name:
|
|
428
|
+
return draft.key
|
|
429
|
+
return None
|
|
430
|
+
|
|
431
|
+
def _populate_sidebar(self) -> None:
|
|
432
|
+
ol = self.query_one("#ac-list", OptionList)
|
|
433
|
+
ol.clear_options()
|
|
434
|
+
drafts = self._visible_drafts()
|
|
435
|
+
for i, draft in enumerate(drafts):
|
|
436
|
+
if i > 0:
|
|
437
|
+
ol.add_option(None)
|
|
438
|
+
profile = draft.profile
|
|
439
|
+
label = profile.display_name or profile.name
|
|
440
|
+
if draft.dirty:
|
|
441
|
+
label = self._render_toast(_MODIFIED_LABEL.bind(name=label))
|
|
442
|
+
is_current = draft.original_name == self._active_profile_name
|
|
443
|
+
prefix = "◦ " if is_current else ""
|
|
444
|
+
content = Content.assemble(
|
|
445
|
+
(f"{prefix}{label}", "bold"),
|
|
446
|
+
(f"\n{profile.description}", "dim") if profile.description else "",
|
|
447
|
+
)
|
|
448
|
+
ol.add_option(Option(content, id=draft.key))
|
|
449
|
+
|
|
450
|
+
def _select_initial_profile(self) -> None:
|
|
451
|
+
"""Highlight and load the initial profile."""
|
|
452
|
+
drafts = self._visible_drafts()
|
|
453
|
+
if not drafts:
|
|
454
|
+
return
|
|
455
|
+
|
|
456
|
+
# Resolve initial profile name from display name first. If caller
|
|
457
|
+
# supplied a display label that happens to match another profile's
|
|
458
|
+
# registry name, the display-label match is the intended target.
|
|
459
|
+
target = ""
|
|
460
|
+
for draft in drafts:
|
|
461
|
+
profile = draft.profile
|
|
462
|
+
if (profile.display_name or profile.name) == self._initial_profile:
|
|
463
|
+
target = draft.key
|
|
464
|
+
break
|
|
465
|
+
if not target:
|
|
466
|
+
visible_by_name = {draft.profile.name: draft.key for draft in drafts}
|
|
467
|
+
target = visible_by_name.get(self._initial_profile, drafts[0].key)
|
|
468
|
+
|
|
469
|
+
ol = self.query_one("#ac-list", OptionList)
|
|
470
|
+
with contextlib.suppress(OptionDoesNotExist):
|
|
471
|
+
ol.highlighted = ol.get_option_index(target)
|
|
472
|
+
|
|
473
|
+
self._load_profile(target)
|
|
474
|
+
|
|
475
|
+
# ── Profile loading ──────────────────────────────────────────────
|
|
476
|
+
|
|
477
|
+
def _load_profile(self, draft_key: str, *, mcp_card_folds_carried: bool = False) -> None:
|
|
478
|
+
"""Clear tab panes and mount fresh config panels for the given draft.
|
|
479
|
+
|
|
480
|
+
``mcp_card_folds_carried``: the caller has already set every draft's
|
|
481
|
+
MCP card folds (Save, which rebuilt the drafts), so leave them as set.
|
|
482
|
+
"""
|
|
483
|
+
draft = self._drafts.get(draft_key)
|
|
484
|
+
if draft is None:
|
|
485
|
+
return
|
|
486
|
+
pending_agent_type = self._pending_agent_type_change
|
|
487
|
+
if pending_agent_type is not None and pending_agent_type[0] != draft_key:
|
|
488
|
+
self._pending_agent_type_change = None
|
|
489
|
+
if not mcp_card_folds_carried:
|
|
490
|
+
# Also on a reload in place: Reset keeps the MCP servers, and a panel
|
|
491
|
+
# ignores folds that no longer match its server count.
|
|
492
|
+
self._remember_mcp_card_folds()
|
|
493
|
+
profile = draft.profile
|
|
494
|
+
self._selected_draft_key = draft_key
|
|
495
|
+
self._selected_profile_name = profile.name
|
|
496
|
+
is_builtin = draft.is_builtin
|
|
497
|
+
|
|
498
|
+
self.query_one("#ac-reset", Button).display = is_builtin
|
|
499
|
+
self.query_one("#ac-delete", Button).display = not is_builtin
|
|
500
|
+
|
|
501
|
+
# Clear all panes, then mount only the active tab. Additional tabs are
|
|
502
|
+
# mounted on first activation so the modal can paint without building
|
|
503
|
+
# every editor panel up front.
|
|
504
|
+
self._hydrating = True
|
|
505
|
+
self._hydration_preserve_dirty = False
|
|
506
|
+
self._mounted_tabs.clear()
|
|
507
|
+
self._clear_tab_panes()
|
|
508
|
+
self._configure_tabs_for_profile(profile)
|
|
509
|
+
active_tab = self._active_tab_id()
|
|
510
|
+
if (profile.acp is not None and active_tab not in {"basic", "acp"}) or (
|
|
511
|
+
profile.acp is None and active_tab == "acp"
|
|
512
|
+
):
|
|
513
|
+
active_tab = "basic"
|
|
514
|
+
self.query_one("#ac-tabs", TabbedContent).active = active_tab
|
|
515
|
+
self._ensure_tab_mounted(active_tab, profile, is_builtin)
|
|
516
|
+
self._schedule_hydration_finish(preserve_dirty=False)
|
|
517
|
+
self._apply_read_only_state()
|
|
518
|
+
|
|
519
|
+
def _active_tab_id(self) -> str:
|
|
520
|
+
with contextlib.suppress(NoMatches):
|
|
521
|
+
active = self.query_one("#ac-tabs", TabbedContent).active
|
|
522
|
+
if active in self._TAB_IDS:
|
|
523
|
+
return active
|
|
524
|
+
return self._initial_tab if self._initial_tab in self._TAB_IDS else "basic"
|
|
525
|
+
|
|
526
|
+
def _clear_tab_panes(self) -> None:
|
|
527
|
+
for tab_id in self._TAB_IDS:
|
|
528
|
+
self.query_one(f"#{tab_id}", TabPane).remove_children()
|
|
529
|
+
|
|
530
|
+
def _configure_tabs_for_profile(self, profile: AgentProfile) -> None:
|
|
531
|
+
tabs = self.query_one("#ac-tabs", TabbedContent)
|
|
532
|
+
builtin_tabs = ("instructions", "tools", "sub-agents", "skills", "mcp", "memory", "compaction")
|
|
533
|
+
if profile.acp is not None:
|
|
534
|
+
for tab_id in builtin_tabs:
|
|
535
|
+
tabs.hide_tab(tab_id)
|
|
536
|
+
tabs.show_tab("acp")
|
|
537
|
+
else:
|
|
538
|
+
for tab_id in builtin_tabs:
|
|
539
|
+
tabs.show_tab(tab_id)
|
|
540
|
+
tabs.hide_tab("acp")
|
|
541
|
+
|
|
542
|
+
def _ensure_tab_mounted(self, tab_id: str, profile, is_builtin: bool) -> None:
|
|
543
|
+
if tab_id in self._mounted_tabs:
|
|
544
|
+
return
|
|
545
|
+
if tab_id == "basic":
|
|
546
|
+
self._mount_basic_tab(profile, is_builtin)
|
|
547
|
+
elif tab_id == "instructions":
|
|
548
|
+
self._mount_instructions_tab(profile)
|
|
549
|
+
elif tab_id == "tools":
|
|
550
|
+
self._mount_tools_tab(profile)
|
|
551
|
+
elif tab_id == "sub-agents":
|
|
552
|
+
self._mount_subagents_tab(profile)
|
|
553
|
+
elif tab_id == "skills":
|
|
554
|
+
self._mount_skills_tab(profile)
|
|
555
|
+
elif tab_id == "mcp":
|
|
556
|
+
self._mount_mcp_tab(profile)
|
|
557
|
+
elif tab_id == "memory":
|
|
558
|
+
self._mount_memory_tab(profile)
|
|
559
|
+
elif tab_id == "compaction":
|
|
560
|
+
self._mount_compaction_tab(profile)
|
|
561
|
+
elif tab_id == "acp" and profile.acp is not None:
|
|
562
|
+
self._mount_acp_tab(profile)
|
|
563
|
+
else:
|
|
564
|
+
return
|
|
565
|
+
self._mounted_tabs.add(tab_id)
|
|
566
|
+
self._apply_read_only_state()
|
|
567
|
+
self.call_after_refresh(self._apply_read_only_state)
|
|
568
|
+
|
|
569
|
+
def _schedule_hydration_finish(self, *, preserve_dirty: bool) -> None:
|
|
570
|
+
self._hydrating = True
|
|
571
|
+
self._hydration_preserve_dirty = self._hydration_preserve_dirty or preserve_dirty
|
|
572
|
+
self._hydrating_generation += 1
|
|
573
|
+
generation = self._hydrating_generation
|
|
574
|
+
self.set_timer(self._HYDRATION_RETRY_SECONDS, lambda: self._finish_hydrating(generation, 0))
|
|
575
|
+
|
|
576
|
+
def _dirty_after_hydration(self, draft: _AgentDraft) -> bool:
|
|
577
|
+
if self._read_only:
|
|
578
|
+
return False
|
|
579
|
+
return True if self._hydration_preserve_dirty else self._is_draft_dirty(draft)
|
|
580
|
+
|
|
581
|
+
def _finish_hydrating(self, generation: int, attempts: int) -> None:
|
|
582
|
+
if generation != self._hydrating_generation or not self.is_attached:
|
|
583
|
+
return
|
|
584
|
+
pending_agent_type = self._pending_agent_type_change
|
|
585
|
+
if pending_agent_type is not None and pending_agent_type[0] == self._selected_draft_key:
|
|
586
|
+
self.call_after_refresh(lambda: self._complete_hydration(generation))
|
|
587
|
+
return
|
|
588
|
+
draft = self._drafts.get(self._selected_draft_key)
|
|
589
|
+
if draft is not None:
|
|
590
|
+
next_attempt = attempts + 1
|
|
591
|
+
try:
|
|
592
|
+
mounted_profile = self._build_profile_from_mounted_panels(draft)
|
|
593
|
+
if mounted_profile != draft.profile:
|
|
594
|
+
if next_attempt < self._HYDRATION_MAX_ATTEMPTS:
|
|
595
|
+
self.set_timer(
|
|
596
|
+
self._HYDRATION_RETRY_SECONDS,
|
|
597
|
+
lambda: self._finish_hydrating(generation, next_attempt),
|
|
598
|
+
)
|
|
599
|
+
return
|
|
600
|
+
logger.warning(
|
|
601
|
+
"Agent config hydration did not settle for profile %r after %s attempts",
|
|
602
|
+
draft.profile.name,
|
|
603
|
+
next_attempt,
|
|
604
|
+
)
|
|
605
|
+
else:
|
|
606
|
+
self._set_draft_dirty(draft, self._dirty_after_hydration(draft))
|
|
607
|
+
except _AgentConfigPanelsNotReady as exc:
|
|
608
|
+
if next_attempt < self._HYDRATION_MAX_ATTEMPTS:
|
|
609
|
+
self.set_timer(
|
|
610
|
+
self._HYDRATION_RETRY_SECONDS,
|
|
611
|
+
lambda: self._finish_hydrating(generation, next_attempt),
|
|
612
|
+
)
|
|
613
|
+
return
|
|
614
|
+
logger.warning(
|
|
615
|
+
"Agent config hydration timed out for profile %r after %s attempts: %s",
|
|
616
|
+
draft.profile.name,
|
|
617
|
+
next_attempt,
|
|
618
|
+
exc,
|
|
619
|
+
)
|
|
620
|
+
except Exception:
|
|
621
|
+
if next_attempt < self._HYDRATION_MAX_ATTEMPTS:
|
|
622
|
+
self.set_timer(
|
|
623
|
+
self._HYDRATION_RETRY_SECONDS,
|
|
624
|
+
lambda: self._finish_hydrating(generation, next_attempt),
|
|
625
|
+
)
|
|
626
|
+
return
|
|
627
|
+
logger.warning(
|
|
628
|
+
"Agent config hydration failed for profile %r after %s attempts",
|
|
629
|
+
draft.profile.name,
|
|
630
|
+
next_attempt,
|
|
631
|
+
exc_info=True,
|
|
632
|
+
)
|
|
633
|
+
# Clear the hydration guard only after the next refresh. Field-change
|
|
634
|
+
# events (e.g. a sub-agent Select.Changed) queued while the panels were
|
|
635
|
+
# mounting are still in the message queue; draining them before the guard
|
|
636
|
+
# drops keeps _mark_selected_dirty gated for that settle traffic. Clearing
|
|
637
|
+
# synchronously let those queued events land after the guard cleared and
|
|
638
|
+
# force a spurious, sticky dirty that wrongly enabled Save -- an
|
|
639
|
+
# intermittent CI failure on the dirty-tracking tests.
|
|
640
|
+
self.call_after_refresh(lambda: self._complete_hydration(generation))
|
|
641
|
+
|
|
642
|
+
def _complete_hydration(self, generation: int) -> None:
|
|
643
|
+
if generation != self._hydrating_generation or not self.is_attached:
|
|
644
|
+
return
|
|
645
|
+
preserve_dirty = self._hydration_preserve_dirty
|
|
646
|
+
self._hydrating = False
|
|
647
|
+
self._hydration_preserve_dirty = False
|
|
648
|
+
draft = self._drafts.get(self._selected_draft_key)
|
|
649
|
+
if draft is not None and not self._read_only:
|
|
650
|
+
self._set_draft_dirty(draft, True if preserve_dirty else self._is_draft_dirty(draft))
|
|
651
|
+
self._update_save_button_state()
|
|
652
|
+
self._apply_read_only_state()
|
|
653
|
+
pending_agent_type = self._pending_agent_type_change
|
|
654
|
+
self._pending_agent_type_change = None
|
|
655
|
+
if pending_agent_type is not None and pending_agent_type[0] == self._selected_draft_key:
|
|
656
|
+
self._apply_agent_type_change(*pending_agent_type)
|
|
657
|
+
|
|
658
|
+
def _mount_basic_tab(self, profile, is_builtin: bool) -> None:
|
|
659
|
+
from chrys.app.tui.screens.agents.panels.basic import BasicConfigPanel
|
|
660
|
+
|
|
661
|
+
pane = self.query_one("#basic", TabPane)
|
|
662
|
+
pane.remove_children()
|
|
663
|
+
pane.mount(
|
|
664
|
+
BasicConfigPanel(
|
|
665
|
+
profile,
|
|
666
|
+
is_builtin=is_builtin,
|
|
667
|
+
model_registry=self._model_registry,
|
|
668
|
+
active_profile_id=self._active_model_profile_id,
|
|
669
|
+
read_only=self._read_only,
|
|
670
|
+
)
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
def _mount_instructions_tab(self, profile) -> None:
|
|
674
|
+
from chrys.app.tui.screens.agents.panels.instructions import InstructionsConfigPanel
|
|
675
|
+
|
|
676
|
+
pane = self.query_one("#instructions", TabPane)
|
|
677
|
+
pane.remove_children()
|
|
678
|
+
pane.mount(InstructionsConfigPanel(profile.instructions))
|
|
679
|
+
|
|
680
|
+
def _mount_tools_tab(self, profile) -> None:
|
|
681
|
+
from chrys.app.tui.screens.agents.panels.tools import ToolsConfigPanel
|
|
682
|
+
|
|
683
|
+
pane = self.query_one("#tools", TabPane)
|
|
684
|
+
pane.remove_children()
|
|
685
|
+
pane.mount(ToolsConfigPanel(profile.tools, read_only=self._read_only))
|
|
686
|
+
|
|
687
|
+
def _mount_subagents_tab(self, profile) -> None:
|
|
688
|
+
from chrys.app.tui.screens.agents.panels.subagents import SubAgentsConfigPanel
|
|
689
|
+
|
|
690
|
+
pane = self.query_one("#sub-agents", TabPane)
|
|
691
|
+
pane.remove_children()
|
|
692
|
+
pane.mount(
|
|
693
|
+
SubAgentsConfigPanel(
|
|
694
|
+
profile.sub_agents,
|
|
695
|
+
registry=self._registry,
|
|
696
|
+
current_profile_name=profile.name,
|
|
697
|
+
available_profiles=self._sub_agent_available_profiles(),
|
|
698
|
+
read_only=self._read_only,
|
|
699
|
+
)
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
def _sub_agent_available_profiles(self) -> list[AgentProfile]:
|
|
703
|
+
profiles: list[AgentProfile] = []
|
|
704
|
+
seen_names: set[str] = set()
|
|
705
|
+
|
|
706
|
+
def add(profile: AgentProfile) -> None:
|
|
707
|
+
if not profile.name or profile.name in seen_names:
|
|
708
|
+
return
|
|
709
|
+
seen_names.add(profile.name)
|
|
710
|
+
profiles.append(profile)
|
|
711
|
+
|
|
712
|
+
visible_drafts = self._visible_drafts()
|
|
713
|
+
represented_originals = {draft.original_name for draft in visible_drafts if draft.original_name is not None}
|
|
714
|
+
for draft in visible_drafts:
|
|
715
|
+
if draft.key == self._selected_draft_key:
|
|
716
|
+
continue
|
|
717
|
+
if draft.original_profile is not None and draft.original_name != draft.profile.name:
|
|
718
|
+
add(draft.original_profile)
|
|
719
|
+
for draft in visible_drafts:
|
|
720
|
+
add(draft.profile)
|
|
721
|
+
for profile in self._registry.list_profiles(include_sub_agent_only=True):
|
|
722
|
+
if profile.name in represented_originals:
|
|
723
|
+
continue
|
|
724
|
+
add(profile)
|
|
725
|
+
return profiles
|
|
726
|
+
|
|
727
|
+
def _mount_skills_tab(self, profile) -> None:
|
|
728
|
+
from chrys.app.tui.screens.agents.panels.skills import SkillsConfigPanel
|
|
729
|
+
|
|
730
|
+
pane = self.query_one("#skills", TabPane)
|
|
731
|
+
pane.remove_children()
|
|
732
|
+
pane.mount(SkillsConfigPanel(profile.skills, workspace_cwd=self._workspace_cwd, read_only=self._read_only))
|
|
733
|
+
|
|
734
|
+
def _remember_mcp_card_folds(self) -> None:
|
|
735
|
+
"""Keep the selected draft's MCP card folds for the next time it loads."""
|
|
736
|
+
from chrys.app.tui.screens.agents.panels.mcp import MCPConfigPanel
|
|
737
|
+
|
|
738
|
+
key = self._selected_draft_key
|
|
739
|
+
if key not in self._drafts or "mcp" not in self._mounted_tabs:
|
|
740
|
+
return
|
|
741
|
+
with contextlib.suppress(NoMatches):
|
|
742
|
+
self._mcp_card_folds[key] = self._live_panel(MCPConfigPanel).card_folds()
|
|
743
|
+
|
|
744
|
+
def _mount_mcp_tab(self, profile) -> None:
|
|
745
|
+
from chrys.app.tui.screens.agents.panels.mcp import MCPConfigPanel
|
|
746
|
+
|
|
747
|
+
pane = self.query_one("#mcp", TabPane)
|
|
748
|
+
pane.remove_children()
|
|
749
|
+
pane.mount(
|
|
750
|
+
MCPConfigPanel(
|
|
751
|
+
profile.tools.mcp,
|
|
752
|
+
card_folds=self._mcp_card_folds.get(self._selected_draft_key),
|
|
753
|
+
workspace_cwd=self._workspace_cwd,
|
|
754
|
+
read_only=self._read_only,
|
|
755
|
+
additional_reserved_tool_names=self._selected_sub_agent_tool_names,
|
|
756
|
+
)
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
def _selected_sub_agent_tool_names(self) -> set[str]:
|
|
760
|
+
"""Return live sub-agent names reserved by the selected draft."""
|
|
761
|
+
draft = self._drafts.get(self._selected_draft_key)
|
|
762
|
+
if draft is None:
|
|
763
|
+
return set()
|
|
764
|
+
return {tool_name for ref in draft.profile.sub_agents.agents if (tool_name := ref.tool_name or ref.profile)}
|
|
765
|
+
|
|
766
|
+
def _mount_memory_tab(self, profile) -> None:
|
|
767
|
+
from chrys.app.tui.screens.agents.panels.memory import MemoryConfigPanel
|
|
768
|
+
|
|
769
|
+
pane = self.query_one("#memory", TabPane)
|
|
770
|
+
pane.remove_children()
|
|
771
|
+
pane.mount(MemoryConfigPanel(profile.memory, workspace_cwd=self._workspace_cwd, read_only=self._read_only))
|
|
772
|
+
|
|
773
|
+
def _mount_compaction_tab(self, profile) -> None:
|
|
774
|
+
from chrys.app.tui.screens.agents.panels.compaction import CompactionConfigPanel
|
|
775
|
+
|
|
776
|
+
pane = self.query_one("#compaction", TabPane)
|
|
777
|
+
pane.remove_children()
|
|
778
|
+
pane.mount(CompactionConfigPanel(profile.compaction))
|
|
779
|
+
|
|
780
|
+
def _mount_acp_tab(self, profile) -> None:
|
|
781
|
+
from chrys.app.tui.screens.agents.panels.acp import AcpConfigPanel
|
|
782
|
+
|
|
783
|
+
if profile.acp is None:
|
|
784
|
+
return
|
|
785
|
+
pane = self.query_one("#acp", TabPane)
|
|
786
|
+
pane.remove_children()
|
|
787
|
+
pane.mount(
|
|
788
|
+
AcpConfigPanel(
|
|
789
|
+
profile.acp,
|
|
790
|
+
workspace_cwd=self._workspace_cwd,
|
|
791
|
+
workspace_roots=self._workspace_roots,
|
|
792
|
+
read_only=self._read_only,
|
|
793
|
+
)
|
|
794
|
+
)
|
|
795
|
+
|
|
796
|
+
# ── Sidebar selection ────────────────────────────────────────────
|
|
797
|
+
|
|
798
|
+
@on(OptionList.OptionSelected, "#ac-list")
|
|
799
|
+
def _on_profile_selected(self, event: OptionList.OptionSelected) -> None:
|
|
800
|
+
target_key = event.option.id
|
|
801
|
+
if target_key and target_key != self._selected_draft_key:
|
|
802
|
+
if not self._read_only:
|
|
803
|
+
try:
|
|
804
|
+
self._sync_selected_draft_from_panels()
|
|
805
|
+
except Exception as exc:
|
|
806
|
+
self.notify(
|
|
807
|
+
str(exc),
|
|
808
|
+
title=self._render_toast(_AGENT_TITLE.bind()),
|
|
809
|
+
severity="error",
|
|
810
|
+
timeout=5,
|
|
811
|
+
markup=False,
|
|
812
|
+
)
|
|
813
|
+
self._restore_sidebar_highlight()
|
|
814
|
+
return
|
|
815
|
+
self._refresh_sidebar_preserving_selection()
|
|
816
|
+
ol = self.query_one("#ac-list", OptionList)
|
|
817
|
+
with contextlib.suppress(OptionDoesNotExist):
|
|
818
|
+
ol.highlighted = ol.get_option_index(target_key)
|
|
819
|
+
self._load_profile(target_key)
|
|
820
|
+
|
|
821
|
+
def _restore_sidebar_highlight(self) -> None:
|
|
822
|
+
if not self._selected_draft_key:
|
|
823
|
+
return
|
|
824
|
+
ol = self.query_one("#ac-list", OptionList)
|
|
825
|
+
with contextlib.suppress(OptionDoesNotExist):
|
|
826
|
+
ol.highlighted = ol.get_option_index(self._selected_draft_key)
|
|
827
|
+
|
|
828
|
+
def _refresh_sidebar_preserving_selection(self) -> None:
|
|
829
|
+
self._populate_sidebar()
|
|
830
|
+
self._restore_sidebar_highlight()
|
|
831
|
+
|
|
832
|
+
@on(TabbedContent.TabActivated, "#ac-tabs")
|
|
833
|
+
def _on_tab_activated(self, event: TabbedContent.TabActivated) -> None:
|
|
834
|
+
tab_id = event.pane.id or ""
|
|
835
|
+
if tab_id not in self._TAB_IDS or not self._selected_draft_key:
|
|
836
|
+
return
|
|
837
|
+
draft = self._drafts.get(self._selected_draft_key)
|
|
838
|
+
if draft is None:
|
|
839
|
+
return
|
|
840
|
+
tab_mounted = tab_id in self._mounted_tabs
|
|
841
|
+
if not self._hydrating and not self._read_only:
|
|
842
|
+
self._mark_selected_dirty(force=True)
|
|
843
|
+
if not tab_mounted:
|
|
844
|
+
preserve_dirty = draft.dirty
|
|
845
|
+
self._hydrating = True
|
|
846
|
+
self._ensure_tab_mounted(tab_id, draft.profile, draft.is_builtin)
|
|
847
|
+
self._schedule_hydration_finish(preserve_dirty=preserve_dirty)
|
|
848
|
+
self._apply_read_only_state()
|
|
849
|
+
|
|
850
|
+
def _update_save_button_state(self) -> None:
|
|
851
|
+
if not self.is_attached:
|
|
852
|
+
return
|
|
853
|
+
try:
|
|
854
|
+
save = self.query_one("#ac-save", Button)
|
|
855
|
+
except NoMatches:
|
|
856
|
+
return
|
|
857
|
+
if self._read_only:
|
|
858
|
+
save.disabled = True
|
|
859
|
+
save.display = False
|
|
860
|
+
return
|
|
861
|
+
dirty = any(draft.dirty for draft in self._drafts.values())
|
|
862
|
+
save.disabled = not dirty
|
|
863
|
+
|
|
864
|
+
@staticmethod
|
|
865
|
+
def _is_read_only_hidden_button(button_id: str) -> bool:
|
|
866
|
+
if button_id in {
|
|
867
|
+
"ac-new",
|
|
868
|
+
"ac-clone",
|
|
869
|
+
"ac-reset",
|
|
870
|
+
"ac-delete",
|
|
871
|
+
"ac-save",
|
|
872
|
+
"sa-add-btn",
|
|
873
|
+
"mem-add-file",
|
|
874
|
+
"mem-add-folder",
|
|
875
|
+
"sk-add-btn",
|
|
876
|
+
"mcp-add-btn",
|
|
877
|
+
"acp-add-arg",
|
|
878
|
+
"acp-add-env",
|
|
879
|
+
"acp-add-option",
|
|
880
|
+
"acp-test",
|
|
881
|
+
}:
|
|
882
|
+
return True
|
|
883
|
+
return button_id.startswith(
|
|
884
|
+
(
|
|
885
|
+
"mcp-hadd-",
|
|
886
|
+
"mcp-eadd-",
|
|
887
|
+
"mcp-hdel-",
|
|
888
|
+
"mcp-edel-",
|
|
889
|
+
"mcp-delete-btn-",
|
|
890
|
+
"mcp-test-btn-",
|
|
891
|
+
"acp-",
|
|
892
|
+
"sa-delete-btn-",
|
|
893
|
+
"mem-delete-btn-",
|
|
894
|
+
"mem-file-browse-",
|
|
895
|
+
"mem-folder-browse-",
|
|
896
|
+
"sk-browse-",
|
|
897
|
+
"sk-delete-btn-",
|
|
898
|
+
)
|
|
899
|
+
)
|
|
900
|
+
|
|
901
|
+
def _apply_read_only_state(self) -> None:
|
|
902
|
+
"""Make mounted profile controls view-only while preserving navigation."""
|
|
903
|
+
if not self._read_only or not self.is_attached:
|
|
904
|
+
return
|
|
905
|
+
try:
|
|
906
|
+
self.query_one("#ac-footer", Vertical).add_class("-read-only")
|
|
907
|
+
self.query_one("#ac-read-only-notice", Static).display = True
|
|
908
|
+
except NoMatches:
|
|
909
|
+
return
|
|
910
|
+
for input_widget in self.query(Input):
|
|
911
|
+
input_widget.disabled = True
|
|
912
|
+
for select in self.query(Select):
|
|
913
|
+
select.disabled = True
|
|
914
|
+
for checkbox in self.query(Checkbox):
|
|
915
|
+
checkbox.disabled = True
|
|
916
|
+
for text_area in self.query(TextArea):
|
|
917
|
+
text_area.read_only = True
|
|
918
|
+
for button in self.query(Button):
|
|
919
|
+
button_id = button.id or ""
|
|
920
|
+
if button_id == "ac-cancel":
|
|
921
|
+
button.disabled = False
|
|
922
|
+
button.display = True
|
|
923
|
+
continue
|
|
924
|
+
button.disabled = True
|
|
925
|
+
if self._is_read_only_hidden_button(button_id):
|
|
926
|
+
button.display = False
|
|
927
|
+
|
|
928
|
+
def _set_dismiss_intent(self, intent: _DismissIntent) -> None:
|
|
929
|
+
if self._dismiss_intent == _DismissIntent.SWITCHED or intent == _DismissIntent.NONE:
|
|
930
|
+
return
|
|
931
|
+
self._dismiss_intent = intent
|
|
932
|
+
|
|
933
|
+
def _is_draft_dirty(self, draft: _AgentDraft) -> bool:
|
|
934
|
+
if draft.original_profile is None:
|
|
935
|
+
return True
|
|
936
|
+
return draft.reset_to_builtin or draft.profile != draft.original_profile
|
|
937
|
+
|
|
938
|
+
def _set_draft_dirty(self, draft: _AgentDraft, dirty: bool) -> None:
|
|
939
|
+
if draft.dirty != dirty:
|
|
940
|
+
draft.dirty = dirty
|
|
941
|
+
self._refresh_sidebar_preserving_selection()
|
|
942
|
+
self._update_save_button_state()
|
|
943
|
+
|
|
944
|
+
def _snapshot_drafts(self) -> dict[str, _AgentDraftSnapshot]:
|
|
945
|
+
return {
|
|
946
|
+
key: _AgentDraftSnapshot(
|
|
947
|
+
profile=copy.deepcopy(draft.profile),
|
|
948
|
+
dirty=draft.dirty,
|
|
949
|
+
reset_to_builtin=draft.reset_to_builtin,
|
|
950
|
+
)
|
|
951
|
+
for key, draft in self._drafts.items()
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
def _restore_drafts(self, snapshot: dict[str, _AgentDraftSnapshot]) -> None:
|
|
955
|
+
for key, state in snapshot.items():
|
|
956
|
+
draft = self._drafts.get(key)
|
|
957
|
+
if draft is None:
|
|
958
|
+
continue
|
|
959
|
+
draft.profile = copy.deepcopy(state.profile)
|
|
960
|
+
draft.dirty = state.dirty
|
|
961
|
+
draft.reset_to_builtin = state.reset_to_builtin
|
|
962
|
+
selected = self._drafts.get(self._selected_draft_key)
|
|
963
|
+
if selected is not None:
|
|
964
|
+
self._selected_profile_name = selected.profile.name
|
|
965
|
+
self._refresh_sidebar_preserving_selection()
|
|
966
|
+
self._update_save_button_state()
|
|
967
|
+
|
|
968
|
+
def _mounted_panel_validation_errors(self) -> list[str]:
|
|
969
|
+
return self._validate_all()
|
|
970
|
+
|
|
971
|
+
def _mounted_panels_have_validation_errors(self) -> bool:
|
|
972
|
+
return bool(self._mounted_panel_validation_errors())
|
|
973
|
+
|
|
974
|
+
def _mark_selected_dirty(self, *, force: bool = False, optimistic: bool = False) -> None:
|
|
975
|
+
if self._read_only:
|
|
976
|
+
return
|
|
977
|
+
if self._hydrating or not self._selected_draft_key:
|
|
978
|
+
return
|
|
979
|
+
draft = self._drafts.get(self._selected_draft_key)
|
|
980
|
+
if draft is None:
|
|
981
|
+
return
|
|
982
|
+
if optimistic:
|
|
983
|
+
self._set_draft_dirty(draft, True)
|
|
984
|
+
return
|
|
985
|
+
try:
|
|
986
|
+
draft.profile = self._build_profile_from_mounted_panels(draft)
|
|
987
|
+
except _AgentConfigPanelsNotReady:
|
|
988
|
+
# Panels are still mounting/settling — a transient read must not flip
|
|
989
|
+
# the dirty flag. A field-change event (e.g. a sub-agent Select.Changed)
|
|
990
|
+
# firing in the post-hydration settle window, after _hydrating clears
|
|
991
|
+
# but before the panels are queryable, would otherwise force a spurious
|
|
992
|
+
# dirty that sticks and wrongly enables Save. Leave dirty untouched; the
|
|
993
|
+
# next settled comparison (or a real edit) sets it correctly.
|
|
994
|
+
self._update_save_button_state()
|
|
995
|
+
return
|
|
996
|
+
except Exception:
|
|
997
|
+
if force:
|
|
998
|
+
self._set_draft_dirty(draft, True)
|
|
999
|
+
else:
|
|
1000
|
+
logger.debug("Could not compare mounted agent config to draft", exc_info=True)
|
|
1001
|
+
self._update_save_button_state()
|
|
1002
|
+
return
|
|
1003
|
+
dirty = self._is_draft_dirty(draft)
|
|
1004
|
+
if not dirty and (force or draft.dirty):
|
|
1005
|
+
dirty = self._mounted_panels_have_validation_errors()
|
|
1006
|
+
self._set_draft_dirty(draft, dirty)
|
|
1007
|
+
|
|
1008
|
+
@on(Input.Changed)
|
|
1009
|
+
@on(Checkbox.Changed)
|
|
1010
|
+
@on(Select.Changed)
|
|
1011
|
+
@on(TextArea.Changed)
|
|
1012
|
+
def _on_config_field_changed(self, event: object) -> None:
|
|
1013
|
+
if isinstance(event, Select.Changed) and event.select.id == "bc-agent-type":
|
|
1014
|
+
# The discriminator handler must sanitize hidden sections before
|
|
1015
|
+
# any generic harvest can install ``acp`` and make the switch
|
|
1016
|
+
# appear already complete.
|
|
1017
|
+
return
|
|
1018
|
+
self._mark_selected_dirty(force=True)
|
|
1019
|
+
|
|
1020
|
+
@on(Select.Changed, "#bc-agent-type")
|
|
1021
|
+
def _on_agent_type_changed(self, event: Select.Changed) -> None:
|
|
1022
|
+
"""Apply the destructive ACP discriminator switch after its inline warning."""
|
|
1023
|
+
if self._read_only or event.value not in {"builtin", "acp"}:
|
|
1024
|
+
return
|
|
1025
|
+
draft = self._drafts.get(self._selected_draft_key)
|
|
1026
|
+
if draft is None or draft.is_builtin:
|
|
1027
|
+
return
|
|
1028
|
+
agent_type = str(event.value)
|
|
1029
|
+
wants_acp = agent_type == "acp"
|
|
1030
|
+
if wants_acp == (draft.profile.acp is not None):
|
|
1031
|
+
pending_agent_type = self._pending_agent_type_change
|
|
1032
|
+
if self._hydrating and pending_agent_type is not None and pending_agent_type[0] == draft.key:
|
|
1033
|
+
self._pending_agent_type_change = None
|
|
1034
|
+
return
|
|
1035
|
+
if self._hydrating:
|
|
1036
|
+
self._pending_agent_type_change = (draft.key, agent_type)
|
|
1037
|
+
return
|
|
1038
|
+
self._apply_agent_type_change(draft.key, agent_type)
|
|
1039
|
+
|
|
1040
|
+
def _apply_agent_type_change(self, draft_key: str, agent_type: str) -> None:
|
|
1041
|
+
if self._read_only or draft_key != self._selected_draft_key:
|
|
1042
|
+
return
|
|
1043
|
+
draft = self._drafts.get(draft_key)
|
|
1044
|
+
if draft is None or draft.is_builtin:
|
|
1045
|
+
return
|
|
1046
|
+
wants_acp = agent_type == "acp"
|
|
1047
|
+
if wants_acp == (draft.profile.acp is not None):
|
|
1048
|
+
return
|
|
1049
|
+
from chrys.app.tui.screens.agents.panels.basic import BasicConfigPanel
|
|
1050
|
+
|
|
1051
|
+
basic = self._live_panel(BasicConfigPanel).get_config()
|
|
1052
|
+
self._apply_basic_to_profile(draft.profile, basic, is_builtin=False)
|
|
1053
|
+
if wants_acp:
|
|
1054
|
+
self._sanitize_profile_for_acp(draft.profile)
|
|
1055
|
+
else:
|
|
1056
|
+
draft.profile.acp = None
|
|
1057
|
+
self._canonicalize_profile_for_ui(draft.profile)
|
|
1058
|
+
self._set_draft_dirty(draft, True)
|
|
1059
|
+
self._selected_profile_name = draft.profile.name
|
|
1060
|
+
self._load_profile(draft.key)
|
|
1061
|
+
|
|
1062
|
+
@staticmethod
|
|
1063
|
+
def _sanitize_profile_for_acp(profile: AgentProfile) -> None:
|
|
1064
|
+
from chrys.service.profiles.agents.schema import (
|
|
1065
|
+
AcpAgentConfig,
|
|
1066
|
+
ApprovalConfig,
|
|
1067
|
+
CompactionConfig,
|
|
1068
|
+
MemoryConfig,
|
|
1069
|
+
ModelConfig,
|
|
1070
|
+
SkillsConfig,
|
|
1071
|
+
SubAgentsConfig,
|
|
1072
|
+
ToolsConfig,
|
|
1073
|
+
)
|
|
1074
|
+
|
|
1075
|
+
profile.acp = profile.acp or AcpAgentConfig()
|
|
1076
|
+
profile.sub_agent_only = True
|
|
1077
|
+
profile.instructions = ""
|
|
1078
|
+
profile.tools = ToolsConfig()
|
|
1079
|
+
profile.skills = SkillsConfig()
|
|
1080
|
+
profile.sub_agents = SubAgentsConfig()
|
|
1081
|
+
profile.memory = MemoryConfig()
|
|
1082
|
+
profile.compaction = CompactionConfig()
|
|
1083
|
+
profile.approval = ApprovalConfig()
|
|
1084
|
+
profile.model = ModelConfig()
|
|
1085
|
+
|
|
1086
|
+
@on(Button.Pressed)
|
|
1087
|
+
def _on_config_structure_button_pressed(self, event: Button.Pressed) -> None:
|
|
1088
|
+
if self._read_only:
|
|
1089
|
+
return
|
|
1090
|
+
button_id = event.button.id or ""
|
|
1091
|
+
if not self._is_config_mutation_button(button_id):
|
|
1092
|
+
return
|
|
1093
|
+
self._mark_structure_changed()
|
|
1094
|
+
|
|
1095
|
+
@on(CardListChanged)
|
|
1096
|
+
def _on_card_list_changed(self, _event: CardListChanged) -> None:
|
|
1097
|
+
if self._read_only:
|
|
1098
|
+
return
|
|
1099
|
+
self._mark_structure_changed()
|
|
1100
|
+
|
|
1101
|
+
def _mark_structure_changed(self) -> None:
|
|
1102
|
+
# The panel rebuilds its rows asynchronously: mark dirty now, then re-read once it has settled.
|
|
1103
|
+
self._mark_selected_dirty(force=True, optimistic=True)
|
|
1104
|
+
self.call_after_refresh(lambda: self._mark_selected_dirty(force=True))
|
|
1105
|
+
|
|
1106
|
+
@staticmethod
|
|
1107
|
+
def _is_config_mutation_button(button_id: str) -> bool:
|
|
1108
|
+
# Not "mcp-delete-btn-": that press opens a confirmation, and the panel
|
|
1109
|
+
# posts CardListChanged once a confirmed delete lands.
|
|
1110
|
+
if button_id in {
|
|
1111
|
+
"sa-add-btn",
|
|
1112
|
+
"mem-add-file",
|
|
1113
|
+
"mem-add-folder",
|
|
1114
|
+
"sk-add-btn",
|
|
1115
|
+
"mcp-add-btn",
|
|
1116
|
+
"acp-add-arg",
|
|
1117
|
+
"acp-add-env",
|
|
1118
|
+
"acp-add-option",
|
|
1119
|
+
}:
|
|
1120
|
+
return True
|
|
1121
|
+
return button_id.startswith(
|
|
1122
|
+
(
|
|
1123
|
+
"sk-ext-",
|
|
1124
|
+
"sk-browse-",
|
|
1125
|
+
"mcp-hadd-",
|
|
1126
|
+
"mcp-eadd-",
|
|
1127
|
+
"mcp-hdel-",
|
|
1128
|
+
"mcp-edel-",
|
|
1129
|
+
"acp-row-delete-",
|
|
1130
|
+
"sa-delete-btn-",
|
|
1131
|
+
"mem-delete-btn-",
|
|
1132
|
+
"sk-delete-btn-",
|
|
1133
|
+
)
|
|
1134
|
+
)
|
|
1135
|
+
|
|
1136
|
+
# ── Buttons ──────────────────────────────────────────────────────
|
|
1137
|
+
|
|
1138
|
+
@on(Button.Pressed, "#ac-new")
|
|
1139
|
+
def _on_new(self, _event: Button.Pressed) -> None:
|
|
1140
|
+
"""Stage a new blank profile and select it."""
|
|
1141
|
+
if self._read_only:
|
|
1142
|
+
return
|
|
1143
|
+
from chrys.service.profiles.agents.schema import AgentProfile
|
|
1144
|
+
|
|
1145
|
+
try:
|
|
1146
|
+
self._sync_selected_draft_from_panels()
|
|
1147
|
+
except Exception as exc:
|
|
1148
|
+
self.notify(
|
|
1149
|
+
str(exc),
|
|
1150
|
+
title=self._render_toast(_AGENT_TITLE.bind()),
|
|
1151
|
+
severity="error",
|
|
1152
|
+
timeout=5,
|
|
1153
|
+
markup=False,
|
|
1154
|
+
)
|
|
1155
|
+
return
|
|
1156
|
+
|
|
1157
|
+
# Generate a unique name
|
|
1158
|
+
base = "new-agent"
|
|
1159
|
+
name = base
|
|
1160
|
+
counter = 1
|
|
1161
|
+
while self._draft_name_exists(name):
|
|
1162
|
+
counter += 1
|
|
1163
|
+
name = f"{base}-{counter}"
|
|
1164
|
+
|
|
1165
|
+
profile = AgentProfile(
|
|
1166
|
+
name=name,
|
|
1167
|
+
id=self._next_profile_id(),
|
|
1168
|
+
display_name=name,
|
|
1169
|
+
description="A custom agent profile",
|
|
1170
|
+
instructions="You are a helpful AI assistant. Follow the user's instructions carefully and provide clear, concise responses.",
|
|
1171
|
+
)
|
|
1172
|
+
self._canonicalize_profile_for_ui(profile)
|
|
1173
|
+
key = self._new_draft_key()
|
|
1174
|
+
self._drafts[key] = _AgentDraft(
|
|
1175
|
+
key=key,
|
|
1176
|
+
original_name=None,
|
|
1177
|
+
profile=profile,
|
|
1178
|
+
original_profile=None,
|
|
1179
|
+
is_builtin=False,
|
|
1180
|
+
dirty=True,
|
|
1181
|
+
)
|
|
1182
|
+
|
|
1183
|
+
self._refresh_sidebar_preserving_selection()
|
|
1184
|
+
ol = self.query_one("#ac-list", OptionList)
|
|
1185
|
+
with contextlib.suppress(OptionDoesNotExist):
|
|
1186
|
+
ol.highlighted = ol.get_option_index(key)
|
|
1187
|
+
self._load_profile(key)
|
|
1188
|
+
self._update_save_button_state()
|
|
1189
|
+
|
|
1190
|
+
def _draft_name_exists(self, name: str) -> bool:
|
|
1191
|
+
lowered = name.casefold()
|
|
1192
|
+
return lowered in {reserved.casefold() for reserved in self._reserved_profile_names()}
|
|
1193
|
+
|
|
1194
|
+
def _reserved_profile_names(self) -> set[str]:
|
|
1195
|
+
"""Names unavailable for generated profiles across drafts and registry entries."""
|
|
1196
|
+
names = set(self._registry.list_names())
|
|
1197
|
+
names.update(draft.profile.name for draft in self._drafts.values() if draft.profile.name)
|
|
1198
|
+
return names
|
|
1199
|
+
|
|
1200
|
+
def _next_profile_id(self) -> str:
|
|
1201
|
+
"""Generate a fresh agent profile id."""
|
|
1202
|
+
existing_ids = {
|
|
1203
|
+
profile.id for profile in self._registry.list_profiles(include_sub_agent_only=True) if profile.id
|
|
1204
|
+
}
|
|
1205
|
+
existing_ids.update(draft.profile.id for draft in self._drafts.values() if draft.profile.id)
|
|
1206
|
+
while True:
|
|
1207
|
+
profile_id = uuid.uuid4().hex[:12]
|
|
1208
|
+
if profile_id not in existing_ids:
|
|
1209
|
+
return profile_id
|
|
1210
|
+
|
|
1211
|
+
@staticmethod
|
|
1212
|
+
def _clone_name_parts(name: str) -> tuple[str, int] | None:
|
|
1213
|
+
"""Return ``(root, suffix_index)`` for generated clone names."""
|
|
1214
|
+
match = re.fullmatch(r"(.+?)-copy(?:-(\d+))?", name, flags=re.IGNORECASE)
|
|
1215
|
+
if match is None:
|
|
1216
|
+
return None
|
|
1217
|
+
suffix = match.group(2)
|
|
1218
|
+
return match.group(1), int(suffix) if suffix is not None else 1
|
|
1219
|
+
|
|
1220
|
+
def _clone_name_indices(self, root_name: str) -> list[int]:
|
|
1221
|
+
"""Return existing generated clone suffixes for the given root."""
|
|
1222
|
+
indices: list[int] = []
|
|
1223
|
+
for name in self._reserved_profile_names():
|
|
1224
|
+
parts = self._clone_name_parts(name)
|
|
1225
|
+
if parts is not None and parts[0].casefold() == root_name.casefold():
|
|
1226
|
+
indices.append(parts[1])
|
|
1227
|
+
return indices
|
|
1228
|
+
|
|
1229
|
+
@staticmethod
|
|
1230
|
+
def _clone_display_root(source_display_name: str) -> str:
|
|
1231
|
+
"""Return the original display root for a generated clone label."""
|
|
1232
|
+
match = re.fullmatch(r"(.+?) copy(?: \d+)?", source_display_name, flags=re.IGNORECASE)
|
|
1233
|
+
return source_display_name if match is None else match.group(1)
|
|
1234
|
+
|
|
1235
|
+
def _next_clone_names(self, source_name: str, source_display_name: str) -> tuple[str, str]:
|
|
1236
|
+
"""Generate unique registry and display names for a cloned agent profile."""
|
|
1237
|
+
existing_names = {name.casefold() for name in self._reserved_profile_names()}
|
|
1238
|
+
source_parts = self._clone_name_parts(source_name)
|
|
1239
|
+
if source_parts is None:
|
|
1240
|
+
root_name = source_name
|
|
1241
|
+
counter = 1
|
|
1242
|
+
else:
|
|
1243
|
+
root_name, source_index = source_parts
|
|
1244
|
+
source_display_name = self._clone_display_root(source_display_name)
|
|
1245
|
+
counter = max([source_index, *self._clone_name_indices(root_name)]) + 1
|
|
1246
|
+
base_name = f"{root_name}-copy"
|
|
1247
|
+
base_display_name = f"{source_display_name} Copy"
|
|
1248
|
+
name = base_name if counter == 1 else f"{base_name}-{counter}"
|
|
1249
|
+
display_name = base_display_name if counter == 1 else f"{base_display_name} {counter}"
|
|
1250
|
+
while name.casefold() in existing_names:
|
|
1251
|
+
counter += 1
|
|
1252
|
+
name = f"{base_name}-{counter}"
|
|
1253
|
+
display_name = f"{base_display_name} {counter}"
|
|
1254
|
+
return name, display_name
|
|
1255
|
+
|
|
1256
|
+
@on(Button.Pressed, "#ac-clone")
|
|
1257
|
+
def _on_clone(self, _event: Button.Pressed) -> None:
|
|
1258
|
+
"""Stage a copy of the selected profile and select it."""
|
|
1259
|
+
if self._read_only:
|
|
1260
|
+
return
|
|
1261
|
+
try:
|
|
1262
|
+
self._sync_selected_draft_from_panels()
|
|
1263
|
+
except Exception as exc:
|
|
1264
|
+
self.notify(
|
|
1265
|
+
str(exc),
|
|
1266
|
+
title=self._render_toast(_AGENT_TITLE.bind()),
|
|
1267
|
+
severity="error",
|
|
1268
|
+
timeout=5,
|
|
1269
|
+
markup=False,
|
|
1270
|
+
)
|
|
1271
|
+
return
|
|
1272
|
+
|
|
1273
|
+
source_draft = self._drafts.get(self._selected_draft_key)
|
|
1274
|
+
if source_draft is None:
|
|
1275
|
+
return
|
|
1276
|
+
|
|
1277
|
+
source = source_draft.profile
|
|
1278
|
+
duplicate = copy.deepcopy(source)
|
|
1279
|
+
duplicate.name, duplicate.display_name = self._next_clone_names(source.name, source.display_name or source.name)
|
|
1280
|
+
duplicate.id = self._next_profile_id()
|
|
1281
|
+
self._canonicalize_profile_for_ui(duplicate)
|
|
1282
|
+
key = self._new_draft_key()
|
|
1283
|
+
self._drafts[key] = _AgentDraft(
|
|
1284
|
+
key=key,
|
|
1285
|
+
original_name=None,
|
|
1286
|
+
profile=duplicate,
|
|
1287
|
+
original_profile=None,
|
|
1288
|
+
is_builtin=False,
|
|
1289
|
+
dirty=True,
|
|
1290
|
+
)
|
|
1291
|
+
# The copy has the same MCP servers: open their cards as the original shows them.
|
|
1292
|
+
self._remember_mcp_card_folds()
|
|
1293
|
+
if (folds := self._mcp_card_folds.get(source_draft.key)) is not None:
|
|
1294
|
+
self._mcp_card_folds[key] = list(folds)
|
|
1295
|
+
|
|
1296
|
+
self._refresh_sidebar_preserving_selection()
|
|
1297
|
+
ol = self.query_one("#ac-list", OptionList)
|
|
1298
|
+
with contextlib.suppress(OptionDoesNotExist):
|
|
1299
|
+
ol.highlighted = ol.get_option_index(key)
|
|
1300
|
+
self._load_profile(key)
|
|
1301
|
+
self._update_save_button_state()
|
|
1302
|
+
|
|
1303
|
+
@on(Button.Pressed, "#ac-reset")
|
|
1304
|
+
def _on_reset(self, _event: Button.Pressed) -> None:
|
|
1305
|
+
"""Stage a built-in profile reset after confirmation."""
|
|
1306
|
+
if self._read_only:
|
|
1307
|
+
return
|
|
1308
|
+
try:
|
|
1309
|
+
self._sync_selected_draft_from_panels()
|
|
1310
|
+
except Exception as exc:
|
|
1311
|
+
self.notify(
|
|
1312
|
+
str(exc),
|
|
1313
|
+
title=self._render_toast(_AGENT_TITLE.bind()),
|
|
1314
|
+
severity="error",
|
|
1315
|
+
timeout=5,
|
|
1316
|
+
markup=False,
|
|
1317
|
+
)
|
|
1318
|
+
return
|
|
1319
|
+
draft_key = self._selected_draft_key
|
|
1320
|
+
draft = self._drafts.get(draft_key)
|
|
1321
|
+
if draft is None or not draft.is_builtin or draft.original_name is None:
|
|
1322
|
+
return
|
|
1323
|
+
display = draft.profile.display_name or draft.profile.name
|
|
1324
|
+
from chrys.app.tui.screens.dialogs.confirm import ConfirmDialog
|
|
1325
|
+
|
|
1326
|
+
dialog = ConfirmDialog(
|
|
1327
|
+
title=_RESET_AGENT_TITLE.bind(),
|
|
1328
|
+
message=_RESET_AGENT_MESSAGE.bind(display=display),
|
|
1329
|
+
confirm_label=_RESET.bind(),
|
|
1330
|
+
confirm_variant="primary",
|
|
1331
|
+
locale_controller=widget_locale_controller(self),
|
|
1332
|
+
)
|
|
1333
|
+
self.app.push_screen(
|
|
1334
|
+
dialog,
|
|
1335
|
+
callback=lambda confirmed: self._do_reset(draft_key) if confirmed else None,
|
|
1336
|
+
)
|
|
1337
|
+
|
|
1338
|
+
def _do_reset(self, draft_key: str) -> None:
|
|
1339
|
+
"""Replace one built-in draft with its reset candidate."""
|
|
1340
|
+
if self._read_only:
|
|
1341
|
+
return
|
|
1342
|
+
draft = self._drafts.get(draft_key)
|
|
1343
|
+
if draft is None or not draft.is_builtin or draft.original_name is None:
|
|
1344
|
+
return
|
|
1345
|
+
reset_profile = self._registry.build_builtin_reset(
|
|
1346
|
+
draft.original_name,
|
|
1347
|
+
preserve_from=draft.profile,
|
|
1348
|
+
)
|
|
1349
|
+
self._canonicalize_profile_for_ui(reset_profile)
|
|
1350
|
+
draft_changed = reset_profile != draft.profile
|
|
1351
|
+
shadow_exists = self._profile_yaml_path(draft.original_name).is_file()
|
|
1352
|
+
original = draft.original_profile
|
|
1353
|
+
reset_matches_template = self._profile_matches_builtin_template(reset_profile, draft.original_name)
|
|
1354
|
+
needs_save = reset_profile != original or (shadow_exists and reset_matches_template)
|
|
1355
|
+
draft.profile = reset_profile
|
|
1356
|
+
draft.reset_to_builtin = needs_save
|
|
1357
|
+
self._selected_profile_name = reset_profile.name
|
|
1358
|
+
self._set_draft_dirty(draft, needs_save)
|
|
1359
|
+
self._load_profile(draft_key)
|
|
1360
|
+
message = (
|
|
1361
|
+
_AGENT_RESET_PENDING if needs_save else _AGENT_RESET_COMPLETE if draft_changed else _AGENT_ALREADY_DEFAULT
|
|
1362
|
+
)
|
|
1363
|
+
self.notify(
|
|
1364
|
+
self._render_toast(message.bind()),
|
|
1365
|
+
title=self._render_toast(_AGENT_SETTINGS_TITLE.bind()),
|
|
1366
|
+
severity="information",
|
|
1367
|
+
timeout=3,
|
|
1368
|
+
markup=False,
|
|
1369
|
+
)
|
|
1370
|
+
|
|
1371
|
+
@on(Button.Pressed, "#ac-delete")
|
|
1372
|
+
def _on_delete(self, _event: Button.Pressed) -> None:
|
|
1373
|
+
"""Delete the selected custom profile after confirmation."""
|
|
1374
|
+
if self._read_only:
|
|
1375
|
+
return
|
|
1376
|
+
draft_key = self._selected_draft_key
|
|
1377
|
+
draft = self._drafts.get(draft_key)
|
|
1378
|
+
if draft is None or draft.is_builtin:
|
|
1379
|
+
return
|
|
1380
|
+
name = draft.profile.name
|
|
1381
|
+
# Block deleting the last profile overall. An
|
|
1382
|
+
# empty registry would leave the main screen's input-bar tag
|
|
1383
|
+
# bound to a missing profile (no label, no description) and
|
|
1384
|
+
# give the engine nothing to fall back to on ``SettingsReload``.
|
|
1385
|
+
remaining_visible = self._remaining_visible_after_delete(draft)
|
|
1386
|
+
if not remaining_visible:
|
|
1387
|
+
self.notify(
|
|
1388
|
+
self._render_toast(_LAST_AGENT.bind()),
|
|
1389
|
+
title=self._render_toast(_AGENT_TITLE.bind()),
|
|
1390
|
+
severity="warning",
|
|
1391
|
+
timeout=5,
|
|
1392
|
+
markup=False,
|
|
1393
|
+
)
|
|
1394
|
+
return
|
|
1395
|
+
# Block deleting the last main-eligible profile (i.e.
|
|
1396
|
+
# one that can serve as the active main agent — not
|
|
1397
|
+
# ``sub_agent_only``). The engine must always be bound to a
|
|
1398
|
+
# real main-agent profile; sub-agent-only profiles cannot fill
|
|
1399
|
+
# that slot. Sub-agent-only profiles themselves may be freely
|
|
1400
|
+
# deleted (having zero sub-agents is a valid state) — as long
|
|
1401
|
+
# as the general guard above still permits it.
|
|
1402
|
+
target = draft.original_profile or draft.profile
|
|
1403
|
+
if not target.sub_agent_only:
|
|
1404
|
+
# ``list_profiles()`` excludes ``sub_agent_only`` by default.
|
|
1405
|
+
remaining_main = [p for p in remaining_visible if not p.sub_agent_only]
|
|
1406
|
+
if not remaining_main:
|
|
1407
|
+
self.notify(
|
|
1408
|
+
self._render_toast(_LAST_MAIN_AGENT.bind()),
|
|
1409
|
+
title=self._render_toast(_AGENT_TITLE.bind()),
|
|
1410
|
+
severity="warning",
|
|
1411
|
+
timeout=5,
|
|
1412
|
+
markup=False,
|
|
1413
|
+
)
|
|
1414
|
+
return
|
|
1415
|
+
|
|
1416
|
+
# Block deleting an agent that is still referenced as a
|
|
1417
|
+
# sub-agent by other profiles. Without this guard the registry
|
|
1418
|
+
# would silently strip the reference from every parent profile
|
|
1419
|
+
# and re-save them — a dangerous action to take without the
|
|
1420
|
+
# user's explicit consent. Make the user unlink manually first.
|
|
1421
|
+
target_names = {target.name}
|
|
1422
|
+
if draft.original_name is not None:
|
|
1423
|
+
target_names.add(draft.original_name)
|
|
1424
|
+
referencing = self._referencing_profiles_for_delete(draft, target_names)
|
|
1425
|
+
if referencing:
|
|
1426
|
+
preview = ", ".join(referencing[:3])
|
|
1427
|
+
suffix = (
|
|
1428
|
+
self._render_toast(_AGENT_MORE_SUFFIX.bind(extra_count=len(referencing) - 3))
|
|
1429
|
+
if len(referencing) > 3
|
|
1430
|
+
else ""
|
|
1431
|
+
)
|
|
1432
|
+
self.notify(
|
|
1433
|
+
self._render_toast(_AGENT_STILL_REFERENCED.bind(name=name, preview=preview, suffix=suffix)),
|
|
1434
|
+
title=self._render_toast(_AGENT_TITLE.bind()),
|
|
1435
|
+
severity="warning",
|
|
1436
|
+
timeout=6,
|
|
1437
|
+
markup=False,
|
|
1438
|
+
)
|
|
1439
|
+
return
|
|
1440
|
+
display = draft.profile.display_name or name
|
|
1441
|
+
from chrys.app.tui.screens.dialogs.confirm import ConfirmDialog
|
|
1442
|
+
|
|
1443
|
+
dialog = ConfirmDialog(
|
|
1444
|
+
title=_DELETE_AGENT_TITLE.bind(),
|
|
1445
|
+
message=_DELETE_AGENT_MESSAGE.bind(display=display),
|
|
1446
|
+
confirm_label=_DELETE.bind(),
|
|
1447
|
+
confirm_variant="error",
|
|
1448
|
+
locale_controller=widget_locale_controller(self),
|
|
1449
|
+
)
|
|
1450
|
+
self.app.push_screen(
|
|
1451
|
+
dialog,
|
|
1452
|
+
callback=lambda confirmed: self._do_delete(draft_key) if confirmed else None,
|
|
1453
|
+
)
|
|
1454
|
+
|
|
1455
|
+
def _referencing_profiles_for_delete(self, draft: _AgentDraft, target_names: set[str]) -> list[str]:
|
|
1456
|
+
referencing: list[str] = []
|
|
1457
|
+
seen_names: set[str] = set()
|
|
1458
|
+
|
|
1459
|
+
def add_if_referencing(profile_name: str, display_name: str, refs) -> None:
|
|
1460
|
+
if profile_name in target_names or profile_name in seen_names:
|
|
1461
|
+
return
|
|
1462
|
+
if any(ref.profile in target_names for ref in refs):
|
|
1463
|
+
seen_names.add(profile_name)
|
|
1464
|
+
referencing.append(display_name or profile_name)
|
|
1465
|
+
|
|
1466
|
+
for other in self._visible_drafts():
|
|
1467
|
+
if other.key == draft.key:
|
|
1468
|
+
continue
|
|
1469
|
+
add_if_referencing(other.profile.name, other.profile.display_name, other.profile.sub_agents.agents)
|
|
1470
|
+
|
|
1471
|
+
represented_originals = {
|
|
1472
|
+
other.original_name for other in self._visible_drafts() if other.original_name is not None
|
|
1473
|
+
}
|
|
1474
|
+
for profile in self._registry.list_profiles(include_sub_agent_only=True):
|
|
1475
|
+
if profile.name in represented_originals:
|
|
1476
|
+
continue
|
|
1477
|
+
add_if_referencing(profile.name, profile.display_name, profile.sub_agents.agents)
|
|
1478
|
+
|
|
1479
|
+
return referencing
|
|
1480
|
+
|
|
1481
|
+
def _remaining_visible_after_delete(self, draft: _AgentDraft) -> list[AgentProfile]:
|
|
1482
|
+
if draft.original_name is None:
|
|
1483
|
+
return [d.profile for d in self._visible_drafts() if d.key != draft.key]
|
|
1484
|
+
return [
|
|
1485
|
+
profile
|
|
1486
|
+
for profile in self._registry.list_profiles(include_sub_agent_only=True)
|
|
1487
|
+
if profile.name != draft.original_name
|
|
1488
|
+
]
|
|
1489
|
+
|
|
1490
|
+
def _do_delete(self, draft_key: str) -> None:
|
|
1491
|
+
"""Delete the profile immediately after user confirmation."""
|
|
1492
|
+
if self._read_only:
|
|
1493
|
+
return
|
|
1494
|
+
from chrys.service.profiles.agents.serializer import delete_profile
|
|
1495
|
+
|
|
1496
|
+
draft = self._drafts.get(draft_key)
|
|
1497
|
+
if draft is None or draft.is_builtin:
|
|
1498
|
+
return
|
|
1499
|
+
old_active_name = self._active_profile_name
|
|
1500
|
+
promoted_profile: AgentProfile | None = None
|
|
1501
|
+
|
|
1502
|
+
if draft.original_name is not None:
|
|
1503
|
+
if draft.original_name == old_active_name:
|
|
1504
|
+
remaining_main = [p for p in self._registry.list_profiles() if p.name != draft.original_name]
|
|
1505
|
+
if not remaining_main:
|
|
1506
|
+
self.notify(
|
|
1507
|
+
self._render_toast(_LAST_MAIN_AGENT.bind()),
|
|
1508
|
+
title=self._render_toast(_AGENT_TITLE.bind()),
|
|
1509
|
+
severity="warning",
|
|
1510
|
+
timeout=5,
|
|
1511
|
+
markup=False,
|
|
1512
|
+
)
|
|
1513
|
+
return
|
|
1514
|
+
promoted_profile = remaining_main[0]
|
|
1515
|
+
delete_profile(draft.original_name)
|
|
1516
|
+
self._registry.remove(draft.original_name)
|
|
1517
|
+
self._set_dismiss_intent(_DismissIntent.UPDATED)
|
|
1518
|
+
if promoted_profile is not None:
|
|
1519
|
+
self._active_profile_name = promoted_profile.name
|
|
1520
|
+
self._current_profile = promoted_profile.display_name or promoted_profile.name
|
|
1521
|
+
self._set_dismiss_intent(_DismissIntent.SWITCHED)
|
|
1522
|
+
if self._on_saved is not None:
|
|
1523
|
+
self._on_saved(None, promoted_profile.name)
|
|
1524
|
+
|
|
1525
|
+
self._drafts.pop(draft_key, None)
|
|
1526
|
+
|
|
1527
|
+
self._populate_sidebar()
|
|
1528
|
+
remaining = self._visible_drafts()
|
|
1529
|
+
if remaining:
|
|
1530
|
+
next_key = remaining[0].key
|
|
1531
|
+
ol = self.query_one("#ac-list", OptionList)
|
|
1532
|
+
with contextlib.suppress(OptionDoesNotExist):
|
|
1533
|
+
ol.highlighted = ol.get_option_index(next_key)
|
|
1534
|
+
self._load_profile(next_key)
|
|
1535
|
+
self.notify(
|
|
1536
|
+
self._render_toast((_AGENT_DELETED if draft.original_name is not None else _AGENT_REMOVED).bind()),
|
|
1537
|
+
title=self._render_toast(_AGENT_SETTINGS_TITLE.bind()),
|
|
1538
|
+
severity="information",
|
|
1539
|
+
timeout=3,
|
|
1540
|
+
markup=False,
|
|
1541
|
+
)
|
|
1542
|
+
self._update_save_button_state()
|
|
1543
|
+
|
|
1544
|
+
@on(Button.Pressed, "#ac-save")
|
|
1545
|
+
def _on_save(self, _event: Button.Pressed) -> None:
|
|
1546
|
+
if self._read_only:
|
|
1547
|
+
return
|
|
1548
|
+
if not self._hydrating:
|
|
1549
|
+
errors = self._validate_all()
|
|
1550
|
+
if errors:
|
|
1551
|
+
self._unfold_invalid_mcp_cards()
|
|
1552
|
+
self.notify(
|
|
1553
|
+
"\n".join(errors),
|
|
1554
|
+
title=self._render_toast(_VALIDATION_ERROR_TITLE.bind()),
|
|
1555
|
+
severity="error",
|
|
1556
|
+
timeout=5,
|
|
1557
|
+
markup=False,
|
|
1558
|
+
)
|
|
1559
|
+
return
|
|
1560
|
+
try:
|
|
1561
|
+
self._sync_selected_draft_from_panels()
|
|
1562
|
+
except Exception as exc:
|
|
1563
|
+
self.notify(
|
|
1564
|
+
str(exc),
|
|
1565
|
+
title=self._render_toast(_SAVE_ERROR_TITLE.bind()),
|
|
1566
|
+
severity="error",
|
|
1567
|
+
timeout=5,
|
|
1568
|
+
markup=False,
|
|
1569
|
+
)
|
|
1570
|
+
return
|
|
1571
|
+
draft_snapshot = self._snapshot_drafts()
|
|
1572
|
+
retargeted_profiles = self._retarget_renamed_sub_agent_refs()
|
|
1573
|
+
errors = self._validate_drafts(retargeted_profiles)
|
|
1574
|
+
if errors:
|
|
1575
|
+
# Read before the restore: it can clear a dirty flag the retargeting set for this pass.
|
|
1576
|
+
invalid_mcp_servers = self._selected_draft_invalid_mcp_servers()
|
|
1577
|
+
self._restore_drafts(draft_snapshot)
|
|
1578
|
+
self._unfold_invalid_mcp_cards(invalid_mcp_servers)
|
|
1579
|
+
self.notify(
|
|
1580
|
+
"\n".join(errors),
|
|
1581
|
+
title=self._render_toast(_VALIDATION_ERROR_TITLE.bind()),
|
|
1582
|
+
severity="error",
|
|
1583
|
+
timeout=5,
|
|
1584
|
+
markup=False,
|
|
1585
|
+
)
|
|
1586
|
+
return
|
|
1587
|
+
try:
|
|
1588
|
+
self._apply_all(retargeted_profiles)
|
|
1589
|
+
except Exception as exc:
|
|
1590
|
+
self._restore_drafts(draft_snapshot)
|
|
1591
|
+
self.notify(
|
|
1592
|
+
str(exc),
|
|
1593
|
+
title=self._render_toast(_SAVE_ERROR_TITLE.bind()),
|
|
1594
|
+
severity="error",
|
|
1595
|
+
timeout=5,
|
|
1596
|
+
markup=False,
|
|
1597
|
+
)
|
|
1598
|
+
return
|
|
1599
|
+
self.notify(
|
|
1600
|
+
self._render_toast(_AGENT_PROFILE_SAVED.bind()),
|
|
1601
|
+
title=self._render_toast(_AGENT_SETTINGS_TITLE.bind()),
|
|
1602
|
+
severity="information",
|
|
1603
|
+
timeout=3,
|
|
1604
|
+
markup=False,
|
|
1605
|
+
)
|
|
1606
|
+
|
|
1607
|
+
def _render_toast(self, reference: MessageRef) -> str:
|
|
1608
|
+
return render_str(widget_localizer(self), reference)
|
|
1609
|
+
|
|
1610
|
+
@on(Button.Pressed, "#ac-cancel")
|
|
1611
|
+
def _on_cancel_btn(self, _event: Button.Pressed) -> None:
|
|
1612
|
+
self.dismiss(self._cancel_result())
|
|
1613
|
+
|
|
1614
|
+
def action_cancel(self) -> None:
|
|
1615
|
+
self.dismiss(self._cancel_result())
|
|
1616
|
+
|
|
1617
|
+
def _cancel_result(self) -> str:
|
|
1618
|
+
"""Return the dismiss result when the user closes the modal.
|
|
1619
|
+
|
|
1620
|
+
``"switched"`` when a delete promoted a different profile to
|
|
1621
|
+
active — the caller rebuilds the engine against that profile,
|
|
1622
|
+
even if a later Save updated the promoted profile in-place.
|
|
1623
|
+
``"updated"`` when any Save happened during the session (or the
|
|
1624
|
+
active profile was edited mid-modal and already synced).
|
|
1625
|
+
``""`` otherwise.
|
|
1626
|
+
"""
|
|
1627
|
+
if self._read_only:
|
|
1628
|
+
return ""
|
|
1629
|
+
return self._dismiss_intent.value
|
|
1630
|
+
|
|
1631
|
+
def _default_dismiss_result(self) -> str: # type: ignore[override]
|
|
1632
|
+
return self._cancel_result()
|
|
1633
|
+
|
|
1634
|
+
# ── Validation ───────────────────────────────────────────────────
|
|
1635
|
+
|
|
1636
|
+
def _validate_all(self) -> list[str]:
|
|
1637
|
+
from chrys.app.tui.screens.agents.panels.acp import AcpConfigPanel
|
|
1638
|
+
from chrys.app.tui.screens.agents.panels.basic import BasicConfigPanel
|
|
1639
|
+
from chrys.app.tui.screens.agents.panels.compaction import CompactionConfigPanel
|
|
1640
|
+
from chrys.app.tui.screens.agents.panels.instructions import InstructionsConfigPanel
|
|
1641
|
+
from chrys.app.tui.screens.agents.panels.mcp import MCPConfigPanel
|
|
1642
|
+
from chrys.app.tui.screens.agents.panels.memory import MemoryConfigPanel
|
|
1643
|
+
from chrys.app.tui.screens.agents.panels.skills import SkillsConfigPanel
|
|
1644
|
+
from chrys.app.tui.screens.agents.panels.subagents import SubAgentsConfigPanel
|
|
1645
|
+
from chrys.app.tui.screens.agents.panels.tools import ToolsConfigPanel
|
|
1646
|
+
|
|
1647
|
+
draft = self._drafts.get(self._selected_draft_key)
|
|
1648
|
+
is_acp = draft is not None and draft.profile.acp is not None
|
|
1649
|
+
panels = (
|
|
1650
|
+
(("basic", BasicConfigPanel), ("acp", AcpConfigPanel))
|
|
1651
|
+
if is_acp
|
|
1652
|
+
else (
|
|
1653
|
+
("basic", BasicConfigPanel),
|
|
1654
|
+
("instructions", InstructionsConfigPanel),
|
|
1655
|
+
("tools", ToolsConfigPanel),
|
|
1656
|
+
("sub-agents", SubAgentsConfigPanel),
|
|
1657
|
+
("skills", SkillsConfigPanel),
|
|
1658
|
+
("mcp", MCPConfigPanel),
|
|
1659
|
+
("memory", MemoryConfigPanel),
|
|
1660
|
+
("compaction", CompactionConfigPanel),
|
|
1661
|
+
)
|
|
1662
|
+
)
|
|
1663
|
+
errors: list[str] = []
|
|
1664
|
+
for tab_id, panel_cls in panels:
|
|
1665
|
+
if tab_id not in self._mounted_tabs:
|
|
1666
|
+
continue
|
|
1667
|
+
try:
|
|
1668
|
+
errors.extend(self._live_panel(panel_cls).validate())
|
|
1669
|
+
except Exception:
|
|
1670
|
+
logger.debug("Validation skipped for %s", panel_cls.__name__, exc_info=True)
|
|
1671
|
+
|
|
1672
|
+
return errors
|
|
1673
|
+
|
|
1674
|
+
def _selected_draft_invalid_mcp_servers(self) -> set[int]:
|
|
1675
|
+
"""Positions of the selected draft's MCP servers that the saved-draft validation rejects.
|
|
1676
|
+
|
|
1677
|
+
That validation checks only dirty drafts: a clean selected draft reported
|
|
1678
|
+
no error, so none of its cards is named by the Save error.
|
|
1679
|
+
"""
|
|
1680
|
+
draft = self._drafts.get(self._selected_draft_key)
|
|
1681
|
+
if draft is None or not draft.dirty:
|
|
1682
|
+
return set()
|
|
1683
|
+
return {index for index, errors in enumerate(mcp_draft_errors_by_server("", draft.profile)) if errors}
|
|
1684
|
+
|
|
1685
|
+
def _unfold_invalid_mcp_cards(self, invalid_servers: Collection[int] = ()) -> None:
|
|
1686
|
+
"""Unfold the MCP server cards that failed validation: a folded card hides the fields its error names.
|
|
1687
|
+
|
|
1688
|
+
The panel checks its own cards; *invalid_servers* adds the positions
|
|
1689
|
+
Save's saved-draft validation rejected, which checks some fields differently.
|
|
1690
|
+
"""
|
|
1691
|
+
from chrys.app.tui.screens.agents.panels.mcp import MCPConfigPanel
|
|
1692
|
+
|
|
1693
|
+
draft = self._drafts.get(self._selected_draft_key)
|
|
1694
|
+
if "mcp" not in self._mounted_tabs or (draft is not None and draft.profile.acp is not None):
|
|
1695
|
+
return
|
|
1696
|
+
try:
|
|
1697
|
+
self._live_panel(MCPConfigPanel).unfold_invalid_cards(invalid_servers)
|
|
1698
|
+
except Exception:
|
|
1699
|
+
# As in _validate_all: a panel that can't validate is skipped, never fails the Save handler.
|
|
1700
|
+
logger.debug("Could not unfold the invalid MCP server cards", exc_info=True)
|
|
1701
|
+
|
|
1702
|
+
def _validate_drafts(self, retargeted_profiles: list[AgentProfile] | None = None) -> list[str]:
|
|
1703
|
+
"""Validate the staged draft graph before writing anything to disk."""
|
|
1704
|
+
model_profile_exists: Callable[[str], bool] | None = None
|
|
1705
|
+
model_registry = self._model_registry
|
|
1706
|
+
if model_registry is not None:
|
|
1707
|
+
|
|
1708
|
+
def model_profile_exists(profile_id: str) -> bool:
|
|
1709
|
+
return model_registry.get(profile_id) is not None
|
|
1710
|
+
|
|
1711
|
+
store = AgentDraftStore(
|
|
1712
|
+
self._visible_drafts(),
|
|
1713
|
+
self._registry.list_names(),
|
|
1714
|
+
model_profile_exists=model_profile_exists,
|
|
1715
|
+
workspace_cwd=self._workspace_cwd,
|
|
1716
|
+
workspace_roots=self._workspace_roots,
|
|
1717
|
+
)
|
|
1718
|
+
return store.validate(retargeted_profiles, render_message=self._render_toast)
|
|
1719
|
+
|
|
1720
|
+
# ── Apply ────────────────────────────────────────────────────────
|
|
1721
|
+
|
|
1722
|
+
def _apply_basic(self, updated, basic: dict) -> None:
|
|
1723
|
+
draft = self._drafts.get(self._selected_draft_key)
|
|
1724
|
+
self._apply_basic_to_profile(updated, basic, is_builtin=bool(draft and draft.is_builtin))
|
|
1725
|
+
|
|
1726
|
+
@staticmethod
|
|
1727
|
+
def _apply_basic_to_profile(updated, basic: dict, *, is_builtin: bool) -> None:
|
|
1728
|
+
from chrys.service.profiles.agents.schema import AcpAgentConfig, ModelConfig
|
|
1729
|
+
|
|
1730
|
+
if not is_builtin:
|
|
1731
|
+
updated.name = basic["name"]
|
|
1732
|
+
updated.display_name = basic["display_name"]
|
|
1733
|
+
updated.description = basic["description"]
|
|
1734
|
+
if basic.get("agent_type") == "acp":
|
|
1735
|
+
updated.acp = updated.acp or AcpAgentConfig()
|
|
1736
|
+
updated.sub_agent_only = True
|
|
1737
|
+
updated.model = ModelConfig()
|
|
1738
|
+
else:
|
|
1739
|
+
updated.acp = None
|
|
1740
|
+
updated.sub_agent_only = basic["sub_agent_only"]
|
|
1741
|
+
updated.model = ModelConfig(profile_id=basic.get("model_profile_id", ""))
|
|
1742
|
+
|
|
1743
|
+
def _sync_selected_draft_from_panels(self) -> None:
|
|
1744
|
+
"""Harvest the mounted panels into the selected draft profile."""
|
|
1745
|
+
if self._read_only:
|
|
1746
|
+
return
|
|
1747
|
+
if self._hydrating:
|
|
1748
|
+
# During tab/profile hydration the draft is already the source of truth.
|
|
1749
|
+
# Reading panels before their children settle can harvest stale widgets
|
|
1750
|
+
# from the previously mounted profile, especially on slower Windows CI.
|
|
1751
|
+
return
|
|
1752
|
+
draft = self._drafts.get(self._selected_draft_key)
|
|
1753
|
+
if draft is None:
|
|
1754
|
+
return
|
|
1755
|
+
try:
|
|
1756
|
+
draft.profile = self._build_profile_from_mounted_panels(draft)
|
|
1757
|
+
except Exception:
|
|
1758
|
+
# A field that can't be read (an MCP command with an unclosed quote) fails before validation.
|
|
1759
|
+
self._unfold_invalid_mcp_cards()
|
|
1760
|
+
raise
|
|
1761
|
+
self._selected_profile_name = draft.profile.name
|
|
1762
|
+
validation_errors = self._mounted_panel_validation_errors()
|
|
1763
|
+
self._set_draft_dirty(draft, self._is_draft_dirty(draft) or bool(validation_errors))
|
|
1764
|
+
if validation_errors:
|
|
1765
|
+
# Switching still validates the mounted draft strictly because leaving
|
|
1766
|
+
# invalid widget state would make the in-memory transaction ambiguous.
|
|
1767
|
+
# The user stays on this agent: show the cards whose fields the error names.
|
|
1768
|
+
self._unfold_invalid_mcp_cards()
|
|
1769
|
+
error_message = "\n".join(
|
|
1770
|
+
[
|
|
1771
|
+
*validation_errors,
|
|
1772
|
+
self._render_toast(FIX_VALIDATION_ERRORS.bind()),
|
|
1773
|
+
]
|
|
1774
|
+
)
|
|
1775
|
+
raise RuntimeError(error_message)
|
|
1776
|
+
|
|
1777
|
+
def _retarget_renamed_sub_agent_refs(self) -> list[AgentProfile]:
|
|
1778
|
+
"""Keep staged sub-agent references pointed at profiles renamed in this transaction."""
|
|
1779
|
+
name_counts: dict[str, int] = {}
|
|
1780
|
+
for draft in self._visible_drafts():
|
|
1781
|
+
name = draft.profile.name.strip()
|
|
1782
|
+
if name:
|
|
1783
|
+
lowered = name.casefold()
|
|
1784
|
+
name_counts[lowered] = name_counts.get(lowered, 0) + 1
|
|
1785
|
+
|
|
1786
|
+
rename_map = {
|
|
1787
|
+
draft.original_name: draft.profile.name
|
|
1788
|
+
for draft in self._visible_drafts()
|
|
1789
|
+
if draft.original_name
|
|
1790
|
+
and draft.profile.name
|
|
1791
|
+
and draft.original_name != draft.profile.name
|
|
1792
|
+
and re.match(r"^[a-zA-Z0-9][a-zA-Z0-9_-]*$", draft.profile.name)
|
|
1793
|
+
and name_counts.get(draft.profile.name.casefold()) == 1
|
|
1794
|
+
}
|
|
1795
|
+
if not rename_map:
|
|
1796
|
+
return []
|
|
1797
|
+
|
|
1798
|
+
for draft in self._visible_drafts():
|
|
1799
|
+
changed = False
|
|
1800
|
+
for ref in draft.profile.sub_agents.agents:
|
|
1801
|
+
new_name = rename_map.get(ref.profile)
|
|
1802
|
+
if new_name is None or new_name == ref.profile:
|
|
1803
|
+
continue
|
|
1804
|
+
ref.profile = new_name
|
|
1805
|
+
changed = True
|
|
1806
|
+
if changed:
|
|
1807
|
+
draft.dirty = True
|
|
1808
|
+
|
|
1809
|
+
represented_originals = {
|
|
1810
|
+
draft.original_name for draft in self._visible_drafts() if draft.original_name is not None
|
|
1811
|
+
}
|
|
1812
|
+
retargeted_profiles: list[AgentProfile] = []
|
|
1813
|
+
for profile in self._registry.list_profiles(include_sub_agent_only=True):
|
|
1814
|
+
if profile.name in represented_originals:
|
|
1815
|
+
continue
|
|
1816
|
+
updated = copy.deepcopy(profile)
|
|
1817
|
+
changed = False
|
|
1818
|
+
for ref in updated.sub_agents.agents:
|
|
1819
|
+
new_name = rename_map.get(ref.profile)
|
|
1820
|
+
if new_name is None or new_name == ref.profile:
|
|
1821
|
+
continue
|
|
1822
|
+
ref.profile = new_name
|
|
1823
|
+
changed = True
|
|
1824
|
+
if changed:
|
|
1825
|
+
retargeted_profiles.append(updated)
|
|
1826
|
+
return retargeted_profiles
|
|
1827
|
+
|
|
1828
|
+
def _live_panel[P: Widget](self, panel_cls: type[P]) -> P:
|
|
1829
|
+
"""Return the mounted ``panel_cls`` panel, skipping one that a profile load is replacing.
|
|
1830
|
+
|
|
1831
|
+
``_load_profile`` removes the old panels without waiting, and a removed
|
|
1832
|
+
panel stays in the DOM, ahead of its replacement, until all its children
|
|
1833
|
+
have exited. Reading it then yields its previous profile's values for the
|
|
1834
|
+
widgets already gone, which a harvest would write into the new draft.
|
|
1835
|
+
"""
|
|
1836
|
+
for panel in self.query(panel_cls).results(panel_cls):
|
|
1837
|
+
if not panel._pruning:
|
|
1838
|
+
return panel
|
|
1839
|
+
raise NoMatches(f"No {panel_cls.__name__} is mounted")
|
|
1840
|
+
|
|
1841
|
+
def _build_profile_from_mounted_panels(self, draft: _AgentDraft):
|
|
1842
|
+
"""Return a profile copy built from the currently mounted tab panels."""
|
|
1843
|
+
from chrys.app.tui.screens.agents.panels.acp import AcpConfigPanel
|
|
1844
|
+
from chrys.app.tui.screens.agents.panels.basic import BasicConfigPanel
|
|
1845
|
+
from chrys.app.tui.screens.agents.panels.compaction import CompactionConfigPanel
|
|
1846
|
+
from chrys.app.tui.screens.agents.panels.instructions import InstructionsConfigPanel
|
|
1847
|
+
from chrys.app.tui.screens.agents.panels.mcp import MCPConfigPanel
|
|
1848
|
+
from chrys.app.tui.screens.agents.panels.memory import MemoryConfigPanel
|
|
1849
|
+
from chrys.app.tui.screens.agents.panels.skills import SkillsConfigPanel
|
|
1850
|
+
from chrys.app.tui.screens.agents.panels.subagents import SubAgentsConfigPanel
|
|
1851
|
+
from chrys.app.tui.screens.agents.panels.tools import ToolsConfigPanel
|
|
1852
|
+
|
|
1853
|
+
builtin_panels: list[tuple[str, MessageDef, type[Any], Callable[[AgentProfile, Any], None]]] = [
|
|
1854
|
+
(
|
|
1855
|
+
"basic",
|
|
1856
|
+
_TAB_BASIC,
|
|
1857
|
+
BasicConfigPanel,
|
|
1858
|
+
lambda p, cfg: self._apply_basic_to_profile(p, cfg, is_builtin=draft.is_builtin),
|
|
1859
|
+
),
|
|
1860
|
+
(
|
|
1861
|
+
"instructions",
|
|
1862
|
+
_TAB_INSTRUCTIONS,
|
|
1863
|
+
InstructionsConfigPanel,
|
|
1864
|
+
lambda p, cfg: setattr(p, "instructions", cfg),
|
|
1865
|
+
),
|
|
1866
|
+
("tools", _TAB_TOOLS, ToolsConfigPanel, lambda p, cfg: setattr(p.tools, "builtins", cfg)),
|
|
1867
|
+
("sub-agents", _TAB_SUB_AGENTS, SubAgentsConfigPanel, lambda p, cfg: setattr(p, "sub_agents", cfg)),
|
|
1868
|
+
("skills", _TAB_SKILLS, SkillsConfigPanel, lambda p, cfg: setattr(p, "skills", cfg)),
|
|
1869
|
+
("mcp", _TAB_MCP, MCPConfigPanel, lambda p, cfg: setattr(p.tools, "mcp", cfg)),
|
|
1870
|
+
("memory", _TAB_MEMORY, MemoryConfigPanel, lambda p, cfg: setattr(p, "memory", cfg)),
|
|
1871
|
+
("compaction", _TAB_COMPACTION, CompactionConfigPanel, lambda p, cfg: setattr(p, "compaction", cfg)),
|
|
1872
|
+
]
|
|
1873
|
+
panels: list[tuple[str, MessageDef, type[Any], Callable[[AgentProfile, Any], None]]] = (
|
|
1874
|
+
[
|
|
1875
|
+
(
|
|
1876
|
+
"basic",
|
|
1877
|
+
_TAB_BASIC,
|
|
1878
|
+
BasicConfigPanel,
|
|
1879
|
+
lambda p, cfg: self._apply_basic_to_profile(p, cfg, is_builtin=draft.is_builtin),
|
|
1880
|
+
),
|
|
1881
|
+
("acp", _TAB_ACP_SHORT, AcpConfigPanel, lambda p, cfg: setattr(p, "acp", cfg)),
|
|
1882
|
+
]
|
|
1883
|
+
if draft.profile.acp is not None
|
|
1884
|
+
else builtin_panels
|
|
1885
|
+
)
|
|
1886
|
+
updated = copy.deepcopy(draft.profile)
|
|
1887
|
+
collected: list[tuple[Callable[[AgentProfile, Any], None], Any]] = []
|
|
1888
|
+
not_ready: list[str] = []
|
|
1889
|
+
failures: list[str] = []
|
|
1890
|
+
for tab_id, tab_title, panel_cls, applier in panels:
|
|
1891
|
+
if tab_id not in self._mounted_tabs:
|
|
1892
|
+
continue
|
|
1893
|
+
try:
|
|
1894
|
+
cfg = self._live_panel(panel_cls).get_config()
|
|
1895
|
+
except NoMatches as exc:
|
|
1896
|
+
not_ready.append(
|
|
1897
|
+
self._render_toast(
|
|
1898
|
+
_TAB_ERROR.bind(
|
|
1899
|
+
tab=self._render_toast(tab_title.bind()),
|
|
1900
|
+
error=DisplayBlock(str(exc)),
|
|
1901
|
+
)
|
|
1902
|
+
)
|
|
1903
|
+
)
|
|
1904
|
+
continue
|
|
1905
|
+
except Exception as exc:
|
|
1906
|
+
logger.debug("Failed to read %s tab config", tab_title.fallback, exc_info=True)
|
|
1907
|
+
failures.append(
|
|
1908
|
+
self._render_toast(
|
|
1909
|
+
_TAB_ERROR.bind(
|
|
1910
|
+
tab=self._render_toast(tab_title.bind()),
|
|
1911
|
+
error=DisplayBlock(str(exc)),
|
|
1912
|
+
)
|
|
1913
|
+
)
|
|
1914
|
+
)
|
|
1915
|
+
continue
|
|
1916
|
+
collected.append((applier, cfg))
|
|
1917
|
+
if not_ready:
|
|
1918
|
+
raise _AgentConfigPanelsNotReady(
|
|
1919
|
+
self._render_toast(_PANELS_STILL_MOUNTING.bind(details=DisplayBlock("\n".join(not_ready))))
|
|
1920
|
+
)
|
|
1921
|
+
if failures:
|
|
1922
|
+
raise RuntimeError(self._render_toast(_PANELS_READ_ERROR.bind(details=DisplayBlock("\n".join(failures)))))
|
|
1923
|
+
for applier, cfg in collected:
|
|
1924
|
+
applier(updated, cfg)
|
|
1925
|
+
self._canonicalize_profile_for_ui(updated)
|
|
1926
|
+
return updated
|
|
1927
|
+
|
|
1928
|
+
@staticmethod
|
|
1929
|
+
def _profile_yaml_path(name: str) -> Path:
|
|
1930
|
+
from chrys.foundation.platform import get_platform
|
|
1931
|
+
|
|
1932
|
+
return get_platform().config_dir / "agents" / f"{name}.yaml"
|
|
1933
|
+
|
|
1934
|
+
def _snapshot_save_state(self, affected_names: set[str]) -> _AgentSaveSnapshot:
|
|
1935
|
+
affected_paths = {self._profile_yaml_path(name) for name in affected_names}
|
|
1936
|
+
return _AgentSaveSnapshot(
|
|
1937
|
+
files={path: path.read_bytes() if path.is_file() else None for path in affected_paths},
|
|
1938
|
+
registry=self._registry.snapshot(),
|
|
1939
|
+
current_profile=self._current_profile,
|
|
1940
|
+
active_profile_name=self._active_profile_name,
|
|
1941
|
+
)
|
|
1942
|
+
|
|
1943
|
+
def _restore_save_state(self, snapshot: _AgentSaveSnapshot) -> None:
|
|
1944
|
+
self._current_profile = snapshot.current_profile
|
|
1945
|
+
self._active_profile_name = snapshot.active_profile_name
|
|
1946
|
+
self._registry.restore(snapshot.registry)
|
|
1947
|
+
from chrys.foundation.platform.files import atomic_write_owner_only_bytes
|
|
1948
|
+
|
|
1949
|
+
for path, content in snapshot.files.items():
|
|
1950
|
+
if content is None:
|
|
1951
|
+
with contextlib.suppress(FileNotFoundError):
|
|
1952
|
+
path.unlink()
|
|
1953
|
+
else:
|
|
1954
|
+
# Same writer as save_profile: a rolled-back file must keep the
|
|
1955
|
+
# owner-only mode the serializer gave it, not the umask default.
|
|
1956
|
+
atomic_write_owner_only_bytes(path, content)
|
|
1957
|
+
|
|
1958
|
+
def _old_profile_path_is_still_targeted(self, old_name: str, target_names: set[str]) -> bool:
|
|
1959
|
+
old_path = self._profile_yaml_path(old_name)
|
|
1960
|
+
for target_name in target_names:
|
|
1961
|
+
target_path = self._profile_yaml_path(target_name)
|
|
1962
|
+
if old_path == target_path:
|
|
1963
|
+
return True
|
|
1964
|
+
with contextlib.suppress(OSError):
|
|
1965
|
+
if old_path.exists() and target_path.exists() and old_path.samefile(target_path):
|
|
1966
|
+
return True
|
|
1967
|
+
return False
|
|
1968
|
+
|
|
1969
|
+
def _should_persist_retargeted_profile(self, profile: AgentProfile) -> bool:
|
|
1970
|
+
return not self._registry.is_builtin(profile.name) or self._profile_yaml_path(profile.name).is_file()
|
|
1971
|
+
|
|
1972
|
+
def _profile_matches_builtin_template(self, profile: AgentProfile, builtin_name: str) -> bool:
|
|
1973
|
+
template = self._registry.get_builtin_template(builtin_name)
|
|
1974
|
+
if template is None:
|
|
1975
|
+
return False
|
|
1976
|
+
from chrys.service.profiles.agents.serializer import profile_to_dict
|
|
1977
|
+
|
|
1978
|
+
candidate = copy.deepcopy(profile)
|
|
1979
|
+
self._canonicalize_profile_for_ui(candidate)
|
|
1980
|
+
self._canonicalize_profile_for_ui(template)
|
|
1981
|
+
return profile_to_dict(candidate) == profile_to_dict(template)
|
|
1982
|
+
|
|
1983
|
+
def _reset_matches_builtin_template(self, draft: _AgentDraft) -> bool:
|
|
1984
|
+
return (
|
|
1985
|
+
draft.reset_to_builtin
|
|
1986
|
+
and draft.original_name is not None
|
|
1987
|
+
and self._profile_matches_builtin_template(draft.profile, draft.original_name)
|
|
1988
|
+
)
|
|
1989
|
+
|
|
1990
|
+
def _perform_save(
|
|
1991
|
+
self,
|
|
1992
|
+
dirty_drafts: list[_AgentDraft],
|
|
1993
|
+
retargeted_profiles: list[AgentProfile],
|
|
1994
|
+
*,
|
|
1995
|
+
old_names: set[str],
|
|
1996
|
+
target_names: set[str],
|
|
1997
|
+
old_active_name: str,
|
|
1998
|
+
) -> tuple[str | None, str | None]:
|
|
1999
|
+
from chrys.service.profiles.agents.serializer import delete_profile, save_profile
|
|
2000
|
+
|
|
2001
|
+
# Write everything before deleting anything: a save that fails after an
|
|
2002
|
+
# exact-template reset already removed its shadow would leave the
|
|
2003
|
+
# rollback to recreate that file instead of never touching it.
|
|
2004
|
+
reset_drafts: list[_AgentDraft] = []
|
|
2005
|
+
for draft in dirty_drafts:
|
|
2006
|
+
if self._reset_matches_builtin_template(draft):
|
|
2007
|
+
reset_drafts.append(draft)
|
|
2008
|
+
else:
|
|
2009
|
+
save_profile(draft.profile)
|
|
2010
|
+
for profile in retargeted_profiles:
|
|
2011
|
+
if self._should_persist_retargeted_profile(profile):
|
|
2012
|
+
save_profile(profile)
|
|
2013
|
+
for draft in reset_drafts:
|
|
2014
|
+
delete_profile(draft.profile.name)
|
|
2015
|
+
|
|
2016
|
+
for old_name in old_names:
|
|
2017
|
+
old_path = self._profile_yaml_path(old_name)
|
|
2018
|
+
if not self._old_profile_path_is_still_targeted(old_name, target_names) and old_path.is_file():
|
|
2019
|
+
old_path.unlink()
|
|
2020
|
+
|
|
2021
|
+
dirty_target_names = {draft.profile.name for draft in dirty_drafts}
|
|
2022
|
+
for old_name in old_names:
|
|
2023
|
+
# Registry keys are intentionally case-sensitive here: case-only renames
|
|
2024
|
+
# need only the old exact key removed, without matching the new key.
|
|
2025
|
+
# cascade=False is required because refs were already retargeted; cascading
|
|
2026
|
+
# here would strip valid parent refs to the renamed profile.
|
|
2027
|
+
if old_name not in dirty_target_names:
|
|
2028
|
+
self._registry.remove(old_name, cascade=False)
|
|
2029
|
+
|
|
2030
|
+
active_new_display: str | None = None
|
|
2031
|
+
active_new_name: str | None = None
|
|
2032
|
+
for draft in dirty_drafts:
|
|
2033
|
+
updated = draft.profile
|
|
2034
|
+
old_name = draft.original_name
|
|
2035
|
+
self._registry.register(updated)
|
|
2036
|
+
|
|
2037
|
+
# Detect edits to the session-active profile so the main
|
|
2038
|
+
# screen can refresh its input-bar tag / subtitle on Save.
|
|
2039
|
+
if old_name == old_active_name:
|
|
2040
|
+
new_display = updated.display_name or updated.name
|
|
2041
|
+
if new_display != self._current_profile:
|
|
2042
|
+
active_new_display = new_display
|
|
2043
|
+
if updated.name != self._active_profile_name:
|
|
2044
|
+
active_new_name = updated.name
|
|
2045
|
+
|
|
2046
|
+
for profile in retargeted_profiles:
|
|
2047
|
+
self._registry.register(profile)
|
|
2048
|
+
return active_new_display, active_new_name
|
|
2049
|
+
|
|
2050
|
+
def _apply_all(self, retargeted_profiles: list[AgentProfile] | None = None) -> None:
|
|
2051
|
+
if self._read_only:
|
|
2052
|
+
return
|
|
2053
|
+
dirty_drafts = [draft for draft in self._drafts.values() if draft.dirty]
|
|
2054
|
+
retargeted_profiles = retargeted_profiles or []
|
|
2055
|
+
if not dirty_drafts and not retargeted_profiles:
|
|
2056
|
+
return
|
|
2057
|
+
|
|
2058
|
+
old_active_name = self._active_profile_name
|
|
2059
|
+
active_new_display: str | None = None
|
|
2060
|
+
active_new_name: str | None = None
|
|
2061
|
+
|
|
2062
|
+
renamed_drafts = [
|
|
2063
|
+
draft for draft in dirty_drafts if draft.original_name and draft.profile.name != draft.original_name
|
|
2064
|
+
]
|
|
2065
|
+
|
|
2066
|
+
old_names = {draft.original_name for draft in renamed_drafts if draft.original_name is not None}
|
|
2067
|
+
target_names = {draft.profile.name for draft in dirty_drafts}
|
|
2068
|
+
target_names.update(
|
|
2069
|
+
profile.name for profile in retargeted_profiles if self._should_persist_retargeted_profile(profile)
|
|
2070
|
+
)
|
|
2071
|
+
snapshot = self._snapshot_save_state(old_names | target_names)
|
|
2072
|
+
|
|
2073
|
+
try:
|
|
2074
|
+
active_new_display, active_new_name = self._perform_save(
|
|
2075
|
+
dirty_drafts,
|
|
2076
|
+
retargeted_profiles,
|
|
2077
|
+
old_names=old_names,
|
|
2078
|
+
target_names=target_names,
|
|
2079
|
+
old_active_name=old_active_name,
|
|
2080
|
+
)
|
|
2081
|
+
except Exception:
|
|
2082
|
+
self._restore_save_state(snapshot)
|
|
2083
|
+
raise
|
|
2084
|
+
|
|
2085
|
+
if active_new_display is not None:
|
|
2086
|
+
self._current_profile = active_new_display
|
|
2087
|
+
if active_new_name is not None:
|
|
2088
|
+
self._active_profile_name = active_new_name
|
|
2089
|
+
|
|
2090
|
+
self._set_dismiss_intent(_DismissIntent.UPDATED)
|
|
2091
|
+
selected_name = self._selected_profile_name
|
|
2092
|
+
|
|
2093
|
+
# Rebuilding the drafts drops their keys: carry each agent's MCP card
|
|
2094
|
+
# folds over by the name it was saved under.
|
|
2095
|
+
self._remember_mcp_card_folds()
|
|
2096
|
+
mcp_card_folds = {
|
|
2097
|
+
self._drafts[key].profile.name: folds for key, folds in self._mcp_card_folds.items() if key in self._drafts
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
# Refresh sidebar — a rename or description change should show
|
|
2101
|
+
# up immediately, and the user may keep editing.
|
|
2102
|
+
self._initialize_drafts()
|
|
2103
|
+
for name, folds in mcp_card_folds.items():
|
|
2104
|
+
if (key := self._find_visible_draft_key(name)) is not None:
|
|
2105
|
+
self._mcp_card_folds[key] = folds
|
|
2106
|
+
self._populate_sidebar()
|
|
2107
|
+
selected_key = self._find_visible_draft_key(selected_name)
|
|
2108
|
+
if selected_key is None:
|
|
2109
|
+
visible = self._visible_drafts()
|
|
2110
|
+
selected_key = visible[0].key if visible else ""
|
|
2111
|
+
ol = self.query_one("#ac-list", OptionList)
|
|
2112
|
+
with contextlib.suppress(OptionDoesNotExist):
|
|
2113
|
+
ol.highlighted = ol.get_option_index(selected_key)
|
|
2114
|
+
if selected_key:
|
|
2115
|
+
# Re-mount all tab panels against the freshly-saved profile.
|
|
2116
|
+
self._load_profile(selected_key, mcp_card_folds_carried=True)
|
|
2117
|
+
self._update_save_button_state()
|
|
2118
|
+
|
|
2119
|
+
# Notify the caller of any active-profile change so main-screen widgets
|
|
2120
|
+
# refresh immediately and registry-name renames queue a switch for Close.
|
|
2121
|
+
if (active_new_display is not None or active_new_name is not None) and self._on_saved is not None:
|
|
2122
|
+
self._on_saved(active_new_display, active_new_name)
|
|
2123
|
+
elif active_new_name is not None:
|
|
2124
|
+
self._set_dismiss_intent(_DismissIntent.SWITCHED)
|
|
2125
|
+
|
|
2126
|
+
# A save can retire the session-active profile from main duty (the
|
|
2127
|
+
# External ACP conversion forces ``sub_agent_only``). The engine must
|
|
2128
|
+
# never be pointed at a sub-agent-only profile — reload rejects an ACP
|
|
2129
|
+
# main and the next startup would refuse the persisted config — so
|
|
2130
|
+
# promote a remaining main-eligible profile, exactly like delete does.
|
|
2131
|
+
# This runs after the rename notification above so its queued switch
|
|
2132
|
+
# overrides a rename that would otherwise re-target the retired name.
|
|
2133
|
+
active = self._registry.get(self._active_profile_name)
|
|
2134
|
+
if active is not None and active.sub_agent_only:
|
|
2135
|
+
remaining_main = [p for p in self._registry.list_profiles() if p.name != active.name]
|
|
2136
|
+
if remaining_main:
|
|
2137
|
+
promoted = remaining_main[0]
|
|
2138
|
+
self._active_profile_name = promoted.name
|
|
2139
|
+
self._current_profile = promoted.display_name or promoted.name
|
|
2140
|
+
self._set_dismiss_intent(_DismissIntent.SWITCHED)
|
|
2141
|
+
if self._on_saved is not None:
|
|
2142
|
+
self._on_saved(None, promoted.name)
|