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,2043 @@
|
|
|
1
|
+
# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Model configuration modal — manage model profiles with sidebar + form layout."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import contextlib
|
|
9
|
+
import copy
|
|
10
|
+
import json
|
|
11
|
+
import math
|
|
12
|
+
import re
|
|
13
|
+
import uuid
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
16
|
+
|
|
17
|
+
from rich.markup import escape
|
|
18
|
+
from rich.text import Text
|
|
19
|
+
from textual import on
|
|
20
|
+
from textual.containers import Horizontal, Vertical, VerticalScroll
|
|
21
|
+
from textual.content import Content
|
|
22
|
+
from textual.css.query import NoMatches
|
|
23
|
+
from textual.widget import Widget
|
|
24
|
+
from textual.widgets import Button, Label, OptionList, Static
|
|
25
|
+
from textual.widgets.option_list import Option, OptionDoesNotExist
|
|
26
|
+
|
|
27
|
+
from chrys.app.tui.binding_display import CANCEL_BINDING, localized_binding
|
|
28
|
+
from chrys.app.tui.i18n import render_str, widget_locale_controller, widget_localizer
|
|
29
|
+
from chrys.app.tui.screens.dialogs.base import BaseDialog
|
|
30
|
+
from chrys.app.tui.widgets import Checkbox, ConfigAddButton, Select
|
|
31
|
+
from chrys.app.tui.widgets import EnhancedInput as Input
|
|
32
|
+
from chrys.foundation.branding import APP_DISPLAY_NAME
|
|
33
|
+
from chrys.foundation.config.runtime_pointer import set_model_pointer
|
|
34
|
+
from chrys.foundation.config.spec import SettingOrigin, Source
|
|
35
|
+
from chrys.foundation.i18n import DisplayBlock, MessageDef, MessageRef, msg
|
|
36
|
+
from chrys.foundation.i18n.formatting import format_message
|
|
37
|
+
from chrys.foundation.text.reading_order import reading_order
|
|
38
|
+
from chrys.foundation.util.chrys_headers import is_chrys_managed_header_name
|
|
39
|
+
from chrys.foundation.util.header_charset import (
|
|
40
|
+
api_key_charset_error,
|
|
41
|
+
header_name_charset_error,
|
|
42
|
+
header_value_charset_error,
|
|
43
|
+
model_id_charset_error,
|
|
44
|
+
)
|
|
45
|
+
from chrys.service.context.compaction.budgets import MIN_DERIVABLE_CONTEXT_TOKENS
|
|
46
|
+
from chrys.service.llm.providers import CHAT_COMPLETIONS_TOKEN_LIMIT_PARAMS, PROVIDERS
|
|
47
|
+
from chrys.service.profiles.models.options import (
|
|
48
|
+
OUTPUT_CAP_OPTION_ALIASES,
|
|
49
|
+
PROTECTED_EXTRA_BODY_CHAT_OPTION_KEYS,
|
|
50
|
+
PROTECTED_TOP_LEVEL_CHAT_OPTION_KEYS,
|
|
51
|
+
is_anthropic_claude_profile,
|
|
52
|
+
lacks_anthropic_prompt_cache_option,
|
|
53
|
+
with_anthropic_prompt_cache_option,
|
|
54
|
+
)
|
|
55
|
+
from chrys.service.profiles.models.schema import (
|
|
56
|
+
API_STYLE_CHAT_COMPLETIONS,
|
|
57
|
+
API_STYLE_RESPONSES,
|
|
58
|
+
DEFAULT_MAX_CONTEXT_TOKENS,
|
|
59
|
+
DEFAULT_MAX_OUTPUT_TOKENS,
|
|
60
|
+
is_model_profile_selectable,
|
|
61
|
+
is_responses_wire_dialect,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
if TYPE_CHECKING:
|
|
65
|
+
from textual.app import ComposeResult
|
|
66
|
+
|
|
67
|
+
from chrys.app.tui.screens.dialogs.prompt_cache import PromptCacheDialogResult
|
|
68
|
+
from chrys.service.profiles.models.registry import ModelProfileRegistry
|
|
69
|
+
from chrys.service.profiles.models.schema import ModelProfile
|
|
70
|
+
|
|
71
|
+
_READ_ONLY_NOTICE = msg(
|
|
72
|
+
"tui.model_config.read_only_notice",
|
|
73
|
+
fallback="• Agent is running. This page is read-only.",
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
_MODEL_SETTINGS_TITLE = msg("tui.model_config.title.settings", fallback="Model Settings")
|
|
77
|
+
_VALIDATION_ERROR_TITLE = msg("tui.model_config.title.validation_error", fallback="Validation Error")
|
|
78
|
+
_SAVE_ERROR_TITLE = msg("tui.model_config.title.save_error", fallback="Save Error")
|
|
79
|
+
_MODEL_PROFILE_SAVED = msg("tui.model_config.saved", fallback="Model profile saved")
|
|
80
|
+
_CONFIGURATION_TITLE = msg("tui.model_config.title.configuration", fallback="Model Configuration")
|
|
81
|
+
_PROFILE_NAME = msg("tui.model_config.profile_name", fallback="Profile Name")
|
|
82
|
+
_PROFILE_NAME_PLACEHOLDER = msg(
|
|
83
|
+
"tui.model_config.placeholder.profile_name",
|
|
84
|
+
fallback="e.g. My Model Config",
|
|
85
|
+
)
|
|
86
|
+
_MODEL_OPTIONS = msg("tui.model_config.model_options", fallback="Model Options")
|
|
87
|
+
_PROVIDER = msg("tui.model_config.provider", fallback="Provider")
|
|
88
|
+
_API_STYLE = msg("tui.model_config.api_style", fallback="API Style")
|
|
89
|
+
_MODEL = msg("tui.model_config.model", fallback="Model")
|
|
90
|
+
_MODEL_PLACEHOLDER = msg(
|
|
91
|
+
"tui.model_config.placeholder.model",
|
|
92
|
+
fallback="Provider-specific model id (required)",
|
|
93
|
+
)
|
|
94
|
+
_MAX_CONTEXT_WINDOW = msg("tui.model_config.max_context_window", fallback="Max Context Window")
|
|
95
|
+
_MAX_OUTPUT_TOKENS = msg(
|
|
96
|
+
"tui.model_config.max_output_tokens",
|
|
97
|
+
fallback="Max Output Tokens ({param})",
|
|
98
|
+
)
|
|
99
|
+
_OUTPUT_CAP_PLACEHOLDER = msg(
|
|
100
|
+
"tui.model_config.placeholder.output_cap",
|
|
101
|
+
fallback="Model output cap (default {default})",
|
|
102
|
+
)
|
|
103
|
+
_BASE_URL = msg("tui.model_config.base_url", fallback="Base URL")
|
|
104
|
+
_API_KEY = msg("tui.model_config.api_key", fallback="API Key")
|
|
105
|
+
_STREAMING = msg("tui.model_config.streaming", fallback="Streaming")
|
|
106
|
+
_VISION = msg("tui.model_config.vision", fallback="Vision Model")
|
|
107
|
+
_CONNECTION_OPTIONS = msg("tui.model_config.connection_options", fallback="Connection Options")
|
|
108
|
+
_SKIP_TLS_VERIFICATION = msg("tui.model_config.skip_tls_verification", fallback="Skip TLS verification")
|
|
109
|
+
_INSECURE_TLS = msg(
|
|
110
|
+
"tui.model_config.insecure_tls",
|
|
111
|
+
fallback="Insecure: disables HTTPS certificate validation. Use only for trusted endpoints.",
|
|
112
|
+
)
|
|
113
|
+
_BYPASS_PROXY = msg("tui.model_config.bypass_proxy", fallback="Bypass proxy")
|
|
114
|
+
_HTTP_OPTIONS = msg("tui.model_config.http_options", fallback="HTTP Options")
|
|
115
|
+
_HTTP_CONNECT_TIMEOUT = msg("tui.model_config.http_connect_timeout", fallback="HTTP Connect Timeout (s)")
|
|
116
|
+
_HTTP_READ_TIMEOUT = msg("tui.model_config.http_read_timeout", fallback="HTTP Read Timeout (s)")
|
|
117
|
+
_HTTP_MAX_RETRIES = msg("tui.model_config.http_max_retries", fallback="HTTP Max Retries")
|
|
118
|
+
_EXTRA_OPTIONS = msg("tui.model_config.extra_options", fallback="Extra Options")
|
|
119
|
+
_HTTP_EXTRA_HEADERS = msg("tui.model_config.http_extra_headers", fallback="HTTP Extra Headers")
|
|
120
|
+
_CHAT_OPTIONS = msg("tui.model_config.chat_options", fallback="Chat Options")
|
|
121
|
+
_NEW = msg("tui.model_config.button.new", fallback="New")
|
|
122
|
+
_CLONE = msg("tui.model_config.button.clone", fallback="Clone")
|
|
123
|
+
_DELETE = msg("tui.model_config.button.delete", fallback="Delete")
|
|
124
|
+
_SAVE = msg("tui.model_config.button.save", fallback="Save")
|
|
125
|
+
_CLOSE = msg("tui.model_config.button.close", fallback="Close")
|
|
126
|
+
_ADD = msg("tui.model_config.button.add", fallback="+ Add")
|
|
127
|
+
_HEADER_NAME_PLACEHOLDER = msg(
|
|
128
|
+
"tui.model_config.placeholder.header_name",
|
|
129
|
+
fallback="e.g. X-Auth-Token",
|
|
130
|
+
)
|
|
131
|
+
_HEADER_VALUE_PLACEHOLDER = msg(
|
|
132
|
+
"tui.model_config.placeholder.header_value",
|
|
133
|
+
fallback="e.g. {{{{AUTH_TOKEN}}}}",
|
|
134
|
+
)
|
|
135
|
+
_OPTION_NAME_PLACEHOLDER = msg(
|
|
136
|
+
"tui.model_config.placeholder.option_name",
|
|
137
|
+
fallback="e.g. extra_body, temperature",
|
|
138
|
+
)
|
|
139
|
+
_OPTION_VALUE_PLACEHOLDER = msg(
|
|
140
|
+
"tui.model_config.placeholder.option_value",
|
|
141
|
+
fallback="e.g. 0.7, true, or {{...}}",
|
|
142
|
+
)
|
|
143
|
+
_PROVIDER_MODEL = msg("tui.model_config.provider_model", fallback="{provider} Model")
|
|
144
|
+
_PROVIDER_BASE_URL = msg("tui.model_config.provider_base_url", fallback="{provider} Base URL")
|
|
145
|
+
_PROVIDER_API_KEY = msg("tui.model_config.provider_api_key", fallback="{provider} API Key")
|
|
146
|
+
_LEAVE_BLANK_FOR_DEFAULT = msg(
|
|
147
|
+
"tui.model_config.placeholder.leave_blank_for_default",
|
|
148
|
+
fallback="Leave blank for default",
|
|
149
|
+
)
|
|
150
|
+
_DELETE_PROFILE_TITLE = msg("tui.model_config.confirm_delete.title", fallback="Delete Model Profile")
|
|
151
|
+
_DELETE_PROFILE_MESSAGE = msg(
|
|
152
|
+
"tui.model_config.confirm_delete.message",
|
|
153
|
+
fallback='Delete model profile\n"{display}"?\n\nThis cannot be undone.',
|
|
154
|
+
multiline=True,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Validation prose.
|
|
158
|
+
_CHAT_OPTION_OBJECT_EXAMPLE = msg(
|
|
159
|
+
"tui.model_config.validation.chat_option.object_example",
|
|
160
|
+
fallback="Chat Options row {row}: '{key}' must be a valid JSON object/map, for example {example}.",
|
|
161
|
+
multiline=True,
|
|
162
|
+
)
|
|
163
|
+
_CHAT_OPTION_OBJECT_TYPE = msg(
|
|
164
|
+
"tui.model_config.validation.chat_option.object_type",
|
|
165
|
+
fallback="Chat Options row {row}: '{key}' must be a JSON object/map, got {type_name}.",
|
|
166
|
+
multiline=True,
|
|
167
|
+
)
|
|
168
|
+
_JSON_NUMBER = msg("tui.model_config.validation.clause.json_number", fallback="a JSON number")
|
|
169
|
+
_JSON_NUMBER_MIN = msg(
|
|
170
|
+
"tui.model_config.validation.clause.json_number_min",
|
|
171
|
+
fallback="a JSON number greater than or equal to {minimum}",
|
|
172
|
+
)
|
|
173
|
+
_JSON_NUMBER_MAX = msg(
|
|
174
|
+
"tui.model_config.validation.clause.json_number_max",
|
|
175
|
+
fallback="a JSON number less than or equal to {maximum}",
|
|
176
|
+
)
|
|
177
|
+
_JSON_NUMBER_RANGE = msg(
|
|
178
|
+
"tui.model_config.validation.clause.json_number_range",
|
|
179
|
+
fallback="a JSON number between {minimum} and {maximum}",
|
|
180
|
+
)
|
|
181
|
+
_JSON_INTEGER = msg("tui.model_config.validation.clause.json_integer", fallback="a JSON integer")
|
|
182
|
+
_JSON_POSITIVE_INTEGER = msg(
|
|
183
|
+
"tui.model_config.validation.clause.json_positive_integer",
|
|
184
|
+
fallback="a positive JSON integer",
|
|
185
|
+
)
|
|
186
|
+
_JSON_INTEGER_MIN = msg(
|
|
187
|
+
"tui.model_config.validation.clause.json_integer_min",
|
|
188
|
+
fallback="a JSON integer greater than or equal to {minimum}",
|
|
189
|
+
)
|
|
190
|
+
_JSON_INTEGER_MAX = msg(
|
|
191
|
+
"tui.model_config.validation.clause.json_integer_max",
|
|
192
|
+
fallback="a JSON integer less than or equal to {maximum}",
|
|
193
|
+
)
|
|
194
|
+
_JSON_INTEGER_RANGE = msg(
|
|
195
|
+
"tui.model_config.validation.clause.json_integer_range",
|
|
196
|
+
fallback="a JSON integer between {minimum} and {maximum}",
|
|
197
|
+
)
|
|
198
|
+
_CHAT_OPTION_MUST_BE = msg(
|
|
199
|
+
"tui.model_config.validation.chat_option.must_be",
|
|
200
|
+
fallback="Chat Options row {row}: '{key}' must be {clause}.",
|
|
201
|
+
multiline=True,
|
|
202
|
+
)
|
|
203
|
+
_CHAT_OPTION_BOOLEAN = msg(
|
|
204
|
+
"tui.model_config.validation.chat_option.boolean",
|
|
205
|
+
fallback="Chat Options row {row}: '{key}' must be a JSON boolean (true or false).",
|
|
206
|
+
multiline=True,
|
|
207
|
+
)
|
|
208
|
+
_CHAT_OPTION_STRING = msg(
|
|
209
|
+
"tui.model_config.validation.chat_option.string",
|
|
210
|
+
fallback="Chat Options row {row}: '{key}' must be a string.",
|
|
211
|
+
multiline=True,
|
|
212
|
+
)
|
|
213
|
+
_CHAT_OPTION_DETAIL = msg(
|
|
214
|
+
"tui.model_config.validation.chat_option.detail",
|
|
215
|
+
fallback="Chat Options row {row}: '{key}': {detail}",
|
|
216
|
+
multiline=True,
|
|
217
|
+
)
|
|
218
|
+
_CHAT_OPTION_STOP = msg(
|
|
219
|
+
"tui.model_config.validation.chat_option.stop",
|
|
220
|
+
fallback="Chat Options row {row}: 'stop' must be a string or a JSON array of strings.",
|
|
221
|
+
)
|
|
222
|
+
_CHAT_OPTION_LOGIT_BIAS = msg(
|
|
223
|
+
"tui.model_config.validation.chat_option.logit_bias",
|
|
224
|
+
fallback=(
|
|
225
|
+
"Chat Options row {row}: 'logit_bias' value for token {token} must be a JSON number between -100 and 100."
|
|
226
|
+
),
|
|
227
|
+
multiline=True,
|
|
228
|
+
)
|
|
229
|
+
_CHAT_OPTION_HEADER_RESERVED = msg(
|
|
230
|
+
"tui.model_config.validation.chat_option.header_reserved",
|
|
231
|
+
fallback="Chat Options row {row}: 'extra_headers' header {header} is reserved for {app}.",
|
|
232
|
+
multiline=True,
|
|
233
|
+
)
|
|
234
|
+
_CHAT_OPTION_HEADER_VALUE_STRING = msg(
|
|
235
|
+
"tui.model_config.validation.chat_option.header_value_string",
|
|
236
|
+
fallback="Chat Options row {row}: 'extra_headers' value for header {header} must be a string.",
|
|
237
|
+
multiline=True,
|
|
238
|
+
)
|
|
239
|
+
_CHAT_OPTION_OUTPUT_CAP = msg(
|
|
240
|
+
"tui.model_config.validation.chat_option.output_cap",
|
|
241
|
+
fallback=(
|
|
242
|
+
"Chat Options row {row}: {key} is not saved on model profiles — remove this row and set the Max Output "
|
|
243
|
+
"Tokens field above instead."
|
|
244
|
+
),
|
|
245
|
+
multiline=True,
|
|
246
|
+
)
|
|
247
|
+
_CHAT_OPTION_PROTECTED = msg(
|
|
248
|
+
"tui.model_config.validation.chat_option.protected",
|
|
249
|
+
fallback="Chat Options row {row}: {key} is protected because it bypasses context admission.",
|
|
250
|
+
multiline=True,
|
|
251
|
+
)
|
|
252
|
+
_CHAT_OPTION_NESTED_PROTECTED = msg(
|
|
253
|
+
"tui.model_config.validation.chat_option.nested_protected",
|
|
254
|
+
fallback="Chat Options row {row}: extra_body contains protected key(s): {keys}.",
|
|
255
|
+
multiline=True,
|
|
256
|
+
)
|
|
257
|
+
_NAME_REQUIRED = msg("tui.model_config.validation.name_required", fallback="Name is required.")
|
|
258
|
+
_PROFILE_EXISTS = msg(
|
|
259
|
+
"tui.model_config.validation.profile_exists",
|
|
260
|
+
fallback="A model profile named '{name}' already exists (names are case-insensitive).",
|
|
261
|
+
multiline=True,
|
|
262
|
+
)
|
|
263
|
+
_PROVIDER_ALLOWED = msg(
|
|
264
|
+
"tui.model_config.validation.provider_allowed",
|
|
265
|
+
fallback="Provider must be one of: {providers}.",
|
|
266
|
+
)
|
|
267
|
+
_MODEL_REQUIRED = msg("tui.model_config.validation.model_required", fallback="Model name is required.")
|
|
268
|
+
_FIELD_REQUIRED_EXAMPLE = msg(
|
|
269
|
+
"tui.model_config.validation.field_required_example",
|
|
270
|
+
fallback="{label} is required (e.g. {example}).",
|
|
271
|
+
)
|
|
272
|
+
_FIELD_VALID_INTEGER = msg(
|
|
273
|
+
"tui.model_config.validation.field_valid_integer",
|
|
274
|
+
fallback="{label} must be a valid integer.",
|
|
275
|
+
)
|
|
276
|
+
_FIELD_POSITIVE_INTEGER = msg(
|
|
277
|
+
"tui.model_config.validation.field_positive_integer",
|
|
278
|
+
fallback="{label} must be a positive integer.",
|
|
279
|
+
)
|
|
280
|
+
_MAX_CONTEXT_TOKENS_LABEL = msg(
|
|
281
|
+
"tui.model_config.validation.label.max_context_tokens",
|
|
282
|
+
fallback="Max context tokens",
|
|
283
|
+
)
|
|
284
|
+
_MAX_OUTPUT_TOKENS_LABEL = msg(
|
|
285
|
+
"tui.model_config.validation.label.max_output_tokens",
|
|
286
|
+
fallback="Max output tokens",
|
|
287
|
+
)
|
|
288
|
+
_MAX_CONTEXT_MINIMUM = msg(
|
|
289
|
+
"tui.model_config.validation.max_context_minimum",
|
|
290
|
+
fallback="Max context tokens must be at least {minimum}.",
|
|
291
|
+
)
|
|
292
|
+
_MAX_OUTPUT_LESS_THAN_CONTEXT = msg(
|
|
293
|
+
"tui.model_config.validation.max_output_less_than_context",
|
|
294
|
+
fallback="Max output tokens must be less than max context tokens.",
|
|
295
|
+
)
|
|
296
|
+
_BASE_URL_SCHEME = msg(
|
|
297
|
+
"tui.model_config.validation.base_url_scheme",
|
|
298
|
+
fallback="Base URL must start with http:// or https://.",
|
|
299
|
+
)
|
|
300
|
+
_HTTP_CONNECT_TIMEOUT_LABEL = msg(
|
|
301
|
+
"tui.model_config.validation.label.http_connect_timeout",
|
|
302
|
+
fallback="HTTP Connect Timeout",
|
|
303
|
+
)
|
|
304
|
+
_HTTP_READ_TIMEOUT_LABEL = msg(
|
|
305
|
+
"tui.model_config.validation.label.http_read_timeout",
|
|
306
|
+
fallback="HTTP Read Timeout",
|
|
307
|
+
)
|
|
308
|
+
_FIELD_POSITIVE_NUMBER = msg(
|
|
309
|
+
"tui.model_config.validation.field_positive_number",
|
|
310
|
+
fallback="{label} must be a positive number.",
|
|
311
|
+
)
|
|
312
|
+
_FIELD_VALID_NUMBER = msg(
|
|
313
|
+
"tui.model_config.validation.field_valid_number",
|
|
314
|
+
fallback="{label} must be a valid number.",
|
|
315
|
+
)
|
|
316
|
+
_HTTP_RETRIES_NON_NEGATIVE = msg(
|
|
317
|
+
"tui.model_config.validation.http_retries_non_negative",
|
|
318
|
+
fallback="HTTP Max Retries must be a non-negative integer.",
|
|
319
|
+
)
|
|
320
|
+
_HTTP_RETRIES_VALID = msg(
|
|
321
|
+
"tui.model_config.validation.http_retries_valid",
|
|
322
|
+
fallback="HTTP Max Retries must be a valid integer.",
|
|
323
|
+
)
|
|
324
|
+
_KV_VALUE_REQUIRED = msg(
|
|
325
|
+
"tui.model_config.validation.kv_value_required",
|
|
326
|
+
fallback="{label} row {row}: value is required for key '{key}'.",
|
|
327
|
+
multiline=True,
|
|
328
|
+
)
|
|
329
|
+
_KV_KEY_REQUIRED = msg(
|
|
330
|
+
"tui.model_config.validation.kv_key_required",
|
|
331
|
+
fallback="{label} row {row}: key name is required when a value is set.",
|
|
332
|
+
)
|
|
333
|
+
_KV_HEADER_RESERVED = msg(
|
|
334
|
+
"tui.model_config.validation.kv_header_reserved",
|
|
335
|
+
fallback="{label} row {row}: header key '{key}' is reserved for {app}.",
|
|
336
|
+
multiline=True,
|
|
337
|
+
)
|
|
338
|
+
_KV_DETAIL = msg(
|
|
339
|
+
"tui.model_config.validation.kv_detail",
|
|
340
|
+
fallback="{label} row {row}: {detail}",
|
|
341
|
+
multiline=True,
|
|
342
|
+
)
|
|
343
|
+
_KV_DUPLICATE_KEY = msg(
|
|
344
|
+
"tui.model_config.validation.kv_duplicate_key",
|
|
345
|
+
fallback="{label} row {row}: duplicate key '{key}'.",
|
|
346
|
+
multiline=True,
|
|
347
|
+
)
|
|
348
|
+
_RESPONSES_STORE_WARNING = msg(
|
|
349
|
+
"tui.model_config.warning.responses_store_continuation",
|
|
350
|
+
fallback=(
|
|
351
|
+
'chat_options {{"store": true}} enables service-side continuation. Context compaction is not supported in '
|
|
352
|
+
"this mode yet: long sessions will not compact and may overflow the context window."
|
|
353
|
+
),
|
|
354
|
+
)
|
|
355
|
+
_PROTECTED_CHAT_OPTIONS_WARNING = msg(
|
|
356
|
+
"tui.model_config.warning.protected_chat_options",
|
|
357
|
+
fallback=(
|
|
358
|
+
"Protected chat option key(s) were stripped: {keys}. These keys can no longer be configured in profile YAML "
|
|
359
|
+
"because they bypass context admission. Reusable prompts and profile-pinned continuation IDs are unavailable "
|
|
360
|
+
"there; store: true only enables automatically managed Responses continuation."
|
|
361
|
+
),
|
|
362
|
+
multiline=True,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
# ── Provider helpers ─────────────────────────────────────────────────
|
|
366
|
+
|
|
367
|
+
_PROVIDERS = [
|
|
368
|
+
("OpenAI", "openai"),
|
|
369
|
+
("Anthropic", "anthropic"),
|
|
370
|
+
("DeepSeek (OpenAI)", "deepseek-openai"),
|
|
371
|
+
("GLM (OpenAI)", "glm-openai"),
|
|
372
|
+
]
|
|
373
|
+
_API_STYLE_CHAT_COMPLETIONS = msg(
|
|
374
|
+
"tui.model_config.api_style.chat_completions",
|
|
375
|
+
fallback="Chat Completions",
|
|
376
|
+
)
|
|
377
|
+
_API_STYLE_RESPONSES = msg("tui.model_config.api_style.responses", fallback="Responses")
|
|
378
|
+
_API_STYLES = [
|
|
379
|
+
(_API_STYLE_CHAT_COMPLETIONS, API_STYLE_CHAT_COMPLETIONS),
|
|
380
|
+
(_API_STYLE_RESPONSES, API_STYLE_RESPONSES),
|
|
381
|
+
]
|
|
382
|
+
|
|
383
|
+
_PROVIDER_LABELS: dict[str, str] = {
|
|
384
|
+
"openai": "OpenAI",
|
|
385
|
+
"anthropic": "Anthropic",
|
|
386
|
+
"deepseek-openai": "DeepSeek (OpenAI)",
|
|
387
|
+
"glm-openai": "GLM (OpenAI)",
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
_PROVIDER_DEFAULT_BASE_URLS: dict[str, str] = {name: spec.default_base_url for name, spec in PROVIDERS.items()}
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def _token_limit_param_name(provider: str, api_style: str) -> str:
|
|
394
|
+
"""Wire parameter that carries the Max Output Tokens value for this selection.
|
|
395
|
+
|
|
396
|
+
Chat-completions values come from the table the client variants take their
|
|
397
|
+
``max_output_param`` from, so the label names what the client sends without
|
|
398
|
+
importing the SDK-backed client modules while the form renders.
|
|
399
|
+
"""
|
|
400
|
+
if provider == "anthropic":
|
|
401
|
+
return "max_tokens"
|
|
402
|
+
if is_responses_wire_dialect(provider, api_style):
|
|
403
|
+
return "max_output_tokens"
|
|
404
|
+
return CHAT_COMPLETIONS_TOKEN_LIMIT_PARAMS.get(provider, CHAT_COMPLETIONS_TOKEN_LIMIT_PARAMS["openai"])
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
_JSON_OBJECT_CHAT_OPTION_KEYS = frozenset(
|
|
408
|
+
{
|
|
409
|
+
"extra_body",
|
|
410
|
+
"extra_headers",
|
|
411
|
+
"extra_query",
|
|
412
|
+
"logit_bias",
|
|
413
|
+
"metadata",
|
|
414
|
+
"response_format",
|
|
415
|
+
}
|
|
416
|
+
)
|
|
417
|
+
_FLOAT_CHAT_OPTION_RULES: dict[str, tuple[float | None, float | None]] = {
|
|
418
|
+
"temperature": (0.0, 2.0),
|
|
419
|
+
"top_p": (0.0, 1.0),
|
|
420
|
+
"frequency_penalty": (-2.0, 2.0),
|
|
421
|
+
"presence_penalty": (-2.0, 2.0),
|
|
422
|
+
}
|
|
423
|
+
_INT_CHAT_OPTION_RULES: dict[str, tuple[int | None, int | None]] = {
|
|
424
|
+
"seed": (None, None),
|
|
425
|
+
}
|
|
426
|
+
_BOOL_CHAT_OPTION_KEYS = frozenset({"allow_multiple_tool_calls", "store"})
|
|
427
|
+
_STRING_CHAT_OPTION_KEYS = frozenset({"conversation_id", "instructions", "model", "user"})
|
|
428
|
+
_CHAT_OPTION_INVALID_JSON_VALUE = object()
|
|
429
|
+
type _RenderMessage = Callable[[MessageRef], str]
|
|
430
|
+
|
|
431
|
+
# ── UID counter for key-value rows ──────────────────────────────────
|
|
432
|
+
|
|
433
|
+
_uid_counter = 0
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def _next_uid() -> int:
|
|
437
|
+
global _uid_counter
|
|
438
|
+
_uid_counter += 1
|
|
439
|
+
return _uid_counter
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _loads_or(text: str, default: object) -> object:
|
|
443
|
+
"""Return ``json.loads(text)``, or *default* for any text JSON rejects.
|
|
444
|
+
|
|
445
|
+
Catches ``ValueError``, not only ``JSONDecodeError``: an integer past
|
|
446
|
+
Python's digit limit raises the plain base class.
|
|
447
|
+
"""
|
|
448
|
+
try:
|
|
449
|
+
return json.loads(text)
|
|
450
|
+
except ValueError:
|
|
451
|
+
return default
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _parse_chat_option_value_for_validation(raw: str) -> object:
|
|
455
|
+
"""Parse a Chat Options value for validation."""
|
|
456
|
+
return _loads_or(raw, _CHAT_OPTION_INVALID_JSON_VALUE)
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def _validate_chat_option_object(
|
|
460
|
+
index: int,
|
|
461
|
+
key: str,
|
|
462
|
+
parsed: object,
|
|
463
|
+
*,
|
|
464
|
+
render_message: _RenderMessage = format_message,
|
|
465
|
+
) -> list[str]:
|
|
466
|
+
if parsed is _CHAT_OPTION_INVALID_JSON_VALUE:
|
|
467
|
+
example = '{"key": "value"}'
|
|
468
|
+
return [
|
|
469
|
+
render_message(
|
|
470
|
+
_CHAT_OPTION_OBJECT_EXAMPLE.bind(
|
|
471
|
+
row=index,
|
|
472
|
+
key=DisplayBlock(key),
|
|
473
|
+
example=DisplayBlock(example),
|
|
474
|
+
)
|
|
475
|
+
)
|
|
476
|
+
]
|
|
477
|
+
if not isinstance(parsed, dict):
|
|
478
|
+
return [
|
|
479
|
+
render_message(
|
|
480
|
+
_CHAT_OPTION_OBJECT_TYPE.bind(
|
|
481
|
+
row=index,
|
|
482
|
+
key=DisplayBlock(key),
|
|
483
|
+
type_name=type(parsed).__name__,
|
|
484
|
+
)
|
|
485
|
+
)
|
|
486
|
+
]
|
|
487
|
+
return []
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _json_number_description(
|
|
491
|
+
min_value: float | None,
|
|
492
|
+
max_value: float | None,
|
|
493
|
+
*,
|
|
494
|
+
render_message: _RenderMessage = format_message,
|
|
495
|
+
) -> str:
|
|
496
|
+
if min_value is not None and max_value is not None:
|
|
497
|
+
return render_message(
|
|
498
|
+
_JSON_NUMBER_RANGE.bind(
|
|
499
|
+
minimum=f"{min_value:.1f}",
|
|
500
|
+
maximum=f"{max_value:.1f}",
|
|
501
|
+
)
|
|
502
|
+
)
|
|
503
|
+
if min_value is not None:
|
|
504
|
+
return render_message(_JSON_NUMBER_MIN.bind(minimum=f"{min_value:.1f}"))
|
|
505
|
+
if max_value is not None:
|
|
506
|
+
return render_message(_JSON_NUMBER_MAX.bind(maximum=f"{max_value:.1f}"))
|
|
507
|
+
return render_message(_JSON_NUMBER.bind())
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
def _json_integer_description(
|
|
511
|
+
min_value: int | None,
|
|
512
|
+
max_value: int | None,
|
|
513
|
+
*,
|
|
514
|
+
render_message: _RenderMessage = format_message,
|
|
515
|
+
) -> str:
|
|
516
|
+
if min_value == 1 and max_value is None:
|
|
517
|
+
return render_message(_JSON_POSITIVE_INTEGER.bind())
|
|
518
|
+
if min_value is not None and max_value is not None:
|
|
519
|
+
return render_message(_JSON_INTEGER_RANGE.bind(minimum=min_value, maximum=max_value))
|
|
520
|
+
if min_value is not None:
|
|
521
|
+
return render_message(_JSON_INTEGER_MIN.bind(minimum=min_value))
|
|
522
|
+
if max_value is not None:
|
|
523
|
+
return render_message(_JSON_INTEGER_MAX.bind(maximum=max_value))
|
|
524
|
+
return render_message(_JSON_INTEGER.bind())
|
|
525
|
+
|
|
526
|
+
|
|
527
|
+
def _validate_chat_option_float(
|
|
528
|
+
index: int,
|
|
529
|
+
key: str,
|
|
530
|
+
parsed: object,
|
|
531
|
+
*,
|
|
532
|
+
render_message: _RenderMessage = format_message,
|
|
533
|
+
) -> list[str]:
|
|
534
|
+
min_value, max_value = _FLOAT_CHAT_OPTION_RULES[key]
|
|
535
|
+
clause = _json_number_description(min_value, max_value, render_message=render_message)
|
|
536
|
+
if isinstance(parsed, bool) or not isinstance(parsed, int | float) or not math.isfinite(float(parsed)):
|
|
537
|
+
return [
|
|
538
|
+
render_message(
|
|
539
|
+
_CHAT_OPTION_MUST_BE.bind(
|
|
540
|
+
row=index,
|
|
541
|
+
key=DisplayBlock(key),
|
|
542
|
+
clause=clause,
|
|
543
|
+
)
|
|
544
|
+
)
|
|
545
|
+
]
|
|
546
|
+
|
|
547
|
+
value = float(parsed)
|
|
548
|
+
if min_value is not None and value < min_value:
|
|
549
|
+
return [render_message(_CHAT_OPTION_MUST_BE.bind(row=index, key=DisplayBlock(key), clause=clause))]
|
|
550
|
+
if max_value is not None and value > max_value:
|
|
551
|
+
return [render_message(_CHAT_OPTION_MUST_BE.bind(row=index, key=DisplayBlock(key), clause=clause))]
|
|
552
|
+
return []
|
|
553
|
+
|
|
554
|
+
|
|
555
|
+
def _validate_chat_option_int(
|
|
556
|
+
index: int,
|
|
557
|
+
key: str,
|
|
558
|
+
parsed: object,
|
|
559
|
+
*,
|
|
560
|
+
render_message: _RenderMessage = format_message,
|
|
561
|
+
) -> list[str]:
|
|
562
|
+
min_value, max_value = _INT_CHAT_OPTION_RULES[key]
|
|
563
|
+
clause = _json_integer_description(min_value, max_value, render_message=render_message)
|
|
564
|
+
if isinstance(parsed, bool) or not isinstance(parsed, int):
|
|
565
|
+
return [render_message(_CHAT_OPTION_MUST_BE.bind(row=index, key=DisplayBlock(key), clause=clause))]
|
|
566
|
+
|
|
567
|
+
if min_value is not None and parsed < min_value:
|
|
568
|
+
return [render_message(_CHAT_OPTION_MUST_BE.bind(row=index, key=DisplayBlock(key), clause=clause))]
|
|
569
|
+
if max_value is not None and parsed > max_value:
|
|
570
|
+
return [render_message(_CHAT_OPTION_MUST_BE.bind(row=index, key=DisplayBlock(key), clause=clause))]
|
|
571
|
+
return []
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def _validate_chat_option_bool(
|
|
575
|
+
index: int,
|
|
576
|
+
key: str,
|
|
577
|
+
parsed: object,
|
|
578
|
+
*,
|
|
579
|
+
render_message: _RenderMessage = format_message,
|
|
580
|
+
) -> list[str]:
|
|
581
|
+
if not isinstance(parsed, bool):
|
|
582
|
+
return [render_message(_CHAT_OPTION_BOOLEAN.bind(row=index, key=DisplayBlock(key)))]
|
|
583
|
+
return []
|
|
584
|
+
|
|
585
|
+
|
|
586
|
+
def _chat_option_string_value(parsed: object, raw: str) -> str | None:
|
|
587
|
+
if parsed is _CHAT_OPTION_INVALID_JSON_VALUE:
|
|
588
|
+
return raw
|
|
589
|
+
if isinstance(parsed, str):
|
|
590
|
+
return parsed
|
|
591
|
+
return None
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _validate_chat_option_string(
|
|
595
|
+
index: int,
|
|
596
|
+
key: str,
|
|
597
|
+
parsed: object,
|
|
598
|
+
raw: str,
|
|
599
|
+
*,
|
|
600
|
+
render_message: _RenderMessage = format_message,
|
|
601
|
+
) -> list[str]:
|
|
602
|
+
value = _chat_option_string_value(parsed, raw)
|
|
603
|
+
if value is None:
|
|
604
|
+
return [render_message(_CHAT_OPTION_STRING.bind(row=index, key=DisplayBlock(key)))]
|
|
605
|
+
if key == "model":
|
|
606
|
+
# A model override replaces the profile's model id on the wire —
|
|
607
|
+
# including the ``Chrys-Model-Id`` header — so it carries the
|
|
608
|
+
# same charset constraint. ``{{ENV_VAR}}`` templates are ASCII
|
|
609
|
+
# and pass; resolved values are re-checked at request time.
|
|
610
|
+
charset_error = model_id_charset_error(value)
|
|
611
|
+
if charset_error:
|
|
612
|
+
return [
|
|
613
|
+
render_message(
|
|
614
|
+
_CHAT_OPTION_DETAIL.bind(
|
|
615
|
+
row=index,
|
|
616
|
+
key=DisplayBlock("model"),
|
|
617
|
+
detail=DisplayBlock(charset_error),
|
|
618
|
+
)
|
|
619
|
+
)
|
|
620
|
+
]
|
|
621
|
+
return []
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def _validate_chat_option_stop(
|
|
625
|
+
index: int,
|
|
626
|
+
parsed: object,
|
|
627
|
+
raw: str,
|
|
628
|
+
*,
|
|
629
|
+
render_message: _RenderMessage = format_message,
|
|
630
|
+
) -> list[str]:
|
|
631
|
+
if _chat_option_string_value(parsed, raw) is not None:
|
|
632
|
+
return []
|
|
633
|
+
if isinstance(parsed, list) and all(isinstance(item, str) for item in parsed):
|
|
634
|
+
return []
|
|
635
|
+
return [render_message(_CHAT_OPTION_STOP.bind(row=index))]
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def _validate_chat_option_logit_bias(
|
|
639
|
+
index: int,
|
|
640
|
+
parsed: object,
|
|
641
|
+
*,
|
|
642
|
+
render_message: _RenderMessage = format_message,
|
|
643
|
+
) -> list[str]:
|
|
644
|
+
if not isinstance(parsed, dict):
|
|
645
|
+
return _validate_chat_option_object(index, "logit_bias", parsed, render_message=render_message)
|
|
646
|
+
|
|
647
|
+
for token_id, bias in parsed.items():
|
|
648
|
+
if isinstance(bias, bool) or not isinstance(bias, int | float) or not math.isfinite(float(bias)):
|
|
649
|
+
return [
|
|
650
|
+
render_message(
|
|
651
|
+
_CHAT_OPTION_LOGIT_BIAS.bind(
|
|
652
|
+
row=index,
|
|
653
|
+
token=DisplayBlock(repr(token_id)),
|
|
654
|
+
)
|
|
655
|
+
)
|
|
656
|
+
]
|
|
657
|
+
if not -100 <= float(bias) <= 100:
|
|
658
|
+
return [
|
|
659
|
+
render_message(
|
|
660
|
+
_CHAT_OPTION_LOGIT_BIAS.bind(
|
|
661
|
+
row=index,
|
|
662
|
+
token=DisplayBlock(repr(token_id)),
|
|
663
|
+
)
|
|
664
|
+
)
|
|
665
|
+
]
|
|
666
|
+
return []
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def _validate_chat_option_extra_headers(
|
|
670
|
+
index: int,
|
|
671
|
+
parsed: object,
|
|
672
|
+
*,
|
|
673
|
+
render_message: _RenderMessage = format_message,
|
|
674
|
+
) -> list[str]:
|
|
675
|
+
if not isinstance(parsed, dict):
|
|
676
|
+
return _validate_chat_option_object(index, "extra_headers", parsed, render_message=render_message)
|
|
677
|
+
|
|
678
|
+
for header, value in parsed.items():
|
|
679
|
+
if is_chrys_managed_header_name(str(header)):
|
|
680
|
+
return [
|
|
681
|
+
render_message(
|
|
682
|
+
_CHAT_OPTION_HEADER_RESERVED.bind(
|
|
683
|
+
row=index,
|
|
684
|
+
header=DisplayBlock(repr(header)),
|
|
685
|
+
app=APP_DISPLAY_NAME,
|
|
686
|
+
)
|
|
687
|
+
)
|
|
688
|
+
]
|
|
689
|
+
if not isinstance(value, str):
|
|
690
|
+
return [
|
|
691
|
+
render_message(
|
|
692
|
+
_CHAT_OPTION_HEADER_VALUE_STRING.bind(
|
|
693
|
+
row=index,
|
|
694
|
+
header=DisplayBlock(repr(header)),
|
|
695
|
+
)
|
|
696
|
+
)
|
|
697
|
+
]
|
|
698
|
+
for charset_error in (
|
|
699
|
+
header_name_charset_error(str(header)),
|
|
700
|
+
header_value_charset_error(str(header), value),
|
|
701
|
+
):
|
|
702
|
+
if charset_error:
|
|
703
|
+
return [
|
|
704
|
+
render_message(
|
|
705
|
+
_CHAT_OPTION_DETAIL.bind(
|
|
706
|
+
row=index,
|
|
707
|
+
key=DisplayBlock("extra_headers"),
|
|
708
|
+
detail=DisplayBlock(charset_error),
|
|
709
|
+
)
|
|
710
|
+
)
|
|
711
|
+
]
|
|
712
|
+
return []
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
# ── Modal screen ─────────────────────────────────────────────────────
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
class ModelConfigScreen(BaseDialog[str]):
|
|
719
|
+
"""Modal for managing model profiles with sidebar selection.
|
|
720
|
+
|
|
721
|
+
Left sidebar lists all model profiles; selecting one loads its
|
|
722
|
+
configuration in the right panel form.
|
|
723
|
+
|
|
724
|
+
Save writes the current form to YAML and updates the in-memory
|
|
725
|
+
registry. When the edited profile is the global default, its file
|
|
726
|
+
pointer is refreshed without changing the runtime-effective model.
|
|
727
|
+
The modal stays open and a success/error toast is shown.
|
|
728
|
+
|
|
729
|
+
In read-only mode, the same screen can be opened while an agent turn
|
|
730
|
+
is running: profile navigation stays available, but mutating controls
|
|
731
|
+
are hidden/disabled and Close/Escape always dismisses with ``""``.
|
|
732
|
+
|
|
733
|
+
Dismisses with ``"switched"`` when deleting the runtime-effective
|
|
734
|
+
profile requires a backend reload, ``"updated"`` after a Save, or
|
|
735
|
+
``""`` on Close / Escape when the runtime can remain unchanged.
|
|
736
|
+
"""
|
|
737
|
+
|
|
738
|
+
CSS_PATH = "screen.tcss"
|
|
739
|
+
|
|
740
|
+
BINDINGS: ClassVar[list] = [
|
|
741
|
+
localized_binding("escape", "cancel", CANCEL_BINDING, show=False, priority=True),
|
|
742
|
+
]
|
|
743
|
+
|
|
744
|
+
def __init__(
|
|
745
|
+
self,
|
|
746
|
+
registry: ModelProfileRegistry,
|
|
747
|
+
global_default_profile_id: str = "",
|
|
748
|
+
*,
|
|
749
|
+
read_only: bool = False,
|
|
750
|
+
) -> None:
|
|
751
|
+
self._registry = registry
|
|
752
|
+
self._global_default_profile_id = global_default_profile_id
|
|
753
|
+
self._read_only = read_only
|
|
754
|
+
self._selected_profile_id: str = ""
|
|
755
|
+
# A successful Save requires a soft restart even when the global
|
|
756
|
+
# default pointer is unchanged because profile fields may affect
|
|
757
|
+
# the runtime-effective model.
|
|
758
|
+
self._saved_anything = False
|
|
759
|
+
self._requires_runtime_reload = False
|
|
760
|
+
# Set while the prompt-caching reminder is pushed or showing: a second
|
|
761
|
+
# Save pressed before it mounts would push a second reminder.
|
|
762
|
+
self._prompt_cache_offer_open = False
|
|
763
|
+
super().__init__()
|
|
764
|
+
|
|
765
|
+
# ── Compose ──────────────────────────────────────────────────────
|
|
766
|
+
|
|
767
|
+
def compose(self) -> ComposeResult:
|
|
768
|
+
localizer = widget_localizer(self)
|
|
769
|
+
with Vertical(id="mc-container") as container:
|
|
770
|
+
container.border_title = Text(render_str(localizer, _CONFIGURATION_TITLE.bind()))
|
|
771
|
+
|
|
772
|
+
with Horizontal(id="mc-main"):
|
|
773
|
+
# Left sidebar
|
|
774
|
+
with Vertical(id="mc-sidebar"):
|
|
775
|
+
yield OptionList(id="mc-list")
|
|
776
|
+
|
|
777
|
+
# Right panel
|
|
778
|
+
with Vertical(id="mc-right"), VerticalScroll(id="mc-scroll"):
|
|
779
|
+
# Profile name
|
|
780
|
+
yield Label(
|
|
781
|
+
f"[red]*[/red] {escape(render_str(localizer, _PROFILE_NAME.bind()))}", classes="mc-label"
|
|
782
|
+
)
|
|
783
|
+
yield Input(
|
|
784
|
+
placeholder=render_str(localizer, _PROFILE_NAME_PLACEHOLDER.bind()),
|
|
785
|
+
id="mc-name",
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
with Vertical(id="mc-model-options", classes="mc-option-section") as options:
|
|
789
|
+
options.border_title = render_str(localizer, _MODEL_OPTIONS.bind())
|
|
790
|
+
# Provider
|
|
791
|
+
yield Label(
|
|
792
|
+
f"[red]*[/red] {escape(render_str(localizer, _PROVIDER.bind()))}", classes="mc-label"
|
|
793
|
+
)
|
|
794
|
+
yield Select(
|
|
795
|
+
_PROVIDERS,
|
|
796
|
+
value="openai",
|
|
797
|
+
id="mc-provider",
|
|
798
|
+
allow_blank=False,
|
|
799
|
+
)
|
|
800
|
+
yield Label(
|
|
801
|
+
render_str(localizer, _API_STYLE.bind()),
|
|
802
|
+
classes="mc-label",
|
|
803
|
+
id="mc-api-style-label",
|
|
804
|
+
)
|
|
805
|
+
yield Select(
|
|
806
|
+
[(render_str(localizer, label.bind()), value) for label, value in _API_STYLES],
|
|
807
|
+
value=API_STYLE_CHAT_COMPLETIONS,
|
|
808
|
+
id="mc-api-style",
|
|
809
|
+
allow_blank=False,
|
|
810
|
+
)
|
|
811
|
+
|
|
812
|
+
# Model name
|
|
813
|
+
yield Label(
|
|
814
|
+
f"[red]*[/red] {escape(render_str(localizer, _MODEL.bind()))}",
|
|
815
|
+
classes="mc-label",
|
|
816
|
+
id="mc-model-label",
|
|
817
|
+
)
|
|
818
|
+
yield Input(
|
|
819
|
+
placeholder=render_str(localizer, _MODEL_PLACEHOLDER.bind()),
|
|
820
|
+
id="mc-model",
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
# Max context tokens
|
|
824
|
+
yield Label(
|
|
825
|
+
f"[red]*[/red] {escape(render_str(localizer, _MAX_CONTEXT_WINDOW.bind()))}",
|
|
826
|
+
classes="mc-label",
|
|
827
|
+
)
|
|
828
|
+
yield Input(
|
|
829
|
+
value=str(DEFAULT_MAX_CONTEXT_TOKENS),
|
|
830
|
+
placeholder=str(DEFAULT_MAX_CONTEXT_TOKENS),
|
|
831
|
+
id="mc-max-tokens",
|
|
832
|
+
)
|
|
833
|
+
|
|
834
|
+
# Max output tokens (the model's hard per-response cap)
|
|
835
|
+
yield Label(
|
|
836
|
+
self._max_output_label("openai", API_STYLE_CHAT_COMPLETIONS),
|
|
837
|
+
classes="mc-label",
|
|
838
|
+
id="mc-max-output-label",
|
|
839
|
+
)
|
|
840
|
+
yield Input(
|
|
841
|
+
placeholder=render_str(
|
|
842
|
+
localizer,
|
|
843
|
+
_OUTPUT_CAP_PLACEHOLDER.bind(default=DEFAULT_MAX_OUTPUT_TOKENS),
|
|
844
|
+
),
|
|
845
|
+
id="mc-max-output-tokens",
|
|
846
|
+
)
|
|
847
|
+
|
|
848
|
+
# Base URL — placeholder is updated per-provider in
|
|
849
|
+
# ``_update_provider_labels``; the initial value matches
|
|
850
|
+
# the default provider above (``openai``).
|
|
851
|
+
yield Label(render_str(localizer, _BASE_URL.bind()), classes="mc-label", id="mc-url-label")
|
|
852
|
+
yield Input(
|
|
853
|
+
placeholder=_PROVIDER_DEFAULT_BASE_URLS["openai"],
|
|
854
|
+
id="mc-base-url",
|
|
855
|
+
)
|
|
856
|
+
|
|
857
|
+
# API Key
|
|
858
|
+
yield Label(render_str(localizer, _API_KEY.bind()), classes="mc-label", id="mc-key-label")
|
|
859
|
+
yield Input(
|
|
860
|
+
placeholder="sk-...",
|
|
861
|
+
password=True,
|
|
862
|
+
id="mc-api-key",
|
|
863
|
+
)
|
|
864
|
+
|
|
865
|
+
yield Checkbox(
|
|
866
|
+
render_str(localizer, _STREAMING.bind()),
|
|
867
|
+
value=True,
|
|
868
|
+
id="mc-stream",
|
|
869
|
+
classes="mc-checkbox",
|
|
870
|
+
)
|
|
871
|
+
yield Checkbox(
|
|
872
|
+
render_str(localizer, _VISION.bind()),
|
|
873
|
+
value=False,
|
|
874
|
+
id="mc-vision",
|
|
875
|
+
classes="mc-checkbox",
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
with Vertical(id="mc-connection-options", classes="mc-option-section") as options:
|
|
879
|
+
options.border_title = render_str(localizer, _CONNECTION_OPTIONS.bind())
|
|
880
|
+
yield Checkbox(
|
|
881
|
+
render_str(localizer, _SKIP_TLS_VERIFICATION.bind()),
|
|
882
|
+
value=False,
|
|
883
|
+
id="mc-skip-tls",
|
|
884
|
+
classes="mc-checkbox",
|
|
885
|
+
)
|
|
886
|
+
tls_hint = Label(
|
|
887
|
+
render_str(localizer, _INSECURE_TLS.bind()),
|
|
888
|
+
id="mc-skip-tls-hint",
|
|
889
|
+
classes="mc-hint mc-tls-hint",
|
|
890
|
+
)
|
|
891
|
+
tls_hint.display = False
|
|
892
|
+
yield tls_hint
|
|
893
|
+
yield Checkbox(
|
|
894
|
+
render_str(localizer, _BYPASS_PROXY.bind()),
|
|
895
|
+
value=False,
|
|
896
|
+
id="mc-bypass-proxy",
|
|
897
|
+
classes="mc-checkbox",
|
|
898
|
+
)
|
|
899
|
+
|
|
900
|
+
with Vertical(id="mc-http-options", classes="mc-option-section") as options:
|
|
901
|
+
options.border_title = render_str(localizer, _HTTP_OPTIONS.bind())
|
|
902
|
+
# HTTP Connect Timeout
|
|
903
|
+
yield Label(render_str(localizer, _HTTP_CONNECT_TIMEOUT.bind()), classes="mc-label")
|
|
904
|
+
yield Input(
|
|
905
|
+
value="10.0",
|
|
906
|
+
placeholder="10.0",
|
|
907
|
+
id="mc-connect-timeout",
|
|
908
|
+
)
|
|
909
|
+
|
|
910
|
+
# HTTP Read Timeout
|
|
911
|
+
yield Label(render_str(localizer, _HTTP_READ_TIMEOUT.bind()), classes="mc-label")
|
|
912
|
+
yield Input(
|
|
913
|
+
value="300.0",
|
|
914
|
+
placeholder="300.0",
|
|
915
|
+
id="mc-read-timeout",
|
|
916
|
+
)
|
|
917
|
+
|
|
918
|
+
# HTTP Max Retries
|
|
919
|
+
yield Label(render_str(localizer, _HTTP_MAX_RETRIES.bind()), classes="mc-label")
|
|
920
|
+
yield Input(
|
|
921
|
+
value="2",
|
|
922
|
+
placeholder="2",
|
|
923
|
+
id="mc-max-retries",
|
|
924
|
+
)
|
|
925
|
+
|
|
926
|
+
with Vertical(id="mc-extra-options", classes="mc-option-section") as options:
|
|
927
|
+
options.border_title = render_str(localizer, _EXTRA_OPTIONS.bind())
|
|
928
|
+
# HTTP Extra Headers — key-value list
|
|
929
|
+
yield Label(render_str(localizer, _HTTP_EXTRA_HEADERS.bind()), classes="mc-label")
|
|
930
|
+
with Vertical(classes="mc-kv-list", id="mc-headers-list"):
|
|
931
|
+
yield self._compose_kv_row("", "", "h", read_only=self._read_only)
|
|
932
|
+
yield self._compose_kv_add_row("h", read_only=self._read_only)
|
|
933
|
+
|
|
934
|
+
# Chat Options — key-value list
|
|
935
|
+
yield Label(render_str(localizer, _CHAT_OPTIONS.bind()), classes="mc-label")
|
|
936
|
+
with Vertical(classes="mc-kv-list", id="mc-options-list"):
|
|
937
|
+
yield self._compose_kv_row("", "", "o", read_only=self._read_only)
|
|
938
|
+
yield self._compose_kv_add_row("o", read_only=self._read_only)
|
|
939
|
+
|
|
940
|
+
# Footer buttons
|
|
941
|
+
with Vertical(id="mc-footer") as footer:
|
|
942
|
+
if self._read_only:
|
|
943
|
+
footer.add_class("-read-only")
|
|
944
|
+
with Horizontal(id="mc-buttons"):
|
|
945
|
+
yield Button(render_str(localizer, _NEW.bind()), variant="primary", id="mc-new", flat=True)
|
|
946
|
+
yield Button(render_str(localizer, _CLONE.bind()), variant="primary", id="mc-clone", flat=True)
|
|
947
|
+
yield Button(render_str(localizer, _DELETE.bind()), variant="error", id="mc-delete", flat=True)
|
|
948
|
+
yield Static(id="mc-buttons-spacer")
|
|
949
|
+
yield Button(render_str(localizer, _SAVE.bind()), variant="primary", id="mc-save", flat=True)
|
|
950
|
+
yield Button(render_str(localizer, _CLOSE.bind()), variant="warning", id="mc-cancel", flat=True)
|
|
951
|
+
notice = Static(Text(render_str(localizer, _READ_ONLY_NOTICE.bind())), id="mc-read-only-notice")
|
|
952
|
+
notice.display = self._read_only
|
|
953
|
+
yield notice
|
|
954
|
+
|
|
955
|
+
# ── Mount ────────────────────────────────────────────────────────
|
|
956
|
+
|
|
957
|
+
async def on_mount(self) -> None:
|
|
958
|
+
profiles = self._listed_profiles()
|
|
959
|
+
if not profiles:
|
|
960
|
+
if self._read_only:
|
|
961
|
+
self._set_form_enabled(False)
|
|
962
|
+
self._apply_read_only_state()
|
|
963
|
+
return
|
|
964
|
+
# Auto-create a default profile so the user can start editing immediately
|
|
965
|
+
await self._create_new_profile()
|
|
966
|
+
return
|
|
967
|
+
|
|
968
|
+
self._populate_sidebar()
|
|
969
|
+
# Select the global default profile, or the first available profile.
|
|
970
|
+
target_id = self._global_default_profile_id
|
|
971
|
+
if not target_id or self._registry.get(target_id) is None:
|
|
972
|
+
target_id = profiles[0].id
|
|
973
|
+
ol = self.query_one("#mc-list", OptionList)
|
|
974
|
+
with contextlib.suppress(OptionDoesNotExist):
|
|
975
|
+
ol.highlighted = ol.get_option_index(target_id)
|
|
976
|
+
self._load_profile(target_id)
|
|
977
|
+
self._apply_read_only_state()
|
|
978
|
+
|
|
979
|
+
def _listed_profiles(self) -> list[ModelProfile]:
|
|
980
|
+
"""The registry's profiles in the order the sidebar lists them: as a person reads their names."""
|
|
981
|
+
return sorted(self._registry.list_profiles(), key=lambda profile: reading_order(profile.name))
|
|
982
|
+
|
|
983
|
+
def _populate_sidebar(self) -> None:
|
|
984
|
+
ol = self.query_one("#mc-list", OptionList)
|
|
985
|
+
ol.clear_options()
|
|
986
|
+
profiles = self._listed_profiles()
|
|
987
|
+
for i, p in enumerate(profiles):
|
|
988
|
+
if i > 0:
|
|
989
|
+
ol.add_option(None) # Divider
|
|
990
|
+
content = Content.assemble((p.name, "bold"))
|
|
991
|
+
ol.add_option(Option(content, id=p.id))
|
|
992
|
+
|
|
993
|
+
def _set_form_enabled(self, enabled: bool) -> None:
|
|
994
|
+
"""Enable or disable the right-panel form and Save/Delete buttons."""
|
|
995
|
+
interactive = enabled and not self._read_only
|
|
996
|
+
self.query_one("#mc-save", Button).disabled = not interactive
|
|
997
|
+
self._update_delete_button()
|
|
998
|
+
self._apply_read_only_state()
|
|
999
|
+
|
|
1000
|
+
def _update_delete_button(self) -> None:
|
|
1001
|
+
"""Disable Delete when there is only one profile (or none selected)."""
|
|
1002
|
+
count = len(self._registry.list_profiles())
|
|
1003
|
+
button = self.query_one("#mc-delete", Button)
|
|
1004
|
+
button.disabled = self._read_only or count <= 1 or not self._selected_profile_id
|
|
1005
|
+
if self._read_only:
|
|
1006
|
+
button.display = False
|
|
1007
|
+
|
|
1008
|
+
def _apply_read_only_state(self) -> None:
|
|
1009
|
+
"""Make the mounted form view-only while keeping sidebar navigation usable."""
|
|
1010
|
+
if not self._read_only or not self.is_attached:
|
|
1011
|
+
return
|
|
1012
|
+
try:
|
|
1013
|
+
self.query_one("#mc-footer", Vertical).add_class("-read-only")
|
|
1014
|
+
self.query_one("#mc-read-only-notice", Static).display = True
|
|
1015
|
+
except NoMatches:
|
|
1016
|
+
return
|
|
1017
|
+
for input_widget in self.query(Input):
|
|
1018
|
+
input_widget.disabled = True
|
|
1019
|
+
for select in self.query(Select):
|
|
1020
|
+
select.disabled = True
|
|
1021
|
+
for checkbox in self.query(Checkbox):
|
|
1022
|
+
checkbox.disabled = True
|
|
1023
|
+
for button in self.query(Button):
|
|
1024
|
+
if button.id == "mc-cancel":
|
|
1025
|
+
button.disabled = False
|
|
1026
|
+
button.display = True
|
|
1027
|
+
else:
|
|
1028
|
+
button.disabled = True
|
|
1029
|
+
button.display = False
|
|
1030
|
+
|
|
1031
|
+
# ── Key-value list helpers ───────────────────────────────────────
|
|
1032
|
+
|
|
1033
|
+
def _compose_kv_row(
|
|
1034
|
+
self,
|
|
1035
|
+
key: str,
|
|
1036
|
+
val: str,
|
|
1037
|
+
prefix: str,
|
|
1038
|
+
*,
|
|
1039
|
+
read_only: bool = False,
|
|
1040
|
+
) -> Horizontal:
|
|
1041
|
+
uid = _next_uid()
|
|
1042
|
+
key_placeholder, value_placeholder = self._kv_placeholders(prefix)
|
|
1043
|
+
key_input = Input(value=key, placeholder=key_placeholder, classes="mc-kv-field mc-kv-key-input")
|
|
1044
|
+
value_input = Input(value=val, placeholder=value_placeholder, classes="mc-kv-field mc-kv-value-input")
|
|
1045
|
+
remove_button = Button("✕", classes="mc-kv-remove-btn", id=f"mc-{prefix}del-{uid}")
|
|
1046
|
+
if read_only:
|
|
1047
|
+
# Seed read-only state here so mounted rows never have a pre-refresh editable window.
|
|
1048
|
+
key_input.disabled = True
|
|
1049
|
+
value_input.disabled = True
|
|
1050
|
+
remove_button.disabled = True
|
|
1051
|
+
remove_button.display = False
|
|
1052
|
+
return Horizontal(
|
|
1053
|
+
key_input,
|
|
1054
|
+
value_input,
|
|
1055
|
+
remove_button,
|
|
1056
|
+
classes="mc-kv-item-row",
|
|
1057
|
+
)
|
|
1058
|
+
|
|
1059
|
+
def _compose_kv_add_row(self, prefix: str, *, read_only: bool = False) -> Horizontal:
|
|
1060
|
+
add_button = ConfigAddButton(
|
|
1061
|
+
self._render_toast(_ADD.bind()),
|
|
1062
|
+
compact=True,
|
|
1063
|
+
classes="mc-kv-add-btn",
|
|
1064
|
+
id=f"mc-{prefix}add",
|
|
1065
|
+
)
|
|
1066
|
+
if read_only:
|
|
1067
|
+
add_button.disabled = True
|
|
1068
|
+
add_button.display = False
|
|
1069
|
+
return Horizontal(
|
|
1070
|
+
add_button,
|
|
1071
|
+
classes="mc-kv-add-row",
|
|
1072
|
+
id=f"mc-{prefix}add-row",
|
|
1073
|
+
)
|
|
1074
|
+
|
|
1075
|
+
def _populate_kv_list(self, container_id: str, items: dict[str, str], prefix: str) -> None:
|
|
1076
|
+
"""Clear and repopulate a key-value list container."""
|
|
1077
|
+
container = self.query_one(f"#{container_id}", Vertical)
|
|
1078
|
+
# Remove all existing item rows (keep the add row)
|
|
1079
|
+
for row in list(container.query(".mc-kv-item-row")):
|
|
1080
|
+
row.remove()
|
|
1081
|
+
# Add rows before the add-row
|
|
1082
|
+
add_row_id = f"mc-{prefix}add-row"
|
|
1083
|
+
add_row = self.query_one(f"#{add_row_id}", Horizontal)
|
|
1084
|
+
for key, val in items.items():
|
|
1085
|
+
container.mount(self._compose_kv_row(key, val, prefix, read_only=self._read_only), before=add_row)
|
|
1086
|
+
if not items:
|
|
1087
|
+
container.mount(self._compose_kv_row("", "", prefix, read_only=self._read_only), before=add_row)
|
|
1088
|
+
self._apply_read_only_state()
|
|
1089
|
+
self.call_after_refresh(self._apply_read_only_state)
|
|
1090
|
+
|
|
1091
|
+
def _read_kv_list(self, container_id: str) -> dict[str, str]:
|
|
1092
|
+
"""Read all key-value pairs from a list container."""
|
|
1093
|
+
result: dict[str, str] = {}
|
|
1094
|
+
container = self.query_one(f"#{container_id}", Vertical)
|
|
1095
|
+
for row in container.query(".mc-kv-item-row"):
|
|
1096
|
+
key = row.query_one(".mc-kv-key-input", Input).value.strip()
|
|
1097
|
+
value = row.query_one(".mc-kv-value-input", Input).value.strip()
|
|
1098
|
+
if key:
|
|
1099
|
+
result[key] = value
|
|
1100
|
+
return result
|
|
1101
|
+
|
|
1102
|
+
def _validate_kv_list(
|
|
1103
|
+
self,
|
|
1104
|
+
container_id: str,
|
|
1105
|
+
label_definition: MessageDef,
|
|
1106
|
+
*,
|
|
1107
|
+
http_headers: bool = False,
|
|
1108
|
+
) -> list[str]:
|
|
1109
|
+
"""Validate that partially-filled key-value rows are not saved silently.
|
|
1110
|
+
|
|
1111
|
+
``http_headers`` marks rows destined for HTTP headers: managed
|
|
1112
|
+
header names are rejected, and names/values must survive ASCII
|
|
1113
|
+
header encoding (``{{ENV_VAR}}`` templates are ASCII and pass;
|
|
1114
|
+
their resolved values are re-checked when the client is built).
|
|
1115
|
+
"""
|
|
1116
|
+
label = self._render_toast(label_definition.bind())
|
|
1117
|
+
errors: list[str] = []
|
|
1118
|
+
seen: set[str] = set()
|
|
1119
|
+
container = self.query_one(f"#{container_id}", Vertical)
|
|
1120
|
+
for index, row in enumerate(container.query(".mc-kv-item-row"), start=1):
|
|
1121
|
+
key = row.query_one(".mc-kv-key-input", Input).value.strip()
|
|
1122
|
+
value = row.query_one(".mc-kv-value-input", Input).value.strip()
|
|
1123
|
+
if not key and not value:
|
|
1124
|
+
continue
|
|
1125
|
+
if key and not value:
|
|
1126
|
+
errors.append(
|
|
1127
|
+
self._render_toast(
|
|
1128
|
+
_KV_VALUE_REQUIRED.bind(
|
|
1129
|
+
label=label,
|
|
1130
|
+
row=index,
|
|
1131
|
+
key=DisplayBlock(key),
|
|
1132
|
+
)
|
|
1133
|
+
)
|
|
1134
|
+
)
|
|
1135
|
+
elif value and not key:
|
|
1136
|
+
errors.append(self._render_toast(_KV_KEY_REQUIRED.bind(label=label, row=index)))
|
|
1137
|
+
if key and http_headers and is_chrys_managed_header_name(key):
|
|
1138
|
+
errors.append(
|
|
1139
|
+
self._render_toast(
|
|
1140
|
+
_KV_HEADER_RESERVED.bind(
|
|
1141
|
+
label=label,
|
|
1142
|
+
row=index,
|
|
1143
|
+
key=DisplayBlock(key),
|
|
1144
|
+
app=APP_DISPLAY_NAME,
|
|
1145
|
+
)
|
|
1146
|
+
)
|
|
1147
|
+
)
|
|
1148
|
+
if key and http_headers:
|
|
1149
|
+
errors.extend(
|
|
1150
|
+
self._render_toast(
|
|
1151
|
+
_KV_DETAIL.bind(
|
|
1152
|
+
label=label,
|
|
1153
|
+
row=index,
|
|
1154
|
+
detail=DisplayBlock(charset_error),
|
|
1155
|
+
)
|
|
1156
|
+
)
|
|
1157
|
+
for charset_error in (
|
|
1158
|
+
header_name_charset_error(key),
|
|
1159
|
+
header_value_charset_error(key, value),
|
|
1160
|
+
)
|
|
1161
|
+
if charset_error
|
|
1162
|
+
)
|
|
1163
|
+
if key in seen:
|
|
1164
|
+
errors.append(
|
|
1165
|
+
self._render_toast(
|
|
1166
|
+
_KV_DUPLICATE_KEY.bind(
|
|
1167
|
+
label=label,
|
|
1168
|
+
row=index,
|
|
1169
|
+
key=DisplayBlock(key),
|
|
1170
|
+
)
|
|
1171
|
+
)
|
|
1172
|
+
)
|
|
1173
|
+
if key:
|
|
1174
|
+
seen.add(key)
|
|
1175
|
+
return errors
|
|
1176
|
+
|
|
1177
|
+
def _validate_required_positive_int(
|
|
1178
|
+
self,
|
|
1179
|
+
field_id: str,
|
|
1180
|
+
label_definition: MessageDef,
|
|
1181
|
+
*,
|
|
1182
|
+
example: int,
|
|
1183
|
+
) -> list[str]:
|
|
1184
|
+
"""Required positive-integer rule shared by the token-count fields."""
|
|
1185
|
+
label = self._render_toast(label_definition.bind())
|
|
1186
|
+
value = self.query_one(field_id, Input).value.strip()
|
|
1187
|
+
if not value:
|
|
1188
|
+
return [self._render_toast(_FIELD_REQUIRED_EXAMPLE.bind(label=label, example=example))]
|
|
1189
|
+
try:
|
|
1190
|
+
parsed = int(value)
|
|
1191
|
+
except ValueError:
|
|
1192
|
+
return [self._render_toast(_FIELD_VALID_INTEGER.bind(label=label))]
|
|
1193
|
+
if parsed <= 0:
|
|
1194
|
+
return [self._render_toast(_FIELD_POSITIVE_INTEGER.bind(label=label))]
|
|
1195
|
+
return []
|
|
1196
|
+
|
|
1197
|
+
def _validate_chat_option_values(self) -> list[str]:
|
|
1198
|
+
"""Validate common ChatOptions values before saving."""
|
|
1199
|
+
errors: list[str] = []
|
|
1200
|
+
container = self.query_one("#mc-options-list", Vertical)
|
|
1201
|
+
for index, row in enumerate(container.query(".mc-kv-item-row"), start=1):
|
|
1202
|
+
key = row.query_one(".mc-kv-key-input", Input).value.strip()
|
|
1203
|
+
value = row.query_one(".mc-kv-value-input", Input).value.strip()
|
|
1204
|
+
if not key or not value:
|
|
1205
|
+
continue
|
|
1206
|
+
|
|
1207
|
+
parsed = _parse_chat_option_value_for_validation(value)
|
|
1208
|
+
|
|
1209
|
+
if key in OUTPUT_CAP_OPTION_ALIASES:
|
|
1210
|
+
# A profile-level output cap (any provider spelling) would
|
|
1211
|
+
# fight the dedicated field: internal calls (e.g. LAST_WORDS
|
|
1212
|
+
# notes) size max_tokens per request and clamp to Max Output
|
|
1213
|
+
# Tokens, so a chat-options copy is at best redundant and at
|
|
1214
|
+
# worst overrides the clamp.
|
|
1215
|
+
errors.append(
|
|
1216
|
+
self._render_toast(
|
|
1217
|
+
_CHAT_OPTION_OUTPUT_CAP.bind(
|
|
1218
|
+
row=index,
|
|
1219
|
+
key=DisplayBlock(repr(key)),
|
|
1220
|
+
)
|
|
1221
|
+
)
|
|
1222
|
+
)
|
|
1223
|
+
continue
|
|
1224
|
+
if key in PROTECTED_TOP_LEVEL_CHAT_OPTION_KEYS:
|
|
1225
|
+
errors.append(
|
|
1226
|
+
self._render_toast(
|
|
1227
|
+
_CHAT_OPTION_PROTECTED.bind(
|
|
1228
|
+
row=index,
|
|
1229
|
+
key=DisplayBlock(repr(key)),
|
|
1230
|
+
)
|
|
1231
|
+
)
|
|
1232
|
+
)
|
|
1233
|
+
continue
|
|
1234
|
+
if key == "extra_body" and isinstance(parsed, dict):
|
|
1235
|
+
protected_nested = sorted(PROTECTED_EXTRA_BODY_CHAT_OPTION_KEYS & parsed.keys())
|
|
1236
|
+
if protected_nested:
|
|
1237
|
+
errors.append(
|
|
1238
|
+
self._render_toast(
|
|
1239
|
+
_CHAT_OPTION_NESTED_PROTECTED.bind(
|
|
1240
|
+
row=index,
|
|
1241
|
+
keys=DisplayBlock(", ".join(protected_nested)),
|
|
1242
|
+
)
|
|
1243
|
+
)
|
|
1244
|
+
)
|
|
1245
|
+
continue
|
|
1246
|
+
if key in _JSON_OBJECT_CHAT_OPTION_KEYS:
|
|
1247
|
+
if key == "logit_bias":
|
|
1248
|
+
errors.extend(_validate_chat_option_logit_bias(index, parsed, render_message=self._render_toast))
|
|
1249
|
+
elif key == "extra_headers":
|
|
1250
|
+
errors.extend(_validate_chat_option_extra_headers(index, parsed, render_message=self._render_toast))
|
|
1251
|
+
else:
|
|
1252
|
+
errors.extend(_validate_chat_option_object(index, key, parsed, render_message=self._render_toast))
|
|
1253
|
+
elif key in _FLOAT_CHAT_OPTION_RULES:
|
|
1254
|
+
errors.extend(_validate_chat_option_float(index, key, parsed, render_message=self._render_toast))
|
|
1255
|
+
elif key in _INT_CHAT_OPTION_RULES:
|
|
1256
|
+
errors.extend(_validate_chat_option_int(index, key, parsed, render_message=self._render_toast))
|
|
1257
|
+
elif key in _BOOL_CHAT_OPTION_KEYS:
|
|
1258
|
+
errors.extend(_validate_chat_option_bool(index, key, parsed, render_message=self._render_toast))
|
|
1259
|
+
elif key in _STRING_CHAT_OPTION_KEYS:
|
|
1260
|
+
errors.extend(
|
|
1261
|
+
_validate_chat_option_string(
|
|
1262
|
+
index,
|
|
1263
|
+
key,
|
|
1264
|
+
parsed,
|
|
1265
|
+
value,
|
|
1266
|
+
render_message=self._render_toast,
|
|
1267
|
+
)
|
|
1268
|
+
)
|
|
1269
|
+
elif key == "stop":
|
|
1270
|
+
errors.extend(_validate_chat_option_stop(index, parsed, value, render_message=self._render_toast))
|
|
1271
|
+
return errors
|
|
1272
|
+
|
|
1273
|
+
def _kv_placeholders(self, prefix: str) -> tuple[str, str]:
|
|
1274
|
+
if prefix == "h":
|
|
1275
|
+
return (
|
|
1276
|
+
self._render_toast(_HEADER_NAME_PLACEHOLDER.bind()),
|
|
1277
|
+
self._render_toast(_HEADER_VALUE_PLACEHOLDER.bind()),
|
|
1278
|
+
)
|
|
1279
|
+
return (
|
|
1280
|
+
self._render_toast(_OPTION_NAME_PLACEHOLDER.bind()),
|
|
1281
|
+
self._render_toast(_OPTION_VALUE_PLACEHOLDER.bind()),
|
|
1282
|
+
)
|
|
1283
|
+
|
|
1284
|
+
async def _add_kv_item(self, container_id: str, prefix: str) -> None:
|
|
1285
|
+
"""Append another editable key-value row."""
|
|
1286
|
+
if self._read_only:
|
|
1287
|
+
return
|
|
1288
|
+
try:
|
|
1289
|
+
container = self.query_one(f"#{container_id}", Vertical)
|
|
1290
|
+
add_row = self.query_one(f"#mc-{prefix}add-row", Horizontal)
|
|
1291
|
+
except Exception:
|
|
1292
|
+
return
|
|
1293
|
+
|
|
1294
|
+
await container.mount(self._compose_kv_row("", "", prefix), before=add_row)
|
|
1295
|
+
container.refresh(layout=True)
|
|
1296
|
+
|
|
1297
|
+
# ── Profile loading ──────────────────────────────────────────────
|
|
1298
|
+
|
|
1299
|
+
def _load_profile(self, profile_id: str) -> None:
|
|
1300
|
+
"""Populate the form fields from the given profile."""
|
|
1301
|
+
profile = self._registry.get(profile_id)
|
|
1302
|
+
if profile is None:
|
|
1303
|
+
return
|
|
1304
|
+
self._selected_profile_id = profile_id
|
|
1305
|
+
self._set_form_enabled(True)
|
|
1306
|
+
|
|
1307
|
+
self.query_one("#mc-name", Input).value = profile.name
|
|
1308
|
+
self.query_one("#mc-provider", Select).value = profile.provider
|
|
1309
|
+
self.query_one("#mc-api-style", Select).value = (
|
|
1310
|
+
profile.api_style if profile.provider in ("openai", "deepseek-openai") else API_STYLE_CHAT_COMPLETIONS
|
|
1311
|
+
)
|
|
1312
|
+
self.query_one("#mc-model", Input).value = profile.model_id
|
|
1313
|
+
self.query_one("#mc-max-tokens", Input).value = str(profile.max_context_tokens)
|
|
1314
|
+
self.query_one("#mc-max-output-tokens", Input).value = str(
|
|
1315
|
+
profile.max_output_tokens if profile.max_output_tokens > 0 else DEFAULT_MAX_OUTPUT_TOKENS
|
|
1316
|
+
)
|
|
1317
|
+
self.query_one("#mc-base-url", Input).value = profile.base_url
|
|
1318
|
+
self.query_one("#mc-api-key", Input).value = profile.api_key
|
|
1319
|
+
self.query_one("#mc-connect-timeout", Input).value = str(profile.http_connect_timeout)
|
|
1320
|
+
self.query_one("#mc-read-timeout", Input).value = str(profile.http_read_timeout)
|
|
1321
|
+
self.query_one("#mc-max-retries", Input).value = str(profile.http_max_retries)
|
|
1322
|
+
skip_tls = not profile.verify_ssl
|
|
1323
|
+
self.query_one("#mc-skip-tls", Checkbox).value = skip_tls
|
|
1324
|
+
self.query_one("#mc-skip-tls-hint", Label).display = skip_tls
|
|
1325
|
+
self.query_one("#mc-bypass-proxy", Checkbox).value = profile.bypass_proxy
|
|
1326
|
+
self.query_one("#mc-stream", Checkbox).value = profile.stream
|
|
1327
|
+
self.query_one("#mc-vision", Checkbox).value = profile.vision
|
|
1328
|
+
|
|
1329
|
+
# Populate key-value lists
|
|
1330
|
+
headers = _parse_json_dict(profile.http_headers, parse_values=False)
|
|
1331
|
+
self._populate_kv_list("mc-headers-list", headers, "h")
|
|
1332
|
+
|
|
1333
|
+
options = _parse_json_dict(profile.chat_options)
|
|
1334
|
+
self._populate_kv_list("mc-options-list", options, "o")
|
|
1335
|
+
|
|
1336
|
+
self._update_provider_labels(profile.provider)
|
|
1337
|
+
self._apply_read_only_state()
|
|
1338
|
+
self.call_after_refresh(self._apply_read_only_state)
|
|
1339
|
+
|
|
1340
|
+
# ── Provider-dependent labels ────────────────────────────────────
|
|
1341
|
+
|
|
1342
|
+
def _update_provider_labels(self, provider: str) -> None:
|
|
1343
|
+
"""Update field labels and the Base URL placeholder for the selected provider."""
|
|
1344
|
+
prefix = _PROVIDER_LABELS.get(provider, provider.title())
|
|
1345
|
+
self.query_one("#mc-model-label", Label).update(
|
|
1346
|
+
f"[red]*[/red] {escape(self._render_toast(_PROVIDER_MODEL.bind(provider=prefix)))}"
|
|
1347
|
+
)
|
|
1348
|
+
self.query_one("#mc-url-label", Label).update(self._render_toast(_PROVIDER_BASE_URL.bind(provider=prefix)))
|
|
1349
|
+
self.query_one("#mc-key-label", Label).update(self._render_toast(_PROVIDER_API_KEY.bind(provider=prefix)))
|
|
1350
|
+
|
|
1351
|
+
default_base_url = _PROVIDER_DEFAULT_BASE_URLS.get(provider, "")
|
|
1352
|
+
self.query_one("#mc-base-url", Input).placeholder = default_base_url or self._render_toast(
|
|
1353
|
+
_LEAVE_BLANK_FOR_DEFAULT.bind()
|
|
1354
|
+
)
|
|
1355
|
+
api_style_label = self.query_one("#mc-api-style-label", Label)
|
|
1356
|
+
api_style_select = self.query_one("#mc-api-style", Select)
|
|
1357
|
+
supports_api_style = provider in ("openai", "deepseek-openai")
|
|
1358
|
+
api_style_label.display = supports_api_style
|
|
1359
|
+
api_style_select.display = supports_api_style
|
|
1360
|
+
if not supports_api_style:
|
|
1361
|
+
api_style_select.value = API_STYLE_CHAT_COMPLETIONS
|
|
1362
|
+
self._refresh_max_output_label(provider, str(api_style_select.value))
|
|
1363
|
+
|
|
1364
|
+
def _refresh_max_output_label(self, provider: str, api_style: str) -> None:
|
|
1365
|
+
self.query_one("#mc-max-output-label", Label).update(self._max_output_label(provider, api_style))
|
|
1366
|
+
|
|
1367
|
+
def _max_output_label(self, provider: str, api_style: str) -> Text:
|
|
1368
|
+
param = _token_limit_param_name(provider, api_style)
|
|
1369
|
+
rendered = self._render_toast(_MAX_OUTPUT_TOKENS.bind(param=param))
|
|
1370
|
+
label = Text(rendered)
|
|
1371
|
+
start = rendered.find(param)
|
|
1372
|
+
if start >= 0:
|
|
1373
|
+
label.stylize("dim", start, start + len(param))
|
|
1374
|
+
return label
|
|
1375
|
+
|
|
1376
|
+
# ── Events ───────────────────────────────────────────────────────
|
|
1377
|
+
|
|
1378
|
+
@on(Select.Changed, "#mc-provider")
|
|
1379
|
+
def _on_provider_changed(self, event: Select.Changed) -> None:
|
|
1380
|
+
self._update_provider_labels(str(event.value))
|
|
1381
|
+
|
|
1382
|
+
@on(Select.Changed, "#mc-api-style")
|
|
1383
|
+
def _on_api_style_changed(self, _event: Select.Changed) -> None:
|
|
1384
|
+
self._refresh_max_output_label(
|
|
1385
|
+
str(self.query_one("#mc-provider", Select).value),
|
|
1386
|
+
str(self.query_one("#mc-api-style", Select).value),
|
|
1387
|
+
)
|
|
1388
|
+
|
|
1389
|
+
@on(Checkbox.Changed, "#mc-skip-tls")
|
|
1390
|
+
def _on_skip_tls_changed(self, event: Checkbox.Changed) -> None:
|
|
1391
|
+
with contextlib.suppress(Exception):
|
|
1392
|
+
self.query_one("#mc-skip-tls-hint", Label).display = bool(event.value)
|
|
1393
|
+
|
|
1394
|
+
@on(OptionList.OptionSelected, "#mc-list")
|
|
1395
|
+
def _on_profile_selected(self, event: OptionList.OptionSelected) -> None:
|
|
1396
|
+
if event.option.id and event.option.id != self._selected_profile_id:
|
|
1397
|
+
self._load_profile(event.option.id)
|
|
1398
|
+
|
|
1399
|
+
@on(Button.Pressed, "#mc-hadd")
|
|
1400
|
+
async def _on_header_add(self, _event: Button.Pressed) -> None:
|
|
1401
|
+
await self._add_kv_item("mc-headers-list", "h")
|
|
1402
|
+
|
|
1403
|
+
@on(Button.Pressed, "#mc-oadd")
|
|
1404
|
+
async def _on_option_add(self, _event: Button.Pressed) -> None:
|
|
1405
|
+
await self._add_kv_item("mc-options-list", "o")
|
|
1406
|
+
|
|
1407
|
+
@on(Button.Pressed)
|
|
1408
|
+
def _on_kv_remove(self, event: Button.Pressed) -> None:
|
|
1409
|
+
"""Handle removal of key-value rows (header or option)."""
|
|
1410
|
+
if self._read_only:
|
|
1411
|
+
return
|
|
1412
|
+
btn_id = event.button.id or ""
|
|
1413
|
+
if btn_id.startswith(("mc-hdel-", "mc-odel-")):
|
|
1414
|
+
row = event.button.parent
|
|
1415
|
+
if isinstance(row, Widget) and isinstance(row.parent, Vertical):
|
|
1416
|
+
container = row.parent
|
|
1417
|
+
was_last_row = len(list(container.query(".mc-kv-item-row"))) <= 1
|
|
1418
|
+
if was_last_row:
|
|
1419
|
+
row.query_one(".mc-kv-key-input", Input).value = ""
|
|
1420
|
+
row.query_one(".mc-kv-value-input", Input).value = ""
|
|
1421
|
+
row.refresh(layout=True)
|
|
1422
|
+
else:
|
|
1423
|
+
row.remove()
|
|
1424
|
+
|
|
1425
|
+
async def _create_new_profile(self) -> None:
|
|
1426
|
+
"""Create a new blank profile, add to sidebar, and select it.
|
|
1427
|
+
|
|
1428
|
+
Names follow the pattern "Profile N" (starting at N=1) and
|
|
1429
|
+
increment until an unused name is found — so the first auto-
|
|
1430
|
+
seeded profile is "Profile 1", the next "Profile 2", etc.
|
|
1431
|
+
"""
|
|
1432
|
+
if self._read_only:
|
|
1433
|
+
return
|
|
1434
|
+
from chrys.service.profiles.models.schema import ModelProfile
|
|
1435
|
+
from chrys.service.profiles.models.serializer import save_profile
|
|
1436
|
+
|
|
1437
|
+
# Case-insensitive collision check: "Profile 1" / "profile 1" are
|
|
1438
|
+
# treated as duplicates to match the uniqueness rule used elsewhere.
|
|
1439
|
+
existing_names = {p.name.casefold() for p in self._registry.list_profiles()}
|
|
1440
|
+
base = "Profile"
|
|
1441
|
+
counter = 1
|
|
1442
|
+
name = f"{base} {counter}"
|
|
1443
|
+
while name.casefold() in existing_names:
|
|
1444
|
+
counter += 1
|
|
1445
|
+
name = f"{base} {counter}"
|
|
1446
|
+
|
|
1447
|
+
profile_id = self._next_profile_id()
|
|
1448
|
+
profile = ModelProfile(id=profile_id, name=name)
|
|
1449
|
+
await asyncio.to_thread(save_profile, profile)
|
|
1450
|
+
self._registry.register(profile)
|
|
1451
|
+
|
|
1452
|
+
self._populate_sidebar()
|
|
1453
|
+
ol = self.query_one("#mc-list", OptionList)
|
|
1454
|
+
with contextlib.suppress(OptionDoesNotExist):
|
|
1455
|
+
ol.highlighted = ol.get_option_index(profile_id)
|
|
1456
|
+
self._load_profile(profile_id)
|
|
1457
|
+
self._update_delete_button()
|
|
1458
|
+
|
|
1459
|
+
def _next_profile_id(self) -> str:
|
|
1460
|
+
"""Generate a fresh model profile id."""
|
|
1461
|
+
while True:
|
|
1462
|
+
profile_id = uuid.uuid4().hex[:12]
|
|
1463
|
+
if self._registry.get(profile_id) is None:
|
|
1464
|
+
return profile_id
|
|
1465
|
+
|
|
1466
|
+
@staticmethod
|
|
1467
|
+
def _clone_name_parts(name: str) -> tuple[str, int] | None:
|
|
1468
|
+
"""Return ``(root, suffix_index)`` for generated clone names."""
|
|
1469
|
+
match = re.fullmatch(r"(.+?) copy(?: (\d+))?", name, flags=re.IGNORECASE)
|
|
1470
|
+
if match is None:
|
|
1471
|
+
return None
|
|
1472
|
+
suffix = match.group(2)
|
|
1473
|
+
return match.group(1), int(suffix) if suffix is not None else 1
|
|
1474
|
+
|
|
1475
|
+
def _clone_name_indices(self, root_name: str) -> list[int]:
|
|
1476
|
+
"""Return existing generated clone suffixes for the given root."""
|
|
1477
|
+
indices: list[int] = []
|
|
1478
|
+
for profile in self._registry.list_profiles():
|
|
1479
|
+
parts = self._clone_name_parts(profile.name)
|
|
1480
|
+
if parts is not None and parts[0].casefold() == root_name.casefold():
|
|
1481
|
+
indices.append(parts[1])
|
|
1482
|
+
return indices
|
|
1483
|
+
|
|
1484
|
+
def _next_clone_name(self, source_name: str) -> str:
|
|
1485
|
+
"""Generate a unique display name for a cloned model profile."""
|
|
1486
|
+
existing_names = {p.name.casefold() for p in self._registry.list_profiles()}
|
|
1487
|
+
source_parts = self._clone_name_parts(source_name)
|
|
1488
|
+
if source_parts is None:
|
|
1489
|
+
root_name = source_name
|
|
1490
|
+
counter = 1
|
|
1491
|
+
else:
|
|
1492
|
+
root_name, source_index = source_parts
|
|
1493
|
+
counter = max([source_index, *self._clone_name_indices(root_name)]) + 1
|
|
1494
|
+
base = f"{root_name} Copy"
|
|
1495
|
+
name = base if counter == 1 else f"{base} {counter}"
|
|
1496
|
+
while name.casefold() in existing_names:
|
|
1497
|
+
counter += 1
|
|
1498
|
+
name = f"{base} {counter}"
|
|
1499
|
+
return name
|
|
1500
|
+
|
|
1501
|
+
@on(Button.Pressed, "#mc-new")
|
|
1502
|
+
async def _on_new(self, _event: Button.Pressed) -> None:
|
|
1503
|
+
"""Create a new blank profile and select it."""
|
|
1504
|
+
if self._read_only:
|
|
1505
|
+
return
|
|
1506
|
+
await self._create_new_profile()
|
|
1507
|
+
|
|
1508
|
+
@on(Button.Pressed, "#mc-clone")
|
|
1509
|
+
async def _on_clone(self, _event: Button.Pressed) -> None:
|
|
1510
|
+
"""Create a saved copy of the selected profile and select it."""
|
|
1511
|
+
if self._read_only:
|
|
1512
|
+
return
|
|
1513
|
+
source = self._registry.get(self._selected_profile_id)
|
|
1514
|
+
if source is None:
|
|
1515
|
+
return
|
|
1516
|
+
|
|
1517
|
+
from chrys.service.profiles.models.serializer import save_profile
|
|
1518
|
+
|
|
1519
|
+
duplicate = copy.deepcopy(source)
|
|
1520
|
+
duplicate.id = self._next_profile_id()
|
|
1521
|
+
duplicate.name = self._next_clone_name(source.name)
|
|
1522
|
+
await asyncio.to_thread(save_profile, duplicate)
|
|
1523
|
+
self._registry.register(duplicate)
|
|
1524
|
+
|
|
1525
|
+
self._populate_sidebar()
|
|
1526
|
+
ol = self.query_one("#mc-list", OptionList)
|
|
1527
|
+
with contextlib.suppress(OptionDoesNotExist):
|
|
1528
|
+
ol.highlighted = ol.get_option_index(duplicate.id)
|
|
1529
|
+
self._load_profile(duplicate.id)
|
|
1530
|
+
self._update_delete_button()
|
|
1531
|
+
self._notify_unsupported_options(duplicate)
|
|
1532
|
+
|
|
1533
|
+
@on(Button.Pressed, "#mc-delete")
|
|
1534
|
+
def _on_delete(self, _event: Button.Pressed) -> None:
|
|
1535
|
+
"""Delete the selected profile after user confirmation."""
|
|
1536
|
+
if self._read_only:
|
|
1537
|
+
return
|
|
1538
|
+
profile_id = self._selected_profile_id
|
|
1539
|
+
if not profile_id:
|
|
1540
|
+
return
|
|
1541
|
+
profile = self._registry.get(profile_id)
|
|
1542
|
+
display = profile.name if profile else profile_id
|
|
1543
|
+
|
|
1544
|
+
from chrys.app.tui.screens.dialogs.confirm import ConfirmDialog
|
|
1545
|
+
|
|
1546
|
+
dialog = ConfirmDialog(
|
|
1547
|
+
title=_DELETE_PROFILE_TITLE.bind(),
|
|
1548
|
+
message=_DELETE_PROFILE_MESSAGE.bind(display=display),
|
|
1549
|
+
confirm_label=_DELETE.bind(),
|
|
1550
|
+
confirm_variant="error",
|
|
1551
|
+
locale_controller=widget_locale_controller(self),
|
|
1552
|
+
)
|
|
1553
|
+
|
|
1554
|
+
async def _on_confirmed(confirmed: bool | None) -> None:
|
|
1555
|
+
if not confirmed:
|
|
1556
|
+
return
|
|
1557
|
+
try:
|
|
1558
|
+
await self._do_delete(profile_id)
|
|
1559
|
+
except Exception as exc:
|
|
1560
|
+
self.notify(
|
|
1561
|
+
str(exc),
|
|
1562
|
+
title=self._render_toast(_MODEL_SETTINGS_TITLE.bind()),
|
|
1563
|
+
severity="error",
|
|
1564
|
+
timeout=5,
|
|
1565
|
+
markup=False,
|
|
1566
|
+
)
|
|
1567
|
+
|
|
1568
|
+
self.app.push_screen(dialog, callback=_on_confirmed)
|
|
1569
|
+
|
|
1570
|
+
async def _do_delete(self, profile_id: str) -> None:
|
|
1571
|
+
"""Actually delete the profile after confirmation.
|
|
1572
|
+
|
|
1573
|
+
Deleting the global default promotes, in the user settings document,
|
|
1574
|
+
the first remaining profile as the sidebar lists them. Deleting the runtime-effective profile also
|
|
1575
|
+
resynchronizes the process pointer to a selectable profile — the
|
|
1576
|
+
file default when it resolves, else the first selectable one —
|
|
1577
|
+
and requests a backend reload. If no profiles remain, a fresh
|
|
1578
|
+
editable profile is created before either pointer is reconciled.
|
|
1579
|
+
"""
|
|
1580
|
+
if self._read_only:
|
|
1581
|
+
return
|
|
1582
|
+
from chrys.service.profiles.models.env_bridge import get_active_profile_id, get_global_default_profile_id
|
|
1583
|
+
from chrys.service.profiles.models.resolver import resolve_profile_selector, resolve_selectable_profile
|
|
1584
|
+
from chrys.service.profiles.models.schema import is_model_profile_selectable
|
|
1585
|
+
from chrys.service.profiles.models.serializer import delete_profile
|
|
1586
|
+
|
|
1587
|
+
was_global_default = profile_id == self._global_default_profile_id
|
|
1588
|
+
# The process pointer may hold a profile NAME (id-or-unique-name
|
|
1589
|
+
# selection is supported); resolve it against the still-complete
|
|
1590
|
+
# registry before deleting, or a name pointer at the deleted profile
|
|
1591
|
+
# would silently keep the dead executor running.
|
|
1592
|
+
active_profile = resolve_profile_selector(self._registry, get_active_profile_id())
|
|
1593
|
+
was_runtime_effective = active_profile is not None and active_profile.id == profile_id
|
|
1594
|
+
|
|
1595
|
+
await asyncio.to_thread(delete_profile, profile_id)
|
|
1596
|
+
self._registry.remove(profile_id)
|
|
1597
|
+
|
|
1598
|
+
profiles = self._listed_profiles()
|
|
1599
|
+
seeded_default = not profiles
|
|
1600
|
+
if seeded_default:
|
|
1601
|
+
await self._create_new_profile()
|
|
1602
|
+
profiles = self._listed_profiles()
|
|
1603
|
+
|
|
1604
|
+
if was_global_default:
|
|
1605
|
+
# Prefer a selectable replacement — the listed order can put a
|
|
1606
|
+
# hollow auto-seeded profile first, and promoting it would point
|
|
1607
|
+
# both pointers at an unusable model while real ones exist.
|
|
1608
|
+
new_global_default = next(
|
|
1609
|
+
(candidate for candidate in profiles if is_model_profile_selectable(candidate)),
|
|
1610
|
+
profiles[0],
|
|
1611
|
+
)
|
|
1612
|
+
await self._persist_active_profile(new_global_default)
|
|
1613
|
+
self._global_default_profile_id = new_global_default.id
|
|
1614
|
+
|
|
1615
|
+
if was_runtime_effective:
|
|
1616
|
+
# The file default may itself be dangling or hollow (deleting a
|
|
1617
|
+
# non-default profile never repairs the file pointer); adopting
|
|
1618
|
+
# it verbatim would strand the session on the placeholder while
|
|
1619
|
+
# a selectable profile exists. Resolve it first and fall back
|
|
1620
|
+
# the same way global-default promotion does.
|
|
1621
|
+
global_default_profile_id = await asyncio.to_thread(get_global_default_profile_id)
|
|
1622
|
+
replacement = resolve_selectable_profile(self._registry, global_default_profile_id)
|
|
1623
|
+
if replacement is None:
|
|
1624
|
+
replacement = next(
|
|
1625
|
+
(candidate for candidate in profiles if is_model_profile_selectable(candidate)),
|
|
1626
|
+
None,
|
|
1627
|
+
)
|
|
1628
|
+
if replacement is not None:
|
|
1629
|
+
set_model_pointer(replacement.id, origin=SettingOrigin(layer=Source.PROCESS_RUNTIME))
|
|
1630
|
+
else:
|
|
1631
|
+
set_model_pointer(None, origin=SettingOrigin(layer=Source.PROCESS_RUNTIME))
|
|
1632
|
+
self._requires_runtime_reload = True
|
|
1633
|
+
|
|
1634
|
+
if seeded_default:
|
|
1635
|
+
return
|
|
1636
|
+
|
|
1637
|
+
self._populate_sidebar()
|
|
1638
|
+
first = profiles[0].id
|
|
1639
|
+
ol = self.query_one("#mc-list", OptionList)
|
|
1640
|
+
with contextlib.suppress(OptionDoesNotExist):
|
|
1641
|
+
ol.highlighted = ol.get_option_index(first)
|
|
1642
|
+
self._load_profile(first)
|
|
1643
|
+
|
|
1644
|
+
async def _persist_active_profile(self, profile: ModelProfile | None) -> None:
|
|
1645
|
+
"""Write the global default profile pointer to the user settings document only.
|
|
1646
|
+
|
|
1647
|
+
``None`` removes ``model.profile.active`` so resolvers fall back to
|
|
1648
|
+
the built-in default profile.
|
|
1649
|
+
"""
|
|
1650
|
+
if self._read_only:
|
|
1651
|
+
return
|
|
1652
|
+
from chrys.service.profiles.models.env_bridge import set_global_default_profile_id
|
|
1653
|
+
|
|
1654
|
+
await asyncio.to_thread(set_global_default_profile_id, profile.id if profile is not None else "")
|
|
1655
|
+
|
|
1656
|
+
@on(Button.Pressed, "#mc-save")
|
|
1657
|
+
async def _on_save(self, _event: Button.Pressed) -> None:
|
|
1658
|
+
"""Save the form without changing the runtime-effective pointer."""
|
|
1659
|
+
if self._read_only or self._prompt_cache_offer_open:
|
|
1660
|
+
return
|
|
1661
|
+
errors = self._validate()
|
|
1662
|
+
if errors:
|
|
1663
|
+
self.notify(
|
|
1664
|
+
"\n".join(errors),
|
|
1665
|
+
title=self._render_toast(_VALIDATION_ERROR_TITLE.bind()),
|
|
1666
|
+
severity="error",
|
|
1667
|
+
timeout=5,
|
|
1668
|
+
markup=False,
|
|
1669
|
+
)
|
|
1670
|
+
return
|
|
1671
|
+
if self._should_offer_prompt_cache():
|
|
1672
|
+
self._offer_prompt_cache()
|
|
1673
|
+
return
|
|
1674
|
+
await self._save_and_report()
|
|
1675
|
+
|
|
1676
|
+
async def _save_and_report(self) -> None:
|
|
1677
|
+
try:
|
|
1678
|
+
profile = await self._save_only()
|
|
1679
|
+
except Exception as exc:
|
|
1680
|
+
self.notify(
|
|
1681
|
+
str(exc),
|
|
1682
|
+
title=self._render_toast(_SAVE_ERROR_TITLE.bind()),
|
|
1683
|
+
severity="error",
|
|
1684
|
+
timeout=5,
|
|
1685
|
+
markup=False,
|
|
1686
|
+
)
|
|
1687
|
+
return
|
|
1688
|
+
self.notify(
|
|
1689
|
+
self._render_toast(_MODEL_PROFILE_SAVED.bind()),
|
|
1690
|
+
title=self._render_toast(_MODEL_SETTINGS_TITLE.bind()),
|
|
1691
|
+
severity="information",
|
|
1692
|
+
timeout=3,
|
|
1693
|
+
markup=False,
|
|
1694
|
+
)
|
|
1695
|
+
self._notify_unsupported_options(profile)
|
|
1696
|
+
|
|
1697
|
+
def _should_offer_prompt_cache(self) -> bool:
|
|
1698
|
+
"""Whether this save makes the profile a Claude profile that never asks for prompt caching.
|
|
1699
|
+
|
|
1700
|
+
A profile already saved as a complete Claude profile on the Anthropic
|
|
1701
|
+
protocol has had its answer; one saved as something else since (another
|
|
1702
|
+
provider or model) is asked again when a save makes it one.
|
|
1703
|
+
"""
|
|
1704
|
+
if not lacks_anthropic_prompt_cache_option(self._build_profile_from_form()):
|
|
1705
|
+
return False
|
|
1706
|
+
previous = self._registry.get(self._selected_profile_id)
|
|
1707
|
+
return previous is None or not (is_model_profile_selectable(previous) and is_anthropic_claude_profile(previous))
|
|
1708
|
+
|
|
1709
|
+
def _offer_prompt_cache(self) -> None:
|
|
1710
|
+
from chrys.app.tui.screens.dialogs.prompt_cache import ADD_AND_SAVE_RESULT, PromptCacheDialog
|
|
1711
|
+
|
|
1712
|
+
async def _on_choice(choice: PromptCacheDialogResult) -> None:
|
|
1713
|
+
self._prompt_cache_offer_open = False
|
|
1714
|
+
if choice is None or not self.is_attached:
|
|
1715
|
+
return
|
|
1716
|
+
if choice == ADD_AND_SAVE_RESULT:
|
|
1717
|
+
await self._add_prompt_cache_option()
|
|
1718
|
+
await self._save_and_report()
|
|
1719
|
+
|
|
1720
|
+
self._prompt_cache_offer_open = True
|
|
1721
|
+
self.app.push_screen(PromptCacheDialog(locale_controller=widget_locale_controller(self)), callback=_on_choice)
|
|
1722
|
+
|
|
1723
|
+
async def _add_prompt_cache_option(self) -> None:
|
|
1724
|
+
"""Merge Anthropic's prompt-caching switch into the form's ``extra_body`` option.
|
|
1725
|
+
|
|
1726
|
+
Edits the existing ``extra_body`` row, else fills the first blank
|
|
1727
|
+
row, else mounts a new one; the save then reads the form as usual.
|
|
1728
|
+
"""
|
|
1729
|
+
container = self.query_one("#mc-options-list", Vertical)
|
|
1730
|
+
rows = list(container.query(".mc-kv-item-row"))
|
|
1731
|
+
blank: tuple[Input, Input] | None = None
|
|
1732
|
+
for row in rows:
|
|
1733
|
+
key_input = row.query_one(".mc-kv-key-input", Input)
|
|
1734
|
+
value_input = row.query_one(".mc-kv-value-input", Input)
|
|
1735
|
+
key = key_input.value.strip()
|
|
1736
|
+
if key == "extra_body":
|
|
1737
|
+
raw = value_input.value.strip()
|
|
1738
|
+
value_input.value = json.dumps(with_anthropic_prompt_cache_option(json.loads(raw) if raw else None))
|
|
1739
|
+
return
|
|
1740
|
+
if blank is None and not key and not value_input.value.strip():
|
|
1741
|
+
blank = (key_input, value_input)
|
|
1742
|
+
value = json.dumps(with_anthropic_prompt_cache_option(None))
|
|
1743
|
+
if blank is not None:
|
|
1744
|
+
blank[0].value = "extra_body"
|
|
1745
|
+
blank[1].value = value
|
|
1746
|
+
return
|
|
1747
|
+
add_row = self.query_one("#mc-oadd-row", Horizontal)
|
|
1748
|
+
await container.mount(self._compose_kv_row("extra_body", value, "o"), before=add_row)
|
|
1749
|
+
|
|
1750
|
+
@on(Button.Pressed, "#mc-cancel")
|
|
1751
|
+
def _on_cancel(self, _event: Button.Pressed) -> None:
|
|
1752
|
+
self.dismiss(self._cancel_result())
|
|
1753
|
+
|
|
1754
|
+
def action_cancel(self) -> None:
|
|
1755
|
+
self.dismiss(self._cancel_result())
|
|
1756
|
+
|
|
1757
|
+
def _cancel_result(self) -> str:
|
|
1758
|
+
"""Return the dismiss result from explicit reload requirements.
|
|
1759
|
+
|
|
1760
|
+
* ``"switched"`` — deleting the runtime-effective profile set
|
|
1761
|
+
the reload latch after reconciling the process pointer.
|
|
1762
|
+
* ``"updated"`` — at least one Save reached disk + registry;
|
|
1763
|
+
the engine must rebuild so profile edits take effect.
|
|
1764
|
+
* ``""`` — no runtime reload is required; a global-default-only
|
|
1765
|
+
deletion applies to future processes.
|
|
1766
|
+
"""
|
|
1767
|
+
if self._read_only:
|
|
1768
|
+
return ""
|
|
1769
|
+
if self._requires_runtime_reload:
|
|
1770
|
+
return "switched"
|
|
1771
|
+
if self._saved_anything:
|
|
1772
|
+
return "updated"
|
|
1773
|
+
return ""
|
|
1774
|
+
|
|
1775
|
+
def _default_dismiss_result(self) -> str: # type: ignore[override]
|
|
1776
|
+
return self._cancel_result()
|
|
1777
|
+
|
|
1778
|
+
# ── Validation ───────────────────────────────────────────────────
|
|
1779
|
+
|
|
1780
|
+
def _validate(self) -> list[str]:
|
|
1781
|
+
errors: list[str] = []
|
|
1782
|
+
|
|
1783
|
+
name = self.query_one("#mc-name", Input).value.strip()
|
|
1784
|
+
if not name:
|
|
1785
|
+
errors.append(self._render_toast(_NAME_REQUIRED.bind()))
|
|
1786
|
+
else:
|
|
1787
|
+
# Uniqueness: no other profile may share this name (case-
|
|
1788
|
+
# insensitive — "Default" and "default" collide). Compare by
|
|
1789
|
+
# id so the currently-edited profile is excluded.
|
|
1790
|
+
lowered = name.casefold()
|
|
1791
|
+
for other in self._registry.list_profiles():
|
|
1792
|
+
if other.id != self._selected_profile_id and other.name.casefold() == lowered:
|
|
1793
|
+
errors.append(self._render_toast(_PROFILE_EXISTS.bind(name=DisplayBlock(other.name))))
|
|
1794
|
+
break
|
|
1795
|
+
|
|
1796
|
+
provider = str(self.query_one("#mc-provider", Select).value)
|
|
1797
|
+
if provider not in {p[1] for p in _PROVIDERS}:
|
|
1798
|
+
allowed = ", ".join(p[0] for p in _PROVIDERS)
|
|
1799
|
+
errors.append(self._render_toast(_PROVIDER_ALLOWED.bind(providers=allowed)))
|
|
1800
|
+
|
|
1801
|
+
model = self.query_one("#mc-model", Input).value.strip()
|
|
1802
|
+
if not model:
|
|
1803
|
+
errors.append(self._render_toast(_MODEL_REQUIRED.bind()))
|
|
1804
|
+
else:
|
|
1805
|
+
model_error = model_id_charset_error(model)
|
|
1806
|
+
if model_error:
|
|
1807
|
+
errors.append(model_error)
|
|
1808
|
+
|
|
1809
|
+
errors.extend(
|
|
1810
|
+
self._validate_required_positive_int(
|
|
1811
|
+
"#mc-max-tokens",
|
|
1812
|
+
_MAX_CONTEXT_TOKENS_LABEL,
|
|
1813
|
+
example=DEFAULT_MAX_CONTEXT_TOKENS,
|
|
1814
|
+
)
|
|
1815
|
+
)
|
|
1816
|
+
errors.extend(
|
|
1817
|
+
self._validate_required_positive_int(
|
|
1818
|
+
"#mc-max-output-tokens",
|
|
1819
|
+
_MAX_OUTPUT_TOKENS_LABEL,
|
|
1820
|
+
example=DEFAULT_MAX_OUTPUT_TOKENS,
|
|
1821
|
+
)
|
|
1822
|
+
)
|
|
1823
|
+
max_context_text = self.query_one("#mc-max-tokens", Input).value.strip()
|
|
1824
|
+
max_output_text = self.query_one("#mc-max-output-tokens", Input).value.strip()
|
|
1825
|
+
try:
|
|
1826
|
+
max_context_tokens = int(max_context_text)
|
|
1827
|
+
except ValueError:
|
|
1828
|
+
max_context_tokens = 0
|
|
1829
|
+
try:
|
|
1830
|
+
max_output_tokens = int(max_output_text)
|
|
1831
|
+
except ValueError:
|
|
1832
|
+
max_output_tokens = 0
|
|
1833
|
+
if 0 < max_context_tokens < MIN_DERIVABLE_CONTEXT_TOKENS:
|
|
1834
|
+
errors.append(self._render_toast(_MAX_CONTEXT_MINIMUM.bind(minimum=MIN_DERIVABLE_CONTEXT_TOKENS)))
|
|
1835
|
+
if max_context_tokens > 0 and max_output_tokens > 0 and max_output_tokens >= max_context_tokens:
|
|
1836
|
+
errors.append(self._render_toast(_MAX_OUTPUT_LESS_THAN_CONTEXT.bind()))
|
|
1837
|
+
|
|
1838
|
+
base_url = self.query_one("#mc-base-url", Input).value.strip()
|
|
1839
|
+
if base_url and not base_url.startswith(("http://", "https://")):
|
|
1840
|
+
errors.append(self._render_toast(_BASE_URL_SCHEME.bind()))
|
|
1841
|
+
|
|
1842
|
+
# A pasted key with a stray non-ASCII character (emoji, CJK text,
|
|
1843
|
+
# smart quotes) would otherwise only fail at request time, as an
|
|
1844
|
+
# opaque encode error. ``{{ENV_VAR}}`` templates are ASCII and
|
|
1845
|
+
# pass untouched; their resolved values are re-checked when the
|
|
1846
|
+
# client is built.
|
|
1847
|
+
api_key = self.query_one("#mc-api-key", Input).value.strip()
|
|
1848
|
+
api_key_error = api_key_charset_error(api_key)
|
|
1849
|
+
if api_key_error:
|
|
1850
|
+
errors.append(api_key_error)
|
|
1851
|
+
|
|
1852
|
+
# HTTP timeouts
|
|
1853
|
+
for field_id, label_definition in [
|
|
1854
|
+
("#mc-connect-timeout", _HTTP_CONNECT_TIMEOUT_LABEL),
|
|
1855
|
+
("#mc-read-timeout", _HTTP_READ_TIMEOUT_LABEL),
|
|
1856
|
+
]:
|
|
1857
|
+
label = self._render_toast(label_definition.bind())
|
|
1858
|
+
val = self.query_one(field_id, Input).value.strip()
|
|
1859
|
+
if val:
|
|
1860
|
+
try:
|
|
1861
|
+
f = float(val)
|
|
1862
|
+
if f <= 0:
|
|
1863
|
+
errors.append(self._render_toast(_FIELD_POSITIVE_NUMBER.bind(label=label)))
|
|
1864
|
+
except ValueError:
|
|
1865
|
+
errors.append(self._render_toast(_FIELD_VALID_NUMBER.bind(label=label)))
|
|
1866
|
+
|
|
1867
|
+
retries = self.query_one("#mc-max-retries", Input).value.strip()
|
|
1868
|
+
if retries:
|
|
1869
|
+
try:
|
|
1870
|
+
r = int(retries)
|
|
1871
|
+
if r < 0:
|
|
1872
|
+
errors.append(self._render_toast(_HTTP_RETRIES_NON_NEGATIVE.bind()))
|
|
1873
|
+
except ValueError:
|
|
1874
|
+
errors.append(self._render_toast(_HTTP_RETRIES_VALID.bind()))
|
|
1875
|
+
|
|
1876
|
+
errors.extend(self._validate_kv_list("mc-headers-list", _HTTP_EXTRA_HEADERS, http_headers=True))
|
|
1877
|
+
errors.extend(self._validate_kv_list("mc-options-list", _CHAT_OPTIONS))
|
|
1878
|
+
errors.extend(self._validate_chat_option_values())
|
|
1879
|
+
|
|
1880
|
+
return errors
|
|
1881
|
+
|
|
1882
|
+
# ── Save / Apply ─────────────────────────────────────────────────
|
|
1883
|
+
|
|
1884
|
+
def _build_profile_from_form(self) -> ModelProfile:
|
|
1885
|
+
"""Construct a ``ModelProfile`` from the current form state."""
|
|
1886
|
+
from chrys.service.profiles.models.schema import ModelProfile
|
|
1887
|
+
|
|
1888
|
+
headers = self._read_kv_list("mc-headers-list")
|
|
1889
|
+
options = self._read_kv_list("mc-options-list")
|
|
1890
|
+
provider = str(self.query_one("#mc-provider", Select).value)
|
|
1891
|
+
api_style_value = str(self.query_one("#mc-api-style", Select).value)
|
|
1892
|
+
api_style = (
|
|
1893
|
+
API_STYLE_RESPONSES if is_responses_wire_dialect(provider, api_style_value) else API_STYLE_CHAT_COMPLETIONS
|
|
1894
|
+
)
|
|
1895
|
+
# Fields the form has no control for keep their stored value.
|
|
1896
|
+
stored = self._registry.get(self._selected_profile_id)
|
|
1897
|
+
|
|
1898
|
+
return ModelProfile(
|
|
1899
|
+
id=self._selected_profile_id,
|
|
1900
|
+
name=self.query_one("#mc-name", Input).value.strip(),
|
|
1901
|
+
provider=provider,
|
|
1902
|
+
api_style=api_style,
|
|
1903
|
+
model_id=self.query_one("#mc-model", Input).value.strip(),
|
|
1904
|
+
max_context_tokens=int(self.query_one("#mc-max-tokens", Input).value.strip()),
|
|
1905
|
+
max_output_tokens=int(self.query_one("#mc-max-output-tokens", Input).value.strip()),
|
|
1906
|
+
base_url=self.query_one("#mc-base-url", Input).value.strip(),
|
|
1907
|
+
api_key=self.query_one("#mc-api-key", Input).value.strip(),
|
|
1908
|
+
http_connect_timeout=float(self.query_one("#mc-connect-timeout", Input).value.strip() or "10.0"),
|
|
1909
|
+
http_read_timeout=float(self.query_one("#mc-read-timeout", Input).value.strip() or "300.0"),
|
|
1910
|
+
http_max_retries=int(self.query_one("#mc-max-retries", Input).value.strip() or "2"),
|
|
1911
|
+
verify_ssl=not self.query_one("#mc-skip-tls", Checkbox).value,
|
|
1912
|
+
bypass_proxy=self.query_one("#mc-bypass-proxy", Checkbox).value,
|
|
1913
|
+
http_headers=_kv_to_json(headers, parse_values=False),
|
|
1914
|
+
chat_options=_kv_to_json(options),
|
|
1915
|
+
stream=self.query_one("#mc-stream", Checkbox).value,
|
|
1916
|
+
vision=self.query_one("#mc-vision", Checkbox).value,
|
|
1917
|
+
stream_requires_finish_reason=stored is not None and stored.stream_requires_finish_reason,
|
|
1918
|
+
thinking_block_binding=stored.thinking_block_binding if stored is not None else "auto",
|
|
1919
|
+
auto_interleaved_thinking=stored.auto_interleaved_thinking if stored is not None else True,
|
|
1920
|
+
)
|
|
1921
|
+
|
|
1922
|
+
async def _save_only(self) -> ModelProfile | None:
|
|
1923
|
+
"""Persist the form to YAML + registry and return the saved profile.
|
|
1924
|
+
|
|
1925
|
+
If the edited profile is the global default — or no usable global
|
|
1926
|
+
default exists yet, in which case the first successful save claims
|
|
1927
|
+
the pointer — also refresh that durable document pointer. A
|
|
1928
|
+
stored default that no longer resolves to a selectable profile
|
|
1929
|
+
(deleted or hollowed out behind our back) counts as absent. The
|
|
1930
|
+
runtime-effective process pointer is deliberately left unchanged.
|
|
1931
|
+
"""
|
|
1932
|
+
if self._read_only:
|
|
1933
|
+
return None
|
|
1934
|
+
from chrys.service.profiles.models.env_bridge import get_active_profile_id
|
|
1935
|
+
from chrys.service.profiles.models.resolver import resolve_profile_selector, resolve_selectable_profile
|
|
1936
|
+
from chrys.service.profiles.models.serializer import save_profile
|
|
1937
|
+
|
|
1938
|
+
profile = self._build_profile_from_form()
|
|
1939
|
+
# The process pointer may reference this profile by NAME; capture
|
|
1940
|
+
# that against the pre-save registry, because a rename would strand
|
|
1941
|
+
# such a pointer and modal close would then misread the runtime as
|
|
1942
|
+
# inactive and adopt the global default — silently switching models.
|
|
1943
|
+
process_selector = get_active_profile_id()
|
|
1944
|
+
process_target = resolve_profile_selector(self._registry, process_selector)
|
|
1945
|
+
process_points_here = process_target is not None and process_target.id == profile.id
|
|
1946
|
+
await asyncio.to_thread(save_profile, profile)
|
|
1947
|
+
self._registry.register(profile)
|
|
1948
|
+
if process_points_here and resolve_profile_selector(self._registry, process_selector) is None:
|
|
1949
|
+
set_model_pointer(profile.id, origin=SettingOrigin(layer=Source.PROCESS_RUNTIME))
|
|
1950
|
+
self._saved_anything = True
|
|
1951
|
+
if (
|
|
1952
|
+
self._selected_profile_id == self._global_default_profile_id
|
|
1953
|
+
or resolve_selectable_profile(self._registry, self._global_default_profile_id) is None
|
|
1954
|
+
):
|
|
1955
|
+
await self._persist_active_profile(profile)
|
|
1956
|
+
self._global_default_profile_id = profile.id
|
|
1957
|
+
# Refresh sidebar so a renamed profile's label reflects the new name.
|
|
1958
|
+
self._populate_sidebar()
|
|
1959
|
+
ol = self.query_one("#mc-list", OptionList)
|
|
1960
|
+
with contextlib.suppress(OptionDoesNotExist):
|
|
1961
|
+
ol.highlighted = ol.get_option_index(self._selected_profile_id)
|
|
1962
|
+
return profile
|
|
1963
|
+
|
|
1964
|
+
def _notify_unsupported_options(self, profile: ModelProfile | None) -> None:
|
|
1965
|
+
"""Surface non-blocking warnings for saved-but-unsupported option combos."""
|
|
1966
|
+
if profile is None:
|
|
1967
|
+
return
|
|
1968
|
+
from chrys.service.profiles.models.options import (
|
|
1969
|
+
protected_chat_option_keys_warning_structured,
|
|
1970
|
+
responses_store_continuation_warning,
|
|
1971
|
+
)
|
|
1972
|
+
|
|
1973
|
+
warnings: list[str] = []
|
|
1974
|
+
if responses_store_continuation_warning(profile) is not None:
|
|
1975
|
+
warnings.append(self._render_toast(_RESPONSES_STORE_WARNING.bind()))
|
|
1976
|
+
protected_warning = protected_chat_option_keys_warning_structured(profile)
|
|
1977
|
+
if protected_warning is not None:
|
|
1978
|
+
warnings.append(
|
|
1979
|
+
self._render_toast(
|
|
1980
|
+
_PROTECTED_CHAT_OPTIONS_WARNING.bind(
|
|
1981
|
+
keys=DisplayBlock(", ".join(protected_warning.keys)),
|
|
1982
|
+
)
|
|
1983
|
+
)
|
|
1984
|
+
)
|
|
1985
|
+
for warning in warnings:
|
|
1986
|
+
self.notify(
|
|
1987
|
+
warning,
|
|
1988
|
+
title=self._render_toast(_MODEL_SETTINGS_TITLE.bind()),
|
|
1989
|
+
severity="warning",
|
|
1990
|
+
timeout=8,
|
|
1991
|
+
markup=False,
|
|
1992
|
+
)
|
|
1993
|
+
|
|
1994
|
+
def _render_toast(self, reference: MessageRef) -> str:
|
|
1995
|
+
return render_str(widget_localizer(self), reference)
|
|
1996
|
+
|
|
1997
|
+
|
|
1998
|
+
def _parse_json_dict(raw: str, *, parse_values: bool = True) -> dict[str, str]:
|
|
1999
|
+
"""Parse a JSON string into a dict, returning empty dict on failure.
|
|
2000
|
+
|
|
2001
|
+
Non-string values (objects, arrays, booleans, numbers) are
|
|
2002
|
+
re-serialised via ``json.dumps`` so that ``_kv_to_json`` can
|
|
2003
|
+
parse them back losslessly (e.g. ``True`` → ``"true"``, not
|
|
2004
|
+
Python's ``"True"``). With *parse_values* (``_kv_to_json``'s
|
|
2005
|
+
default), a string that would read back as JSON (``"1"``,
|
|
2006
|
+
``"true"``) or that the form's whitespace trim would change is
|
|
2007
|
+
shown quoted too, so saving the form again keeps it the same string.
|
|
2008
|
+
"""
|
|
2009
|
+
if not raw:
|
|
2010
|
+
return {}
|
|
2011
|
+
try:
|
|
2012
|
+
parsed = json.loads(raw)
|
|
2013
|
+
if isinstance(parsed, dict):
|
|
2014
|
+
result: dict[str, str] = {}
|
|
2015
|
+
for k, v in parsed.items():
|
|
2016
|
+
if not isinstance(v, str) or (parse_values and (v != v.strip() or _reads_as_json(v))):
|
|
2017
|
+
v = json.dumps(v)
|
|
2018
|
+
result[str(k)] = v
|
|
2019
|
+
return result
|
|
2020
|
+
except json.JSONDecodeError, TypeError:
|
|
2021
|
+
pass
|
|
2022
|
+
return {}
|
|
2023
|
+
|
|
2024
|
+
|
|
2025
|
+
_NOT_JSON = object()
|
|
2026
|
+
|
|
2027
|
+
|
|
2028
|
+
def _reads_as_json(text: str) -> bool:
|
|
2029
|
+
return _loads_or(text, _NOT_JSON) is not _NOT_JSON
|
|
2030
|
+
|
|
2031
|
+
|
|
2032
|
+
def _kv_to_json(kv: dict[str, str], *, parse_values: bool = True) -> str:
|
|
2033
|
+
"""Serialise a key-value dict to a JSON string.
|
|
2034
|
+
|
|
2035
|
+
Values that are valid JSON (objects, arrays, numbers, booleans) are
|
|
2036
|
+
embedded as-is so that nested structures survive the round-trip.
|
|
2037
|
+
Plain strings are kept as strings.
|
|
2038
|
+
"""
|
|
2039
|
+
if not kv:
|
|
2040
|
+
return ""
|
|
2041
|
+
if not parse_values:
|
|
2042
|
+
return json.dumps(kv)
|
|
2043
|
+
return json.dumps({k: _loads_or(v, v) for k, v in kv.items()})
|