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,2262 @@
|
|
|
1
|
+
# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""MCP configuration panel — composable widget for the MCP tab."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import contextlib
|
|
9
|
+
import logging
|
|
10
|
+
import shlex
|
|
11
|
+
import socket
|
|
12
|
+
from collections.abc import Callable, Collection, Sequence
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from functools import partial
|
|
15
|
+
from typing import TYPE_CHECKING, Literal, TypeGuard
|
|
16
|
+
|
|
17
|
+
from rich.markup import escape
|
|
18
|
+
from textual import on, work
|
|
19
|
+
from textual.containers import Horizontal, Vertical, VerticalScroll
|
|
20
|
+
from textual.css.query import NoMatches
|
|
21
|
+
from textual.message import Message
|
|
22
|
+
from textual.widget import Widget
|
|
23
|
+
from textual.widgets import Button, Label
|
|
24
|
+
|
|
25
|
+
from chrys.app.tui.i18n import render_str, widget_locale_controller, widget_localizer
|
|
26
|
+
from chrys.app.tui.screens.agents.panels.config_card import CardListChanged, CollapsibleConfigCard
|
|
27
|
+
from chrys.app.tui.screens.agents.panels.markdown_report import (
|
|
28
|
+
NOT_ADVERTISED,
|
|
29
|
+
SUPPORTED_MARK,
|
|
30
|
+
UNSUPPORTED_MARK,
|
|
31
|
+
code_span,
|
|
32
|
+
definition_bullets,
|
|
33
|
+
inline_text,
|
|
34
|
+
md_table,
|
|
35
|
+
)
|
|
36
|
+
from chrys.app.tui.screens.agents.validation_messages import (
|
|
37
|
+
CONTEXT_ERROR,
|
|
38
|
+
DUPLICATE_KEY_ROW,
|
|
39
|
+
FIELD_REQUIRED,
|
|
40
|
+
MCP_COMMAND_REQUIRED,
|
|
41
|
+
MCP_DUPLICATE_SERVER,
|
|
42
|
+
MCP_INITIAL_TOOLS_PERMITTED,
|
|
43
|
+
MCP_INITIALLY_VISIBLE_TOOLS_VALIDATION,
|
|
44
|
+
MCP_INVALID_COMMAND,
|
|
45
|
+
MCP_ROW_KEY_REQUIRED,
|
|
46
|
+
MCP_ROW_VALUE_REQUIRED,
|
|
47
|
+
MCP_SELECT_PERMITTED,
|
|
48
|
+
MCP_SERVER_CONTEXT,
|
|
49
|
+
MCP_TIMEOUT_POSITIVE,
|
|
50
|
+
MCP_TIMEOUT_VALID,
|
|
51
|
+
MCP_TOOL_LIST_DUPLICATE,
|
|
52
|
+
MCP_TOOL_LIST_EMPTY,
|
|
53
|
+
MCP_URL_REQUIRED,
|
|
54
|
+
MCP_URL_SCHEME,
|
|
55
|
+
NAME_FIELD,
|
|
56
|
+
SERVER_ERROR,
|
|
57
|
+
TOOL_NAME_ITEM,
|
|
58
|
+
)
|
|
59
|
+
from chrys.app.tui.screens.agents.validation_messages import (
|
|
60
|
+
MCP_ENVIRONMENT_VARIABLES as _ENVIRONMENT_VARIABLES,
|
|
61
|
+
)
|
|
62
|
+
from chrys.app.tui.screens.agents.validation_messages import (
|
|
63
|
+
MCP_HEADERS as _HEADERS,
|
|
64
|
+
)
|
|
65
|
+
from chrys.app.tui.screens.agents.validation_messages import (
|
|
66
|
+
MCP_INITIALLY_VISIBLE_TOOLS as _INITIALLY_VISIBLE_TOOLS,
|
|
67
|
+
)
|
|
68
|
+
from chrys.app.tui.screens.agents.validation_messages import (
|
|
69
|
+
MCP_SELECTED_TOOL_NAMES as _SELECTED_TOOL_NAMES,
|
|
70
|
+
)
|
|
71
|
+
from chrys.app.tui.screens.agents.validation_messages import (
|
|
72
|
+
MCP_TOOL_ACCESS_NONE as _TOOL_ACCESS_NONE_LABEL,
|
|
73
|
+
)
|
|
74
|
+
from chrys.app.tui.screens.dialogs.confirm import ConfirmDialog
|
|
75
|
+
from chrys.app.tui.screens.dialogs.connection_test import ConnectionTestDialog
|
|
76
|
+
from chrys.app.tui.widgets import Checkbox, ConfigAddButton, EnhancedTextArea, HatchedEmptyState, Select
|
|
77
|
+
from chrys.app.tui.widgets import EnhancedInput as Input
|
|
78
|
+
from chrys.foundation.branding import APP_DISPLAY_NAME
|
|
79
|
+
from chrys.foundation.i18n import DisplayBlock, MessageDef, MessageRef, msg
|
|
80
|
+
from chrys.foundation.i18n.formatting import format_message
|
|
81
|
+
from chrys.foundation.platform import get_platform
|
|
82
|
+
from chrys.foundation.platform.command_line import join_windows_command_line, split_windows_command_line
|
|
83
|
+
from chrys.service.mcp.adapter import (
|
|
84
|
+
DEFAULT_CONNECT_TIMEOUT_SECONDS,
|
|
85
|
+
STANDARD_CAPABILITY_GROUPS,
|
|
86
|
+
MCPAdapter,
|
|
87
|
+
MCPToolConfigurationError,
|
|
88
|
+
)
|
|
89
|
+
from chrys.service.mcp.validation import MCP_PROGRESSIVE_CONTROL_TOOL_NAMES, validate_mcp_tool_name_prefix
|
|
90
|
+
from chrys.service.tools.names import chrys_reserved_tool_names
|
|
91
|
+
|
|
92
|
+
logger = logging.getLogger(__name__)
|
|
93
|
+
|
|
94
|
+
_MCP_TITLE = msg("tui.mcp.title", fallback="MCP")
|
|
95
|
+
_MCP_SERVER_SUBJECT = msg("tui.mcp.connection_test.subject", fallback="MCP Server")
|
|
96
|
+
_WAIT_BEFORE_ADDING = msg(
|
|
97
|
+
"tui.mcp.wait_before_adding",
|
|
98
|
+
fallback="Wait for MCP connection tests to finish before adding servers.",
|
|
99
|
+
)
|
|
100
|
+
_WAIT_BEFORE_REMOVING = msg(
|
|
101
|
+
"tui.mcp.wait_before_removing",
|
|
102
|
+
fallback="Wait for MCP connection tests to finish before removing servers.",
|
|
103
|
+
)
|
|
104
|
+
_DELETE_SERVER_TITLE = msg("tui.mcp.confirm_delete.title", fallback="Delete MCP Server")
|
|
105
|
+
_DELETE_SERVER_MESSAGE = msg(
|
|
106
|
+
"tui.mcp.confirm_delete.message",
|
|
107
|
+
fallback='Delete MCP server\n"{name}"?',
|
|
108
|
+
multiline=True,
|
|
109
|
+
)
|
|
110
|
+
_DELETE_SERVER_CONFIRM = msg("tui.mcp.confirm_delete.confirm", fallback="Delete")
|
|
111
|
+
_TOOL_ACCESS_ALL_LABEL = msg("tui.mcp.tool_access.all", fallback="All server tools")
|
|
112
|
+
_TOOL_ACCESS_SELECTED_LABEL = msg("tui.mcp.tool_access.selected", fallback="Only selected tools")
|
|
113
|
+
_TOOL_LOADING_FULL_LABEL = msg(
|
|
114
|
+
"tui.mcp.tool_loading.full",
|
|
115
|
+
fallback="Full — load all available tools",
|
|
116
|
+
)
|
|
117
|
+
_TOOL_LOADING_PROGRESSIVE_LABEL = msg(
|
|
118
|
+
"tui.mcp.tool_loading.progressive",
|
|
119
|
+
fallback="On demand — load tools as needed",
|
|
120
|
+
)
|
|
121
|
+
_LOAD_PROMPTS_TOOLTIP = msg(
|
|
122
|
+
"tui.mcp.load_prompts_tooltip",
|
|
123
|
+
fallback=(
|
|
124
|
+
"When enabled, {app} loads prompt templates advertised by this MCP server as callable tools. If the server "
|
|
125
|
+
"does not advertise prompt support, prompt loading is skipped automatically. Disable this to keep server "
|
|
126
|
+
"prompt templates out of the model's tool list."
|
|
127
|
+
),
|
|
128
|
+
)
|
|
129
|
+
_LOAD_PROMPTS_HINT = msg(
|
|
130
|
+
"tui.mcp.load_prompts_hint",
|
|
131
|
+
fallback="Prompt templates advertised by the server are loaded as callable tools in the model's tool list.",
|
|
132
|
+
)
|
|
133
|
+
_EXPOSE_INSTRUCTIONS_TOOLTIP = msg(
|
|
134
|
+
"tui.mcp.expose_instructions_tooltip",
|
|
135
|
+
fallback=(
|
|
136
|
+
"When enabled, {app} includes the server's usage instructions from the MCP handshake in the model's context "
|
|
137
|
+
"as a per-turn system reminder. Servers that provide no instructions are unaffected. Disable this to keep "
|
|
138
|
+
"the server's guidance out of the model's context."
|
|
139
|
+
),
|
|
140
|
+
)
|
|
141
|
+
_EXPOSE_INSTRUCTIONS_HINT = msg(
|
|
142
|
+
"tui.mcp.expose_instructions_hint",
|
|
143
|
+
fallback="The server's usage guidance from the MCP handshake is injected as a system reminder.",
|
|
144
|
+
)
|
|
145
|
+
_TOOL_ACCESS_TOOLTIP = msg(
|
|
146
|
+
"tui.mcp.tool_access_tooltip",
|
|
147
|
+
fallback=(
|
|
148
|
+
"Defines the MCP catalog boundary. Choose all tools advertised by the server, an explicit allowlist, or no "
|
|
149
|
+
"callable tools."
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
_PROGRESSIVE_DISCLOSURE_TOOLTIP = msg(
|
|
153
|
+
"tui.mcp.progressive_disclosure_tooltip",
|
|
154
|
+
fallback=(
|
|
155
|
+
"When enabled, only loader controls and the optional initially visible tools are sent first. The model can "
|
|
156
|
+
"load other tools from the available-tool scope during the current run."
|
|
157
|
+
),
|
|
158
|
+
)
|
|
159
|
+
_ALWAYS_LOAD_TOOLTIP = msg(
|
|
160
|
+
"tui.mcp.always_load_tooltip",
|
|
161
|
+
fallback="Optional comma-separated names from the available-tool scope to send to the model from the first request.",
|
|
162
|
+
)
|
|
163
|
+
_HEADER_NAME_PLACEHOLDER = msg(
|
|
164
|
+
"tui.mcp.placeholder.header_name",
|
|
165
|
+
fallback="e.g. X-Auth-Token",
|
|
166
|
+
)
|
|
167
|
+
_HEADER_VALUE_PLACEHOLDER = msg(
|
|
168
|
+
"tui.mcp.placeholder.header_value",
|
|
169
|
+
fallback="e.g. {{{{AUTH_TOKEN}}}}",
|
|
170
|
+
)
|
|
171
|
+
_VARIABLE_NAME_PLACEHOLDER = msg("tui.mcp.placeholder.variable_name", fallback="Variable name")
|
|
172
|
+
_VALUE_PLACEHOLDER = msg("tui.mcp.placeholder.value", fallback="Value")
|
|
173
|
+
_TRANSPORT = msg("tui.mcp.transport", fallback="Transport")
|
|
174
|
+
_SERVER_NAME = msg("tui.mcp.server_name", fallback="Server Name")
|
|
175
|
+
_SERVER_NAME_PLACEHOLDER = msg("tui.mcp.placeholder.server_name", fallback="Server name")
|
|
176
|
+
_DESCRIPTION = msg("tui.mcp.description", fallback="Description")
|
|
177
|
+
_PROMPTS_AND_INSTRUCTIONS = msg(
|
|
178
|
+
"tui.mcp.prompts_and_instructions",
|
|
179
|
+
fallback="Server Instructions & Prompt Templates",
|
|
180
|
+
)
|
|
181
|
+
_EXPOSE_SERVER_INSTRUCTIONS = msg(
|
|
182
|
+
"tui.mcp.expose_server_instructions",
|
|
183
|
+
fallback="Include server instructions in model context (if available)",
|
|
184
|
+
)
|
|
185
|
+
_EXPOSE_SERVER_PROMPTS = msg(
|
|
186
|
+
"tui.mcp.expose_server_prompts",
|
|
187
|
+
fallback="Load server prompt templates as tools (if available)",
|
|
188
|
+
)
|
|
189
|
+
_TOOL_ACCESS = msg("tui.mcp.tool_access", fallback="Tool Access")
|
|
190
|
+
_PERMITTED_TOOL_SET = msg("tui.mcp.permitted_tool_set", fallback="Available Tool Scope")
|
|
191
|
+
_SELECTED_TOOLS_PLACEHOLDER = msg("tui.mcp.placeholder.selected_tools", fallback="tool_a, tool_b")
|
|
192
|
+
_TOOL_LOADING = msg("tui.mcp.tool_loading", fallback="Tool Loading")
|
|
193
|
+
_LOADING_STRATEGY = msg("tui.mcp.loading_strategy", fallback="Loading Strategy")
|
|
194
|
+
_INITIAL_TOOLS_PLACEHOLDER = msg("tui.mcp.placeholder.initial_tools", fallback="search, read_file")
|
|
195
|
+
_NAMING_AND_LIMITS = msg("tui.mcp.naming_and_limits", fallback="Naming & Limits")
|
|
196
|
+
_TOOL_NAME_PREFIX = msg("tui.mcp.tool_name_prefix", fallback="Tool Name Prefix")
|
|
197
|
+
_TOOL_NAME_PREFIX_PLACEHOLDER = msg(
|
|
198
|
+
"tui.mcp.placeholder.tool_name_prefix",
|
|
199
|
+
fallback="e.g. github → tools named github_<tool>",
|
|
200
|
+
)
|
|
201
|
+
_REQUEST_TIMEOUT = msg("tui.mcp.request_timeout", fallback="MCP Request Timeout (seconds)")
|
|
202
|
+
_REQUEST_TIMEOUT_PLACEHOLDER = msg(
|
|
203
|
+
"tui.mcp.placeholder.request_timeout",
|
|
204
|
+
fallback="Leave blank for default ({default})",
|
|
205
|
+
)
|
|
206
|
+
_ENABLED = msg("tui.mcp.enabled", fallback="Enabled")
|
|
207
|
+
_TEST = msg("tui.mcp.test", fallback="Test")
|
|
208
|
+
_TESTING = msg("tui.mcp.testing", fallback="Testing...")
|
|
209
|
+
_URL = msg("tui.mcp.url", fallback="URL")
|
|
210
|
+
_URL_PLACEHOLDER = msg("tui.mcp.placeholder.url", fallback="https://api.example.com/mcp")
|
|
211
|
+
_SKIP_TLS_VERIFICATION = msg("tui.mcp.skip_tls_verification", fallback="Skip TLS verification")
|
|
212
|
+
_INSECURE_TLS = msg(
|
|
213
|
+
"tui.mcp.insecure_tls",
|
|
214
|
+
fallback="Insecure: disables HTTPS certificate validation. Use only for trusted self-signed servers.",
|
|
215
|
+
)
|
|
216
|
+
_BYPASS_PROXY = msg("tui.mcp.bypass_proxy", fallback="Bypass proxy")
|
|
217
|
+
_COMMAND = msg("tui.mcp.command", fallback="Command")
|
|
218
|
+
_COMMAND_PLACEHOLDER = msg(
|
|
219
|
+
"tui.mcp.placeholder.command",
|
|
220
|
+
fallback="e.g. npx -y @modelcontextprotocol/server-filesystem /path",
|
|
221
|
+
)
|
|
222
|
+
_HTTP_OPTIONS = msg("tui.mcp.http_options", fallback="HTTP Options")
|
|
223
|
+
_STDIO_OPTIONS = msg("tui.mcp.stdio_options", fallback="STDIO Options")
|
|
224
|
+
_ADD = msg("tui.mcp.add", fallback="+ Add")
|
|
225
|
+
_SERVERS = msg("tui.mcp.servers", fallback="MCP Servers")
|
|
226
|
+
_SERVERS_DESCRIPTION = msg(
|
|
227
|
+
"tui.mcp.servers_description",
|
|
228
|
+
fallback="Configure Model Context Protocol servers",
|
|
229
|
+
)
|
|
230
|
+
_EMPTY = msg("tui.mcp.empty", fallback="No MCP servers configured")
|
|
231
|
+
_CONNECTION_SUCCESSFUL = msg("tui.mcp.connection_test.successful", fallback="Connection successful.")
|
|
232
|
+
_CONNECTION_CONFIGURATION_CONFLICT = msg(
|
|
233
|
+
"tui.mcp.connection_test.error.configuration_conflict",
|
|
234
|
+
fallback="MCP tool configuration conflict.",
|
|
235
|
+
)
|
|
236
|
+
_CONNECTION_CANCELLED = msg(
|
|
237
|
+
"tui.mcp.connection_test.error.cancelled",
|
|
238
|
+
fallback="Connection test was cancelled. Please try again.",
|
|
239
|
+
)
|
|
240
|
+
_CONNECTION_TIMED_OUT = msg(
|
|
241
|
+
"tui.mcp.connection_test.error.timed_out",
|
|
242
|
+
fallback="Connection test timed out. Please check server and timeout settings.",
|
|
243
|
+
)
|
|
244
|
+
_CONNECTION_UNREACHABLE = msg(
|
|
245
|
+
"tui.mcp.connection_test.error.unreachable",
|
|
246
|
+
fallback="Unable to reach MCP server. Please check URL, network, and server status.",
|
|
247
|
+
)
|
|
248
|
+
_CONNECTION_REQUEST_FAILED = msg(
|
|
249
|
+
"tui.mcp.connection_test.error.request_failed",
|
|
250
|
+
fallback="MCP HTTP request failed. Please verify endpoint and authentication.",
|
|
251
|
+
)
|
|
252
|
+
_CONNECTION_FAILED = msg(
|
|
253
|
+
"tui.mcp.connection_test.error.failed",
|
|
254
|
+
fallback="MCP server connection failed.",
|
|
255
|
+
)
|
|
256
|
+
_CAPABILITIES_HEADING = msg("tui.mcp.connection_report.heading.capabilities", fallback="Capabilities")
|
|
257
|
+
_INSTRUCTIONS_HEADING = msg("tui.mcp.connection_report.heading.instructions", fallback="Instructions")
|
|
258
|
+
_TOOLS_HEADING = msg("tui.mcp.connection_report.heading.tools", fallback="Tools")
|
|
259
|
+
_PROMPTS_HEADING = msg("tui.mcp.connection_report.heading.prompts", fallback="Prompts")
|
|
260
|
+
_PROGRESSIVE_DISCLOSURE_HEADING = msg(
|
|
261
|
+
"tui.mcp.connection_report.heading.progressive_disclosure",
|
|
262
|
+
fallback="On-demand Loading",
|
|
263
|
+
)
|
|
264
|
+
_IDENTITY_NOT_REPORTED = msg(
|
|
265
|
+
"tui.mcp.connection_report.identity_not_reported",
|
|
266
|
+
fallback="*The server did not report its identity.*",
|
|
267
|
+
)
|
|
268
|
+
_NO_COMMAND_CONFIGURED = msg(
|
|
269
|
+
"tui.mcp.connection_report.no_command_configured",
|
|
270
|
+
fallback="*no command configured*",
|
|
271
|
+
)
|
|
272
|
+
_NO_URL_CONFIGURED = msg(
|
|
273
|
+
"tui.mcp.connection_report.no_url_configured",
|
|
274
|
+
fallback="*no URL configured*",
|
|
275
|
+
)
|
|
276
|
+
_CONNECTED_VIA = msg(
|
|
277
|
+
"tui.mcp.connection_report.connected_via",
|
|
278
|
+
fallback="Connected via {transport}: {target}",
|
|
279
|
+
)
|
|
280
|
+
_TOOLS_EXPOSED = msg(
|
|
281
|
+
"tui.mcp.connection_report.tools_exposed",
|
|
282
|
+
fallback="{count} tool available to the model.\n\n{catalog}",
|
|
283
|
+
plural_fallback="{count} tools available to the model.\n\n{catalog}",
|
|
284
|
+
multiline=True,
|
|
285
|
+
)
|
|
286
|
+
_ADVERTISED_NO_TOOLS = msg(
|
|
287
|
+
"tui.mcp.connection_report.advertised_no_tools",
|
|
288
|
+
fallback="*The server supports tools, but none are available (check the available-tool scope).*",
|
|
289
|
+
)
|
|
290
|
+
_MORE_ENTRIES = msg("tui.mcp.connection_report.more_entries", fallback="…and {extra_count} more.")
|
|
291
|
+
_CAPABILITY_HEADER = msg("tui.mcp.connection_report.header.capability", fallback="Capability")
|
|
292
|
+
_ADVERTISED_HEADER = msg("tui.mcp.connection_report.header.advertised", fallback="Advertised")
|
|
293
|
+
_PROMPTS_EXPOSED = msg(
|
|
294
|
+
"tui.mcp.connection_report.prompts_exposed",
|
|
295
|
+
fallback="{count} prompt template loaded as a tool.\n\n{catalog}",
|
|
296
|
+
plural_fallback="{count} prompt templates loaded as tools.\n\n{catalog}",
|
|
297
|
+
multiline=True,
|
|
298
|
+
)
|
|
299
|
+
_PROMPT_LOADING_DISABLED = msg(
|
|
300
|
+
"tui.mcp.connection_report.prompt_loading_disabled",
|
|
301
|
+
fallback="*Prompt loading is disabled for this server.*",
|
|
302
|
+
)
|
|
303
|
+
_ADVERTISED_NO_PROMPTS = msg(
|
|
304
|
+
"tui.mcp.connection_report.advertised_no_prompts",
|
|
305
|
+
fallback="*The server supports prompt templates, but none are available.*",
|
|
306
|
+
)
|
|
307
|
+
_PROGRESSIVE_ENABLED = msg(
|
|
308
|
+
"tui.mcp.connection_report.progressive_enabled",
|
|
309
|
+
fallback="Enabled — only these tools are visible initially; the rest load on demand:",
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
if TYPE_CHECKING:
|
|
313
|
+
from textual.app import ComposeResult
|
|
314
|
+
|
|
315
|
+
from chrys.service.mcp.adapter import MCPTestReport
|
|
316
|
+
from chrys.service.profiles.agents.schema import MCPServerConfig
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
_TRANSPORT_OPTIONS = [
|
|
320
|
+
("HTTP", "http"),
|
|
321
|
+
("STDIO", "stdio"),
|
|
322
|
+
]
|
|
323
|
+
|
|
324
|
+
_TOOL_ACCESS_ALL = "all"
|
|
325
|
+
_TOOL_ACCESS_SELECTED = "selected"
|
|
326
|
+
_TOOL_ACCESS_NONE = "none"
|
|
327
|
+
_TOOL_ACCESS_OPTIONS = [
|
|
328
|
+
(_TOOL_ACCESS_ALL_LABEL, _TOOL_ACCESS_ALL),
|
|
329
|
+
(_TOOL_ACCESS_SELECTED_LABEL, _TOOL_ACCESS_SELECTED),
|
|
330
|
+
(_TOOL_ACCESS_NONE_LABEL, _TOOL_ACCESS_NONE),
|
|
331
|
+
]
|
|
332
|
+
_TOOL_LOADING_FULL = "full"
|
|
333
|
+
_TOOL_LOADING_PROGRESSIVE = "progressive"
|
|
334
|
+
_TOOL_LOADING_OPTIONS = [
|
|
335
|
+
(_TOOL_LOADING_FULL_LABEL, _TOOL_LOADING_FULL),
|
|
336
|
+
(_TOOL_LOADING_PROGRESSIVE_LABEL, _TOOL_LOADING_PROGRESSIVE),
|
|
337
|
+
]
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _is_transport(value: str) -> TypeGuard[Literal["stdio", "http"]]:
|
|
341
|
+
"""Narrow the schema-backed transport discriminator."""
|
|
342
|
+
return value in {"stdio", "http"}
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
@dataclass(frozen=True, slots=True)
|
|
346
|
+
class _MCPKeyValueRows:
|
|
347
|
+
"""Widget-id and placeholder policy for one MCP key/value row list."""
|
|
348
|
+
|
|
349
|
+
container_suffix: str
|
|
350
|
+
action_prefix: str
|
|
351
|
+
key_placeholder: MessageDef
|
|
352
|
+
value_placeholder: MessageDef
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
@dataclass(frozen=True, slots=True)
|
|
356
|
+
class _MCPKeyValueDrafts:
|
|
357
|
+
"""UI-only key/value row text preserved across card rebuilds."""
|
|
358
|
+
|
|
359
|
+
headers: list[tuple[str, str]] | None = None
|
|
360
|
+
env: list[tuple[str, str]] | None = None
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
@dataclass(slots=True)
|
|
364
|
+
class _MCPServerRow:
|
|
365
|
+
"""One server in the panel: its config plus the UI-only state a card rebuild carries over."""
|
|
366
|
+
|
|
367
|
+
config: MCPServerConfig
|
|
368
|
+
command_line_text: str | None = None
|
|
369
|
+
key_value_drafts: _MCPKeyValueDrafts = field(default_factory=_MCPKeyValueDrafts)
|
|
370
|
+
# Saved servers open folded to their name row; a server added in the panel opens unfolded.
|
|
371
|
+
collapsed: bool = True
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
_MCP_HEADER_ROWS = _MCPKeyValueRows(
|
|
375
|
+
container_suffix="headers",
|
|
376
|
+
action_prefix="h",
|
|
377
|
+
key_placeholder=_HEADER_NAME_PLACEHOLDER,
|
|
378
|
+
value_placeholder=_HEADER_VALUE_PLACEHOLDER,
|
|
379
|
+
)
|
|
380
|
+
_MCP_ENV_ROWS = _MCPKeyValueRows(
|
|
381
|
+
container_suffix="env",
|
|
382
|
+
action_prefix="e",
|
|
383
|
+
key_placeholder=_VARIABLE_NAME_PLACEHOLDER,
|
|
384
|
+
value_placeholder=_VALUE_PLACEHOLDER,
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
class MCPConnectionCard(CollapsibleConfigCard):
|
|
389
|
+
"""A single MCP server entry with name, transport, and transport-specific fields.
|
|
390
|
+
|
|
391
|
+
The header shows the server name, so a collapsed card shows just that name in the server list.
|
|
392
|
+
"""
|
|
393
|
+
|
|
394
|
+
DEFAULT_CSS = """
|
|
395
|
+
MCPConnectionCard .mcp-row {
|
|
396
|
+
height: auto;
|
|
397
|
+
margin: 0 0 1 0;
|
|
398
|
+
}
|
|
399
|
+
MCPConnectionCard .mcp-row Input {
|
|
400
|
+
margin: 0 1 0 0;
|
|
401
|
+
}
|
|
402
|
+
MCPConnectionCard .mcp-header-row {
|
|
403
|
+
height: auto;
|
|
404
|
+
margin: 0;
|
|
405
|
+
}
|
|
406
|
+
MCPConnectionCard .mcp-allowed-tools-row {
|
|
407
|
+
height: auto;
|
|
408
|
+
margin: 1 0 0 0;
|
|
409
|
+
}
|
|
410
|
+
MCPConnectionCard .mcp-progressive-fields,
|
|
411
|
+
MCPConnectionCard .mcp-policy-fields {
|
|
412
|
+
height: auto;
|
|
413
|
+
}
|
|
414
|
+
MCPConnectionCard .mcp-option-section {
|
|
415
|
+
height: auto;
|
|
416
|
+
margin: 1 0 0 0;
|
|
417
|
+
padding: 1 0 1 2;
|
|
418
|
+
border: solid $tui-border-foreground 15%;
|
|
419
|
+
border-title-color: $text-muted;
|
|
420
|
+
}
|
|
421
|
+
MCPConnectionCard .mcp-tool-access-section {
|
|
422
|
+
margin: 1 0 0 0;
|
|
423
|
+
}
|
|
424
|
+
MCPConnectionCard .mcp-option-section .mcp-label:first-of-type {
|
|
425
|
+
margin-top: 0;
|
|
426
|
+
}
|
|
427
|
+
MCPConnectionCard .mcp-option-section .mcp-policy-fields .mcp-label:first-of-type,
|
|
428
|
+
MCPConnectionCard .mcp-option-section .mcp-progressive-fields .mcp-label:first-of-type {
|
|
429
|
+
margin-top: 1;
|
|
430
|
+
}
|
|
431
|
+
MCPConnectionCard .mcp-option-section Select,
|
|
432
|
+
MCPConnectionCard .mcp-option-section .mcp-policy-fields Input,
|
|
433
|
+
MCPConnectionCard .mcp-option-section .mcp-progressive-fields Input,
|
|
434
|
+
MCPConnectionCard .mcp-naming-input {
|
|
435
|
+
margin: 0 2 0 0;
|
|
436
|
+
}
|
|
437
|
+
MCPConnectionCard .mcp-transport-option-section > Input,
|
|
438
|
+
MCPConnectionCard .mcp-transport-option-section > EnhancedTextArea,
|
|
439
|
+
MCPConnectionCard .mcp-transport-option-section > .mcp-list {
|
|
440
|
+
margin-right: 2;
|
|
441
|
+
}
|
|
442
|
+
MCPConnectionCard .mcp-transport-option-section .mcp-list > .mcp-row:last-child {
|
|
443
|
+
margin-bottom: 0;
|
|
444
|
+
}
|
|
445
|
+
MCPConnectionCard .mcp-field {
|
|
446
|
+
width: 1fr;
|
|
447
|
+
height: auto;
|
|
448
|
+
margin: 0;
|
|
449
|
+
}
|
|
450
|
+
MCPConnectionCard .mcp-label {
|
|
451
|
+
height: 1;
|
|
452
|
+
color: $text-muted;
|
|
453
|
+
margin: 1 0 0 0;
|
|
454
|
+
}
|
|
455
|
+
MCPConnectionCard .mcp-allowed-tools-row .mcp-label {
|
|
456
|
+
width: auto;
|
|
457
|
+
margin: 0 2 0 0;
|
|
458
|
+
}
|
|
459
|
+
MCPConnectionCard Select {
|
|
460
|
+
height: auto;
|
|
461
|
+
margin: 0;
|
|
462
|
+
}
|
|
463
|
+
MCPConnectionCard SelectCurrent {
|
|
464
|
+
height: 1;
|
|
465
|
+
border: none;
|
|
466
|
+
background: $foreground 8%;
|
|
467
|
+
padding: 0 1;
|
|
468
|
+
}
|
|
469
|
+
MCPConnectionCard SelectOverlay {
|
|
470
|
+
border: round $tui-border-primary $border-opacity;
|
|
471
|
+
background: $surface;
|
|
472
|
+
}
|
|
473
|
+
MCPConnectionCard .mcp-transport-fields,
|
|
474
|
+
MCPConnectionCard .mcp-common-fields,
|
|
475
|
+
MCPConnectionCard .mcp-list {
|
|
476
|
+
height: auto;
|
|
477
|
+
}
|
|
478
|
+
MCPConnectionCard .mcp-footer-row {
|
|
479
|
+
height: auto;
|
|
480
|
+
margin: 1 0 0 0;
|
|
481
|
+
}
|
|
482
|
+
MCPConnectionCard .mcp-footer-left {
|
|
483
|
+
width: 1fr;
|
|
484
|
+
height: auto;
|
|
485
|
+
}
|
|
486
|
+
MCPConnectionCard .mcp-test-btn {
|
|
487
|
+
min-width: 14;
|
|
488
|
+
height: 1;
|
|
489
|
+
}
|
|
490
|
+
MCPConnectionCard .mcp-test-btn.-disabled {
|
|
491
|
+
color: $text-muted;
|
|
492
|
+
}
|
|
493
|
+
MCPConnectionCard EnhancedTextArea {
|
|
494
|
+
height: 5;
|
|
495
|
+
border: none;
|
|
496
|
+
background: $foreground 8%;
|
|
497
|
+
padding: 0 0;
|
|
498
|
+
margin: 0;
|
|
499
|
+
scrollbar-size-vertical: 1;
|
|
500
|
+
}
|
|
501
|
+
MCPConnectionCard EnhancedTextArea:focus {
|
|
502
|
+
border: none;
|
|
503
|
+
background: $foreground 12%;
|
|
504
|
+
}
|
|
505
|
+
MCPConnectionCard EnhancedTextArea .text-area--cursor-line {
|
|
506
|
+
background: $primary 15%;
|
|
507
|
+
}
|
|
508
|
+
MCPConnectionCard Input {
|
|
509
|
+
height: 1;
|
|
510
|
+
border: none;
|
|
511
|
+
background: $foreground 8%;
|
|
512
|
+
padding: 0 1;
|
|
513
|
+
margin: 0;
|
|
514
|
+
}
|
|
515
|
+
MCPConnectionCard Input:focus {
|
|
516
|
+
border: none;
|
|
517
|
+
background: $foreground 12%;
|
|
518
|
+
}
|
|
519
|
+
MCPConnectionCard Checkbox {
|
|
520
|
+
width: auto;
|
|
521
|
+
height: 1;
|
|
522
|
+
color: $text-muted;
|
|
523
|
+
margin: 0 1 0 0;
|
|
524
|
+
padding: 0 2;
|
|
525
|
+
border: solid $tui-border-foreground 15%;
|
|
526
|
+
background: $foreground 6%;
|
|
527
|
+
}
|
|
528
|
+
MCPConnectionCard Checkbox.-on {
|
|
529
|
+
color: $secondary;
|
|
530
|
+
border: solid $tui-border-secondary $border-opacity;
|
|
531
|
+
background: $secondary 12%;
|
|
532
|
+
}
|
|
533
|
+
MCPConnectionCard Checkbox > .toggle--button {
|
|
534
|
+
color: $foreground 35%;
|
|
535
|
+
background: $foreground 6%;
|
|
536
|
+
}
|
|
537
|
+
MCPConnectionCard Checkbox.-on > .toggle--button {
|
|
538
|
+
color: $success;
|
|
539
|
+
background: $secondary 12%;
|
|
540
|
+
}
|
|
541
|
+
MCPConnectionCard Checkbox .toggle--label {
|
|
542
|
+
text-style: bold;
|
|
543
|
+
}
|
|
544
|
+
MCPConnectionCard .mcp-skip-tls {
|
|
545
|
+
margin: 1 1 0 0;
|
|
546
|
+
}
|
|
547
|
+
MCPConnectionCard .mcp-bypass-proxy {
|
|
548
|
+
margin: 1 1 1 0;
|
|
549
|
+
}
|
|
550
|
+
MCPConnectionCard .mcp-skip-tls-hint {
|
|
551
|
+
width: 100%;
|
|
552
|
+
height: auto;
|
|
553
|
+
color: $warning;
|
|
554
|
+
margin: 0 1 1 0;
|
|
555
|
+
padding: 0 0 0 3;
|
|
556
|
+
}
|
|
557
|
+
MCPConnectionCard .mcp-checkbox-hint {
|
|
558
|
+
width: 100%;
|
|
559
|
+
height: auto;
|
|
560
|
+
color: $text-disabled;
|
|
561
|
+
margin: 0 1 1 0;
|
|
562
|
+
padding: 0 0 0 3;
|
|
563
|
+
}
|
|
564
|
+
MCPConnectionCard .mcp-checkbox-hint:last-of-type {
|
|
565
|
+
margin-bottom: 0;
|
|
566
|
+
}
|
|
567
|
+
MCPConnectionCard Checkbox.mcp-guidance-checkbox .toggle--label {
|
|
568
|
+
color: $text;
|
|
569
|
+
}
|
|
570
|
+
MCPConnectionCard Checkbox.mcp-guidance-checkbox.-hint-collapsed {
|
|
571
|
+
margin-bottom: 1;
|
|
572
|
+
}
|
|
573
|
+
MCPConnectionCard Checkbox.mcp-guidance-checkbox.-hint-collapsed:last-of-type {
|
|
574
|
+
margin-bottom: 0;
|
|
575
|
+
}
|
|
576
|
+
MCPConnectionCard .mcp-item-row {
|
|
577
|
+
height: auto;
|
|
578
|
+
background: transparent;
|
|
579
|
+
padding: 0;
|
|
580
|
+
margin: 0 0 1 0;
|
|
581
|
+
}
|
|
582
|
+
MCPConnectionCard .mcp-item-row Input {
|
|
583
|
+
margin: 0 1 0 0;
|
|
584
|
+
}
|
|
585
|
+
MCPConnectionCard .mcp-item-row .mcp-item-value-input {
|
|
586
|
+
margin-right: 0;
|
|
587
|
+
}
|
|
588
|
+
MCPConnectionCard .mcp-small-btn {
|
|
589
|
+
min-width: 6;
|
|
590
|
+
height: 1;
|
|
591
|
+
text-style: bold;
|
|
592
|
+
padding: 0 1;
|
|
593
|
+
}
|
|
594
|
+
MCPConnectionCard .mcp-remove-btn {
|
|
595
|
+
min-width: 3;
|
|
596
|
+
height: 1;
|
|
597
|
+
border: none;
|
|
598
|
+
background: transparent;
|
|
599
|
+
color: $error;
|
|
600
|
+
padding: 0;
|
|
601
|
+
content-align: right middle;
|
|
602
|
+
text-align: right;
|
|
603
|
+
}
|
|
604
|
+
"""
|
|
605
|
+
|
|
606
|
+
class TestRequested(Message):
|
|
607
|
+
"""Posted when the user requests a connection test; carries the card, which a later rebuild may re-index."""
|
|
608
|
+
|
|
609
|
+
def __init__(self, card: MCPConnectionCard) -> None:
|
|
610
|
+
self.card = card
|
|
611
|
+
super().__init__()
|
|
612
|
+
|
|
613
|
+
@property
|
|
614
|
+
def control(self) -> MCPConnectionCard:
|
|
615
|
+
return self.card
|
|
616
|
+
|
|
617
|
+
class Removed(CollapsibleConfigCard.Removed):
|
|
618
|
+
"""Posted when the user clicks delete; carries the card, which a later rebuild may re-index."""
|
|
619
|
+
|
|
620
|
+
def __init__(self, card: MCPConnectionCard) -> None:
|
|
621
|
+
self.card = card
|
|
622
|
+
super().__init__(card.index)
|
|
623
|
+
|
|
624
|
+
@property
|
|
625
|
+
def control(self) -> MCPConnectionCard:
|
|
626
|
+
return self.card
|
|
627
|
+
|
|
628
|
+
# Class-level counter for generating unique widget IDs across all card instances.
|
|
629
|
+
_uid_counter: int = 0
|
|
630
|
+
_delete_button_prefix = "mcp-delete-btn"
|
|
631
|
+
|
|
632
|
+
def __init__(
|
|
633
|
+
self,
|
|
634
|
+
config: MCPServerConfig,
|
|
635
|
+
index: int,
|
|
636
|
+
*,
|
|
637
|
+
command_line_text: str | None = None,
|
|
638
|
+
key_value_drafts: _MCPKeyValueDrafts | None = None,
|
|
639
|
+
read_only: bool = False,
|
|
640
|
+
collapsed: bool = False,
|
|
641
|
+
) -> None:
|
|
642
|
+
self._config = config
|
|
643
|
+
self._index = index
|
|
644
|
+
self._command_line_text = command_line_text
|
|
645
|
+
self._key_value_drafts = key_value_drafts or _MCPKeyValueDrafts()
|
|
646
|
+
self._transport: Literal["stdio", "http"] = config.transport if _is_transport(config.transport) else "stdio"
|
|
647
|
+
self._headers_user_modified = False
|
|
648
|
+
self._env_user_modified = False
|
|
649
|
+
super().__init__(index=index, collapsed=collapsed, read_only=read_only)
|
|
650
|
+
|
|
651
|
+
@property
|
|
652
|
+
def index(self) -> int:
|
|
653
|
+
"""Stable server index assigned by the panel."""
|
|
654
|
+
return self._index
|
|
655
|
+
|
|
656
|
+
def _removed_message(self) -> MCPConnectionCard.Removed:
|
|
657
|
+
return self.Removed(self)
|
|
658
|
+
|
|
659
|
+
@staticmethod
|
|
660
|
+
def _format_command_line(command: str, args: list[str]) -> str:
|
|
661
|
+
if not command:
|
|
662
|
+
return ""
|
|
663
|
+
parts = [command, *args]
|
|
664
|
+
if get_platform().is_windows:
|
|
665
|
+
return join_windows_command_line(parts)
|
|
666
|
+
return shlex.join(parts)
|
|
667
|
+
|
|
668
|
+
@staticmethod
|
|
669
|
+
def _split_command_line(
|
|
670
|
+
command_line: str,
|
|
671
|
+
*,
|
|
672
|
+
render_message: Callable[[MessageRef], str] = format_message,
|
|
673
|
+
) -> tuple[str, list[str]]:
|
|
674
|
+
text = command_line.strip()
|
|
675
|
+
if not text:
|
|
676
|
+
return "", []
|
|
677
|
+
try:
|
|
678
|
+
parts = split_windows_command_line(text) if get_platform().is_windows else shlex.split(text)
|
|
679
|
+
except ValueError as exc:
|
|
680
|
+
error_message = render_message(MCP_INVALID_COMMAND.bind(detail=DisplayBlock(str(exc))))
|
|
681
|
+
raise ValueError(error_message) from exc
|
|
682
|
+
if not parts:
|
|
683
|
+
return "", []
|
|
684
|
+
return parts[0], parts[1:]
|
|
685
|
+
|
|
686
|
+
@staticmethod
|
|
687
|
+
def _parse_allowed_tools(value: str) -> list[str]:
|
|
688
|
+
return [item.strip() for item in value.split(",") if item.strip()]
|
|
689
|
+
|
|
690
|
+
def _validate_comma_separated_tool_names(
|
|
691
|
+
self,
|
|
692
|
+
value: str,
|
|
693
|
+
*,
|
|
694
|
+
label_definition: MessageDef,
|
|
695
|
+
) -> list[str]:
|
|
696
|
+
"""Validate editable tool-name list syntax without constraining MCP names."""
|
|
697
|
+
if not value.strip():
|
|
698
|
+
return []
|
|
699
|
+
localizer = widget_localizer(self)
|
|
700
|
+
label = render_str(localizer, label_definition.bind())
|
|
701
|
+
names = [item.strip() for item in value.split(",")]
|
|
702
|
+
errors: list[str] = []
|
|
703
|
+
if any(not name for name in names):
|
|
704
|
+
errors.append(
|
|
705
|
+
render_str(
|
|
706
|
+
localizer,
|
|
707
|
+
MCP_TOOL_LIST_EMPTY.bind(
|
|
708
|
+
label=label,
|
|
709
|
+
item=render_str(localizer, TOOL_NAME_ITEM.bind()),
|
|
710
|
+
),
|
|
711
|
+
)
|
|
712
|
+
)
|
|
713
|
+
seen: set[str] = set()
|
|
714
|
+
for name in names:
|
|
715
|
+
if not name:
|
|
716
|
+
continue
|
|
717
|
+
if name in seen:
|
|
718
|
+
errors.append(
|
|
719
|
+
render_str(
|
|
720
|
+
localizer,
|
|
721
|
+
MCP_TOOL_LIST_DUPLICATE.bind(
|
|
722
|
+
label=label,
|
|
723
|
+
name=DisplayBlock(name),
|
|
724
|
+
),
|
|
725
|
+
)
|
|
726
|
+
)
|
|
727
|
+
seen.add(name)
|
|
728
|
+
return errors
|
|
729
|
+
|
|
730
|
+
@staticmethod
|
|
731
|
+
def _tool_access_mode(allowed_tools: list[str] | None) -> str:
|
|
732
|
+
if allowed_tools is None:
|
|
733
|
+
return _TOOL_ACCESS_ALL
|
|
734
|
+
return _TOOL_ACCESS_SELECTED if allowed_tools else _TOOL_ACCESS_NONE
|
|
735
|
+
|
|
736
|
+
def _mounted_tool_access_mode(self) -> str:
|
|
737
|
+
mode = self._tool_access_mode(self._config.allowed_tools)
|
|
738
|
+
with contextlib.suppress(NoMatches):
|
|
739
|
+
mode = str(self.query_one(f"#mcp-tool-access-{self._index}", Select).value)
|
|
740
|
+
return mode
|
|
741
|
+
|
|
742
|
+
def _mounted_loading_strategy(self) -> str:
|
|
743
|
+
strategy = _TOOL_LOADING_PROGRESSIVE if self._config.use_progressive_disclosure else _TOOL_LOADING_FULL
|
|
744
|
+
with contextlib.suppress(NoMatches):
|
|
745
|
+
strategy = str(self.query_one(f"#mcp-loading-strategy-{self._index}", Select).value)
|
|
746
|
+
return strategy
|
|
747
|
+
|
|
748
|
+
def _sync_tool_policy_fields(self) -> None:
|
|
749
|
+
"""Apply the access -> loading strategy -> initial subset hierarchy."""
|
|
750
|
+
with contextlib.suppress(NoMatches):
|
|
751
|
+
access_mode = self._mounted_tool_access_mode()
|
|
752
|
+
selected_fields = self.query_one(f"#mcp-selected-tools-fields-{self._index}", Vertical)
|
|
753
|
+
selected_input = self.query_one(f"#mcp-tools-{self._index}", Input)
|
|
754
|
+
exposure_fields = self.query_one(f"#mcp-model-exposure-fields-{self._index}", Vertical)
|
|
755
|
+
loading = self.query_one(f"#mcp-loading-strategy-{self._index}", Select)
|
|
756
|
+
initial_fields = self.query_one(f"#mcp-always-load-fields-{self._index}", Vertical)
|
|
757
|
+
initial_input = self.query_one(f"#mcp-always-load-{self._index}", Input)
|
|
758
|
+
|
|
759
|
+
selected = access_mode == _TOOL_ACCESS_SELECTED
|
|
760
|
+
no_tools = access_mode == _TOOL_ACCESS_NONE
|
|
761
|
+
progressive = self._mounted_loading_strategy() == _TOOL_LOADING_PROGRESSIVE
|
|
762
|
+
|
|
763
|
+
selected_fields.display = selected
|
|
764
|
+
selected_input.disabled = not selected or self._read_only
|
|
765
|
+
exposure_fields.display = not no_tools
|
|
766
|
+
loading.disabled = no_tools or self._read_only
|
|
767
|
+
initial_fields.display = not no_tools and progressive
|
|
768
|
+
initial_input.disabled = no_tools or not progressive or self._read_only
|
|
769
|
+
|
|
770
|
+
@staticmethod
|
|
771
|
+
def _format_timeout(value: int | None) -> str:
|
|
772
|
+
return "" if value is None else str(value)
|
|
773
|
+
|
|
774
|
+
def _parse_key_value_rows(
|
|
775
|
+
self,
|
|
776
|
+
container_id: str,
|
|
777
|
+
add_row_id: str,
|
|
778
|
+
*,
|
|
779
|
+
user_modified: bool,
|
|
780
|
+
) -> dict[str, str]:
|
|
781
|
+
container = self.query_one(f"#{container_id}", Vertical)
|
|
782
|
+
if not user_modified:
|
|
783
|
+
self.query_one(f"#{add_row_id}", Horizontal)
|
|
784
|
+
|
|
785
|
+
parsed: dict[str, str] = {}
|
|
786
|
+
for row in container.query(".mcp-item-row"):
|
|
787
|
+
key = row.query_one(".mcp-item-key-input", Input).value.strip()
|
|
788
|
+
value = row.query_one(".mcp-item-value-input", Input).value.strip()
|
|
789
|
+
if key:
|
|
790
|
+
parsed[key] = value
|
|
791
|
+
return parsed
|
|
792
|
+
|
|
793
|
+
def compose(self) -> ComposeResult:
|
|
794
|
+
cfg = self._config
|
|
795
|
+
localizer = widget_localizer(self)
|
|
796
|
+
yield from self.compose_header(
|
|
797
|
+
self._header_title(cfg.name),
|
|
798
|
+
row_class="mcp-header-row",
|
|
799
|
+
title_class="mcp-title",
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
with self.card_body():
|
|
803
|
+
with Vertical(classes="mcp-field"):
|
|
804
|
+
yield Label(render_str(localizer, _TRANSPORT.bind()), classes="mcp-label")
|
|
805
|
+
transport = Select(
|
|
806
|
+
_TRANSPORT_OPTIONS,
|
|
807
|
+
value=self._transport,
|
|
808
|
+
id=f"mcp-transport-{self._index}",
|
|
809
|
+
allow_blank=False,
|
|
810
|
+
)
|
|
811
|
+
transport.disabled = self._read_only
|
|
812
|
+
yield transport
|
|
813
|
+
yield Label(f"[red]*[/red] {escape(render_str(localizer, _SERVER_NAME.bind()))}", classes="mcp-label")
|
|
814
|
+
name = Input(
|
|
815
|
+
value=cfg.name,
|
|
816
|
+
placeholder=render_str(localizer, _SERVER_NAME_PLACEHOLDER.bind()),
|
|
817
|
+
id=f"mcp-name-{self._index}",
|
|
818
|
+
)
|
|
819
|
+
name.disabled = self._read_only
|
|
820
|
+
yield name
|
|
821
|
+
yield Label(render_str(localizer, _DESCRIPTION.bind()), classes="mcp-label")
|
|
822
|
+
description = EnhancedTextArea(
|
|
823
|
+
cfg.description or "",
|
|
824
|
+
id=f"mcp-desc-{self._index}",
|
|
825
|
+
)
|
|
826
|
+
description.read_only = self._read_only
|
|
827
|
+
yield description
|
|
828
|
+
|
|
829
|
+
with Vertical(
|
|
830
|
+
classes="mcp-transport-fields mcp-option-section mcp-transport-option-section",
|
|
831
|
+
id=f"mcp-transport-fields-{self._index}",
|
|
832
|
+
) as transport_section:
|
|
833
|
+
transport_section.border_title = self._transport_options_title()
|
|
834
|
+
yield from self._compose_transport_fields(cfg)
|
|
835
|
+
|
|
836
|
+
with Vertical(classes="mcp-common-fields"):
|
|
837
|
+
with Vertical(
|
|
838
|
+
id=f"mcp-prompts-instructions-section-{self._index}",
|
|
839
|
+
classes="mcp-option-section",
|
|
840
|
+
) as guidance_section:
|
|
841
|
+
guidance_section.border_title = render_str(localizer, _PROMPTS_AND_INSTRUCTIONS.bind())
|
|
842
|
+
expose_instructions_cb = Checkbox(
|
|
843
|
+
render_str(localizer, _EXPOSE_SERVER_INSTRUCTIONS.bind()),
|
|
844
|
+
value=cfg.expose_instructions,
|
|
845
|
+
id=f"mcp-expose-instructions-{self._index}",
|
|
846
|
+
classes="mcp-guidance-checkbox",
|
|
847
|
+
)
|
|
848
|
+
expose_instructions_cb.tooltip = render_str(
|
|
849
|
+
localizer,
|
|
850
|
+
_EXPOSE_INSTRUCTIONS_TOOLTIP.bind(app=APP_DISPLAY_NAME),
|
|
851
|
+
)
|
|
852
|
+
expose_instructions_cb.disabled = self._read_only
|
|
853
|
+
expose_instructions_cb.set_class(not cfg.expose_instructions, "-hint-collapsed")
|
|
854
|
+
yield expose_instructions_cb
|
|
855
|
+
expose_instructions_hint = Label(
|
|
856
|
+
render_str(localizer, _EXPOSE_INSTRUCTIONS_HINT.bind()),
|
|
857
|
+
id=f"mcp-expose-instructions-hint-{self._index}",
|
|
858
|
+
classes="mcp-checkbox-hint",
|
|
859
|
+
)
|
|
860
|
+
expose_instructions_hint.display = cfg.expose_instructions
|
|
861
|
+
yield expose_instructions_hint
|
|
862
|
+
load_prompts_cb = Checkbox(
|
|
863
|
+
render_str(localizer, _EXPOSE_SERVER_PROMPTS.bind()),
|
|
864
|
+
value=cfg.load_prompts,
|
|
865
|
+
id=f"mcp-load-prompts-{self._index}",
|
|
866
|
+
classes="mcp-guidance-checkbox",
|
|
867
|
+
)
|
|
868
|
+
load_prompts_cb.tooltip = render_str(localizer, _LOAD_PROMPTS_TOOLTIP.bind(app=APP_DISPLAY_NAME))
|
|
869
|
+
load_prompts_cb.disabled = self._read_only
|
|
870
|
+
load_prompts_cb.set_class(not cfg.load_prompts, "-hint-collapsed")
|
|
871
|
+
yield load_prompts_cb
|
|
872
|
+
load_prompts_hint = Label(
|
|
873
|
+
render_str(localizer, _LOAD_PROMPTS_HINT.bind()),
|
|
874
|
+
id=f"mcp-load-prompts-hint-{self._index}",
|
|
875
|
+
classes="mcp-checkbox-hint",
|
|
876
|
+
)
|
|
877
|
+
load_prompts_hint.display = cfg.load_prompts
|
|
878
|
+
yield load_prompts_hint
|
|
879
|
+
|
|
880
|
+
access_mode = self._tool_access_mode(cfg.allowed_tools)
|
|
881
|
+
with Vertical(
|
|
882
|
+
id=f"mcp-tool-access-section-{self._index}",
|
|
883
|
+
classes="mcp-option-section mcp-tool-access-section",
|
|
884
|
+
) as access_section:
|
|
885
|
+
access_section.border_title = render_str(localizer, _TOOL_ACCESS.bind())
|
|
886
|
+
yield Label(render_str(localizer, _PERMITTED_TOOL_SET.bind()), classes="mcp-label")
|
|
887
|
+
access = Select(
|
|
888
|
+
[(render_str(localizer, label.bind()), value) for label, value in _TOOL_ACCESS_OPTIONS],
|
|
889
|
+
value=access_mode,
|
|
890
|
+
id=f"mcp-tool-access-{self._index}",
|
|
891
|
+
allow_blank=False,
|
|
892
|
+
)
|
|
893
|
+
access.tooltip = render_str(localizer, _TOOL_ACCESS_TOOLTIP.bind())
|
|
894
|
+
access.disabled = self._read_only
|
|
895
|
+
yield access
|
|
896
|
+
selected_tools_fields = Vertical(
|
|
897
|
+
id=f"mcp-selected-tools-fields-{self._index}",
|
|
898
|
+
classes="mcp-policy-fields",
|
|
899
|
+
)
|
|
900
|
+
selected_tools_fields.display = access_mode == _TOOL_ACCESS_SELECTED
|
|
901
|
+
with selected_tools_fields:
|
|
902
|
+
yield Label(render_str(localizer, _SELECTED_TOOL_NAMES.bind()), classes="mcp-label")
|
|
903
|
+
allowed_tools_input = Input(
|
|
904
|
+
value=", ".join(cfg.allowed_tools or []),
|
|
905
|
+
placeholder=render_str(localizer, _SELECTED_TOOLS_PLACEHOLDER.bind()),
|
|
906
|
+
id=f"mcp-tools-{self._index}",
|
|
907
|
+
)
|
|
908
|
+
allowed_tools_input.disabled = access_mode != _TOOL_ACCESS_SELECTED or self._read_only
|
|
909
|
+
yield allowed_tools_input
|
|
910
|
+
|
|
911
|
+
no_tools = access_mode == _TOOL_ACCESS_NONE
|
|
912
|
+
loading_strategy = _TOOL_LOADING_PROGRESSIVE if cfg.use_progressive_disclosure else _TOOL_LOADING_FULL
|
|
913
|
+
model_exposure_fields = Vertical(
|
|
914
|
+
id=f"mcp-model-exposure-fields-{self._index}",
|
|
915
|
+
classes="mcp-option-section",
|
|
916
|
+
)
|
|
917
|
+
model_exposure_fields.display = not no_tools
|
|
918
|
+
with model_exposure_fields:
|
|
919
|
+
model_exposure_fields.border_title = render_str(localizer, _TOOL_LOADING.bind())
|
|
920
|
+
yield Label(render_str(localizer, _LOADING_STRATEGY.bind()), classes="mcp-label")
|
|
921
|
+
loading = Select(
|
|
922
|
+
[(render_str(localizer, label.bind()), value) for label, value in _TOOL_LOADING_OPTIONS],
|
|
923
|
+
value=loading_strategy,
|
|
924
|
+
id=f"mcp-loading-strategy-{self._index}",
|
|
925
|
+
allow_blank=False,
|
|
926
|
+
)
|
|
927
|
+
loading.tooltip = render_str(localizer, _PROGRESSIVE_DISCLOSURE_TOOLTIP.bind())
|
|
928
|
+
loading.disabled = no_tools or self._read_only
|
|
929
|
+
yield loading
|
|
930
|
+
always_load_fields = Vertical(
|
|
931
|
+
id=f"mcp-always-load-fields-{self._index}",
|
|
932
|
+
classes="mcp-progressive-fields",
|
|
933
|
+
)
|
|
934
|
+
always_load_fields.display = not no_tools and cfg.use_progressive_disclosure
|
|
935
|
+
with always_load_fields:
|
|
936
|
+
always_load_label = Label(
|
|
937
|
+
render_str(localizer, _INITIALLY_VISIBLE_TOOLS.bind()),
|
|
938
|
+
classes="mcp-label",
|
|
939
|
+
)
|
|
940
|
+
always_load_label.tooltip = render_str(localizer, _ALWAYS_LOAD_TOOLTIP.bind())
|
|
941
|
+
yield always_load_label
|
|
942
|
+
always_load = Input(
|
|
943
|
+
value=", ".join(cfg.always_load),
|
|
944
|
+
placeholder=render_str(localizer, _INITIAL_TOOLS_PLACEHOLDER.bind()),
|
|
945
|
+
id=f"mcp-always-load-{self._index}",
|
|
946
|
+
)
|
|
947
|
+
always_load.tooltip = render_str(localizer, _ALWAYS_LOAD_TOOLTIP.bind())
|
|
948
|
+
always_load.disabled = no_tools or not cfg.use_progressive_disclosure or self._read_only
|
|
949
|
+
yield always_load
|
|
950
|
+
|
|
951
|
+
with Vertical(
|
|
952
|
+
id=f"mcp-naming-limits-section-{self._index}",
|
|
953
|
+
classes="mcp-option-section",
|
|
954
|
+
) as naming_section:
|
|
955
|
+
naming_section.border_title = render_str(localizer, _NAMING_AND_LIMITS.bind())
|
|
956
|
+
yield Label(render_str(localizer, _TOOL_NAME_PREFIX.bind()), classes="mcp-label")
|
|
957
|
+
prefix = Input(
|
|
958
|
+
value=cfg.tool_name_prefix,
|
|
959
|
+
placeholder=render_str(localizer, _TOOL_NAME_PREFIX_PLACEHOLDER.bind()),
|
|
960
|
+
id=f"mcp-prefix-{self._index}",
|
|
961
|
+
classes="mcp-naming-input",
|
|
962
|
+
)
|
|
963
|
+
prefix.disabled = self._read_only
|
|
964
|
+
yield prefix
|
|
965
|
+
yield Label(render_str(localizer, _REQUEST_TIMEOUT.bind()), classes="mcp-label")
|
|
966
|
+
timeout = Input(
|
|
967
|
+
value=self._format_timeout(cfg.request_timeout),
|
|
968
|
+
placeholder=render_str(
|
|
969
|
+
localizer,
|
|
970
|
+
_REQUEST_TIMEOUT_PLACEHOLDER.bind(default=DEFAULT_CONNECT_TIMEOUT_SECONDS),
|
|
971
|
+
),
|
|
972
|
+
id=f"mcp-timeout-{self._index}",
|
|
973
|
+
classes="mcp-naming-input",
|
|
974
|
+
)
|
|
975
|
+
timeout.disabled = self._read_only
|
|
976
|
+
yield timeout
|
|
977
|
+
|
|
978
|
+
with Horizontal(classes="mcp-footer-row"):
|
|
979
|
+
with Horizontal(classes="mcp-footer-left"):
|
|
980
|
+
enabled = Checkbox(
|
|
981
|
+
render_str(localizer, _ENABLED.bind()),
|
|
982
|
+
value=cfg.enabled,
|
|
983
|
+
id=f"mcp-enabled-{self._index}",
|
|
984
|
+
)
|
|
985
|
+
enabled.disabled = self._read_only
|
|
986
|
+
yield enabled
|
|
987
|
+
test_button = ConfigAddButton(
|
|
988
|
+
render_str(localizer, _TEST.bind()),
|
|
989
|
+
id=f"mcp-test-btn-{self._index}",
|
|
990
|
+
classes="mcp-test-btn",
|
|
991
|
+
)
|
|
992
|
+
test_button.disabled = self._read_only
|
|
993
|
+
test_button.display = not self._read_only
|
|
994
|
+
yield test_button
|
|
995
|
+
|
|
996
|
+
def _header_title(self, name: str) -> str:
|
|
997
|
+
# An unnamed server is told apart by position, with the label its Save errors use.
|
|
998
|
+
return name.strip() or render_str(widget_localizer(self), MCP_SERVER_CONTEXT.bind(index=self._index + 1))
|
|
999
|
+
|
|
1000
|
+
@on(Input.Changed)
|
|
1001
|
+
def _on_name_changed(self, event: Input.Changed) -> None:
|
|
1002
|
+
# Not stopped: the config screen's dirty tracking also reads this event.
|
|
1003
|
+
if event.input.id == f"mcp-name-{self._index}":
|
|
1004
|
+
self.set_title(self._header_title(event.value))
|
|
1005
|
+
|
|
1006
|
+
def _compose_transport_fields(self, cfg: MCPServerConfig) -> ComposeResult:
|
|
1007
|
+
localizer = widget_localizer(self)
|
|
1008
|
+
if self._transport == "http":
|
|
1009
|
+
yield Label(f"[red]*[/red] {escape(render_str(localizer, _URL.bind()))}", classes="mcp-label")
|
|
1010
|
+
url = Input(
|
|
1011
|
+
value=cfg.url or "",
|
|
1012
|
+
placeholder=render_str(localizer, _URL_PLACEHOLDER.bind()),
|
|
1013
|
+
id=f"mcp-url-{self._index}",
|
|
1014
|
+
)
|
|
1015
|
+
url.disabled = self._read_only
|
|
1016
|
+
yield url
|
|
1017
|
+
skip_tls = not cfg.verify_ssl
|
|
1018
|
+
skip_tls_checkbox = Checkbox(
|
|
1019
|
+
render_str(localizer, _SKIP_TLS_VERIFICATION.bind()),
|
|
1020
|
+
value=skip_tls,
|
|
1021
|
+
id=f"mcp-skip-tls-{self._index}",
|
|
1022
|
+
classes="mcp-skip-tls",
|
|
1023
|
+
)
|
|
1024
|
+
skip_tls_checkbox.disabled = self._read_only
|
|
1025
|
+
yield skip_tls_checkbox
|
|
1026
|
+
hint = Label(
|
|
1027
|
+
render_str(localizer, _INSECURE_TLS.bind()),
|
|
1028
|
+
id=f"mcp-skip-tls-hint-{self._index}",
|
|
1029
|
+
classes="mcp-skip-tls-hint",
|
|
1030
|
+
)
|
|
1031
|
+
hint.display = skip_tls
|
|
1032
|
+
yield hint
|
|
1033
|
+
bypass_proxy = Checkbox(
|
|
1034
|
+
render_str(localizer, _BYPASS_PROXY.bind()),
|
|
1035
|
+
value=cfg.bypass_proxy,
|
|
1036
|
+
id=f"mcp-bypass-proxy-{self._index}",
|
|
1037
|
+
classes="mcp-bypass-proxy",
|
|
1038
|
+
)
|
|
1039
|
+
bypass_proxy.disabled = self._read_only
|
|
1040
|
+
yield bypass_proxy
|
|
1041
|
+
yield Label(render_str(localizer, _HEADERS.bind()), classes="mcp-label")
|
|
1042
|
+
with Vertical(classes="mcp-list", id=f"mcp-headers-{self._index}"):
|
|
1043
|
+
header_rows = self._key_value_drafts.headers
|
|
1044
|
+
if header_rows is None:
|
|
1045
|
+
header_rows = list(cfg.headers.items())
|
|
1046
|
+
for key, val in header_rows:
|
|
1047
|
+
yield self._compose_key_value_row(_MCP_HEADER_ROWS, key, val)
|
|
1048
|
+
if not header_rows:
|
|
1049
|
+
yield self._compose_key_value_row(_MCP_HEADER_ROWS, "", "")
|
|
1050
|
+
yield self._make_key_value_add_row(_MCP_HEADER_ROWS)
|
|
1051
|
+
return
|
|
1052
|
+
|
|
1053
|
+
yield Label(f"[red]*[/red] {escape(render_str(localizer, _COMMAND.bind()))}", classes="mcp-label")
|
|
1054
|
+
command_line_text = (
|
|
1055
|
+
self._command_line_text
|
|
1056
|
+
if self._command_line_text is not None
|
|
1057
|
+
else self._format_command_line(cfg.command, cfg.args)
|
|
1058
|
+
)
|
|
1059
|
+
command = EnhancedTextArea(
|
|
1060
|
+
command_line_text,
|
|
1061
|
+
id=f"mcp-cmd-{self._index}",
|
|
1062
|
+
placeholder=render_str(localizer, _COMMAND_PLACEHOLDER.bind()),
|
|
1063
|
+
)
|
|
1064
|
+
command.read_only = self._read_only
|
|
1065
|
+
yield command
|
|
1066
|
+
yield Label(render_str(localizer, _ENVIRONMENT_VARIABLES.bind()), classes="mcp-label")
|
|
1067
|
+
with Vertical(classes="mcp-list", id=f"mcp-env-{self._index}"):
|
|
1068
|
+
env_rows = self._key_value_drafts.env
|
|
1069
|
+
if env_rows is None:
|
|
1070
|
+
env_rows = list(cfg.env.items())
|
|
1071
|
+
for key, val in env_rows:
|
|
1072
|
+
yield self._compose_key_value_row(_MCP_ENV_ROWS, key, val)
|
|
1073
|
+
if not env_rows:
|
|
1074
|
+
yield self._compose_key_value_row(_MCP_ENV_ROWS, "", "")
|
|
1075
|
+
yield self._make_key_value_add_row(_MCP_ENV_ROWS)
|
|
1076
|
+
|
|
1077
|
+
def _transport_options_title(self) -> str:
|
|
1078
|
+
definition = _HTTP_OPTIONS if self._transport == "http" else _STDIO_OPTIONS
|
|
1079
|
+
return render_str(widget_localizer(self), definition.bind())
|
|
1080
|
+
|
|
1081
|
+
@classmethod
|
|
1082
|
+
def _next_uid(cls) -> int:
|
|
1083
|
+
cls._uid_counter += 1
|
|
1084
|
+
return cls._uid_counter
|
|
1085
|
+
|
|
1086
|
+
def _compose_key_value_row(self, spec: _MCPKeyValueRows, key: str, val: str) -> Horizontal:
|
|
1087
|
+
uid = f"{self._index}-{self._next_uid()}"
|
|
1088
|
+
localizer = widget_localizer(self)
|
|
1089
|
+
key_input = Input(
|
|
1090
|
+
value=key,
|
|
1091
|
+
placeholder=render_str(localizer, spec.key_placeholder.bind()),
|
|
1092
|
+
classes="mcp-field mcp-item-key-input",
|
|
1093
|
+
)
|
|
1094
|
+
key_input.disabled = self._read_only
|
|
1095
|
+
value_input = Input(
|
|
1096
|
+
value=val,
|
|
1097
|
+
placeholder=render_str(localizer, spec.value_placeholder.bind()),
|
|
1098
|
+
classes="mcp-field mcp-item-value-input",
|
|
1099
|
+
)
|
|
1100
|
+
value_input.disabled = self._read_only
|
|
1101
|
+
remove_button = Button("✕", classes="mcp-remove-btn", id=f"mcp-{spec.action_prefix}del-{uid}")
|
|
1102
|
+
remove_button.disabled = self._read_only
|
|
1103
|
+
remove_button.display = not self._read_only
|
|
1104
|
+
return Horizontal(
|
|
1105
|
+
key_input,
|
|
1106
|
+
value_input,
|
|
1107
|
+
remove_button,
|
|
1108
|
+
classes="mcp-item-row",
|
|
1109
|
+
)
|
|
1110
|
+
|
|
1111
|
+
def _key_value_container_id(self, spec: _MCPKeyValueRows) -> str:
|
|
1112
|
+
return f"mcp-{spec.container_suffix}-{self._index}"
|
|
1113
|
+
|
|
1114
|
+
def _key_value_add_button_id(self, spec: _MCPKeyValueRows) -> str:
|
|
1115
|
+
return f"mcp-{spec.action_prefix}add-{self._index}"
|
|
1116
|
+
|
|
1117
|
+
def _key_value_add_row_id(self, spec: _MCPKeyValueRows) -> str:
|
|
1118
|
+
return f"mcp-{spec.action_prefix}add-row-{self._index}"
|
|
1119
|
+
|
|
1120
|
+
def _key_value_delete_prefix(self, spec: _MCPKeyValueRows) -> str:
|
|
1121
|
+
return f"mcp-{spec.action_prefix}del-{self._index}-"
|
|
1122
|
+
|
|
1123
|
+
def _mark_key_value_user_modified(self, spec: _MCPKeyValueRows) -> None:
|
|
1124
|
+
if spec is _MCP_HEADER_ROWS:
|
|
1125
|
+
self._headers_user_modified = True
|
|
1126
|
+
return
|
|
1127
|
+
if spec is _MCP_ENV_ROWS:
|
|
1128
|
+
self._env_user_modified = True
|
|
1129
|
+
return
|
|
1130
|
+
raise ValueError("Unknown MCP key/value row spec")
|
|
1131
|
+
|
|
1132
|
+
def _make_key_value_add_row(self, spec: _MCPKeyValueRows) -> Horizontal:
|
|
1133
|
+
add_button = ConfigAddButton(
|
|
1134
|
+
render_str(widget_localizer(self), _ADD.bind()),
|
|
1135
|
+
compact=True,
|
|
1136
|
+
classes="mcp-small-btn",
|
|
1137
|
+
id=self._key_value_add_button_id(spec),
|
|
1138
|
+
)
|
|
1139
|
+
add_button.disabled = self._read_only
|
|
1140
|
+
add_button.display = not self._read_only
|
|
1141
|
+
return Horizontal(
|
|
1142
|
+
add_button,
|
|
1143
|
+
classes="mcp-row",
|
|
1144
|
+
id=self._key_value_add_row_id(spec),
|
|
1145
|
+
)
|
|
1146
|
+
|
|
1147
|
+
async def _add_key_value_row(self, spec: _MCPKeyValueRows) -> None:
|
|
1148
|
+
if self._read_only:
|
|
1149
|
+
return
|
|
1150
|
+
try:
|
|
1151
|
+
container = self.query_one(f"#{self._key_value_container_id(spec)}", Vertical)
|
|
1152
|
+
add_row = self.query_one(f"#{self._key_value_add_row_id(spec)}", Horizontal)
|
|
1153
|
+
except Exception:
|
|
1154
|
+
return
|
|
1155
|
+
|
|
1156
|
+
self._mark_key_value_user_modified(spec)
|
|
1157
|
+
await container.mount(self._compose_key_value_row(spec, "", ""), before=add_row)
|
|
1158
|
+
container.refresh(layout=True)
|
|
1159
|
+
self.refresh(layout=True)
|
|
1160
|
+
|
|
1161
|
+
@on(Checkbox.Changed)
|
|
1162
|
+
def _on_checkbox_changed(self, event: Checkbox.Changed) -> None:
|
|
1163
|
+
if self._read_only:
|
|
1164
|
+
return
|
|
1165
|
+
hint_id_by_checkbox_id = {
|
|
1166
|
+
f"mcp-skip-tls-{self._index}": f"mcp-skip-tls-hint-{self._index}",
|
|
1167
|
+
f"mcp-load-prompts-{self._index}": f"mcp-load-prompts-hint-{self._index}",
|
|
1168
|
+
f"mcp-expose-instructions-{self._index}": f"mcp-expose-instructions-hint-{self._index}",
|
|
1169
|
+
}
|
|
1170
|
+
hint_id = hint_id_by_checkbox_id.get(event.checkbox.id or "")
|
|
1171
|
+
if hint_id is None:
|
|
1172
|
+
return
|
|
1173
|
+
with contextlib.suppress(Exception):
|
|
1174
|
+
hint = self.query_one(f"#{hint_id}", Label)
|
|
1175
|
+
hint.display = bool(event.value)
|
|
1176
|
+
event.checkbox.set_class(not event.value, "-hint-collapsed")
|
|
1177
|
+
|
|
1178
|
+
@on(Select.Changed)
|
|
1179
|
+
async def _on_select_changed(self, event: Select.Changed) -> None:
|
|
1180
|
+
if self._read_only:
|
|
1181
|
+
return
|
|
1182
|
+
widget_id = event.select.id or ""
|
|
1183
|
+
if widget_id in {
|
|
1184
|
+
f"mcp-tool-access-{self._index}",
|
|
1185
|
+
f"mcp-loading-strategy-{self._index}",
|
|
1186
|
+
}:
|
|
1187
|
+
self._sync_tool_policy_fields()
|
|
1188
|
+
return
|
|
1189
|
+
if widget_id != f"mcp-transport-{self._index}":
|
|
1190
|
+
return
|
|
1191
|
+
transport = str(event.value)
|
|
1192
|
+
if not _is_transport(transport) or transport == self._transport:
|
|
1193
|
+
return
|
|
1194
|
+
self._transport = transport
|
|
1195
|
+
await self._rebuild_transport_fields()
|
|
1196
|
+
|
|
1197
|
+
@on(Button.Pressed)
|
|
1198
|
+
async def _on_button(self, event: Button.Pressed) -> None:
|
|
1199
|
+
if self._read_only:
|
|
1200
|
+
return
|
|
1201
|
+
button = event.button
|
|
1202
|
+
if button.id == f"mcp-test-btn-{self._index}":
|
|
1203
|
+
self.post_message(self.TestRequested(self))
|
|
1204
|
+
return
|
|
1205
|
+
|
|
1206
|
+
if button.id == self._key_value_add_button_id(_MCP_HEADER_ROWS):
|
|
1207
|
+
await self._add_key_value_row(_MCP_HEADER_ROWS)
|
|
1208
|
+
return
|
|
1209
|
+
|
|
1210
|
+
if button.id == self._key_value_add_button_id(_MCP_ENV_ROWS):
|
|
1211
|
+
await self._add_key_value_row(_MCP_ENV_ROWS)
|
|
1212
|
+
return
|
|
1213
|
+
|
|
1214
|
+
button_id = button.id or ""
|
|
1215
|
+
for spec in (_MCP_HEADER_ROWS, _MCP_ENV_ROWS):
|
|
1216
|
+
if button_id.startswith(self._key_value_delete_prefix(spec)):
|
|
1217
|
+
# Await the removal so the row is gone from the DOM before this
|
|
1218
|
+
# handler returns and Button.Pressed bubbles to AgentsConfigScreen.
|
|
1219
|
+
# The screen's call_after_refresh dirty re-evaluation rebuilds the
|
|
1220
|
+
# profile from the mounted rows; if the row were still present (the
|
|
1221
|
+
# prior fire-and-forget row.remove()/call_later races lost this on
|
|
1222
|
+
# CI) the rebuilt profile would match saved and clear the dirty flag.
|
|
1223
|
+
await self._remove_key_value_row(spec, button)
|
|
1224
|
+
return
|
|
1225
|
+
|
|
1226
|
+
async def _remove_key_value_row(self, spec: _MCPKeyValueRows, button: Button) -> None:
|
|
1227
|
+
if self._read_only:
|
|
1228
|
+
return
|
|
1229
|
+
self._mark_key_value_user_modified(spec)
|
|
1230
|
+
row = button.parent
|
|
1231
|
+
if isinstance(row, Widget) and isinstance(row.parent, Vertical):
|
|
1232
|
+
container = row.parent
|
|
1233
|
+
was_last_row = len(list(container.query(".mcp-item-row"))) <= 1
|
|
1234
|
+
if was_last_row:
|
|
1235
|
+
row.query_one(".mcp-item-key-input", Input).value = ""
|
|
1236
|
+
row.query_one(".mcp-item-value-input", Input).value = ""
|
|
1237
|
+
row.refresh(layout=True)
|
|
1238
|
+
else:
|
|
1239
|
+
await row.remove()
|
|
1240
|
+
self.refresh(layout=True)
|
|
1241
|
+
|
|
1242
|
+
async def _rebuild_transport_fields(self) -> None:
|
|
1243
|
+
container = self.query_one(f"#mcp-transport-fields-{self._index}", Vertical)
|
|
1244
|
+
await container.remove_children()
|
|
1245
|
+
container.border_title = self._transport_options_title()
|
|
1246
|
+
if self._transport == "http":
|
|
1247
|
+
self._headers_user_modified = True
|
|
1248
|
+
else:
|
|
1249
|
+
self._env_user_modified = True
|
|
1250
|
+
|
|
1251
|
+
# Build the new subtree fully composed before mounting. Mounting
|
|
1252
|
+
# an empty container and then mounting children into it races on
|
|
1253
|
+
# Windows: ``await parent.mount(child)`` doesn't always leave
|
|
1254
|
+
# ``child.is_attached`` True by the time the next mount fires.
|
|
1255
|
+
new_widgets: list[Widget] = []
|
|
1256
|
+
if self._transport == "http":
|
|
1257
|
+
localizer = widget_localizer(self)
|
|
1258
|
+
hint = Label(
|
|
1259
|
+
render_str(localizer, _INSECURE_TLS.bind()),
|
|
1260
|
+
id=f"mcp-skip-tls-hint-{self._index}",
|
|
1261
|
+
classes="mcp-skip-tls-hint",
|
|
1262
|
+
)
|
|
1263
|
+
hint.display = False
|
|
1264
|
+
headers_container = Vertical(
|
|
1265
|
+
self._compose_key_value_row(_MCP_HEADER_ROWS, "", ""),
|
|
1266
|
+
self._make_key_value_add_row(_MCP_HEADER_ROWS),
|
|
1267
|
+
classes="mcp-list",
|
|
1268
|
+
id=f"mcp-headers-{self._index}",
|
|
1269
|
+
)
|
|
1270
|
+
new_widgets.extend(
|
|
1271
|
+
[
|
|
1272
|
+
Label(f"[red]*[/red] {escape(render_str(localizer, _URL.bind()))}", classes="mcp-label"),
|
|
1273
|
+
Input(
|
|
1274
|
+
placeholder=render_str(localizer, _URL_PLACEHOLDER.bind()),
|
|
1275
|
+
id=f"mcp-url-{self._index}",
|
|
1276
|
+
),
|
|
1277
|
+
Checkbox(
|
|
1278
|
+
render_str(localizer, _SKIP_TLS_VERIFICATION.bind()),
|
|
1279
|
+
value=False,
|
|
1280
|
+
id=f"mcp-skip-tls-{self._index}",
|
|
1281
|
+
classes="mcp-skip-tls",
|
|
1282
|
+
),
|
|
1283
|
+
hint,
|
|
1284
|
+
Checkbox(
|
|
1285
|
+
render_str(localizer, _BYPASS_PROXY.bind()),
|
|
1286
|
+
value=False,
|
|
1287
|
+
id=f"mcp-bypass-proxy-{self._index}",
|
|
1288
|
+
classes="mcp-bypass-proxy",
|
|
1289
|
+
),
|
|
1290
|
+
Label(render_str(localizer, _HEADERS.bind()), classes="mcp-label"),
|
|
1291
|
+
headers_container,
|
|
1292
|
+
]
|
|
1293
|
+
)
|
|
1294
|
+
else:
|
|
1295
|
+
localizer = widget_localizer(self)
|
|
1296
|
+
env_container = Vertical(
|
|
1297
|
+
self._compose_key_value_row(_MCP_ENV_ROWS, "", ""),
|
|
1298
|
+
self._make_key_value_add_row(_MCP_ENV_ROWS),
|
|
1299
|
+
classes="mcp-list",
|
|
1300
|
+
id=f"mcp-env-{self._index}",
|
|
1301
|
+
)
|
|
1302
|
+
new_widgets.extend(
|
|
1303
|
+
[
|
|
1304
|
+
Label(f"[red]*[/red] {escape(render_str(localizer, _COMMAND.bind()))}", classes="mcp-label"),
|
|
1305
|
+
EnhancedTextArea(
|
|
1306
|
+
id=f"mcp-cmd-{self._index}",
|
|
1307
|
+
placeholder=render_str(localizer, _COMMAND_PLACEHOLDER.bind()),
|
|
1308
|
+
),
|
|
1309
|
+
Label(render_str(localizer, _ENVIRONMENT_VARIABLES.bind()), classes="mcp-label"),
|
|
1310
|
+
env_container,
|
|
1311
|
+
]
|
|
1312
|
+
)
|
|
1313
|
+
|
|
1314
|
+
await container.mount(*new_widgets)
|
|
1315
|
+
container.refresh(layout=True)
|
|
1316
|
+
self.refresh(layout=True)
|
|
1317
|
+
|
|
1318
|
+
def get_config(self) -> MCPServerConfig:
|
|
1319
|
+
"""Read current widget state into an MCPServerConfig."""
|
|
1320
|
+
return self._read_config(default_blank_name=True)
|
|
1321
|
+
|
|
1322
|
+
def _snapshot_config(self) -> MCPServerConfig:
|
|
1323
|
+
"""Read widget state without normalizing incomplete user input."""
|
|
1324
|
+
return self._read_config(default_blank_name=False, allow_invalid_command=True)
|
|
1325
|
+
|
|
1326
|
+
def _snapshot_command_line_text(self) -> str | None:
|
|
1327
|
+
"""Return raw stdio command text for UI-only rebuild preservation."""
|
|
1328
|
+
if self._transport != "stdio":
|
|
1329
|
+
return None
|
|
1330
|
+
with contextlib.suppress(NoMatches):
|
|
1331
|
+
return self.query_one(f"#mcp-cmd-{self._index}", EnhancedTextArea).text
|
|
1332
|
+
return self._command_line_text
|
|
1333
|
+
|
|
1334
|
+
def _snapshot_key_value_rows(self, spec: _MCPKeyValueRows) -> list[tuple[str, str]] | None:
|
|
1335
|
+
"""Return raw header/env rows for UI-only rebuild preservation."""
|
|
1336
|
+
if spec is _MCP_HEADER_ROWS and self._transport != "http":
|
|
1337
|
+
return None
|
|
1338
|
+
if spec is _MCP_ENV_ROWS and self._transport != "stdio":
|
|
1339
|
+
return None
|
|
1340
|
+
|
|
1341
|
+
with contextlib.suppress(NoMatches):
|
|
1342
|
+
container = self.query_one(f"#{self._key_value_container_id(spec)}", Vertical)
|
|
1343
|
+
rows: list[tuple[str, str]] = []
|
|
1344
|
+
for row in container.query(".mcp-item-row"):
|
|
1345
|
+
key = row.query_one(".mcp-item-key-input", Input).value
|
|
1346
|
+
value = row.query_one(".mcp-item-value-input", Input).value
|
|
1347
|
+
rows.append((key, value))
|
|
1348
|
+
return rows
|
|
1349
|
+
|
|
1350
|
+
if spec is _MCP_HEADER_ROWS:
|
|
1351
|
+
return self._key_value_drafts.headers
|
|
1352
|
+
return self._key_value_drafts.env
|
|
1353
|
+
|
|
1354
|
+
def _snapshot_key_value_drafts(self) -> _MCPKeyValueDrafts:
|
|
1355
|
+
"""Return raw key/value rows for UI-only rebuild preservation."""
|
|
1356
|
+
return _MCPKeyValueDrafts(
|
|
1357
|
+
headers=self._snapshot_key_value_rows(_MCP_HEADER_ROWS),
|
|
1358
|
+
env=self._snapshot_key_value_rows(_MCP_ENV_ROWS),
|
|
1359
|
+
)
|
|
1360
|
+
|
|
1361
|
+
def _read_config(self, *, default_blank_name: bool, allow_invalid_command: bool = False) -> MCPServerConfig:
|
|
1362
|
+
from chrys.service.profiles.agents.schema import MCPServerConfig as MCPCfg
|
|
1363
|
+
|
|
1364
|
+
enabled = self._config.enabled
|
|
1365
|
+
with contextlib.suppress(NoMatches):
|
|
1366
|
+
enabled = bool(self.query_one(f"#mcp-enabled-{self._index}", Checkbox).value)
|
|
1367
|
+
|
|
1368
|
+
name = self._config.name.strip()
|
|
1369
|
+
with contextlib.suppress(NoMatches):
|
|
1370
|
+
name = self.query_one(f"#mcp-name-{self._index}", Input).value.strip()
|
|
1371
|
+
transport = self._transport
|
|
1372
|
+
description = self._config.description.strip()
|
|
1373
|
+
with contextlib.suppress(NoMatches):
|
|
1374
|
+
description = self.query_one(f"#mcp-desc-{self._index}", EnhancedTextArea).text.strip()
|
|
1375
|
+
access_mode = self._tool_access_mode(self._config.allowed_tools)
|
|
1376
|
+
with contextlib.suppress(NoMatches):
|
|
1377
|
+
access_mode = str(self.query_one(f"#mcp-tool-access-{self._index}", Select).value)
|
|
1378
|
+
if access_mode == _TOOL_ACCESS_ALL:
|
|
1379
|
+
allowed_tools = None
|
|
1380
|
+
elif access_mode == _TOOL_ACCESS_NONE:
|
|
1381
|
+
allowed_tools = []
|
|
1382
|
+
else:
|
|
1383
|
+
allowed_tools = list(self._config.allowed_tools or [])
|
|
1384
|
+
with contextlib.suppress(NoMatches):
|
|
1385
|
+
allowed_tools = self._parse_allowed_tools(self.query_one(f"#mcp-tools-{self._index}", Input).value)
|
|
1386
|
+
tool_name_prefix = self._config.tool_name_prefix.strip()
|
|
1387
|
+
with contextlib.suppress(NoMatches):
|
|
1388
|
+
tool_name_prefix = self.query_one(f"#mcp-prefix-{self._index}", Input).value.strip()
|
|
1389
|
+
|
|
1390
|
+
timeout = self._config.request_timeout
|
|
1391
|
+
with contextlib.suppress(NoMatches):
|
|
1392
|
+
timeout_text = self.query_one(f"#mcp-timeout-{self._index}", Input).value.strip()
|
|
1393
|
+
timeout = None
|
|
1394
|
+
if timeout_text:
|
|
1395
|
+
with contextlib.suppress(ValueError):
|
|
1396
|
+
timeout = int(timeout_text)
|
|
1397
|
+
|
|
1398
|
+
url = self._config.url if transport == "http" else ""
|
|
1399
|
+
command = self._config.command if transport == "stdio" else ""
|
|
1400
|
+
args = list(self._config.args) if transport == "stdio" else []
|
|
1401
|
+
headers = dict(self._config.headers) if transport == "http" else {}
|
|
1402
|
+
env = dict(self._config.env) if transport == "stdio" else {}
|
|
1403
|
+
verify_ssl = self._config.verify_ssl if transport == "http" else True
|
|
1404
|
+
bypass_proxy = self._config.bypass_proxy if transport == "http" else False
|
|
1405
|
+
|
|
1406
|
+
if transport == "http":
|
|
1407
|
+
with contextlib.suppress(NoMatches):
|
|
1408
|
+
url = self.query_one(f"#mcp-url-{self._index}", Input).value.strip()
|
|
1409
|
+
with contextlib.suppress(NoMatches):
|
|
1410
|
+
skip_tls = bool(self.query_one(f"#mcp-skip-tls-{self._index}", Checkbox).value)
|
|
1411
|
+
verify_ssl = not skip_tls
|
|
1412
|
+
with contextlib.suppress(NoMatches):
|
|
1413
|
+
bypass_proxy = bool(self.query_one(f"#mcp-bypass-proxy-{self._index}", Checkbox).value)
|
|
1414
|
+
# Keep seeded headers unless the mounted rows can be read as a complete set.
|
|
1415
|
+
try:
|
|
1416
|
+
parsed_headers = self._parse_key_value_rows(
|
|
1417
|
+
self._key_value_container_id(_MCP_HEADER_ROWS),
|
|
1418
|
+
self._key_value_add_row_id(_MCP_HEADER_ROWS),
|
|
1419
|
+
user_modified=self._headers_user_modified,
|
|
1420
|
+
)
|
|
1421
|
+
except NoMatches:
|
|
1422
|
+
pass
|
|
1423
|
+
else:
|
|
1424
|
+
headers = parsed_headers
|
|
1425
|
+
else:
|
|
1426
|
+
with contextlib.suppress(NoMatches):
|
|
1427
|
+
command_line = self.query_one(f"#mcp-cmd-{self._index}", EnhancedTextArea).text.strip()
|
|
1428
|
+
try:
|
|
1429
|
+
command, args = self._split_command_line(command_line, render_message=self._render_message)
|
|
1430
|
+
except ValueError:
|
|
1431
|
+
if not allow_invalid_command:
|
|
1432
|
+
raise
|
|
1433
|
+
# Keep seeded env unless the mounted rows can be read as a complete set.
|
|
1434
|
+
try:
|
|
1435
|
+
parsed_env = self._parse_key_value_rows(
|
|
1436
|
+
self._key_value_container_id(_MCP_ENV_ROWS),
|
|
1437
|
+
self._key_value_add_row_id(_MCP_ENV_ROWS),
|
|
1438
|
+
user_modified=self._env_user_modified,
|
|
1439
|
+
)
|
|
1440
|
+
except NoMatches:
|
|
1441
|
+
pass
|
|
1442
|
+
else:
|
|
1443
|
+
env = parsed_env
|
|
1444
|
+
|
|
1445
|
+
load_prompts = self._config.load_prompts
|
|
1446
|
+
with contextlib.suppress(NoMatches):
|
|
1447
|
+
load_prompts = bool(self.query_one(f"#mcp-load-prompts-{self._index}", Checkbox).value)
|
|
1448
|
+
expose_instructions = self._config.expose_instructions
|
|
1449
|
+
with contextlib.suppress(NoMatches):
|
|
1450
|
+
expose_instructions = bool(self.query_one(f"#mcp-expose-instructions-{self._index}", Checkbox).value)
|
|
1451
|
+
loading_strategy = _TOOL_LOADING_PROGRESSIVE if self._config.use_progressive_disclosure else _TOOL_LOADING_FULL
|
|
1452
|
+
with contextlib.suppress(NoMatches):
|
|
1453
|
+
loading_strategy = str(self.query_one(f"#mcp-loading-strategy-{self._index}", Select).value)
|
|
1454
|
+
use_progressive_disclosure = access_mode != _TOOL_ACCESS_NONE and loading_strategy == _TOOL_LOADING_PROGRESSIVE
|
|
1455
|
+
always_load: list[str] = []
|
|
1456
|
+
if use_progressive_disclosure:
|
|
1457
|
+
always_load = list(self._config.always_load)
|
|
1458
|
+
with contextlib.suppress(NoMatches):
|
|
1459
|
+
always_load = self._parse_allowed_tools(self.query_one(f"#mcp-always-load-{self._index}", Input).value)
|
|
1460
|
+
|
|
1461
|
+
if default_blank_name and not name:
|
|
1462
|
+
name = f"server-{self._index}"
|
|
1463
|
+
|
|
1464
|
+
return MCPCfg(
|
|
1465
|
+
name=name,
|
|
1466
|
+
transport=transport,
|
|
1467
|
+
command=command,
|
|
1468
|
+
args=args,
|
|
1469
|
+
encoding=self._config.encoding if transport == "stdio" else None,
|
|
1470
|
+
env=env,
|
|
1471
|
+
url=url,
|
|
1472
|
+
headers=headers,
|
|
1473
|
+
resolve_header_templates=self._config.resolve_header_templates if transport == "http" else True,
|
|
1474
|
+
terminate_on_close=self._config.terminate_on_close if transport == "http" else None,
|
|
1475
|
+
verify_ssl=verify_ssl,
|
|
1476
|
+
bypass_proxy=bypass_proxy,
|
|
1477
|
+
enabled=enabled,
|
|
1478
|
+
description=description,
|
|
1479
|
+
tool_name_prefix=tool_name_prefix,
|
|
1480
|
+
allowed_tools=allowed_tools,
|
|
1481
|
+
request_timeout=timeout,
|
|
1482
|
+
max_tool_result_tokens=self._config.max_tool_result_tokens,
|
|
1483
|
+
load_prompts=load_prompts,
|
|
1484
|
+
expose_instructions=expose_instructions,
|
|
1485
|
+
use_progressive_disclosure=use_progressive_disclosure,
|
|
1486
|
+
always_load=always_load,
|
|
1487
|
+
)
|
|
1488
|
+
|
|
1489
|
+
def validate(self) -> list[str]:
|
|
1490
|
+
"""Return validation errors for this server."""
|
|
1491
|
+
localizer = widget_localizer(self)
|
|
1492
|
+
errors: list[str] = []
|
|
1493
|
+
name = self.query_one(f"#mcp-name-{self._index}", Input).value.strip()
|
|
1494
|
+
display_name = name or render_str(localizer, MCP_SERVER_CONTEXT.bind(index=self._index + 1))
|
|
1495
|
+
|
|
1496
|
+
if not name:
|
|
1497
|
+
context = render_str(localizer, MCP_SERVER_CONTEXT.bind(index=self._index + 1))
|
|
1498
|
+
errors.append(
|
|
1499
|
+
self._render_context_error(
|
|
1500
|
+
context,
|
|
1501
|
+
FIELD_REQUIRED.bind(field=render_str(localizer, NAME_FIELD.bind())),
|
|
1502
|
+
)
|
|
1503
|
+
)
|
|
1504
|
+
|
|
1505
|
+
if self._transport == "http":
|
|
1506
|
+
with contextlib.suppress(Exception):
|
|
1507
|
+
url = self.query_one(f"#mcp-url-{self._index}", Input).value.strip()
|
|
1508
|
+
if not url:
|
|
1509
|
+
errors.append(self._render_server_error(display_name, MCP_URL_REQUIRED.bind()))
|
|
1510
|
+
elif not url.startswith(("http://", "https://")):
|
|
1511
|
+
errors.append(self._render_server_error(display_name, MCP_URL_SCHEME.bind()))
|
|
1512
|
+
else:
|
|
1513
|
+
with contextlib.suppress(Exception):
|
|
1514
|
+
command_line = self.query_one(f"#mcp-cmd-{self._index}", EnhancedTextArea).text.strip()
|
|
1515
|
+
if not command_line:
|
|
1516
|
+
errors.append(self._render_server_error(display_name, MCP_COMMAND_REQUIRED.bind()))
|
|
1517
|
+
else:
|
|
1518
|
+
try:
|
|
1519
|
+
command, _args = self._split_command_line(command_line, render_message=self._render_message)
|
|
1520
|
+
if not command:
|
|
1521
|
+
errors.append(self._render_server_error(display_name, MCP_COMMAND_REQUIRED.bind()))
|
|
1522
|
+
except ValueError as exc:
|
|
1523
|
+
errors.append(self._render_server_message(display_name, str(exc)))
|
|
1524
|
+
|
|
1525
|
+
timeout_text = self.query_one(f"#mcp-timeout-{self._index}", Input).value.strip()
|
|
1526
|
+
if timeout_text:
|
|
1527
|
+
try:
|
|
1528
|
+
timeout = int(timeout_text)
|
|
1529
|
+
if timeout <= 0:
|
|
1530
|
+
errors.append(self._render_server_error(display_name, MCP_TIMEOUT_POSITIVE.bind()))
|
|
1531
|
+
except ValueError:
|
|
1532
|
+
errors.append(self._render_server_error(display_name, MCP_TIMEOUT_VALID.bind()))
|
|
1533
|
+
|
|
1534
|
+
prefix_text = self.query_one(f"#mcp-prefix-{self._index}", Input).value.strip()
|
|
1535
|
+
|
|
1536
|
+
access_mode = str(self.query_one(f"#mcp-tool-access-{self._index}", Select).value)
|
|
1537
|
+
selected_tools_text = self.query_one(f"#mcp-tools-{self._index}", Input).value
|
|
1538
|
+
selected_tools = self._parse_allowed_tools(selected_tools_text)
|
|
1539
|
+
if access_mode == _TOOL_ACCESS_SELECTED:
|
|
1540
|
+
errors.extend(
|
|
1541
|
+
self._render_server_message(display_name, error)
|
|
1542
|
+
for error in self._validate_comma_separated_tool_names(
|
|
1543
|
+
selected_tools_text,
|
|
1544
|
+
label_definition=_SELECTED_TOOL_NAMES,
|
|
1545
|
+
)
|
|
1546
|
+
)
|
|
1547
|
+
if not selected_tools:
|
|
1548
|
+
errors.append(
|
|
1549
|
+
self._render_server_error(
|
|
1550
|
+
display_name,
|
|
1551
|
+
MCP_SELECT_PERMITTED.bind(
|
|
1552
|
+
no_tools=render_str(localizer, _TOOL_ACCESS_NONE_LABEL.bind()),
|
|
1553
|
+
),
|
|
1554
|
+
)
|
|
1555
|
+
)
|
|
1556
|
+
|
|
1557
|
+
loading_strategy = str(self.query_one(f"#mcp-loading-strategy-{self._index}", Select).value)
|
|
1558
|
+
progressive = access_mode != _TOOL_ACCESS_NONE and loading_strategy == _TOOL_LOADING_PROGRESSIVE
|
|
1559
|
+
prefix_error = validate_mcp_tool_name_prefix(
|
|
1560
|
+
prefix_text,
|
|
1561
|
+
generated_suffixes=MCP_PROGRESSIVE_CONTROL_TOOL_NAMES if progressive else (),
|
|
1562
|
+
)
|
|
1563
|
+
if prefix_error is not None:
|
|
1564
|
+
errors.append(self._render_server_message(display_name, prefix_error))
|
|
1565
|
+
if progressive:
|
|
1566
|
+
always_load_text = self.query_one(f"#mcp-always-load-{self._index}", Input).value
|
|
1567
|
+
always_load = self._parse_allowed_tools(always_load_text)
|
|
1568
|
+
errors.extend(
|
|
1569
|
+
self._render_server_message(display_name, error)
|
|
1570
|
+
for error in self._validate_comma_separated_tool_names(
|
|
1571
|
+
always_load_text,
|
|
1572
|
+
label_definition=MCP_INITIALLY_VISIBLE_TOOLS_VALIDATION,
|
|
1573
|
+
)
|
|
1574
|
+
)
|
|
1575
|
+
if access_mode == _TOOL_ACCESS_SELECTED:
|
|
1576
|
+
outside_scope = [name for name in always_load if name not in set(selected_tools)]
|
|
1577
|
+
if outside_scope:
|
|
1578
|
+
errors.append(
|
|
1579
|
+
self._render_server_error(
|
|
1580
|
+
display_name,
|
|
1581
|
+
MCP_INITIAL_TOOLS_PERMITTED.bind(
|
|
1582
|
+
label=render_str(localizer, MCP_INITIALLY_VISIBLE_TOOLS_VALIDATION.bind()),
|
|
1583
|
+
names=DisplayBlock(", ".join(outside_scope)),
|
|
1584
|
+
),
|
|
1585
|
+
)
|
|
1586
|
+
)
|
|
1587
|
+
|
|
1588
|
+
if self._transport == "http":
|
|
1589
|
+
with contextlib.suppress(Exception):
|
|
1590
|
+
errors.extend(
|
|
1591
|
+
self._validate_key_value_rows(
|
|
1592
|
+
self._key_value_container_id(_MCP_HEADER_ROWS),
|
|
1593
|
+
_HEADERS,
|
|
1594
|
+
display_name,
|
|
1595
|
+
)
|
|
1596
|
+
)
|
|
1597
|
+
|
|
1598
|
+
if self._transport == "stdio":
|
|
1599
|
+
with contextlib.suppress(Exception):
|
|
1600
|
+
errors.extend(
|
|
1601
|
+
self._validate_key_value_rows(
|
|
1602
|
+
self._key_value_container_id(_MCP_ENV_ROWS),
|
|
1603
|
+
_ENVIRONMENT_VARIABLES,
|
|
1604
|
+
display_name,
|
|
1605
|
+
)
|
|
1606
|
+
)
|
|
1607
|
+
|
|
1608
|
+
return errors
|
|
1609
|
+
|
|
1610
|
+
def _render_message(self, reference: MessageRef) -> str:
|
|
1611
|
+
return render_str(widget_localizer(self), reference)
|
|
1612
|
+
|
|
1613
|
+
def _render_context_error(self, context: str, reference: MessageRef) -> str:
|
|
1614
|
+
return self._render_message(
|
|
1615
|
+
CONTEXT_ERROR.bind(
|
|
1616
|
+
context=DisplayBlock(context),
|
|
1617
|
+
message=DisplayBlock(self._render_message(reference)),
|
|
1618
|
+
)
|
|
1619
|
+
)
|
|
1620
|
+
|
|
1621
|
+
def _render_server_error(self, display_name: str, reference: MessageRef) -> str:
|
|
1622
|
+
return self._render_server_message(display_name, self._render_message(reference))
|
|
1623
|
+
|
|
1624
|
+
def _render_server_message(self, display_name: str, message: str) -> str:
|
|
1625
|
+
return self._render_message(
|
|
1626
|
+
SERVER_ERROR.bind(
|
|
1627
|
+
name=DisplayBlock(display_name),
|
|
1628
|
+
message=DisplayBlock(message),
|
|
1629
|
+
)
|
|
1630
|
+
)
|
|
1631
|
+
|
|
1632
|
+
def _validate_key_value_rows(
|
|
1633
|
+
self,
|
|
1634
|
+
container_id: str,
|
|
1635
|
+
label_definition: MessageDef,
|
|
1636
|
+
display_name: str,
|
|
1637
|
+
) -> list[str]:
|
|
1638
|
+
"""Validate that partially-filled key-value rows are not saved silently."""
|
|
1639
|
+
localizer = widget_localizer(self)
|
|
1640
|
+
label = render_str(localizer, label_definition.bind())
|
|
1641
|
+
errors: list[str] = []
|
|
1642
|
+
seen: set[str] = set()
|
|
1643
|
+
container = self.query_one(f"#{container_id}", Vertical)
|
|
1644
|
+
for index, row in enumerate(container.query(".mcp-item-row"), start=1):
|
|
1645
|
+
key = row.query_one(".mcp-item-key-input", Input).value.strip()
|
|
1646
|
+
value = row.query_one(".mcp-item-value-input", Input).value.strip()
|
|
1647
|
+
if not key and not value:
|
|
1648
|
+
continue
|
|
1649
|
+
if key and not value:
|
|
1650
|
+
errors.append(
|
|
1651
|
+
self._render_server_error(
|
|
1652
|
+
display_name,
|
|
1653
|
+
MCP_ROW_VALUE_REQUIRED.bind(
|
|
1654
|
+
label=label,
|
|
1655
|
+
row=index,
|
|
1656
|
+
key=DisplayBlock(key),
|
|
1657
|
+
),
|
|
1658
|
+
)
|
|
1659
|
+
)
|
|
1660
|
+
elif value and not key:
|
|
1661
|
+
errors.append(
|
|
1662
|
+
self._render_server_error(
|
|
1663
|
+
display_name,
|
|
1664
|
+
MCP_ROW_KEY_REQUIRED.bind(label=label, row=index),
|
|
1665
|
+
)
|
|
1666
|
+
)
|
|
1667
|
+
if key in seen:
|
|
1668
|
+
errors.append(
|
|
1669
|
+
self._render_server_error(
|
|
1670
|
+
display_name,
|
|
1671
|
+
DUPLICATE_KEY_ROW.bind(
|
|
1672
|
+
label=label,
|
|
1673
|
+
row=index,
|
|
1674
|
+
key=DisplayBlock(key),
|
|
1675
|
+
),
|
|
1676
|
+
)
|
|
1677
|
+
)
|
|
1678
|
+
if key:
|
|
1679
|
+
seen.add(key)
|
|
1680
|
+
return errors
|
|
1681
|
+
|
|
1682
|
+
|
|
1683
|
+
class MCPConfigPanel(VerticalScroll):
|
|
1684
|
+
"""Composable widget for the MCP configuration tab."""
|
|
1685
|
+
|
|
1686
|
+
DEFAULT_CSS = """
|
|
1687
|
+
MCPConfigPanel {
|
|
1688
|
+
height: 1fr;
|
|
1689
|
+
padding: 0 0 0 2;
|
|
1690
|
+
scrollbar-size-vertical: 1;
|
|
1691
|
+
}
|
|
1692
|
+
MCPConfigPanel .mcp-section-title {
|
|
1693
|
+
color: $secondary;
|
|
1694
|
+
text-style: bold;
|
|
1695
|
+
height: 1;
|
|
1696
|
+
}
|
|
1697
|
+
MCPConfigPanel .mcp-section-desc {
|
|
1698
|
+
color: $text-muted;
|
|
1699
|
+
height: 1;
|
|
1700
|
+
margin: 0 0 1 0;
|
|
1701
|
+
}
|
|
1702
|
+
MCPConfigPanel .mcp-field {
|
|
1703
|
+
width: 1fr;
|
|
1704
|
+
height: auto;
|
|
1705
|
+
margin: 0 1 0 0;
|
|
1706
|
+
}
|
|
1707
|
+
MCPConfigPanel #mcp-add-btn {
|
|
1708
|
+
min-width: 10;
|
|
1709
|
+
height: 1;
|
|
1710
|
+
margin: 0;
|
|
1711
|
+
}
|
|
1712
|
+
MCPConfigPanel .mcp-header-bar {
|
|
1713
|
+
height: auto;
|
|
1714
|
+
margin: 0 0 1 0;
|
|
1715
|
+
}
|
|
1716
|
+
MCPConfigPanel #mcp-cards {
|
|
1717
|
+
height: auto;
|
|
1718
|
+
}
|
|
1719
|
+
MCPConfigPanel .mcp-empty {
|
|
1720
|
+
margin: 0 2 1 0;
|
|
1721
|
+
}
|
|
1722
|
+
"""
|
|
1723
|
+
|
|
1724
|
+
def __init__(
|
|
1725
|
+
self,
|
|
1726
|
+
mcp_servers: list[MCPServerConfig] | None = None,
|
|
1727
|
+
*,
|
|
1728
|
+
card_folds: Sequence[bool] | None = None,
|
|
1729
|
+
workspace_cwd: str | None = None,
|
|
1730
|
+
read_only: bool = False,
|
|
1731
|
+
additional_reserved_tool_names: Callable[[], Collection[str]] | None = None,
|
|
1732
|
+
) -> None:
|
|
1733
|
+
servers = list(mcp_servers or [])
|
|
1734
|
+
# Folds from an earlier panel for these servers (see ``card_folds``); otherwise saved servers open folded.
|
|
1735
|
+
folds = list(card_folds) if card_folds is not None and len(card_folds) == len(servers) else None
|
|
1736
|
+
self._rows = [
|
|
1737
|
+
_MCPServerRow(server, collapsed=True if folds is None else folds[index])
|
|
1738
|
+
for index, server in enumerate(servers)
|
|
1739
|
+
]
|
|
1740
|
+
self._read_only = read_only
|
|
1741
|
+
self._additional_reserved_tool_names = additional_reserved_tool_names
|
|
1742
|
+
self._testing: set[int] = set()
|
|
1743
|
+
self._adapter = MCPAdapter(
|
|
1744
|
+
stdio_cwd=workspace_cwd or None,
|
|
1745
|
+
reserved_tool_names=self._current_reserved_tool_names(),
|
|
1746
|
+
)
|
|
1747
|
+
super().__init__()
|
|
1748
|
+
|
|
1749
|
+
def _render_message(self, reference: MessageRef) -> str:
|
|
1750
|
+
return render_str(widget_localizer(self), reference)
|
|
1751
|
+
|
|
1752
|
+
def _current_reserved_tool_names(self) -> set[str]:
|
|
1753
|
+
names = chrys_reserved_tool_names()
|
|
1754
|
+
if self._additional_reserved_tool_names is not None:
|
|
1755
|
+
names.update(self._additional_reserved_tool_names())
|
|
1756
|
+
return names
|
|
1757
|
+
|
|
1758
|
+
@property
|
|
1759
|
+
def test_adapter(self) -> MCPAdapter:
|
|
1760
|
+
"""Expose adapter for tests."""
|
|
1761
|
+
return self._adapter
|
|
1762
|
+
|
|
1763
|
+
def compose(self) -> ComposeResult:
|
|
1764
|
+
localizer = widget_localizer(self)
|
|
1765
|
+
with Vertical(classes="mcp-header-bar"), Vertical(classes="mcp-field"):
|
|
1766
|
+
yield Label(render_str(localizer, _SERVERS.bind()), classes="mcp-section-title")
|
|
1767
|
+
yield Label(render_str(localizer, _SERVERS_DESCRIPTION.bind()), classes="mcp-section-desc")
|
|
1768
|
+
add_button = ConfigAddButton(render_str(localizer, _ADD.bind()), id="mcp-add-btn")
|
|
1769
|
+
add_button.disabled = self._read_only
|
|
1770
|
+
add_button.display = not self._read_only
|
|
1771
|
+
yield add_button
|
|
1772
|
+
yield Vertical(id="mcp-cards")
|
|
1773
|
+
|
|
1774
|
+
async def on_mount(self) -> None:
|
|
1775
|
+
await self._rebuild_cards()
|
|
1776
|
+
|
|
1777
|
+
async def _rebuild_cards(self) -> None:
|
|
1778
|
+
container = self.query_one("#mcp-cards", Vertical)
|
|
1779
|
+
focused = self.screen.focused
|
|
1780
|
+
if focused is not None and container in focused.ancestors and self.focusable:
|
|
1781
|
+
# Removing the focused card (its ✕ after a delete) makes Textual focus a neighbour and
|
|
1782
|
+
# animate the scroll to it; the panel itself stays put and keeps the keyboard.
|
|
1783
|
+
self.screen.set_focus(self, scroll_visible=False)
|
|
1784
|
+
replacements = (
|
|
1785
|
+
[
|
|
1786
|
+
MCPConnectionCard(
|
|
1787
|
+
row.config,
|
|
1788
|
+
index,
|
|
1789
|
+
command_line_text=row.command_line_text,
|
|
1790
|
+
key_value_drafts=row.key_value_drafts,
|
|
1791
|
+
read_only=self._read_only,
|
|
1792
|
+
collapsed=row.collapsed,
|
|
1793
|
+
)
|
|
1794
|
+
for index, row in enumerate(self._rows)
|
|
1795
|
+
]
|
|
1796
|
+
if self._rows
|
|
1797
|
+
else [HatchedEmptyState(render_str(widget_localizer(self), _EMPTY.bind()), classes="mcp-empty")]
|
|
1798
|
+
)
|
|
1799
|
+
# One batch: a layout between the removal and the mount would clamp the scroll to the emptied list.
|
|
1800
|
+
with self.app.batch_update():
|
|
1801
|
+
await container.remove_children()
|
|
1802
|
+
await container.mount(*replacements)
|
|
1803
|
+
|
|
1804
|
+
@on(Button.Pressed, "#mcp-add-btn")
|
|
1805
|
+
async def _on_add(self, _event: Button.Pressed) -> None:
|
|
1806
|
+
if self._read_only:
|
|
1807
|
+
return
|
|
1808
|
+
from chrys.service.profiles.agents.schema import MCPServerConfig
|
|
1809
|
+
|
|
1810
|
+
if self._refuse_while_testing(_WAIT_BEFORE_ADDING):
|
|
1811
|
+
return
|
|
1812
|
+
new_config = MCPServerConfig(name="New Server", transport="stdio")
|
|
1813
|
+
self._rows = self._snapshot_rows(preserve_blank_names=True)
|
|
1814
|
+
self._rows.insert(0, _MCPServerRow(new_config, collapsed=False))
|
|
1815
|
+
await self._rebuild_cards()
|
|
1816
|
+
|
|
1817
|
+
def _refuse_while_testing(self, reason: MessageDef) -> bool:
|
|
1818
|
+
"""Tell the user why the card list can't change yet; tests hold card indexes until they finish."""
|
|
1819
|
+
if not self._testing:
|
|
1820
|
+
return False
|
|
1821
|
+
localizer = widget_localizer(self)
|
|
1822
|
+
self.notify(
|
|
1823
|
+
render_str(localizer, reason.bind()),
|
|
1824
|
+
title=render_str(localizer, _MCP_TITLE.bind()),
|
|
1825
|
+
timeout=3,
|
|
1826
|
+
markup=False,
|
|
1827
|
+
)
|
|
1828
|
+
return True
|
|
1829
|
+
|
|
1830
|
+
@on(MCPConnectionCard.Removed)
|
|
1831
|
+
def _on_remove(self, event: MCPConnectionCard.Removed) -> None:
|
|
1832
|
+
card = event.card
|
|
1833
|
+
if self._read_only or not self._shows(card) or self._refuse_while_testing(_WAIT_BEFORE_REMOVING):
|
|
1834
|
+
return
|
|
1835
|
+
dialog = ConfirmDialog(
|
|
1836
|
+
title=_DELETE_SERVER_TITLE.bind(),
|
|
1837
|
+
message=_DELETE_SERVER_MESSAGE.bind(name=card.header_title),
|
|
1838
|
+
confirm_label=_DELETE_SERVER_CONFIRM.bind(),
|
|
1839
|
+
confirm_variant="error",
|
|
1840
|
+
locale_controller=widget_locale_controller(self),
|
|
1841
|
+
)
|
|
1842
|
+
self.app.push_screen(dialog, callback=partial(self._remove_confirmed, card))
|
|
1843
|
+
|
|
1844
|
+
async def _remove_confirmed(self, card: MCPConnectionCard, confirmed: bool | None) -> None:
|
|
1845
|
+
if not confirmed or self._read_only or self._refuse_while_testing(_WAIT_BEFORE_REMOVING):
|
|
1846
|
+
return
|
|
1847
|
+
if not self._shows(card):
|
|
1848
|
+
# The cards were rebuilt while the dialog was open (or a second ✕ press
|
|
1849
|
+
# opened a second dialog): the confirmation names a card that is gone.
|
|
1850
|
+
return
|
|
1851
|
+
index = card.index
|
|
1852
|
+
self._rows = self._snapshot_rows(preserve_blank_names=True)
|
|
1853
|
+
if 0 <= index < len(self._rows):
|
|
1854
|
+
self._rows.pop(index)
|
|
1855
|
+
await self._rebuild_cards()
|
|
1856
|
+
self.post_message(CardListChanged())
|
|
1857
|
+
|
|
1858
|
+
@on(MCPConnectionCard.TestRequested)
|
|
1859
|
+
def _on_test_requested(self, event: MCPConnectionCard.TestRequested) -> None:
|
|
1860
|
+
card = event.card
|
|
1861
|
+
if self._read_only or not self._shows(card) or card.index in self._testing:
|
|
1862
|
+
return
|
|
1863
|
+
# Held until the test ends; adding and removing servers wait for it, so the index stays put.
|
|
1864
|
+
self._testing.add(card.index)
|
|
1865
|
+
self._set_test_button_busy(card, True)
|
|
1866
|
+
self._run_test_requested(card)
|
|
1867
|
+
|
|
1868
|
+
@work(thread=False)
|
|
1869
|
+
async def _run_test_requested(self, card: MCPConnectionCard) -> None:
|
|
1870
|
+
index = card.index
|
|
1871
|
+
server_name = ""
|
|
1872
|
+
with contextlib.suppress(Exception):
|
|
1873
|
+
server_name = card.query_one(f"#mcp-name-{index}", Input).value.strip()
|
|
1874
|
+
|
|
1875
|
+
dialog = ConnectionTestDialog(server_name=server_name, subject_label=_MCP_SERVER_SUBJECT.bind())
|
|
1876
|
+
self.app.push_screen(dialog)
|
|
1877
|
+
|
|
1878
|
+
try:
|
|
1879
|
+
errors = card.validate()
|
|
1880
|
+
if errors:
|
|
1881
|
+
dialog.set_result(success=False, message="\n".join(errors))
|
|
1882
|
+
return
|
|
1883
|
+
|
|
1884
|
+
config = card.get_config()
|
|
1885
|
+
self._adapter.set_reserved_tool_names(self._current_reserved_tool_names())
|
|
1886
|
+
report = await self._adapter.test_connection(config)
|
|
1887
|
+
except asyncio.CancelledError as exc:
|
|
1888
|
+
logger.info("MCP connection test cancelled: %s", self._exception_chain_summary(exc))
|
|
1889
|
+
dialog.set_result(
|
|
1890
|
+
success=False,
|
|
1891
|
+
message=self._format_connection_error(exc, render_message=self._render_message),
|
|
1892
|
+
)
|
|
1893
|
+
current = asyncio.current_task()
|
|
1894
|
+
if current is not None and current.cancelling() > 0:
|
|
1895
|
+
raise
|
|
1896
|
+
except Exception as exc:
|
|
1897
|
+
kind = self._classify_connection_error(exc)
|
|
1898
|
+
logger.warning(
|
|
1899
|
+
"MCP connection test failed (%s): %s",
|
|
1900
|
+
kind,
|
|
1901
|
+
self._exception_chain_summary(exc),
|
|
1902
|
+
exc_info=exc,
|
|
1903
|
+
)
|
|
1904
|
+
dialog.set_result(
|
|
1905
|
+
success=False,
|
|
1906
|
+
message=self._format_connection_error(exc, render_message=self._render_message),
|
|
1907
|
+
allow_esc=(kind == "timeout"),
|
|
1908
|
+
)
|
|
1909
|
+
else:
|
|
1910
|
+
# Test doubles may stub test_connection to return None; keep the
|
|
1911
|
+
# plain confirmation for that shape instead of crashing the worker.
|
|
1912
|
+
if report is None:
|
|
1913
|
+
dialog.set_result(success=True, message=self._render_message(_CONNECTION_SUCCESSFUL.bind()))
|
|
1914
|
+
else:
|
|
1915
|
+
dialog.set_result(
|
|
1916
|
+
success=True,
|
|
1917
|
+
message=_format_mcp_report(config, report, render_message=self._render_message),
|
|
1918
|
+
markdown=True,
|
|
1919
|
+
)
|
|
1920
|
+
finally:
|
|
1921
|
+
self._testing.discard(index)
|
|
1922
|
+
self._set_test_button_busy(card, False)
|
|
1923
|
+
|
|
1924
|
+
@staticmethod
|
|
1925
|
+
def _exception_chain(exc: BaseException) -> list[BaseException]:
|
|
1926
|
+
chain: list[BaseException] = []
|
|
1927
|
+
seen: set[int] = set()
|
|
1928
|
+
current: BaseException | None = exc
|
|
1929
|
+
while current is not None and id(current) not in seen:
|
|
1930
|
+
chain.append(current)
|
|
1931
|
+
seen.add(id(current))
|
|
1932
|
+
next_exc = current.__cause__ if current.__cause__ is not None else current.__context__
|
|
1933
|
+
current = next_exc if isinstance(next_exc, BaseException) else None
|
|
1934
|
+
return chain
|
|
1935
|
+
|
|
1936
|
+
@classmethod
|
|
1937
|
+
def _classify_connection_error(cls, exc: BaseException) -> str:
|
|
1938
|
+
chain = cls._exception_chain(exc)
|
|
1939
|
+
|
|
1940
|
+
if any(isinstance(err, MCPToolConfigurationError) for err in chain):
|
|
1941
|
+
return "configuration"
|
|
1942
|
+
|
|
1943
|
+
if any(isinstance(err, asyncio.CancelledError) for err in chain):
|
|
1944
|
+
return "cancelled"
|
|
1945
|
+
|
|
1946
|
+
if any(isinstance(err, (TimeoutError, asyncio.TimeoutError)) for err in chain):
|
|
1947
|
+
return "timeout"
|
|
1948
|
+
|
|
1949
|
+
with contextlib.suppress(ModuleNotFoundError):
|
|
1950
|
+
import httpx
|
|
1951
|
+
|
|
1952
|
+
if any(isinstance(err, httpx.TimeoutException) for err in chain):
|
|
1953
|
+
return "timeout"
|
|
1954
|
+
if any(isinstance(err, httpx.ConnectError) for err in chain):
|
|
1955
|
+
return "connect_failed"
|
|
1956
|
+
if any(isinstance(err, httpx.RequestError) for err in chain):
|
|
1957
|
+
return "request_failed"
|
|
1958
|
+
|
|
1959
|
+
if any(isinstance(err, (socket.gaierror, ConnectionRefusedError, ConnectionError)) for err in chain):
|
|
1960
|
+
return "connect_failed"
|
|
1961
|
+
|
|
1962
|
+
return "unknown"
|
|
1963
|
+
|
|
1964
|
+
@classmethod
|
|
1965
|
+
def _exception_chain_summary(cls, exc: BaseException) -> str:
|
|
1966
|
+
parts: list[str] = []
|
|
1967
|
+
for err in cls._exception_chain(exc):
|
|
1968
|
+
text = str(err).strip()
|
|
1969
|
+
parts.append(f"{err.__class__.__name__}: {text}" if text else err.__class__.__name__)
|
|
1970
|
+
return " <- ".join(parts)
|
|
1971
|
+
|
|
1972
|
+
@classmethod
|
|
1973
|
+
def _format_connection_error(
|
|
1974
|
+
cls,
|
|
1975
|
+
exc: BaseException,
|
|
1976
|
+
*,
|
|
1977
|
+
render_message: Callable[[MessageRef], str] = format_message,
|
|
1978
|
+
) -> str:
|
|
1979
|
+
"""Normalize transport/query errors into readable text for the test dialog."""
|
|
1980
|
+
kind = cls._classify_connection_error(exc)
|
|
1981
|
+
|
|
1982
|
+
raw = str(exc).strip() or exc.__class__.__name__
|
|
1983
|
+
marker = " on TabPane(id='mcp')"
|
|
1984
|
+
if marker in raw:
|
|
1985
|
+
raw = raw.replace(marker, "")
|
|
1986
|
+
|
|
1987
|
+
if kind == "configuration":
|
|
1988
|
+
friendly = render_message(_CONNECTION_CONFIGURATION_CONFLICT.bind())
|
|
1989
|
+
elif kind == "cancelled":
|
|
1990
|
+
friendly = render_message(_CONNECTION_CANCELLED.bind())
|
|
1991
|
+
elif kind == "timeout":
|
|
1992
|
+
friendly = render_message(_CONNECTION_TIMED_OUT.bind())
|
|
1993
|
+
elif kind == "connect_failed":
|
|
1994
|
+
friendly = render_message(_CONNECTION_UNREACHABLE.bind())
|
|
1995
|
+
elif kind == "request_failed":
|
|
1996
|
+
friendly = render_message(_CONNECTION_REQUEST_FAILED.bind())
|
|
1997
|
+
else:
|
|
1998
|
+
friendly = render_message(_CONNECTION_FAILED.bind())
|
|
1999
|
+
|
|
2000
|
+
return f"{friendly}\n\n{raw}"
|
|
2001
|
+
|
|
2002
|
+
def _shows(self, card: MCPConnectionCard) -> bool:
|
|
2003
|
+
"""Whether *card* is one of the mounted cards; a rebuild replaces them all, and re-indexes the new ones."""
|
|
2004
|
+
return any(mounted is card for mounted in self.query(MCPConnectionCard))
|
|
2005
|
+
|
|
2006
|
+
def _set_test_button_busy(self, card: MCPConnectionCard, busy: bool) -> None:
|
|
2007
|
+
with contextlib.suppress(Exception):
|
|
2008
|
+
button = card.query_one(f"#mcp-test-btn-{card.index}", Button)
|
|
2009
|
+
if self._read_only:
|
|
2010
|
+
button.disabled = True
|
|
2011
|
+
button.display = False
|
|
2012
|
+
return
|
|
2013
|
+
button.disabled = busy
|
|
2014
|
+
button.label = render_str(widget_localizer(self), (_TESTING if busy else _TEST).bind())
|
|
2015
|
+
|
|
2016
|
+
def _snapshot_rows(self, *, preserve_blank_names: bool = False) -> list[_MCPServerRow]:
|
|
2017
|
+
"""Snapshot each card's config and UI-only state, in card order, for a rebuild or ``get_config``."""
|
|
2018
|
+
cards = list(self.query(MCPConnectionCard))
|
|
2019
|
+
if len(cards) != len(self._rows):
|
|
2020
|
+
# Not mounted yet (or mid-rebuild): the rows are still the truth.
|
|
2021
|
+
return list(self._rows)
|
|
2022
|
+
return [
|
|
2023
|
+
_MCPServerRow(
|
|
2024
|
+
card._snapshot_config() if preserve_blank_names else card.get_config(),
|
|
2025
|
+
command_line_text=card._snapshot_command_line_text(),
|
|
2026
|
+
key_value_drafts=card._snapshot_key_value_drafts(),
|
|
2027
|
+
collapsed=card.collapsed,
|
|
2028
|
+
)
|
|
2029
|
+
for card in cards
|
|
2030
|
+
]
|
|
2031
|
+
|
|
2032
|
+
def get_config(self) -> list[MCPServerConfig]:
|
|
2033
|
+
"""Collect config from all connection cards."""
|
|
2034
|
+
return [row.config for row in self._snapshot_rows()]
|
|
2035
|
+
|
|
2036
|
+
def card_folds(self) -> list[bool]:
|
|
2037
|
+
"""Whether each server's card is folded, in ``get_config`` order.
|
|
2038
|
+
|
|
2039
|
+
The agent config screen rebuilds this panel from the draft when it loads
|
|
2040
|
+
the agent again, and passes these back so an unsaved server's card stays
|
|
2041
|
+
unfolded.
|
|
2042
|
+
"""
|
|
2043
|
+
cards = list(self.query(MCPConnectionCard))
|
|
2044
|
+
if len(cards) != len(self._rows):
|
|
2045
|
+
return [row.collapsed for row in self._rows]
|
|
2046
|
+
return [card.collapsed for card in cards]
|
|
2047
|
+
|
|
2048
|
+
def _validation_errors_by_card(self) -> list[tuple[MCPConnectionCard, list[str]]]:
|
|
2049
|
+
localizer = widget_localizer(self)
|
|
2050
|
+
results: list[tuple[MCPConnectionCard, list[str]]] = []
|
|
2051
|
+
seen_names: set[str] = set()
|
|
2052
|
+
for card in self.query(MCPConnectionCard):
|
|
2053
|
+
errors = list(card.validate())
|
|
2054
|
+
try:
|
|
2055
|
+
name = card.query_one(f"#mcp-name-{card.index}", Input).value.strip()
|
|
2056
|
+
if name:
|
|
2057
|
+
lower_name = name.lower()
|
|
2058
|
+
if lower_name in seen_names:
|
|
2059
|
+
errors.append(
|
|
2060
|
+
render_str(
|
|
2061
|
+
localizer,
|
|
2062
|
+
MCP_DUPLICATE_SERVER.bind(name=DisplayBlock(name)),
|
|
2063
|
+
)
|
|
2064
|
+
)
|
|
2065
|
+
seen_names.add(lower_name)
|
|
2066
|
+
except Exception:
|
|
2067
|
+
pass
|
|
2068
|
+
results.append((card, errors))
|
|
2069
|
+
return results
|
|
2070
|
+
|
|
2071
|
+
def validate(self) -> list[str]:
|
|
2072
|
+
"""Validate all connection cards."""
|
|
2073
|
+
return [error for _card, errors in self._validation_errors_by_card() for error in errors]
|
|
2074
|
+
|
|
2075
|
+
def unfold_invalid_cards(self, invalid_indexes: Collection[int] = ()) -> None:
|
|
2076
|
+
"""Unfold the cards that fail validation or sit at *invalid_indexes*, and scroll to the first.
|
|
2077
|
+
|
|
2078
|
+
A folded card hides the fields a Save error names, and the error names
|
|
2079
|
+
the first invalid card first.
|
|
2080
|
+
"""
|
|
2081
|
+
invalid = [
|
|
2082
|
+
card for card, errors in self._validation_errors_by_card() if errors or card.index in invalid_indexes
|
|
2083
|
+
]
|
|
2084
|
+
for card in invalid:
|
|
2085
|
+
card.set_collapsed(False, scroll_visible=False)
|
|
2086
|
+
if invalid:
|
|
2087
|
+
self.call_after_refresh(invalid[0].scroll_visible, animate=False)
|
|
2088
|
+
|
|
2089
|
+
|
|
2090
|
+
# --- Test-dialog Markdown report -------------------------------------------
|
|
2091
|
+
#
|
|
2092
|
+
# Every server-controlled string below must pass through ``inline_text`` /
|
|
2093
|
+
# ``code_span`` so a crafted server cannot forge block structure in the
|
|
2094
|
+
# rendered report. Env values and HTTP headers are NEVER rendered — they
|
|
2095
|
+
# routinely carry secrets.
|
|
2096
|
+
|
|
2097
|
+
# Caps keep a pathological server (thousands of tools, book-length
|
|
2098
|
+
# instructions) from turning the report into an unbounded document.
|
|
2099
|
+
_MAX_LISTED_ENTRIES = 40
|
|
2100
|
+
_INSTRUCTIONS_PREVIEW_CHARS = 600
|
|
2101
|
+
_DESCRIPTION_PREVIEW_CHARS = 200
|
|
2102
|
+
_IDENTITY_PREVIEW_CHARS = 120
|
|
2103
|
+
|
|
2104
|
+
|
|
2105
|
+
def _preview(value: object, limit: int) -> str:
|
|
2106
|
+
flat = inline_text(value or "")
|
|
2107
|
+
if len(flat) > limit:
|
|
2108
|
+
return flat[:limit].rstrip() + "…"
|
|
2109
|
+
return flat
|
|
2110
|
+
|
|
2111
|
+
|
|
2112
|
+
def _format_mcp_report(
|
|
2113
|
+
config: MCPServerConfig,
|
|
2114
|
+
report: MCPTestReport,
|
|
2115
|
+
*,
|
|
2116
|
+
render_message: Callable[[MessageRef], str] = format_message,
|
|
2117
|
+
) -> str:
|
|
2118
|
+
"""Render a successful connection test as the dialog's Markdown report."""
|
|
2119
|
+
sections = [
|
|
2120
|
+
_server_heading(report, render_message),
|
|
2121
|
+
_connection_caption(config, render_message),
|
|
2122
|
+
f"### {render_message(_CAPABILITIES_HEADING.bind())}",
|
|
2123
|
+
_mcp_capabilities_section(report.capabilities, render_message),
|
|
2124
|
+
f"### {render_message(_INSTRUCTIONS_HEADING.bind())}",
|
|
2125
|
+
_instructions_section(report.instructions, render_message),
|
|
2126
|
+
f"### {render_message(_TOOLS_HEADING.bind())}",
|
|
2127
|
+
_tools_section(report, render_message),
|
|
2128
|
+
f"### {render_message(_PROMPTS_HEADING.bind())}",
|
|
2129
|
+
_prompts_section(config, report, render_message),
|
|
2130
|
+
]
|
|
2131
|
+
if report.initial_tool_names is not None:
|
|
2132
|
+
sections.extend(
|
|
2133
|
+
(
|
|
2134
|
+
f"### {render_message(_PROGRESSIVE_DISCLOSURE_HEADING.bind())}",
|
|
2135
|
+
_progressive_section(report.initial_tool_names, render_message),
|
|
2136
|
+
)
|
|
2137
|
+
)
|
|
2138
|
+
return "\n\n".join(sections)
|
|
2139
|
+
|
|
2140
|
+
|
|
2141
|
+
def _server_heading(report: MCPTestReport, render_message: Callable[[MessageRef], str] = format_message) -> str:
|
|
2142
|
+
name = _preview(report.server_name, _IDENTITY_PREVIEW_CHARS)
|
|
2143
|
+
title = _preview(report.server_title, _IDENTITY_PREVIEW_CHARS) or name
|
|
2144
|
+
parts = []
|
|
2145
|
+
if title:
|
|
2146
|
+
parts.append(f"**{title}**")
|
|
2147
|
+
if name and name != title:
|
|
2148
|
+
parts.append(code_span(name))
|
|
2149
|
+
if version := _preview(report.server_version, _IDENTITY_PREVIEW_CHARS):
|
|
2150
|
+
parts.append(f"v{version}")
|
|
2151
|
+
if protocol := _preview(report.protocol_version, _IDENTITY_PREVIEW_CHARS):
|
|
2152
|
+
parts.append(f"MCP {code_span(protocol)}")
|
|
2153
|
+
if not parts:
|
|
2154
|
+
return render_message(_IDENTITY_NOT_REPORTED.bind())
|
|
2155
|
+
return " · ".join(parts)
|
|
2156
|
+
|
|
2157
|
+
|
|
2158
|
+
def _connection_caption(config: MCPServerConfig, render_message: Callable[[MessageRef], str] = format_message) -> str:
|
|
2159
|
+
# Command line / URL only: env values and headers may carry secrets.
|
|
2160
|
+
if config.transport == "stdio":
|
|
2161
|
+
command_line = " ".join([config.command, *config.args]).strip()
|
|
2162
|
+
target = code_span(command_line) if command_line else render_message(_NO_COMMAND_CONFIGURED.bind())
|
|
2163
|
+
else:
|
|
2164
|
+
target = code_span(config.url) if config.url.strip() else render_message(_NO_URL_CONFIGURED.bind())
|
|
2165
|
+
return render_message(_CONNECTED_VIA.bind(transport=code_span(config.transport), target=target))
|
|
2166
|
+
|
|
2167
|
+
|
|
2168
|
+
def _mcp_capabilities_section(
|
|
2169
|
+
advertised: tuple[str, ...],
|
|
2170
|
+
render_message: Callable[[MessageRef], str] = format_message,
|
|
2171
|
+
) -> str:
|
|
2172
|
+
advertised_set = set(advertised)
|
|
2173
|
+
if not advertised_set:
|
|
2174
|
+
return render_message(NOT_ADVERTISED.bind())
|
|
2175
|
+
# Standard groups always render (that is the point of the ✓/— column);
|
|
2176
|
+
# server-invented extras only fill the remaining row budget, and each
|
|
2177
|
+
# name is clamped — extras are unbounded server-controlled strings.
|
|
2178
|
+
extras = sorted((name for name in advertised_set if name not in STANDARD_CAPABILITY_GROUPS), key=str.casefold)
|
|
2179
|
+
shown = list(STANDARD_CAPABILITY_GROUPS) + extras[: _MAX_LISTED_ENTRIES - len(STANDARD_CAPABILITY_GROUPS)]
|
|
2180
|
+
table = md_table(
|
|
2181
|
+
(render_message(_CAPABILITY_HEADER.bind()), render_message(_ADVERTISED_HEADER.bind())),
|
|
2182
|
+
[
|
|
2183
|
+
(
|
|
2184
|
+
code_span(_preview(name, _IDENTITY_PREVIEW_CHARS)),
|
|
2185
|
+
SUPPORTED_MARK if name in advertised_set else UNSUPPORTED_MARK,
|
|
2186
|
+
)
|
|
2187
|
+
for name in shown
|
|
2188
|
+
],
|
|
2189
|
+
)
|
|
2190
|
+
if hidden := len(extras) - (len(shown) - len(STANDARD_CAPABILITY_GROUPS)):
|
|
2191
|
+
table += f"\n\n*{render_message(_MORE_ENTRIES.bind(extra_count=hidden))}*"
|
|
2192
|
+
return table
|
|
2193
|
+
|
|
2194
|
+
|
|
2195
|
+
def _instructions_section(
|
|
2196
|
+
instructions: str | None,
|
|
2197
|
+
render_message: Callable[[MessageRef], str] = format_message,
|
|
2198
|
+
) -> str:
|
|
2199
|
+
flat = _preview(instructions, _INSTRUCTIONS_PREVIEW_CHARS)
|
|
2200
|
+
if not flat:
|
|
2201
|
+
return render_message(NOT_ADVERTISED.bind())
|
|
2202
|
+
# Blockquote + italic wrap: blockquote content is block-parsed, so bare
|
|
2203
|
+
# remote text at line start could still forge a heading/bullet — the
|
|
2204
|
+
# leading ``*`` keeps our own marker first on the line.
|
|
2205
|
+
return f"> *{flat}*"
|
|
2206
|
+
|
|
2207
|
+
|
|
2208
|
+
def _catalog_bullets(
|
|
2209
|
+
entries: tuple[tuple[str, str], ...],
|
|
2210
|
+
render_message: Callable[[MessageRef], str] = format_message,
|
|
2211
|
+
) -> str:
|
|
2212
|
+
shown = entries[:_MAX_LISTED_ENTRIES]
|
|
2213
|
+
bullets = definition_bullets(
|
|
2214
|
+
[(name, "", _preview(description, _DESCRIPTION_PREVIEW_CHARS)) for name, description in shown]
|
|
2215
|
+
)
|
|
2216
|
+
if len(entries) > len(shown):
|
|
2217
|
+
bullets += f"\n- *{render_message(_MORE_ENTRIES.bind(extra_count=len(entries) - len(shown)))}*"
|
|
2218
|
+
return bullets
|
|
2219
|
+
|
|
2220
|
+
|
|
2221
|
+
def _tools_section(report: MCPTestReport, render_message: Callable[[MessageRef], str] = format_message) -> str:
|
|
2222
|
+
if report.tools:
|
|
2223
|
+
return render_message(
|
|
2224
|
+
_TOOLS_EXPOSED.bind(
|
|
2225
|
+
count=len(report.tools),
|
|
2226
|
+
catalog=DisplayBlock(_catalog_bullets(report.tools, render_message)),
|
|
2227
|
+
)
|
|
2228
|
+
)
|
|
2229
|
+
if "tools" in report.capabilities:
|
|
2230
|
+
return render_message(_ADVERTISED_NO_TOOLS.bind())
|
|
2231
|
+
return render_message(NOT_ADVERTISED.bind())
|
|
2232
|
+
|
|
2233
|
+
|
|
2234
|
+
def _prompts_section(
|
|
2235
|
+
config: MCPServerConfig,
|
|
2236
|
+
report: MCPTestReport,
|
|
2237
|
+
render_message: Callable[[MessageRef], str] = format_message,
|
|
2238
|
+
) -> str:
|
|
2239
|
+
if report.prompts:
|
|
2240
|
+
return render_message(
|
|
2241
|
+
_PROMPTS_EXPOSED.bind(
|
|
2242
|
+
count=len(report.prompts),
|
|
2243
|
+
catalog=DisplayBlock(_catalog_bullets(report.prompts, render_message)),
|
|
2244
|
+
)
|
|
2245
|
+
)
|
|
2246
|
+
if not config.load_prompts:
|
|
2247
|
+
return render_message(_PROMPT_LOADING_DISABLED.bind())
|
|
2248
|
+
if "prompts" in report.capabilities:
|
|
2249
|
+
return render_message(_ADVERTISED_NO_PROMPTS.bind())
|
|
2250
|
+
return render_message(NOT_ADVERTISED.bind())
|
|
2251
|
+
|
|
2252
|
+
|
|
2253
|
+
def _progressive_section(
|
|
2254
|
+
initial_tool_names: tuple[str, ...],
|
|
2255
|
+
render_message: Callable[[MessageRef], str] = format_message,
|
|
2256
|
+
) -> str:
|
|
2257
|
+
lines = [render_message(_PROGRESSIVE_ENABLED.bind()), ""]
|
|
2258
|
+
shown = initial_tool_names[:_MAX_LISTED_ENTRIES]
|
|
2259
|
+
lines.extend(f"- {code_span(name)}" for name in shown)
|
|
2260
|
+
if len(initial_tool_names) > len(shown):
|
|
2261
|
+
lines.append(f"- *{render_message(_MORE_ENTRIES.bind(extra_count=len(initial_tool_names) - len(shown)))}*")
|
|
2262
|
+
return "\n".join(lines)
|