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,2265 @@
|
|
|
1
|
+
# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Main chat screen — the primary interaction surface."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import contextlib
|
|
9
|
+
import logging
|
|
10
|
+
import reprlib
|
|
11
|
+
from collections.abc import Awaitable, Callable
|
|
12
|
+
from functools import partial
|
|
13
|
+
from types import FunctionType, MethodType
|
|
14
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Literal, cast
|
|
15
|
+
|
|
16
|
+
from textual import __version__ as TEXTUAL_VERSION
|
|
17
|
+
from textual import on, work
|
|
18
|
+
from textual.actions import SkipAction
|
|
19
|
+
from textual.binding import Binding
|
|
20
|
+
from textual.containers import Horizontal
|
|
21
|
+
from textual.content import Content
|
|
22
|
+
from textual.events import DescendantFocus, Paste, Resize, ScreenResume, ScreenSuspend
|
|
23
|
+
from textual.geometry import Offset, Size
|
|
24
|
+
from textual.reactive import reactive
|
|
25
|
+
from textual.screen import Screen
|
|
26
|
+
from textual.selection import Selection, SelectState
|
|
27
|
+
from textual.timer import Timer
|
|
28
|
+
from textual.widget import Widget
|
|
29
|
+
|
|
30
|
+
from chrys.app.tui.behaviors.right_click_copy import RightClickScreenCopyMixin
|
|
31
|
+
from chrys.app.tui.binding_display import QUIT_BINDING, localized_binding
|
|
32
|
+
from chrys.app.tui.i18n import render_content, render_str
|
|
33
|
+
from chrys.app.tui.language import LANGUAGE_OPTIONS, LANGUAGE_UNKNOWN_LOCALE
|
|
34
|
+
from chrys.app.tui.screens.dialogs.app_mode import AppModeDialog
|
|
35
|
+
from chrys.app.tui.screens.dialogs.editor import EditorDialog, EditorDialogResult
|
|
36
|
+
from chrys.app.tui.screens.main.action_availability import check_main_action
|
|
37
|
+
from chrys.app.tui.screens.main.commands import INVALID_COMMAND_TITLE_REF, SlashCommandActions
|
|
38
|
+
from chrys.app.tui.screens.main.config_actions import (
|
|
39
|
+
RuntimeConfigCallbacks,
|
|
40
|
+
RuntimeConfigController,
|
|
41
|
+
)
|
|
42
|
+
from chrys.app.tui.screens.main.copy_actions import CopyActionController
|
|
43
|
+
from chrys.app.tui.screens.main.diff_controller import DiffController, LiveDiffOwner, LiveDiffTracker
|
|
44
|
+
from chrys.app.tui.screens.main.event_handlers import BackendEventCallbacks, BackendEventHandler
|
|
45
|
+
from chrys.app.tui.screens.main.input_flow import InputFlowController
|
|
46
|
+
from chrys.app.tui.screens.main.model_indicator import (
|
|
47
|
+
compute_model_indicator_state,
|
|
48
|
+
is_model_selection_locked,
|
|
49
|
+
)
|
|
50
|
+
from chrys.app.tui.screens.main.navigation import MainNavigationController
|
|
51
|
+
from chrys.app.tui.screens.main.ports import StatusMessage
|
|
52
|
+
from chrys.app.tui.screens.main.rollback_controller import RollbackController
|
|
53
|
+
from chrys.app.tui.screens.main.runtime_info import RegistryRuntimeInfoProvider
|
|
54
|
+
from chrys.app.tui.screens.main.session_handlers import RestoreRequest, SessionCallbacks, SessionHandler
|
|
55
|
+
from chrys.app.tui.screens.main.session_title import SessionTitleController
|
|
56
|
+
from chrys.app.tui.screens.main.settings_coordinator import (
|
|
57
|
+
SETTINGS_TITLE,
|
|
58
|
+
SettingsCoordinator,
|
|
59
|
+
SettingsCoordinatorCallbacks,
|
|
60
|
+
)
|
|
61
|
+
from chrys.app.tui.screens.main.settings_persistence import (
|
|
62
|
+
SETTINGS_FLUSH_LOCK_TIMEOUT_SECONDS,
|
|
63
|
+
SETTINGS_SAVE_DELAY_SECONDS,
|
|
64
|
+
SettingsPersistenceQueue,
|
|
65
|
+
)
|
|
66
|
+
from chrys.app.tui.screens.main.shell_mode import ShellModeController
|
|
67
|
+
from chrys.app.tui.screens.main.state import MainScreenServices, MainScreenState
|
|
68
|
+
from chrys.app.tui.screens.main.subscriptions import MainScreenSubscriptions
|
|
69
|
+
from chrys.app.tui.screens.main.suggestions import SuggestionCallbacks, SuggestionHandler
|
|
70
|
+
from chrys.app.tui.screens.main.tool_action_bridge import ToolActionBridge
|
|
71
|
+
from chrys.app.tui.screens.main.view_adapter import MainScreenViewAdapter
|
|
72
|
+
from chrys.app.tui.screens.main.workflow_controller import WorkflowController
|
|
73
|
+
from chrys.app.tui.screens.main.workspace_actions import WorkspaceCallbacks, WorkspaceController
|
|
74
|
+
from chrys.app.tui.screens.main.workspace_branch import WorkspaceBranchController
|
|
75
|
+
from chrys.app.tui.screens.settings import GENERAL_TAB_ID
|
|
76
|
+
from chrys.app.tui.screens.settings.dialog import THEME_PREVIEW_HINT
|
|
77
|
+
from chrys.app.tui.screens.themes import ThemeEditorPanel
|
|
78
|
+
from chrys.app.tui.support.gc_freeze import (
|
|
79
|
+
GcFreezeBlockReason,
|
|
80
|
+
GcFreezeParticipant,
|
|
81
|
+
abort_textual_screen_gc_freeze,
|
|
82
|
+
after_textual_screen_gc_freeze,
|
|
83
|
+
prepare_textual_screen_for_gc,
|
|
84
|
+
raise_gc_freeze_hook_errors,
|
|
85
|
+
)
|
|
86
|
+
from chrys.app.tui.terminal.panel import ShellPanel
|
|
87
|
+
from chrys.app.tui.terminal.widget import Terminal
|
|
88
|
+
from chrys.app.tui.widgets.chat.messages import ConversationStatusAction
|
|
89
|
+
from chrys.app.tui.widgets.chat.panel import ChatPanel
|
|
90
|
+
from chrys.app.tui.widgets.chat.ports import TranscriptLocalizationPort
|
|
91
|
+
from chrys.app.tui.widgets.chat.renderers.ask_user import AskUserInlineSubmitted
|
|
92
|
+
from chrys.app.tui.widgets.chat.renderers.sleep import SleepSkipClicked
|
|
93
|
+
from chrys.app.tui.widgets.chat.renderers.sub_agent import SubAgentAbortClicked, SubAgentRetryClicked
|
|
94
|
+
from chrys.app.tui.widgets.chat.scroll_controller import scroll_gc_paused
|
|
95
|
+
from chrys.app.tui.widgets.chat.selection_controller import ChatSelectionController
|
|
96
|
+
from chrys.app.tui.widgets.chat.session_json import SessionJsonPanel
|
|
97
|
+
from chrys.app.tui.widgets.chat.tool_call import ToolViewRequested, is_tool_copy_excluded
|
|
98
|
+
from chrys.app.tui.widgets.chrome.app_header import AppHeader
|
|
99
|
+
from chrys.app.tui.widgets.chrome.commands import is_slash_command_candidate
|
|
100
|
+
from chrys.app.tui.widgets.chrome.footer import ChrysFooter
|
|
101
|
+
from chrys.app.tui.widgets.chrome.input_bar import InputBar
|
|
102
|
+
from chrys.app.tui.widgets.chrome.status_bar import StatusBar
|
|
103
|
+
from chrys.app.tui.widgets.chrome.suggestion_list import SuggestionList
|
|
104
|
+
from chrys.app.tui.widgets.editor import MESSAGE_EDITOR_MAX_CHARACTERS, EditorBufferSnapshot, EditorMode
|
|
105
|
+
from chrys.app.tui.widgets.sidebar.context import ContextUsageState
|
|
106
|
+
from chrys.app.tui.widgets.sidebar.panel import SidebarPanel
|
|
107
|
+
from chrys.app.tui.widgets.sidebar.tasks import TodoListState
|
|
108
|
+
from chrys.app.tui.widgets.sidebar.toc import ConversationToc
|
|
109
|
+
from chrys.app.tui.widgets.trajectory import TrajectoryDashboard
|
|
110
|
+
from chrys.app.tui.widgets.workflow import text as workflow_text
|
|
111
|
+
from chrys.app.tui.widgets.workflow.graph import WorkflowGraph
|
|
112
|
+
from chrys.app.tui.widgets.workflow.panel import WorkflowPanel
|
|
113
|
+
from chrys.foundation.config.settings import (
|
|
114
|
+
DEFAULT_LOCALE,
|
|
115
|
+
DEFAULT_TRAJECTORY_VERIFY_COMMANDS,
|
|
116
|
+
DEFAULT_WORKSPACE_MRU_MAX_ENTRIES,
|
|
117
|
+
persist_editor_keymap,
|
|
118
|
+
persist_tool_groups_expanded,
|
|
119
|
+
)
|
|
120
|
+
from chrys.foundation.events.types import (
|
|
121
|
+
ApprovalModeUpdated,
|
|
122
|
+
Error,
|
|
123
|
+
ExecutionChanged,
|
|
124
|
+
SessionRestored,
|
|
125
|
+
)
|
|
126
|
+
from chrys.foundation.i18n import DisplaySequence, Localizer, MessageRef, msg
|
|
127
|
+
from chrys.foundation.models.approval_reuse import ReuseChoice
|
|
128
|
+
from chrys.foundation.models.ask_user import AskUserAnswer
|
|
129
|
+
from chrys.foundation.platform import get_platform, safe_getcwd
|
|
130
|
+
from chrys.service.approval.policy import ApprovalMode
|
|
131
|
+
from chrys.service.profiles.models.schema import UNCONFIGURED_MODEL_ID, is_model_profile_selectable
|
|
132
|
+
from chrys.service.session.sub_agent_transcript import load_persisted_sub_agent_transcript
|
|
133
|
+
|
|
134
|
+
if TYPE_CHECKING:
|
|
135
|
+
from textual.app import ComposeResult
|
|
136
|
+
from textual.theme import Theme
|
|
137
|
+
|
|
138
|
+
from chrys.app.tui.app import ChrysApp
|
|
139
|
+
from chrys.app.tui.i18n import LocaleController
|
|
140
|
+
from chrys.app.tui.notifications import NotificationService
|
|
141
|
+
from chrys.app.tui.notifications.settings import NotificationSettings
|
|
142
|
+
from chrys.app.tui.screens.sessions import WorkflowSessionPick
|
|
143
|
+
from chrys.app.tui.screens.themes.picker import ThemesScreen
|
|
144
|
+
from chrys.app.tui.themes.store import ThemeFileRevision, UserThemeStore
|
|
145
|
+
from chrys.foundation.config.settings_store import PersistResult, SettingsHandle
|
|
146
|
+
from chrys.foundation.events.bus import EventBus
|
|
147
|
+
from chrys.orchestration.engine.engine import AgentEngine
|
|
148
|
+
from chrys.service.profiles.agents.registry import AgentProfileRegistry
|
|
149
|
+
from chrys.service.profiles.models.registry import ModelProfileRegistry
|
|
150
|
+
from chrys.service.session.sub_agent_transcript import PersistedSubAgentTranscript
|
|
151
|
+
from chrys.service.state.store import StateStore
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
logger = logging.getLogger(__name__)
|
|
155
|
+
|
|
156
|
+
type StartupRestore = Literal["restored", "declined", "failed"]
|
|
157
|
+
"""Startup restore result; ``declined`` means the user chose no folder for a session whose folder is gone."""
|
|
158
|
+
|
|
159
|
+
_SESSIONS_BINDING = msg("tui.binding.sessions", fallback="Sessions")
|
|
160
|
+
_AGENTS_BINDING = msg("tui.binding.agents", fallback="Agents")
|
|
161
|
+
_MODELS_BINDING = msg("tui.binding.models", fallback="Models")
|
|
162
|
+
_LOGS_BINDING = msg("tui.binding.logs", fallback="Logs")
|
|
163
|
+
_HELP_BINDING = msg("tui.binding.help", fallback="Help")
|
|
164
|
+
_SIDEBAR_BINDING = msg("tui.binding.sidebar", fallback="Sidebar")
|
|
165
|
+
_THEMES_BINDING = msg("tui.binding.themes", fallback="Themes")
|
|
166
|
+
_SETTINGS_BINDING = msg("tui.binding.settings", fallback="Settings")
|
|
167
|
+
_SETTINGS_REJECTED = msg(
|
|
168
|
+
"tui.settings.save_rejected",
|
|
169
|
+
fallback="Could not save settings; the store rejected: {keys}.",
|
|
170
|
+
)
|
|
171
|
+
_BREAK_BINDING = msg("tui.binding.break_agent", fallback="Break")
|
|
172
|
+
_TRAJECTORY_BINDING = msg("tui.binding.trajectory", fallback="Trajectory")
|
|
173
|
+
_CHAT_PAGE_UP_BINDING = msg("tui.binding.chat_page_up", fallback="Chat Page Up")
|
|
174
|
+
_CHAT_PAGE_DOWN_BINDING = msg("tui.binding.chat_page_down", fallback="Chat Page Down")
|
|
175
|
+
_CHAT_SCROLL_BOTTOM_BINDING = msg("tui.binding.chat_scroll_bottom", fallback="Chat Scroll Bottom")
|
|
176
|
+
_EDITOR_DRAFT_TOO_LARGE_TITLE = msg(
|
|
177
|
+
"tui.editor.title.draft_too_large",
|
|
178
|
+
fallback="Draft too large for editor",
|
|
179
|
+
)
|
|
180
|
+
_EDITOR_DRAFT_TOO_LARGE = msg(
|
|
181
|
+
"tui.editor.draft_too_large",
|
|
182
|
+
fallback="Editor supports drafts up to {limit} characters.",
|
|
183
|
+
)
|
|
184
|
+
_MODEL_UNCONFIGURED_TITLE = msg("tui.model_guard.title", fallback="Model not configured or selected")
|
|
185
|
+
_MODEL_UNCONFIGURED_MESSAGE = msg(
|
|
186
|
+
"tui.model_guard.message",
|
|
187
|
+
fallback="Your message was not sent. Configure and select a model to get started.",
|
|
188
|
+
)
|
|
189
|
+
_MODEL_UNCONFIGURED_SETUP = msg("tui.model_guard.button.setup", fallback="Set up model")
|
|
190
|
+
|
|
191
|
+
TEXTUAL_BACKGROUND_REFRESH_FORK_VERSION = "8.2.7"
|
|
192
|
+
"""The Textual release whose private ``Screen._compositor_refresh`` ``MainScreen`` mirrors."""
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
# Work arguments can be whole prompts; worker descriptions show them abbreviated.
|
|
196
|
+
_WORK_ARGUMENT_REPR = reprlib.Repr(maxstring=60, maxother=60)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _describe_work(work: Callable[[], Awaitable[object]]) -> tuple[str, str]:
|
|
200
|
+
"""Name and describe a worker after its work, as ``@work`` did: ``method(args)``."""
|
|
201
|
+
target: Callable[..., object] = work
|
|
202
|
+
args: tuple[object, ...] = ()
|
|
203
|
+
keywords: dict[str, object] = {}
|
|
204
|
+
if isinstance(work, partial):
|
|
205
|
+
target, args, keywords = work.func, work.args, work.keywords
|
|
206
|
+
name = target.__name__ if isinstance(target, FunctionType | MethodType) else type(target).__name__
|
|
207
|
+
arguments = [
|
|
208
|
+
*(_WORK_ARGUMENT_REPR.repr(arg) for arg in args),
|
|
209
|
+
*(f"{key}={_WORK_ARGUMENT_REPR.repr(value)}" for key, value in keywords.items()),
|
|
210
|
+
]
|
|
211
|
+
return name, f"{name}({', '.join(arguments)})"
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class MainScreen(RightClickScreenCopyMixin, Screen):
|
|
215
|
+
"""Primary chat screen with header, chat panel, side panel, input bar, and footer."""
|
|
216
|
+
|
|
217
|
+
BINDINGS: ClassVar[list] = [
|
|
218
|
+
localized_binding("ctrl+b", "interrupt", _BREAK_BINDING, show=False),
|
|
219
|
+
localized_binding("f1", "sessions", _SESSIONS_BINDING, priority=True),
|
|
220
|
+
localized_binding("f2", "agents_config", _AGENTS_BINDING, priority=True),
|
|
221
|
+
localized_binding("f4", "models_config", _MODELS_BINDING, priority=True),
|
|
222
|
+
localized_binding("f6", "show_log_viewer", _LOGS_BINDING, priority=True),
|
|
223
|
+
localized_binding("f8", "open_guide", _HELP_BINDING, priority=True),
|
|
224
|
+
Binding("ctrl+r", "prompt_history", show=False, priority=True),
|
|
225
|
+
localized_binding("f9", "pick_theme", _THEMES_BINDING, priority=True),
|
|
226
|
+
localized_binding("f10", "settings", _SETTINGS_BINDING, priority=True),
|
|
227
|
+
localized_binding("f12", "toggle_trajectory_dashboard", _TRAJECTORY_BINDING, show=True, priority=True),
|
|
228
|
+
localized_binding("ctrl+g", "toggle_sidebar", _SIDEBAR_BINDING, priority=True),
|
|
229
|
+
localized_binding("pageup", "chat_page_up", _CHAT_PAGE_UP_BINDING, show=False, priority=True),
|
|
230
|
+
localized_binding("pagedown", "chat_page_down", _CHAT_PAGE_DOWN_BINDING, show=False, priority=True),
|
|
231
|
+
localized_binding(
|
|
232
|
+
"ctrl+end",
|
|
233
|
+
"chat_scroll_bottom",
|
|
234
|
+
_CHAT_SCROLL_BOTTOM_BINDING,
|
|
235
|
+
show=False,
|
|
236
|
+
priority=True,
|
|
237
|
+
),
|
|
238
|
+
Binding("escape", "escape", show=False, priority=True),
|
|
239
|
+
localized_binding("ctrl+q", "quit", QUIT_BINDING, show=False, priority=True),
|
|
240
|
+
]
|
|
241
|
+
|
|
242
|
+
CSS_PATH = "screen.tcss"
|
|
243
|
+
|
|
244
|
+
agent_running_state: reactive[bool] = reactive(False, init=False, always_update=True)
|
|
245
|
+
agent_loading_state: reactive[bool] = reactive(False, init=False, always_update=True)
|
|
246
|
+
has_messages_state: reactive[bool] = reactive(False, init=False, always_update=True)
|
|
247
|
+
header_subtitle_parts: reactive[tuple[str, ...]] = reactive(())
|
|
248
|
+
header_approval_mode: reactive[ApprovalMode] = reactive(ApprovalMode.MANUAL, always_update=True)
|
|
249
|
+
chat_profile_name: reactive[str] = reactive("")
|
|
250
|
+
chat_session_id: reactive[str] = reactive("")
|
|
251
|
+
chat_session_title: reactive[str] = reactive("")
|
|
252
|
+
chat_workspace_cwd: reactive[str] = reactive("")
|
|
253
|
+
chat_workspace_branch: reactive[str] = reactive("")
|
|
254
|
+
context_usage_state: reactive[ContextUsageState | None] = reactive(None, always_update=True)
|
|
255
|
+
todo_state: reactive[TodoListState | None] = reactive(None, always_update=True)
|
|
256
|
+
shell_mode_state: reactive[bool] = reactive(False, init=False)
|
|
257
|
+
_reactive_state_initialized: bool = False
|
|
258
|
+
|
|
259
|
+
def __init__(
|
|
260
|
+
self,
|
|
261
|
+
event_bus: EventBus,
|
|
262
|
+
state_store: StateStore | None = None,
|
|
263
|
+
agent_registry: AgentProfileRegistry | None = None,
|
|
264
|
+
model_registry: ModelProfileRegistry | None = None,
|
|
265
|
+
active_model_profile_id: str = "",
|
|
266
|
+
apply_saved_model_on_restore: bool = True,
|
|
267
|
+
workspace_mru_max_entries: int = DEFAULT_WORKSPACE_MRU_MAX_ENTRIES,
|
|
268
|
+
editor_keymap: EditorMode | str = EditorMode.STANDARD,
|
|
269
|
+
trajectory_verify_commands: str = DEFAULT_TRAJECTORY_VERIFY_COMMANDS,
|
|
270
|
+
*,
|
|
271
|
+
engine_provider: Callable[[], AgentEngine] | None,
|
|
272
|
+
settings_handle: SettingsHandle | None = None,
|
|
273
|
+
localization: TranscriptLocalizationPort | None = None,
|
|
274
|
+
locale_controller: LocaleController | None = None,
|
|
275
|
+
on_editor_keymap_changed: Callable[[str], None] | None = None,
|
|
276
|
+
tool_groups_expanded: Callable[[], bool] | None = None,
|
|
277
|
+
approval_mode: ApprovalMode = ApprovalMode.MANUAL,
|
|
278
|
+
) -> None:
|
|
279
|
+
self._services = MainScreenServices(
|
|
280
|
+
bus=event_bus,
|
|
281
|
+
settings_handle=settings_handle,
|
|
282
|
+
state_store=state_store,
|
|
283
|
+
agent_registry=agent_registry,
|
|
284
|
+
model_registry=model_registry,
|
|
285
|
+
active_model_profile_id=active_model_profile_id,
|
|
286
|
+
apply_saved_model_on_restore=apply_saved_model_on_restore,
|
|
287
|
+
engine_provider=engine_provider,
|
|
288
|
+
workspace_mru_max_entries=workspace_mru_max_entries,
|
|
289
|
+
)
|
|
290
|
+
self._state = MainScreenState()
|
|
291
|
+
self._editor_mode = EditorMode.parse(editor_keymap)
|
|
292
|
+
self._trajectory_verify_commands = trajectory_verify_commands
|
|
293
|
+
self._on_editor_keymap_changed = on_editor_keymap_changed
|
|
294
|
+
self._tool_groups_expanded = tool_groups_expanded or (lambda: True)
|
|
295
|
+
self._localization = localization
|
|
296
|
+
self._locale_controller = locale_controller
|
|
297
|
+
self._gc_freeze_participants: tuple[GcFreezeParticipant, ...] = ()
|
|
298
|
+
self.theme_editor: ThemeEditorPanel | None = None
|
|
299
|
+
self._theme_editor_opening = False
|
|
300
|
+
self._shell_requested_after_editor = False
|
|
301
|
+
# Seeded with the engine's launch mode: the startup ApprovalModeUpdated
|
|
302
|
+
# sync then confirms what the header already shows, instead of
|
|
303
|
+
# flipping a placeholder and toasting a change nobody made.
|
|
304
|
+
self._state.runtime.approval_mode = approval_mode
|
|
305
|
+
self._settings_persistence_queue = self._new_settings_persistence()
|
|
306
|
+
super().__init__()
|
|
307
|
+
self.set_reactive(MainScreen.header_approval_mode, approval_mode)
|
|
308
|
+
self._reactive_state_initialized = True
|
|
309
|
+
|
|
310
|
+
# Live mutation tracking for /diff during agent runs.
|
|
311
|
+
self._live_diff = LiveDiffTracker()
|
|
312
|
+
|
|
313
|
+
# Handler instances
|
|
314
|
+
self._view_adapter = MainScreenViewAdapter(
|
|
315
|
+
self,
|
|
316
|
+
state=self._state,
|
|
317
|
+
state_store=self._services.state_store,
|
|
318
|
+
locale_controller=self._locale_controller,
|
|
319
|
+
)
|
|
320
|
+
self._session_title = SessionTitleController(
|
|
321
|
+
run=self._state.run,
|
|
322
|
+
app=lambda: self.app,
|
|
323
|
+
workspace_cwd=self._workspace_cwd,
|
|
324
|
+
show_display_title=self._show_session_title,
|
|
325
|
+
set_interval=self.set_interval,
|
|
326
|
+
current_session_id=lambda: self.chat_session_id,
|
|
327
|
+
push_screen=self._view_adapter.push_screen,
|
|
328
|
+
start_custom_title_save=lambda title, session_id: self._start_worker(
|
|
329
|
+
partial(self._sessions.apply_custom_session_title, title, session_id)
|
|
330
|
+
),
|
|
331
|
+
locale_controller=self._locale_controller,
|
|
332
|
+
)
|
|
333
|
+
self._workspace_branch = WorkspaceBranchController(
|
|
334
|
+
workspace=self._state.workspace,
|
|
335
|
+
workspace_cwd=self._workspace_cwd,
|
|
336
|
+
displayed_cwd=lambda: self.chat_workspace_cwd or self._workspace_cwd(),
|
|
337
|
+
show_branch=self._show_workspace_branch,
|
|
338
|
+
set_timer=self.set_timer,
|
|
339
|
+
set_interval=self.set_interval,
|
|
340
|
+
call_from_thread=lambda callback: self.app.call_from_thread(callback), # noqa: PLW0108 — app bound later
|
|
341
|
+
)
|
|
342
|
+
self._runtime_info = RegistryRuntimeInfoProvider(self._services)
|
|
343
|
+
self._config_actions = RuntimeConfigController(
|
|
344
|
+
state=self._state,
|
|
345
|
+
services=self._services,
|
|
346
|
+
view=self._view_adapter,
|
|
347
|
+
callbacks=RuntimeConfigCallbacks(
|
|
348
|
+
start_worker=self._start_worker,
|
|
349
|
+
set_profile_display=self._set_profile_display,
|
|
350
|
+
update_subtitle=self._update_subtitle,
|
|
351
|
+
debug=self._debug,
|
|
352
|
+
notification_service=self._notification_service,
|
|
353
|
+
settings_coordinator=self._settings_coordinator,
|
|
354
|
+
),
|
|
355
|
+
profile_descriptions=self._runtime_info,
|
|
356
|
+
locale_controller=self._locale_controller,
|
|
357
|
+
)
|
|
358
|
+
self._workspace_actions = WorkspaceController(
|
|
359
|
+
state=self._state,
|
|
360
|
+
services=self._services,
|
|
361
|
+
view=self._view_adapter,
|
|
362
|
+
callbacks=WorkspaceCallbacks(
|
|
363
|
+
start_worker=self._start_worker,
|
|
364
|
+
debug=self._debug,
|
|
365
|
+
allow_change=lambda: self._workflow.browser.request_workspace_change(), # noqa: PLW0108 — constructed later
|
|
366
|
+
selected_cwd=lambda: self._workflow.project_cwd if self._workflow.workflow_mode else "",
|
|
367
|
+
apply_workflow_cwd=self._apply_workflow_cwd,
|
|
368
|
+
workflow_mode=lambda: self._workflow.workflow_mode,
|
|
369
|
+
),
|
|
370
|
+
locale_controller=self._locale_controller,
|
|
371
|
+
)
|
|
372
|
+
self._copy_actions = CopyActionController(view=self._view_adapter, debug=self._debug)
|
|
373
|
+
self._chat_selection = ChatSelectionController(self)
|
|
374
|
+
self._diff_controller = self._new_diff_controller()
|
|
375
|
+
self._rollback_controller = self._new_rollback_controller()
|
|
376
|
+
self._navigation = self._new_navigation_controller()
|
|
377
|
+
self._tool_actions = ToolActionBridge(publisher=self._services.bus, debug=self._debug)
|
|
378
|
+
self._events = BackendEventHandler(
|
|
379
|
+
state=self._state,
|
|
380
|
+
services=self._services,
|
|
381
|
+
view=self._view_adapter,
|
|
382
|
+
callbacks=BackendEventCallbacks(
|
|
383
|
+
accept_approval_update=self._accept_approval_update,
|
|
384
|
+
route_session_error=self._route_session_error,
|
|
385
|
+
prompt_missing_working_dir=self._workspace_actions.prompt_missing_working_dir,
|
|
386
|
+
set_agent_running=self._set_agent_running,
|
|
387
|
+
set_agent_loading=self._set_agent_loading,
|
|
388
|
+
set_has_messages=self._set_has_messages,
|
|
389
|
+
set_profile_display=self._set_profile_display,
|
|
390
|
+
set_active_model_profile_id=self._set_active_model_profile_id,
|
|
391
|
+
set_creating_new_session=self._set_creating_new_session,
|
|
392
|
+
set_restoring_session=self._set_restoring_session,
|
|
393
|
+
set_workspace_cwd=self._set_workspace_cwd,
|
|
394
|
+
refresh_git_branch=self._workspace_branch.schedule_refresh,
|
|
395
|
+
update_subtitle=self._update_subtitle,
|
|
396
|
+
update_toc=self._update_toc,
|
|
397
|
+
on_session_fork_error=lambda event, message, severity: self._sessions.on_session_fork_error(
|
|
398
|
+
event,
|
|
399
|
+
message=message,
|
|
400
|
+
severity=severity,
|
|
401
|
+
),
|
|
402
|
+
on_session_clear_error=lambda event, message: self._sessions.on_session_clear_error(
|
|
403
|
+
event,
|
|
404
|
+
message=message,
|
|
405
|
+
),
|
|
406
|
+
handle_approval_response=self._handle_approval_response,
|
|
407
|
+
handle_ask_user_response=self._handle_ask_user_response,
|
|
408
|
+
question_inline_preferred=self._question_inline_preferred,
|
|
409
|
+
approval_defer_while_judging=self._approval_defer_while_judging,
|
|
410
|
+
post_gc_message=self.post_message,
|
|
411
|
+
debug=self._debug,
|
|
412
|
+
refresh_model_indicator=self._refresh_model_indicator,
|
|
413
|
+
refresh_notification_settings=self._refresh_notification_settings,
|
|
414
|
+
refresh_trajectory_verify_commands=self._refresh_trajectory_verify_commands,
|
|
415
|
+
settings_reloaded=self._on_settings_reloaded_for_panel,
|
|
416
|
+
),
|
|
417
|
+
runtime_info=self._runtime_info,
|
|
418
|
+
live_diff=self._live_diff,
|
|
419
|
+
locale_controller=self._locale_controller,
|
|
420
|
+
)
|
|
421
|
+
self._sessions = SessionHandler(
|
|
422
|
+
state=self._state,
|
|
423
|
+
services=self._services,
|
|
424
|
+
view=self._view_adapter,
|
|
425
|
+
callbacks=SessionCallbacks(
|
|
426
|
+
set_agent_loading=self._set_agent_loading,
|
|
427
|
+
set_has_messages=self._set_has_messages,
|
|
428
|
+
set_creating_new_session=self._set_creating_new_session,
|
|
429
|
+
set_restoring_session=self._set_restoring_session,
|
|
430
|
+
set_profile_display=self._set_profile_display,
|
|
431
|
+
set_active_model_profile_id=self._set_active_model_profile_id,
|
|
432
|
+
set_workspace_cwd=self._set_workspace_cwd,
|
|
433
|
+
update_subtitle=self._update_subtitle,
|
|
434
|
+
update_toc=self._update_toc,
|
|
435
|
+
clear_suggestion_file_cache=self._clear_suggestion_file_cache,
|
|
436
|
+
start_worker=self._start_worker,
|
|
437
|
+
post_gc_message=self.post_message,
|
|
438
|
+
debug=self._debug,
|
|
439
|
+
refresh_model_indicator=self._refresh_model_indicator,
|
|
440
|
+
choose_missing_working_dir=partial(self._workspace_actions.choose_replacement_dir, reason="restore"),
|
|
441
|
+
),
|
|
442
|
+
agent_load=self._events,
|
|
443
|
+
runtime_info=self._runtime_info,
|
|
444
|
+
profile_descriptions=self._runtime_info,
|
|
445
|
+
locale_controller=self._locale_controller,
|
|
446
|
+
)
|
|
447
|
+
self._slash_actions = self._new_slash_command_actions()
|
|
448
|
+
self._suggestions = SuggestionHandler(
|
|
449
|
+
state=self._state,
|
|
450
|
+
services=self._services,
|
|
451
|
+
view=self._view_adapter,
|
|
452
|
+
command_actions=self._slash_actions,
|
|
453
|
+
callbacks=SuggestionCallbacks(
|
|
454
|
+
notify_warning=self._warn_slash_command,
|
|
455
|
+
start_worker=self._start_worker,
|
|
456
|
+
submit_user_text=self._submit_user_text,
|
|
457
|
+
start_agent_profile_switch=self._config_actions.start_agent_profile_switch,
|
|
458
|
+
start_model_profile_switch=self._switch_model_profile,
|
|
459
|
+
),
|
|
460
|
+
buddy_view=self._view_adapter,
|
|
461
|
+
locale_controller=self._locale_controller,
|
|
462
|
+
)
|
|
463
|
+
self._shell_mode_controller = ShellModeController(
|
|
464
|
+
state=self._state,
|
|
465
|
+
shell_view=self._view_adapter,
|
|
466
|
+
focus_view=self._view_adapter,
|
|
467
|
+
set_shell_mode_state=self._set_shell_mode_state,
|
|
468
|
+
panel_focus_changed=self._sync_sidebar_tab_strip_focus,
|
|
469
|
+
dismiss_suggestions=self._suggestions.dismiss_suggestions,
|
|
470
|
+
debug=self._debug,
|
|
471
|
+
)
|
|
472
|
+
self._input_flow = InputFlowController(
|
|
473
|
+
state=self._state,
|
|
474
|
+
services=self._services,
|
|
475
|
+
view=self._view_adapter,
|
|
476
|
+
start_worker=self._start_worker,
|
|
477
|
+
handle_agent_message=self._events.on_agent_message,
|
|
478
|
+
handle_error=self._events.on_error,
|
|
479
|
+
set_agent_running=self._set_agent_running,
|
|
480
|
+
set_has_messages=self._set_has_messages,
|
|
481
|
+
clear_workspace_marker=self._clear_workspace_marker_for_user_message,
|
|
482
|
+
clear_pending_questions=self._events.clear_pending_questions,
|
|
483
|
+
commit_profile_switch_marker=self._commit_profile_switch_marker,
|
|
484
|
+
post_gc_message=self.post_message,
|
|
485
|
+
debug=self._debug,
|
|
486
|
+
)
|
|
487
|
+
self._subscriptions = MainScreenSubscriptions(
|
|
488
|
+
bus=self._services.bus,
|
|
489
|
+
events=self._events,
|
|
490
|
+
sessions=self._sessions,
|
|
491
|
+
rollback_result_handler=self._rollback_controller.on_result,
|
|
492
|
+
execution_handler=self._on_execution_changed,
|
|
493
|
+
)
|
|
494
|
+
self._workflow_panel = WorkflowPanel(locale_controller=self._locale_controller)
|
|
495
|
+
self._workflow = WorkflowController(
|
|
496
|
+
services=self._services,
|
|
497
|
+
panel=self._workflow_panel,
|
|
498
|
+
cwd=self._workspace_cwd,
|
|
499
|
+
# Keep projection callbacks on MainScreen's pump. call_after_refresh
|
|
500
|
+
# forwards to the active modal, which can close with a refresh still queued.
|
|
501
|
+
schedule=self.call_later,
|
|
502
|
+
sync_chrome=self._sync_workflow_chrome,
|
|
503
|
+
locale_controller=self._locale_controller,
|
|
504
|
+
)
|
|
505
|
+
self._workflow_timer: Timer | None = None
|
|
506
|
+
self._trajectory_binding_available = True
|
|
507
|
+
self._execution_binding_busy = False
|
|
508
|
+
self.chat_workspace_cwd = self._workspace_cwd()
|
|
509
|
+
|
|
510
|
+
def _new_settings_persistence(self) -> SettingsPersistenceQueue:
|
|
511
|
+
return SettingsPersistenceQueue(
|
|
512
|
+
notify_failure=self._notify_settings_failure,
|
|
513
|
+
notify_rejected=self._notify_settings_rejected,
|
|
514
|
+
logger=logger,
|
|
515
|
+
on_written=self._on_settings_written,
|
|
516
|
+
save_delay_seconds=SETTINGS_SAVE_DELAY_SECONDS,
|
|
517
|
+
flush_lock_timeout_seconds=SETTINGS_FLUSH_LOCK_TIMEOUT_SECONDS,
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
def _settings_persistence(self) -> SettingsPersistenceQueue:
|
|
521
|
+
"""The one debounced writer for every panel-owned setting, built on first use."""
|
|
522
|
+
persistence = self.__dict__.get("_settings_persistence_queue")
|
|
523
|
+
if not isinstance(persistence, SettingsPersistenceQueue):
|
|
524
|
+
persistence = self._new_settings_persistence()
|
|
525
|
+
self.__dict__["_settings_persistence_queue"] = persistence
|
|
526
|
+
return persistence
|
|
527
|
+
|
|
528
|
+
def _settings_coordinator(self) -> SettingsCoordinator:
|
|
529
|
+
"""Process-level state behind the Settings dialog, built on first use.
|
|
530
|
+
|
|
531
|
+
Lazy for the same reason as the queue: most sessions never open the
|
|
532
|
+
panel. The RESTART baseline it captures is the same whenever it is
|
|
533
|
+
taken — those values are held in force for the life of the process.
|
|
534
|
+
"""
|
|
535
|
+
coordinator = self.__dict__.get("_settings_coordinator_instance")
|
|
536
|
+
if not isinstance(coordinator, SettingsCoordinator):
|
|
537
|
+
coordinator = self._new_settings_coordinator()
|
|
538
|
+
self.__dict__["_settings_coordinator_instance"] = coordinator
|
|
539
|
+
return coordinator
|
|
540
|
+
|
|
541
|
+
def _switch_locale_for_panel(self, requested_locale: str) -> None:
|
|
542
|
+
"""The panel's locale row goes through the same action as the picker and
|
|
543
|
+
``/language``: a bundle that fails to load is reported the same way, and
|
|
544
|
+
the coordinator then reprojects the row from what is still in force."""
|
|
545
|
+
self._navigation.set_language(requested_locale)
|
|
546
|
+
|
|
547
|
+
def _apply_trajectory_verify_commands(self, value: str) -> None:
|
|
548
|
+
app = cast("ChrysApp", self.app)
|
|
549
|
+
app.settings_handle.override(trajectory_verify_commands=value)
|
|
550
|
+
self._trajectory_verify_commands = value
|
|
551
|
+
self.query_one(TrajectoryDashboard).set_verify_commands(value)
|
|
552
|
+
|
|
553
|
+
def _apply_tool_groups_expanded(self, value: bool) -> None:
|
|
554
|
+
"""Persist the live choice; existing groups deliberately keep their state."""
|
|
555
|
+
persist_tool_groups_expanded(value)
|
|
556
|
+
cast("ChrysApp", self.app).settings_handle.override(tool_groups_expanded=value)
|
|
557
|
+
|
|
558
|
+
def _apply_approval_defer_while_judging(self, value: bool) -> None:
|
|
559
|
+
"""Put the choice in force; requests already waiting keep the choice they arrived under."""
|
|
560
|
+
cast("ChrysApp", self.app).settings_handle.override(approval_defer_while_judging=value)
|
|
561
|
+
|
|
562
|
+
def _approval_defer_while_judging(self) -> bool:
|
|
563
|
+
return cast("ChrysApp", self.app).settings_handle.settings.approval_defer_while_judging
|
|
564
|
+
|
|
565
|
+
def _refresh_trajectory_verify_commands(self) -> None:
|
|
566
|
+
"""Re-project the word list a reload only installed into the handle.
|
|
567
|
+
|
|
568
|
+
Reads the effective value rather than routing through the panel apply:
|
|
569
|
+
recording a reload as an override would credit it to the runtime layer
|
|
570
|
+
and pin it over every later reload.
|
|
571
|
+
"""
|
|
572
|
+
value = cast("ChrysApp", self.app).settings_handle.settings.trajectory_verify_commands
|
|
573
|
+
if value == self._trajectory_verify_commands:
|
|
574
|
+
return
|
|
575
|
+
self._trajectory_verify_commands = value
|
|
576
|
+
self.query_one(TrajectoryDashboard).set_verify_commands(value)
|
|
577
|
+
|
|
578
|
+
def _new_settings_coordinator(self) -> SettingsCoordinator:
|
|
579
|
+
app = cast("ChrysApp", self.app)
|
|
580
|
+
|
|
581
|
+
return SettingsCoordinator(
|
|
582
|
+
services=self._services,
|
|
583
|
+
settings_handle=app.settings_handle,
|
|
584
|
+
queue=self._settings_persistence(),
|
|
585
|
+
view=self._view_adapter,
|
|
586
|
+
locale_controller=self._locale_controller,
|
|
587
|
+
callbacks=SettingsCoordinatorCallbacks(
|
|
588
|
+
apply_theme=app.apply_theme_setting,
|
|
589
|
+
switch_locale=self._switch_locale_for_panel,
|
|
590
|
+
apply_trajectory_verify_commands=self._apply_trajectory_verify_commands,
|
|
591
|
+
apply_tool_groups_expanded=self._apply_tool_groups_expanded,
|
|
592
|
+
apply_approval_defer_while_judging=self._apply_approval_defer_while_judging,
|
|
593
|
+
list_themes=lambda: sorted(app.available_themes),
|
|
594
|
+
save_notifications=self._schedule_notification_settings_save,
|
|
595
|
+
notification_service=self._notification_service,
|
|
596
|
+
turn_lifecycle_task=self._services.turn_lifecycle_task,
|
|
597
|
+
turn_in_progress=lambda: self._state.run.agent_running or self._services.execution_busy(),
|
|
598
|
+
),
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
def _existing_settings_coordinator(self) -> SettingsCoordinator | None:
|
|
602
|
+
"""The coordinator if the panel has ever been opened; never builds one.
|
|
603
|
+
|
|
604
|
+
Every queue write originates from the panel, so a write outcome or a
|
|
605
|
+
reload arriving before the coordinator exists has nothing to update.
|
|
606
|
+
"""
|
|
607
|
+
coordinator = self.__dict__.get("_settings_coordinator_instance")
|
|
608
|
+
return coordinator if isinstance(coordinator, SettingsCoordinator) else None
|
|
609
|
+
|
|
610
|
+
def _on_settings_written(self, result: PersistResult) -> None:
|
|
611
|
+
coordinator = self._existing_settings_coordinator()
|
|
612
|
+
if coordinator is not None:
|
|
613
|
+
coordinator.on_written(result)
|
|
614
|
+
|
|
615
|
+
def _on_settings_reloaded_for_panel(self) -> None:
|
|
616
|
+
coordinator = self._existing_settings_coordinator()
|
|
617
|
+
if coordinator is not None:
|
|
618
|
+
coordinator.on_reloaded()
|
|
619
|
+
|
|
620
|
+
def _notify_settings_failure(self, exc: Exception) -> None:
|
|
621
|
+
self._view_adapter.notify(str(exc), title=SETTINGS_TITLE.bind(), severity="error", timeout=5)
|
|
622
|
+
coordinator = self._existing_settings_coordinator()
|
|
623
|
+
if coordinator is not None:
|
|
624
|
+
coordinator.on_write_failed()
|
|
625
|
+
|
|
626
|
+
def _notify_settings_rejected(self, result: PersistResult) -> None:
|
|
627
|
+
message = _SETTINGS_REJECTED.bind(keys=DisplaySequence(tuple(result.rejected)))
|
|
628
|
+
self._view_adapter.notify(message, title=SETTINGS_TITLE.bind(), severity="error", timeout=5)
|
|
629
|
+
coordinator = self._existing_settings_coordinator()
|
|
630
|
+
if coordinator is not None:
|
|
631
|
+
coordinator.on_write_failed()
|
|
632
|
+
|
|
633
|
+
def _workspace_cwd(self) -> str:
|
|
634
|
+
"""Return the TUI-tracked workspace cwd."""
|
|
635
|
+
return self._state.workspace_marker.current_cwd or safe_getcwd()
|
|
636
|
+
|
|
637
|
+
def _notification_service(self) -> NotificationService:
|
|
638
|
+
return cast("ChrysApp", self.app).notification_service
|
|
639
|
+
|
|
640
|
+
def _refresh_notification_settings(self) -> None:
|
|
641
|
+
cast("ChrysApp", self.app).refresh_notification_settings()
|
|
642
|
+
|
|
643
|
+
def _set_active_model_profile_id(self, profile_id: str) -> None:
|
|
644
|
+
self._services.active_model_profile_id = profile_id
|
|
645
|
+
|
|
646
|
+
def _set_profile_display(self, profile: str) -> None:
|
|
647
|
+
self._state.runtime.profile = profile
|
|
648
|
+
self.chat_profile_name = profile
|
|
649
|
+
|
|
650
|
+
def _show_session_title(self, title: str) -> None:
|
|
651
|
+
# ``chat_session_title`` is data-bound to the ChatPanel border.
|
|
652
|
+
self.chat_session_title = title
|
|
653
|
+
|
|
654
|
+
def _set_workspace_cwd(self, cwd: str) -> None:
|
|
655
|
+
self._state.workspace.current_cwd = cwd
|
|
656
|
+
self._state.workspace_marker.current_cwd = cwd
|
|
657
|
+
self._workspace_branch.configure(cwd)
|
|
658
|
+
|
|
659
|
+
def _show_workspace_branch(self, branch: str) -> None:
|
|
660
|
+
self.chat_workspace_branch = branch
|
|
661
|
+
with contextlib.suppress(Exception):
|
|
662
|
+
self.query_one(ChatPanel).set_workspace_branch(branch)
|
|
663
|
+
|
|
664
|
+
def _set_creating_new_session(self, creating: bool) -> None:
|
|
665
|
+
self._state.session.creating_new_session = creating
|
|
666
|
+
if creating:
|
|
667
|
+
self._session_title.clear_terminal_title_result()
|
|
668
|
+
|
|
669
|
+
def _set_restoring_session(self, restoring: bool) -> None:
|
|
670
|
+
self._state.session.restoring_session = restoring
|
|
671
|
+
if restoring:
|
|
672
|
+
self._session_title.clear_terminal_title_result()
|
|
673
|
+
|
|
674
|
+
def _clear_suggestion_file_cache(self) -> None:
|
|
675
|
+
self._suggestions.file_cache = None
|
|
676
|
+
|
|
677
|
+
def _warn_slash_command(
|
|
678
|
+
self,
|
|
679
|
+
message: StatusMessage,
|
|
680
|
+
title: StatusMessage = INVALID_COMMAND_TITLE_REF,
|
|
681
|
+
timeout: float | None = 3,
|
|
682
|
+
) -> None:
|
|
683
|
+
self._view_adapter.notify(message, title=title, severity="warning", timeout=timeout)
|
|
684
|
+
|
|
685
|
+
def _language_localizer(self) -> Localizer:
|
|
686
|
+
controller = self._locale_controller
|
|
687
|
+
return Localizer(DEFAULT_LOCALE) if controller is None else controller.localizer
|
|
688
|
+
|
|
689
|
+
def _has_selectable_model_profile(self) -> bool:
|
|
690
|
+
"""Return whether the registry offers at least one selectable model profile."""
|
|
691
|
+
registry = self._services.model_registry
|
|
692
|
+
return registry is not None and any(
|
|
693
|
+
is_model_profile_selectable(profile) for profile in registry.list_profiles()
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
def _refresh_model_indicator(self) -> None:
|
|
697
|
+
"""Recompute the model tag from registry availability and confirmed runtime state."""
|
|
698
|
+
details = self._state.runtime.details.model if self._state.runtime.details_confirmed else None
|
|
699
|
+
state = compute_model_indicator_state(
|
|
700
|
+
details,
|
|
701
|
+
self._has_selectable_model_profile(),
|
|
702
|
+
self._state.runtime.profile,
|
|
703
|
+
self._language_localizer(),
|
|
704
|
+
runtime_confirmed=self._state.runtime.details_confirmed,
|
|
705
|
+
)
|
|
706
|
+
self.query_one(StatusBar).set_model(state)
|
|
707
|
+
|
|
708
|
+
def refresh_localization(self) -> None:
|
|
709
|
+
"""Recompute screen-owned localized chrome text in place."""
|
|
710
|
+
self._refresh_model_indicator()
|
|
711
|
+
if self._workflow.workflow_mode:
|
|
712
|
+
self._workflow.request_refresh()
|
|
713
|
+
|
|
714
|
+
def _available_languages(self) -> list[tuple[str, Content]]:
|
|
715
|
+
localizer = self._language_localizer()
|
|
716
|
+
return [
|
|
717
|
+
(requested_locale, render_content(localizer, definition.bind()))
|
|
718
|
+
for requested_locale, definition in LANGUAGE_OPTIONS
|
|
719
|
+
]
|
|
720
|
+
|
|
721
|
+
def _current_language(self) -> str:
|
|
722
|
+
controller = self._locale_controller
|
|
723
|
+
return DEFAULT_LOCALE if controller is None else controller.requested_locale
|
|
724
|
+
|
|
725
|
+
def _unknown_language_warning(self, requested_locale: str) -> str:
|
|
726
|
+
return render_str(
|
|
727
|
+
self._language_localizer(),
|
|
728
|
+
LANGUAGE_UNKNOWN_LOCALE.bind(locale=requested_locale),
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
def _new_slash_command_actions(self) -> SlashCommandActions:
|
|
732
|
+
return SlashCommandActions(
|
|
733
|
+
list_themes=lambda: sorted(self.app.available_themes),
|
|
734
|
+
get_theme=lambda: self.app.theme,
|
|
735
|
+
apply_theme=self._view_adapter.apply_theme_setting,
|
|
736
|
+
pick_theme=self.action_pick_theme,
|
|
737
|
+
list_languages=self._available_languages,
|
|
738
|
+
get_language=self._current_language,
|
|
739
|
+
apply_language=self.action_set_language,
|
|
740
|
+
pick_language=self.action_pick_language,
|
|
741
|
+
render_unknown_language_warning=self._unknown_language_warning,
|
|
742
|
+
debug_event=self._debug,
|
|
743
|
+
new_session=self._create_new_session,
|
|
744
|
+
clear_session=self._clear_current_session,
|
|
745
|
+
quit_app=self.action_quit,
|
|
746
|
+
resume_session=self._resume_last_session,
|
|
747
|
+
fork_session=self._fork_current_session,
|
|
748
|
+
browse_session_list=self.action_sessions,
|
|
749
|
+
workflow_selection=self.action_workflow,
|
|
750
|
+
open_guide=self._navigation.open_guide,
|
|
751
|
+
edit_session_title=self._session_title.open_editor,
|
|
752
|
+
apply_session_title=self._session_title.apply_custom_title,
|
|
753
|
+
change_directory=self._workspace_actions.start_chdir,
|
|
754
|
+
copy_conversation=self._copy_agent_responses,
|
|
755
|
+
fold_tools=self._toggle_fold,
|
|
756
|
+
open_diff=self.action_show_diff,
|
|
757
|
+
open_rollback=self.action_show_rollback,
|
|
758
|
+
get_approval_mode=lambda: self._state.runtime.approval_mode.value,
|
|
759
|
+
change_approval_mode=self._config_actions.start_approval_mode_change,
|
|
760
|
+
configure_model=self._open_model_config,
|
|
761
|
+
configure_agent=self._open_agent_config,
|
|
762
|
+
configure_agent_tab=self._open_agent_config_tab,
|
|
763
|
+
show_runtime_details=self.action_runtime_details,
|
|
764
|
+
configure_settings=self._open_settings,
|
|
765
|
+
show_manual_pages=lambda pages, start_index: self._view_adapter.show_man_pages(
|
|
766
|
+
pages,
|
|
767
|
+
start_index=start_index,
|
|
768
|
+
),
|
|
769
|
+
warn=self._warn_slash_command,
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
def _new_diff_controller(self) -> DiffController:
|
|
773
|
+
return DiffController(
|
|
774
|
+
services=self._services,
|
|
775
|
+
live_diff=self._live_diff,
|
|
776
|
+
view=self._view_adapter,
|
|
777
|
+
workspace_cwd=self._workspace_cwd,
|
|
778
|
+
is_agent_running=lambda: self._state.run.agent_running,
|
|
779
|
+
run_generation=lambda: self._state.run.generation,
|
|
780
|
+
session_generation=self._services.session_generation,
|
|
781
|
+
turn_lifecycle_task=self._services.turn_lifecycle_task,
|
|
782
|
+
turn_lifecycle_saved=self._services.was_turn_lifecycle_saved,
|
|
783
|
+
)
|
|
784
|
+
|
|
785
|
+
def _new_rollback_controller(self) -> RollbackController:
|
|
786
|
+
def _reset_welcome_workspace_marker(cwd: str) -> None:
|
|
787
|
+
self._state.workspace_marker.original_cwd = None
|
|
788
|
+
self._set_workspace_cwd(cwd)
|
|
789
|
+
|
|
790
|
+
return RollbackController(
|
|
791
|
+
services=self._services,
|
|
792
|
+
view=self._view_adapter,
|
|
793
|
+
workspace_cwd=self._workspace_cwd,
|
|
794
|
+
is_agent_busy=lambda: (
|
|
795
|
+
self._state.run.agent_running or self._state.run.agent_loading or self._services.execution_busy()
|
|
796
|
+
),
|
|
797
|
+
current_session_id=self._view_adapter.current_chat_session_id,
|
|
798
|
+
session_generation=self._services.session_generation,
|
|
799
|
+
turn_lifecycle_task=self._services.turn_lifecycle_task,
|
|
800
|
+
profile_name=lambda: self._state.runtime.profile,
|
|
801
|
+
reset_welcome_workspace_marker=_reset_welcome_workspace_marker,
|
|
802
|
+
set_has_messages=self._set_has_messages,
|
|
803
|
+
post_gc_message=self.post_message,
|
|
804
|
+
debug=self._debug,
|
|
805
|
+
locale_controller=self._locale_controller,
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
def _set_interrupt_confirm_active(self, active: bool) -> None:
|
|
809
|
+
self._state.overlays.interrupt_confirm_active = active
|
|
810
|
+
|
|
811
|
+
def _new_navigation_controller(self) -> MainNavigationController:
|
|
812
|
+
async def _flush_notifications() -> None:
|
|
813
|
+
await self._flush_settings_save()
|
|
814
|
+
|
|
815
|
+
async def _delete_current_and_new(session_id: str) -> None:
|
|
816
|
+
await self._delete_session_and_new(session_id)
|
|
817
|
+
|
|
818
|
+
async def _restore_session(session_id: str | WorkflowSessionPick) -> None:
|
|
819
|
+
await self._sessions.do_session_restore(session_id)
|
|
820
|
+
|
|
821
|
+
def _cancel_pending_injection() -> bool:
|
|
822
|
+
# Deferred attribute lookup: the navigation controller is built
|
|
823
|
+
# before the input-flow controller during screen construction.
|
|
824
|
+
return self._input_flow.cancel_pending_injection()
|
|
825
|
+
|
|
826
|
+
def _dismiss_suggestions() -> bool:
|
|
827
|
+
if not self._suggestions_active_for_bindings():
|
|
828
|
+
return False
|
|
829
|
+
self._suggestions.dismiss_suggestions()
|
|
830
|
+
return True
|
|
831
|
+
|
|
832
|
+
return MainNavigationController(
|
|
833
|
+
services=self._services,
|
|
834
|
+
view=self._view_adapter,
|
|
835
|
+
is_agent_loading=lambda: self._state.run.agent_loading,
|
|
836
|
+
is_agent_running=lambda: self._state.run.agent_running or self._services.execution_busy(),
|
|
837
|
+
is_submit_pending=lambda: self._state.submit.active or bool(self._workflow.awaiting_engine),
|
|
838
|
+
is_workflow_mode=lambda: self._workflow.workflow_mode,
|
|
839
|
+
has_messages=lambda: self._state.run.has_messages,
|
|
840
|
+
is_dashboard_visible=self._dashboard_visible,
|
|
841
|
+
set_interrupt_confirm_active=lambda active: MainScreen._set_interrupt_confirm_active(self, active),
|
|
842
|
+
publish_interrupt=lambda: MainScreen._publish_interrupt(self),
|
|
843
|
+
dismiss_suggestions=_dismiss_suggestions,
|
|
844
|
+
cancel_pending_injection=_cancel_pending_injection,
|
|
845
|
+
delete_current_and_new=_delete_current_and_new,
|
|
846
|
+
restore_session=_restore_session,
|
|
847
|
+
flush_notifications=_flush_notifications,
|
|
848
|
+
start_worker=self._start_worker,
|
|
849
|
+
debug=self._debug,
|
|
850
|
+
locale_controller=self._locale_controller,
|
|
851
|
+
)
|
|
852
|
+
|
|
853
|
+
def _start_worker(self, work: Callable[[], Awaitable[object]]) -> object:
|
|
854
|
+
"""Run *work* in a worker on this screen's event loop.
|
|
855
|
+
|
|
856
|
+
As with ``@work``, the worker is named after its work, and the
|
|
857
|
+
coroutine is made only when the worker starts, so a worker cancelled
|
|
858
|
+
before it runs leaves nothing unawaited.
|
|
859
|
+
"""
|
|
860
|
+
|
|
861
|
+
async def run() -> object:
|
|
862
|
+
return await work()
|
|
863
|
+
|
|
864
|
+
name, description = _describe_work(work)
|
|
865
|
+
return self.run_worker(run, name=name, description=description, thread=False)
|
|
866
|
+
|
|
867
|
+
def set_startup_agent_loading(self, active: bool) -> None:
|
|
868
|
+
"""Startup-facing facade for setting agent loading state."""
|
|
869
|
+
self._set_agent_loading(active)
|
|
870
|
+
|
|
871
|
+
def is_startup_agent_loading(self) -> bool:
|
|
872
|
+
"""Return whether startup still considers the agent loading."""
|
|
873
|
+
return self._state.run.agent_loading
|
|
874
|
+
|
|
875
|
+
def gc_freeze_block_reason(self) -> GcFreezeBlockReason | None:
|
|
876
|
+
"""Return the first active MainScreen or participant hard gate."""
|
|
877
|
+
if self._state.run.agent_loading:
|
|
878
|
+
return GcFreezeBlockReason.AGENT_LOADING
|
|
879
|
+
if self._state.run.agent_running:
|
|
880
|
+
return GcFreezeBlockReason.AGENT_RUNNING
|
|
881
|
+
if scroll_gc_paused():
|
|
882
|
+
return GcFreezeBlockReason.SCROLL_GC_PAUSED
|
|
883
|
+
for participant in self._gc_freeze_participants:
|
|
884
|
+
if (reason := participant.gc_freeze_block_reason()) is not None:
|
|
885
|
+
return reason
|
|
886
|
+
return None
|
|
887
|
+
|
|
888
|
+
def prepare_for_gc_freeze(self) -> None:
|
|
889
|
+
"""Synchronously prepare every registered freeze participant."""
|
|
890
|
+
for participant in self._gc_freeze_participants:
|
|
891
|
+
participant.prepare_for_gc_freeze()
|
|
892
|
+
prepare_textual_screen_for_gc(self)
|
|
893
|
+
|
|
894
|
+
def after_gc_freeze(self) -> None:
|
|
895
|
+
"""Synchronously renew all caches, continuing past individual failures."""
|
|
896
|
+
errors: list[Exception] = []
|
|
897
|
+
try:
|
|
898
|
+
after_textual_screen_gc_freeze(self)
|
|
899
|
+
except Exception as error:
|
|
900
|
+
errors.append(error)
|
|
901
|
+
for participant in self._gc_freeze_participants:
|
|
902
|
+
try:
|
|
903
|
+
participant.after_gc_freeze()
|
|
904
|
+
except Exception as error:
|
|
905
|
+
errors.append(error)
|
|
906
|
+
raise_gc_freeze_hook_errors("MainScreen GC-freeze renewal failed", errors)
|
|
907
|
+
|
|
908
|
+
def abort_gc_freeze(self) -> None:
|
|
909
|
+
"""Best-effort restore every cache after an incomplete prepare/after pass."""
|
|
910
|
+
abort_textual_screen_gc_freeze(self)
|
|
911
|
+
for participant in self._gc_freeze_participants:
|
|
912
|
+
try:
|
|
913
|
+
participant.abort_gc_freeze()
|
|
914
|
+
except Exception:
|
|
915
|
+
logger.exception("Failed to restore GC-freeze participant %s", type(participant).__name__)
|
|
916
|
+
|
|
917
|
+
async def restore_startup_session(self, session_id: str) -> StartupRestore:
|
|
918
|
+
"""Restore at startup and confirm the matching backend success event."""
|
|
919
|
+
restored = False
|
|
920
|
+
|
|
921
|
+
async def observe_restored(event: SessionRestored) -> None:
|
|
922
|
+
nonlocal restored
|
|
923
|
+
if event.session_id == session_id:
|
|
924
|
+
restored = True
|
|
925
|
+
|
|
926
|
+
await self._services.bus.subscribe(SessionRestored, observe_restored)
|
|
927
|
+
try:
|
|
928
|
+
request = await self._sessions.do_session_restore(session_id, allow_while_loading=True)
|
|
929
|
+
finally:
|
|
930
|
+
await self._services.bus.unsubscribe(SessionRestored, observe_restored)
|
|
931
|
+
if restored:
|
|
932
|
+
return "restored"
|
|
933
|
+
return "declined" if request is RestoreRequest.DECLINED else "failed"
|
|
934
|
+
|
|
935
|
+
def cancel_startup_session_restore(self) -> None:
|
|
936
|
+
"""Close restore loading UI before falling back to a fresh runtime."""
|
|
937
|
+
self._events.cancel_agent_load()
|
|
938
|
+
self._set_restoring_session(False)
|
|
939
|
+
|
|
940
|
+
async def dismiss_startup_load_dialog_before_restore(self) -> None:
|
|
941
|
+
"""Dismiss the startup load modal before opening restore-specific loading UI."""
|
|
942
|
+
from chrys.app.tui.screens.dialogs.agent_load import AgentLoadDialog
|
|
943
|
+
|
|
944
|
+
if self._events._agent_load_dialog is not None:
|
|
945
|
+
with contextlib.suppress(Exception):
|
|
946
|
+
self._events.cancel_agent_load()
|
|
947
|
+
startup_modals: list[AgentLoadDialog] = []
|
|
948
|
+
with contextlib.suppress(Exception):
|
|
949
|
+
startup_modals = [modal for modal in list(self.app.screen_stack) if isinstance(modal, AgentLoadDialog)]
|
|
950
|
+
for modal in startup_modals:
|
|
951
|
+
with contextlib.suppress(Exception):
|
|
952
|
+
modal.request_dismiss()
|
|
953
|
+
await asyncio.sleep(0)
|
|
954
|
+
|
|
955
|
+
def check_action(self, action: str, parameters: tuple[object, ...]) -> bool | None:
|
|
956
|
+
"""Conditionally show/hide bindings."""
|
|
957
|
+
if action == "toggle_trajectory_dashboard" and not self._navigation.can_toggle_trajectory_dashboard():
|
|
958
|
+
return False
|
|
959
|
+
suggestions_active = (
|
|
960
|
+
self._suggestions_active_for_bindings()
|
|
961
|
+
if action in {"chat_page_up", "chat_page_down", "chat_scroll_bottom"}
|
|
962
|
+
else False
|
|
963
|
+
)
|
|
964
|
+
return check_main_action(
|
|
965
|
+
action,
|
|
966
|
+
fullscreen_terminal=self._state.shell.fullscreen_terminal,
|
|
967
|
+
shell_mode=self._state.shell.active,
|
|
968
|
+
chat_foreground=self._chat_foreground(),
|
|
969
|
+
suggestions_active=suggestions_active,
|
|
970
|
+
agent_running=self._state.run.agent_running or self._services.execution_busy(),
|
|
971
|
+
)
|
|
972
|
+
|
|
973
|
+
def _dashboard_visible(self) -> bool:
|
|
974
|
+
return self.query_one(TrajectoryDashboard).foreground
|
|
975
|
+
|
|
976
|
+
def _chat_foreground(self) -> bool:
|
|
977
|
+
return not self._workflow.workflow_mode and not self._dashboard_visible()
|
|
978
|
+
|
|
979
|
+
def _suggestions_active_for_bindings(self) -> bool:
|
|
980
|
+
"""Return whether the input suggestion popup is active."""
|
|
981
|
+
with contextlib.suppress(Exception):
|
|
982
|
+
return bool(self.query_one(InputBar).suggestions_active)
|
|
983
|
+
return False
|
|
984
|
+
|
|
985
|
+
def compose(self) -> ComposeResult:
|
|
986
|
+
yield AppHeader(locale_controller=self._locale_controller, show_mode_badge=True).data_bind(
|
|
987
|
+
subtitle_parts=MainScreen.header_subtitle_parts,
|
|
988
|
+
approval_mode=MainScreen.header_approval_mode,
|
|
989
|
+
)
|
|
990
|
+
with Horizontal():
|
|
991
|
+
yield ChatPanel(
|
|
992
|
+
cwd=self._workspace_cwd(),
|
|
993
|
+
localization=self._localization,
|
|
994
|
+
locale_controller=self._locale_controller,
|
|
995
|
+
tool_groups_expanded=self._tool_groups_expanded,
|
|
996
|
+
sub_agent_transcript_loader=self._load_sub_agent_transcript,
|
|
997
|
+
).data_bind(
|
|
998
|
+
agent_running=MainScreen.agent_running_state,
|
|
999
|
+
profile_name=MainScreen.chat_profile_name,
|
|
1000
|
+
session_id_value=MainScreen.chat_session_id,
|
|
1001
|
+
session_title_value=MainScreen.chat_session_title,
|
|
1002
|
+
workspace_cwd=MainScreen.chat_workspace_cwd,
|
|
1003
|
+
workspace_branch=MainScreen.chat_workspace_branch,
|
|
1004
|
+
)
|
|
1005
|
+
yield self._workflow_panel
|
|
1006
|
+
yield TrajectoryDashboard(
|
|
1007
|
+
locale_controller=self._locale_controller,
|
|
1008
|
+
verify_commands=self._trajectory_verify_commands,
|
|
1009
|
+
)
|
|
1010
|
+
yield ShellPanel(working_directory=self._workspace_cwd())
|
|
1011
|
+
yield SidebarPanel(locale_controller=self._locale_controller).data_bind(
|
|
1012
|
+
context_usage_state=MainScreen.context_usage_state,
|
|
1013
|
+
todo_state=MainScreen.todo_state,
|
|
1014
|
+
)
|
|
1015
|
+
yield StatusBar(locale_controller=self._locale_controller).data_bind(
|
|
1016
|
+
agent_running=MainScreen.agent_running_state,
|
|
1017
|
+
agent_loading=MainScreen.agent_loading_state,
|
|
1018
|
+
shell_mode=MainScreen.shell_mode_state,
|
|
1019
|
+
)
|
|
1020
|
+
# Anchor the screen overlay at InputBar: its upward offset keeps the
|
|
1021
|
+
# popup adjacent to input and paints over the reserved status row
|
|
1022
|
+
# without changing transcript geometry while suggestions are filtered.
|
|
1023
|
+
yield SuggestionList(locale_controller=self._locale_controller)
|
|
1024
|
+
yield InputBar(locale_controller=self._locale_controller).data_bind(
|
|
1025
|
+
agent_running=MainScreen.agent_running_state,
|
|
1026
|
+
agent_loading=MainScreen.agent_loading_state,
|
|
1027
|
+
has_messages=MainScreen.has_messages_state,
|
|
1028
|
+
shell_mode=MainScreen.shell_mode_state,
|
|
1029
|
+
)
|
|
1030
|
+
yield ChrysFooter(locale_controller=self._locale_controller)
|
|
1031
|
+
|
|
1032
|
+
async def _load_sub_agent_transcript(self, log_file: str) -> PersistedSubAgentTranscript | None:
|
|
1033
|
+
"""Load a transcript relative to the currently displayed session."""
|
|
1034
|
+
session_id = self.chat_session_id
|
|
1035
|
+
if self._services.state_store is None or not session_id:
|
|
1036
|
+
return None
|
|
1037
|
+
return await load_persisted_sub_agent_transcript(self._services.state_store.session_dir(session_id), log_file)
|
|
1038
|
+
|
|
1039
|
+
def sync_footer_bindings(self) -> None:
|
|
1040
|
+
"""Refresh visible Footer content only when its signature changed."""
|
|
1041
|
+
self.query_one(ChrysFooter).sync_bindings()
|
|
1042
|
+
|
|
1043
|
+
def on_screen_resume(self, _event: ScreenResume) -> None:
|
|
1044
|
+
"""Recover deferred workflow and Footer updates after an overlay closes."""
|
|
1045
|
+
if self._workflow.workflow_mode:
|
|
1046
|
+
self._workflow.shown()
|
|
1047
|
+
self._sync_workflow_timer()
|
|
1048
|
+
self.query_one(InputBar).sync_deferred_button_geometry()
|
|
1049
|
+
self.sync_footer_bindings()
|
|
1050
|
+
self.query_one(SuggestionList).resume_marquee()
|
|
1051
|
+
if self.theme_editor is not None:
|
|
1052
|
+
self.theme_editor.resume()
|
|
1053
|
+
self._resume_shell_request()
|
|
1054
|
+
|
|
1055
|
+
def on_resize(self, event: Resize) -> None:
|
|
1056
|
+
self._set_theme_editor_compact(self.theme_editor is not None and event.size.width < 128)
|
|
1057
|
+
|
|
1058
|
+
def _set_theme_editor_compact(self, compact: bool) -> None:
|
|
1059
|
+
if self.has_class("--theme-editor-compact") == compact:
|
|
1060
|
+
return
|
|
1061
|
+
sidebar = self.query_one(SidebarPanel)
|
|
1062
|
+
sidebar.suppress_visibility("theme-editor", compact)
|
|
1063
|
+
# Only these panel roots and the editor use the compact selectors.
|
|
1064
|
+
# Updating MainScreen recursively would restyle the whole transcript.
|
|
1065
|
+
self.update_classes({"--theme-editor-compact": compact}, update=False)
|
|
1066
|
+
self.app.stylesheet.update_nodes((self.query_one(ChatPanel),))
|
|
1067
|
+
if self.theme_editor is not None and self.theme_editor.is_mounted:
|
|
1068
|
+
self.theme_editor.update_node_styles(animate=False)
|
|
1069
|
+
|
|
1070
|
+
def on_screen_suspend(self, _event: ScreenSuspend) -> None:
|
|
1071
|
+
"""Stop suggestion animation while an overlay covers this screen."""
|
|
1072
|
+
self.query_one(SuggestionList).pause_marquee()
|
|
1073
|
+
if self._workflow_timer is not None:
|
|
1074
|
+
self._workflow_timer.stop()
|
|
1075
|
+
self._workflow_timer = None
|
|
1076
|
+
|
|
1077
|
+
def _update_subtitle(self) -> None:
|
|
1078
|
+
"""Update header to reflect platform info."""
|
|
1079
|
+
platform_label = get_platform().display_label
|
|
1080
|
+
self.header_subtitle_parts = (platform_label,) if platform_label else ()
|
|
1081
|
+
|
|
1082
|
+
def watch_agent_running_state(self, running: bool) -> None:
|
|
1083
|
+
self._apply_agent_running_state(running)
|
|
1084
|
+
|
|
1085
|
+
def watch_agent_loading_state(self, loading: bool) -> None:
|
|
1086
|
+
self._apply_agent_loading_state(loading)
|
|
1087
|
+
|
|
1088
|
+
def watch_has_messages_state(self, has_messages: bool) -> None:
|
|
1089
|
+
self._state.run.has_messages = has_messages
|
|
1090
|
+
|
|
1091
|
+
def watch_header_approval_mode(self, mode: ApprovalMode) -> None:
|
|
1092
|
+
self._state.runtime.approval_mode = mode
|
|
1093
|
+
|
|
1094
|
+
def watch_chat_workspace_cwd(self, old_cwd: str, cwd: str) -> None:
|
|
1095
|
+
self._workspace_branch.displayed_cwd_changed(old_cwd, cwd)
|
|
1096
|
+
|
|
1097
|
+
def watch_shell_mode_state(self, active: bool) -> None:
|
|
1098
|
+
self._shell_mode_controller.apply(active)
|
|
1099
|
+
|
|
1100
|
+
def _reactive_state_ready(self) -> bool:
|
|
1101
|
+
return isinstance(self, MainScreen) and self._reactive_state_initialized
|
|
1102
|
+
|
|
1103
|
+
def get_widget_and_offset_at(self, x: int, y: int) -> tuple[Widget | None, Offset | None]:
|
|
1104
|
+
"""Return no selectable target for tool-renderer UI.
|
|
1105
|
+
|
|
1106
|
+
Textual uses this method to start and update text selection. Tool
|
|
1107
|
+
renderers are compact status UI in the chat transcript; their details
|
|
1108
|
+
will get a separate copy path when expanded details mode lands.
|
|
1109
|
+
"""
|
|
1110
|
+
widget, offset = super().get_widget_and_offset_at(x, y)
|
|
1111
|
+
if widget is not None and is_tool_copy_excluded(widget):
|
|
1112
|
+
return None, None
|
|
1113
|
+
return widget, offset
|
|
1114
|
+
|
|
1115
|
+
def _watch__select_state(self, select_state: SelectState | None) -> None:
|
|
1116
|
+
"""Route chat drags through the O(visible) logical selection model.
|
|
1117
|
+
|
|
1118
|
+
Textual's generic handler re-walks and re-sorts every widget under the
|
|
1119
|
+
selection container on each mouse move — O(total transcript widgets)
|
|
1120
|
+
in a long chat. Drags whose both endpoints sit in the ChatPanel go
|
|
1121
|
+
through :class:`ChatSelectionController` instead; anything else
|
|
1122
|
+
(single-widget selections, non-chat surfaces) keeps stock behavior.
|
|
1123
|
+
"""
|
|
1124
|
+
if select_state is None or select_state.end is None:
|
|
1125
|
+
if select_state is None:
|
|
1126
|
+
self._chat_selection.clear()
|
|
1127
|
+
super()._watch__select_state(select_state)
|
|
1128
|
+
return
|
|
1129
|
+
if not select_state.is_attached_to_dom() or select_state.is_single_content_widget:
|
|
1130
|
+
self._chat_selection.clear()
|
|
1131
|
+
super()._watch__select_state(select_state)
|
|
1132
|
+
return
|
|
1133
|
+
if self._chat_selection.handle_select_state(select_state):
|
|
1134
|
+
return
|
|
1135
|
+
self._chat_selection.clear()
|
|
1136
|
+
super()._watch__select_state(select_state)
|
|
1137
|
+
|
|
1138
|
+
async def _watch_selections(
|
|
1139
|
+
self,
|
|
1140
|
+
old_selections: dict[Widget, Selection],
|
|
1141
|
+
selections: dict[Widget, Selection],
|
|
1142
|
+
) -> None:
|
|
1143
|
+
"""Notify only widgets whose selection actually changed.
|
|
1144
|
+
|
|
1145
|
+
Textual notifies the union of old and new maps unconditionally.
|
|
1146
|
+
``VirtualizedMarkdown.selection_updated`` drops its rendered-line and
|
|
1147
|
+
copy-text caches, so blanket notification re-renders every selected
|
|
1148
|
+
transcript widget on every mouse move of a drag.
|
|
1149
|
+
"""
|
|
1150
|
+
for widget in old_selections.keys() | selections.keys():
|
|
1151
|
+
new_selection = selections.get(widget)
|
|
1152
|
+
if old_selections.get(widget) != new_selection:
|
|
1153
|
+
widget.selection_updated(new_selection)
|
|
1154
|
+
|
|
1155
|
+
def clear_selection(self) -> None:
|
|
1156
|
+
"""Clear the chat logical selection along with screen selection state."""
|
|
1157
|
+
self._chat_selection.clear()
|
|
1158
|
+
super().clear_selection()
|
|
1159
|
+
|
|
1160
|
+
def schedule_chat_selection_reprojection(self, panel: ChatPanel) -> None:
|
|
1161
|
+
"""Keep chat selection highlights aligned after the transcript scrolls."""
|
|
1162
|
+
self._chat_selection.schedule_reproject(panel)
|
|
1163
|
+
|
|
1164
|
+
def _compositor_refresh(self) -> None:
|
|
1165
|
+
"""Forward each repaint to the dialog above this screen once.
|
|
1166
|
+
|
|
1167
|
+
Every ``ModalScreen`` lets this screen show through (``chrys.tcss``),
|
|
1168
|
+
so a dialog paints this screen's frame as its backdrop and must receive
|
|
1169
|
+
every region this screen repaints while covered. Textual's background
|
|
1170
|
+
branch forwards those regions, but it also marks them dirty again on
|
|
1171
|
+
this screen and sets ``_repaint_required``. The next update then swaps
|
|
1172
|
+
the dirty widgets for this screen alone and forwards the old regions
|
|
1173
|
+
once more, together with any new damage, and marks the union again.
|
|
1174
|
+
Whatever was forwarded once is forwarded on every later update: after
|
|
1175
|
+
the first layout, scroll or status change under the dialog, every
|
|
1176
|
+
spinner tick makes the dialog re-render and re-blend the whole backdrop.
|
|
1177
|
+
|
|
1178
|
+
Forward only the regions this update repainted. The re-marking keeps
|
|
1179
|
+
nothing fresh: the dialog repaints a forwarded region from this
|
|
1180
|
+
screen's current compositor output, and Textual repaints this screen's
|
|
1181
|
+
whole frame when the dialog closes (``Screen._on_screen_resume`` and
|
|
1182
|
+
``App._replace_screen``). Underlay animation stays visible because
|
|
1183
|
+
every tick still reaches the dialog.
|
|
1184
|
+
|
|
1185
|
+
This is a version-gated fork of the non-inline background branch of
|
|
1186
|
+
Textual's private ``Screen._compositor_refresh``. The only intentional
|
|
1187
|
+
semantic delta is dropping the re-marking; the top-screen and inline
|
|
1188
|
+
branches, and other Textual releases, take the stock path.
|
|
1189
|
+
"""
|
|
1190
|
+
app = self.app
|
|
1191
|
+
dirty_regions = self._compositor._dirty_regions
|
|
1192
|
+
if (
|
|
1193
|
+
TEXTUAL_VERSION != TEXTUAL_BACKGROUND_REFRESH_FORK_VERSION
|
|
1194
|
+
or app.is_inline
|
|
1195
|
+
or not dirty_regions
|
|
1196
|
+
or self is app.screen
|
|
1197
|
+
or self not in app._background_screens
|
|
1198
|
+
):
|
|
1199
|
+
super()._compositor_refresh()
|
|
1200
|
+
return
|
|
1201
|
+
app.screen.refresh(*dirty_regions)
|
|
1202
|
+
dirty_regions.clear()
|
|
1203
|
+
self._dirty_widgets.clear()
|
|
1204
|
+
app._update_mouse_over(self)
|
|
1205
|
+
|
|
1206
|
+
def _screen_resized(self, size: Size) -> None:
|
|
1207
|
+
"""Ignore unchanged resize broadcasts while this screen is under an overlay."""
|
|
1208
|
+
if size == self._size:
|
|
1209
|
+
return
|
|
1210
|
+
super()._screen_resized(size)
|
|
1211
|
+
|
|
1212
|
+
def _refresh_layout(self, size: Size | None = None, scroll: bool = False) -> None:
|
|
1213
|
+
if self._theme_editor_opening:
|
|
1214
|
+
# App.refresh_css calls this directly even inside batch_update.
|
|
1215
|
+
# Mount the complete dock before measuring the resized transcript.
|
|
1216
|
+
self.refresh(layout=True)
|
|
1217
|
+
return
|
|
1218
|
+
super()._refresh_layout(size, scroll)
|
|
1219
|
+
# Now — and only now — the compositor's visible map matches the new
|
|
1220
|
+
# scroll offset, so a pending chat-highlight re-projection is safe.
|
|
1221
|
+
self._chat_selection.on_screen_layout_refreshed()
|
|
1222
|
+
# Likewise a workflow panel shown before this layout placed it.
|
|
1223
|
+
self._workflow.screen_layout_refreshed()
|
|
1224
|
+
|
|
1225
|
+
async def on_mount(self) -> None:
|
|
1226
|
+
"""Subscribe to backend events when the screen mounts."""
|
|
1227
|
+
chat_panel = self.query_one(ChatPanel)
|
|
1228
|
+
chat_panel.set_replay_progress_callback(self._events.update_session_history_progress)
|
|
1229
|
+
self._gc_freeze_participants = (
|
|
1230
|
+
chat_panel,
|
|
1231
|
+
self.query_one(TrajectoryDashboard),
|
|
1232
|
+
self.query_one(SessionJsonPanel),
|
|
1233
|
+
self.query_one(ShellPanel),
|
|
1234
|
+
self._suggestions,
|
|
1235
|
+
)
|
|
1236
|
+
self._suggestions.build_slash_commands()
|
|
1237
|
+
|
|
1238
|
+
await self._subscriptions.subscribe_all()
|
|
1239
|
+
await self._workflow.subscribe()
|
|
1240
|
+
self._sync_execution_chrome()
|
|
1241
|
+
self._sync_workflow_timer()
|
|
1242
|
+
if self._locale_controller is not None:
|
|
1243
|
+
self._locale_controller.register_surface(self)
|
|
1244
|
+
self._refresh_model_indicator()
|
|
1245
|
+
self._update_subtitle()
|
|
1246
|
+
self._workspace_branch.start(self._workspace_cwd())
|
|
1247
|
+
|
|
1248
|
+
async def on_unmount(self) -> None:
|
|
1249
|
+
"""Flush pending UI-owned settings before the screen is torn down."""
|
|
1250
|
+
if self._locale_controller is not None:
|
|
1251
|
+
self._locale_controller.unregister_surface(self)
|
|
1252
|
+
if self._workflow_timer is not None:
|
|
1253
|
+
self._workflow_timer.stop()
|
|
1254
|
+
await self._workflow.close()
|
|
1255
|
+
try:
|
|
1256
|
+
await self._subscriptions.unsubscribe_all()
|
|
1257
|
+
finally:
|
|
1258
|
+
self._session_title.stop_activity()
|
|
1259
|
+
await self._workspace_branch.close()
|
|
1260
|
+
try:
|
|
1261
|
+
await self._suggestions.buddy_command.shutdown()
|
|
1262
|
+
finally:
|
|
1263
|
+
await self._flush_settings_save()
|
|
1264
|
+
|
|
1265
|
+
# ------------------------------------------------------------------ #
|
|
1266
|
+
# Focus guard — keep input bar focused
|
|
1267
|
+
# ------------------------------------------------------------------ #
|
|
1268
|
+
|
|
1269
|
+
def on_descendant_focus(self, event: DescendantFocus) -> None:
|
|
1270
|
+
"""Redirect focus back to the input bar when it lands on a display-only panel."""
|
|
1271
|
+
if self._workflow.workflow_mode:
|
|
1272
|
+
return
|
|
1273
|
+
self._shell_mode_controller.on_descendant_focus(event.widget)
|
|
1274
|
+
|
|
1275
|
+
def on_paste(self, event: Paste) -> None:
|
|
1276
|
+
"""Route image path drops/pastes into the chat input when it is not focused."""
|
|
1277
|
+
if self._workflow.workflow_mode:
|
|
1278
|
+
return
|
|
1279
|
+
self._shell_mode_controller.on_paste(event)
|
|
1280
|
+
|
|
1281
|
+
# ------------------------------------------------------------------ #
|
|
1282
|
+
# User input
|
|
1283
|
+
# ------------------------------------------------------------------ #
|
|
1284
|
+
|
|
1285
|
+
@on(InputBar.UserSubmitted)
|
|
1286
|
+
def _on_user_input(self, event: InputBar.UserSubmitted) -> None:
|
|
1287
|
+
"""User pressed Enter — handle slash commands or publish message."""
|
|
1288
|
+
if self._workflow.workflow_mode:
|
|
1289
|
+
return
|
|
1290
|
+
text = event.text.strip()
|
|
1291
|
+
if not text:
|
|
1292
|
+
return
|
|
1293
|
+
|
|
1294
|
+
# Dispatch slash commands
|
|
1295
|
+
if is_slash_command_candidate(text):
|
|
1296
|
+
if self._state.run.agent_loading:
|
|
1297
|
+
return
|
|
1298
|
+
if self._suggestions.dispatch_slash_command(text):
|
|
1299
|
+
return
|
|
1300
|
+
|
|
1301
|
+
if self._state.run.agent_loading:
|
|
1302
|
+
return
|
|
1303
|
+
self._submit_user_text(text)
|
|
1304
|
+
|
|
1305
|
+
@on(InputBar.LockedChanged)
|
|
1306
|
+
def _on_input_locked_changed(self, event: InputBar.LockedChanged) -> None:
|
|
1307
|
+
"""Keep status-bar selector guards synchronized with input locking."""
|
|
1308
|
+
self.query_one(StatusBar).set_input_locked(event.locked)
|
|
1309
|
+
|
|
1310
|
+
def _model_unconfigured(self) -> bool:
|
|
1311
|
+
"""Return whether a chat submit would run with no usable model.
|
|
1312
|
+
|
|
1313
|
+
Confirmed runtime details are authoritative when they name a model:
|
|
1314
|
+
the resolver's built-in fallback loads fine but carries the
|
|
1315
|
+
non-functional placeholder model id, while a real model id proves
|
|
1316
|
+
the session can serve. A blank confirmed id carries no information
|
|
1317
|
+
(event sources may omit runtime details), so it falls back to the
|
|
1318
|
+
registry's selectable profiles — as does the pre-confirmation state.
|
|
1319
|
+
Screens constructed without a registry keep the legacy submit path.
|
|
1320
|
+
"""
|
|
1321
|
+
registry = self._services.model_registry
|
|
1322
|
+
if registry is None:
|
|
1323
|
+
return False
|
|
1324
|
+
if self._state.runtime.details_confirmed:
|
|
1325
|
+
model_id = self._state.runtime.details.model.model_id
|
|
1326
|
+
if model_id == UNCONFIGURED_MODEL_ID:
|
|
1327
|
+
return True
|
|
1328
|
+
if model_id.strip():
|
|
1329
|
+
return False
|
|
1330
|
+
return not self._has_selectable_model_profile()
|
|
1331
|
+
|
|
1332
|
+
def _show_model_unconfigured_dialog(self) -> None:
|
|
1333
|
+
"""Explain the blocked submit and offer the model tag's action."""
|
|
1334
|
+
from chrys.app.tui.screens.dialogs.confirm import ConfirmDialog
|
|
1335
|
+
|
|
1336
|
+
def _on_result(confirmed: bool | None) -> None:
|
|
1337
|
+
if confirmed:
|
|
1338
|
+
self._config_actions.on_model_tag_clicked(
|
|
1339
|
+
"select" if self._has_selectable_model_profile() else "configure"
|
|
1340
|
+
)
|
|
1341
|
+
else:
|
|
1342
|
+
# Dismissal must hand the keyboard back to the composer.
|
|
1343
|
+
self.query_one(InputBar).focus_input()
|
|
1344
|
+
|
|
1345
|
+
self.app.push_screen(
|
|
1346
|
+
ConfirmDialog(
|
|
1347
|
+
title=_MODEL_UNCONFIGURED_TITLE.bind(),
|
|
1348
|
+
message=_MODEL_UNCONFIGURED_MESSAGE.bind(),
|
|
1349
|
+
confirm_label=_MODEL_UNCONFIGURED_SETUP.bind(),
|
|
1350
|
+
locale_controller=self._locale_controller,
|
|
1351
|
+
),
|
|
1352
|
+
_on_result,
|
|
1353
|
+
)
|
|
1354
|
+
|
|
1355
|
+
def _submit_user_text(self, text: str) -> None:
|
|
1356
|
+
"""Submit normal user text as a new turn or mid-run injection.
|
|
1357
|
+
|
|
1358
|
+
Sole chokepoint for chat submissions — composer submits and
|
|
1359
|
+
suggestion-originated ones (runtime skills, unmatched fallback
|
|
1360
|
+
text) alike — so the unconfigured-model guard cannot be bypassed.
|
|
1361
|
+
"""
|
|
1362
|
+
if self._workflow.workflow_mode:
|
|
1363
|
+
return
|
|
1364
|
+
if self._model_unconfigured():
|
|
1365
|
+
# Every submit path clears the composer before reaching here; hand
|
|
1366
|
+
# the text back so the blocked message is not lost. The hand-back
|
|
1367
|
+
# travels as a message, so a draft typed in the meantime wins.
|
|
1368
|
+
self.query_one(InputBar).restore_draft(text)
|
|
1369
|
+
self._show_model_unconfigured_dialog()
|
|
1370
|
+
return
|
|
1371
|
+
self._input_flow.submit_user_text(text)
|
|
1372
|
+
|
|
1373
|
+
def action_chat_page_up(self) -> None:
|
|
1374
|
+
"""Scroll the transcript up one page, even while the input is focused."""
|
|
1375
|
+
panel = self.query_one(ChatPanel)
|
|
1376
|
+
panel.note_user_scroll()
|
|
1377
|
+
panel.scroll_page_up(animate=False)
|
|
1378
|
+
|
|
1379
|
+
def action_chat_page_down(self) -> None:
|
|
1380
|
+
"""Scroll the transcript down one page, even while the input is focused."""
|
|
1381
|
+
panel = self.query_one(ChatPanel)
|
|
1382
|
+
panel.note_user_scroll()
|
|
1383
|
+
panel.scroll_page_down(animate=False)
|
|
1384
|
+
|
|
1385
|
+
def action_chat_scroll_bottom(self) -> None:
|
|
1386
|
+
"""Scroll the transcript to the bottom, even while the input is focused."""
|
|
1387
|
+
self.query_one(ChatPanel).jump_to_bottom()
|
|
1388
|
+
|
|
1389
|
+
# ------------------------------------------------------------------ #
|
|
1390
|
+
# Message editor
|
|
1391
|
+
# ------------------------------------------------------------------ #
|
|
1392
|
+
|
|
1393
|
+
@on(InputBar.EditorRequested)
|
|
1394
|
+
def _on_editor_requested(self, _event: InputBar.EditorRequested) -> None:
|
|
1395
|
+
if isinstance(self.app.screen, EditorDialog):
|
|
1396
|
+
return
|
|
1397
|
+
input_bar = self.query_one(InputBar)
|
|
1398
|
+
snapshot = input_bar.snapshot_draft()
|
|
1399
|
+
if len(snapshot.text) > MESSAGE_EDITOR_MAX_CHARACTERS:
|
|
1400
|
+
self._view_adapter.notify(
|
|
1401
|
+
_EDITOR_DRAFT_TOO_LARGE.bind(limit=f"{MESSAGE_EDITOR_MAX_CHARACTERS:,}"),
|
|
1402
|
+
title=_EDITOR_DRAFT_TOO_LARGE_TITLE.bind(),
|
|
1403
|
+
severity="warning",
|
|
1404
|
+
)
|
|
1405
|
+
return
|
|
1406
|
+
dialog = EditorDialog(
|
|
1407
|
+
EditorBufferSnapshot(snapshot.text, snapshot.cursor_location, snapshot.revision),
|
|
1408
|
+
mode=self._editor_mode,
|
|
1409
|
+
can_commit=input_bar.can_commit_editor_draft,
|
|
1410
|
+
current_draft_revision=lambda: input_bar.draft_revision,
|
|
1411
|
+
)
|
|
1412
|
+
self._view_adapter.push_screen(dialog, self._on_editor_result)
|
|
1413
|
+
|
|
1414
|
+
def _on_editor_result(self, result: EditorDialogResult) -> None:
|
|
1415
|
+
if result.mode is not self._editor_mode:
|
|
1416
|
+
self._editor_mode = result.mode
|
|
1417
|
+
persist_editor_keymap(result.mode.value)
|
|
1418
|
+
# The same write-point contract as theme and locale: the shared
|
|
1419
|
+
# settings handle records the choice as a RUNTIME override, so
|
|
1420
|
+
# settings keep describing the screen even when the swallowed
|
|
1421
|
+
# disk write above failed.
|
|
1422
|
+
if self._on_editor_keymap_changed is not None:
|
|
1423
|
+
self._on_editor_keymap_changed(result.mode.value)
|
|
1424
|
+
if not result.accepted:
|
|
1425
|
+
return
|
|
1426
|
+
input_bar = self.query_one(InputBar)
|
|
1427
|
+
input_bar.replace_draft(result.text)
|
|
1428
|
+
input_bar.focus_input()
|
|
1429
|
+
|
|
1430
|
+
# ------------------------------------------------------------------ #
|
|
1431
|
+
# Shell mode
|
|
1432
|
+
# ------------------------------------------------------------------ #
|
|
1433
|
+
|
|
1434
|
+
@on(InputBar.ShellModeRequested)
|
|
1435
|
+
def _on_shell_mode_requested(self, _event: InputBar.ShellModeRequested) -> None:
|
|
1436
|
+
if self.theme_editor is not None:
|
|
1437
|
+
self.theme_editor.request_close(after_close=self._enter_shell_after_editor)
|
|
1438
|
+
else:
|
|
1439
|
+
self.shell_mode_state = True
|
|
1440
|
+
|
|
1441
|
+
def _enter_shell_after_editor(self) -> None:
|
|
1442
|
+
self._shell_requested_after_editor = True
|
|
1443
|
+
self._resume_shell_request()
|
|
1444
|
+
|
|
1445
|
+
def _resume_shell_request(self) -> None:
|
|
1446
|
+
# A dialog may have opened while the dock was unmounting. Wait for
|
|
1447
|
+
# MainScreen to resume before changing the terminal layout or focus.
|
|
1448
|
+
if self._shell_requested_after_editor and self.is_mounted and self.app.screen is self:
|
|
1449
|
+
self._shell_requested_after_editor = False
|
|
1450
|
+
if self.theme_editor is None:
|
|
1451
|
+
self.shell_mode_state = True
|
|
1452
|
+
|
|
1453
|
+
def _set_shell_mode_state(self, active: bool) -> None:
|
|
1454
|
+
self.shell_mode_state = active
|
|
1455
|
+
|
|
1456
|
+
def _sync_sidebar_tab_strip_focus(self) -> None:
|
|
1457
|
+
"""Make the sidebar tab strip focusable exactly where sidebar focus is not handed back.
|
|
1458
|
+
|
|
1459
|
+
Asks what ``on_descendant_focus`` asks: workflow mode returns before the shell
|
|
1460
|
+
controller, whose ``keeps_panel_focus`` covers shell mode and the fullscreen terminal.
|
|
1461
|
+
Focus itself moves only with the view switch that follows each mode change
|
|
1462
|
+
(``exit_shell_mode`` returns it to a visible input).
|
|
1463
|
+
"""
|
|
1464
|
+
self.query_one(SidebarPanel).set_tab_strip_focusable(
|
|
1465
|
+
self._workflow.workflow_mode or self._shell_mode_controller.keeps_panel_focus()
|
|
1466
|
+
)
|
|
1467
|
+
|
|
1468
|
+
@on(Terminal.EscapeExited)
|
|
1469
|
+
def _on_terminal_escape_exited(self, _event: Terminal.EscapeExited) -> None:
|
|
1470
|
+
"""User double-tapped Escape in terminal — exit shell mode."""
|
|
1471
|
+
self._shell_mode_controller.exit_on_escape()
|
|
1472
|
+
|
|
1473
|
+
@on(ShellPanel.CommandExecuted)
|
|
1474
|
+
def _on_shell_command_executed(self, event: ShellPanel.CommandExecuted) -> None:
|
|
1475
|
+
"""Shell command recorded via shell integration hooks."""
|
|
1476
|
+
event.stop()
|
|
1477
|
+
self._shell_mode_controller.command_executed(event.command)
|
|
1478
|
+
|
|
1479
|
+
@on(Terminal.AlternateScreenChanged)
|
|
1480
|
+
def _on_alternate_screen_changed(self, event: Terminal.AlternateScreenChanged) -> None:
|
|
1481
|
+
"""When interactive app (vim/htop) activates, enter fullscreen terminal mode."""
|
|
1482
|
+
event.stop()
|
|
1483
|
+
self._shell_mode_controller.set_alternate_screen_active(event.enabled)
|
|
1484
|
+
|
|
1485
|
+
@on(ShellPanel.Exited)
|
|
1486
|
+
def _on_shell_exited(self, _event: ShellPanel.Exited) -> None:
|
|
1487
|
+
"""Shell process exited — exit shell mode."""
|
|
1488
|
+
self._shell_mode_controller.exit_on_shell_closed()
|
|
1489
|
+
|
|
1490
|
+
# ------------------------------------------------------------------ #
|
|
1491
|
+
# Suggestion list (/ commands, @ files, # agents, $ models)
|
|
1492
|
+
# ------------------------------------------------------------------ #
|
|
1493
|
+
|
|
1494
|
+
@on(InputBar.SlashTriggered)
|
|
1495
|
+
def _on_slash_triggered(self, _event: InputBar.SlashTriggered) -> None:
|
|
1496
|
+
self._suggestions.on_slash_triggered()
|
|
1497
|
+
|
|
1498
|
+
@on(InputBar.FileTriggered)
|
|
1499
|
+
def _on_file_triggered(self, _event: InputBar.FileTriggered) -> None:
|
|
1500
|
+
self._suggestions.on_file_triggered()
|
|
1501
|
+
|
|
1502
|
+
@on(InputBar.AgentTriggered)
|
|
1503
|
+
def _on_agent_triggered(self, _event: InputBar.AgentTriggered) -> None:
|
|
1504
|
+
self._suggestions.on_agent_triggered()
|
|
1505
|
+
|
|
1506
|
+
@on(InputBar.ModelTriggered)
|
|
1507
|
+
def _on_model_triggered(self, _event: InputBar.ModelTriggered) -> None:
|
|
1508
|
+
self._suggestions.on_model_triggered()
|
|
1509
|
+
|
|
1510
|
+
def action_prompt_history(self) -> None:
|
|
1511
|
+
"""Open the cross-session prompt-history suggestion list."""
|
|
1512
|
+
if self._state.shell.fullscreen_terminal or self._state.shell.active or not self._chat_foreground():
|
|
1513
|
+
return
|
|
1514
|
+
input_bar = self.query_one(InputBar)
|
|
1515
|
+
if input_bar.locked:
|
|
1516
|
+
return
|
|
1517
|
+
input_bar.focus_input()
|
|
1518
|
+
revision = self._suggestions.start_prompt_history()
|
|
1519
|
+
self._show_prompt_history(revision)
|
|
1520
|
+
|
|
1521
|
+
@work(thread=False)
|
|
1522
|
+
async def _show_prompt_history(self, revision: int) -> None:
|
|
1523
|
+
await self._suggestions.show_prompt_history_async(revision=revision)
|
|
1524
|
+
|
|
1525
|
+
@on(InputBar.TextChanged)
|
|
1526
|
+
def _on_text_changed_for_suggestions(self, event: InputBar.TextChanged) -> None:
|
|
1527
|
+
self._suggestions.on_text_changed(event.text)
|
|
1528
|
+
|
|
1529
|
+
@on(InputBar.SuggestionNavigate)
|
|
1530
|
+
def _on_suggestion_navigate(self, event: InputBar.SuggestionNavigate) -> None:
|
|
1531
|
+
self._suggestions.on_suggestion_navigate(event.direction)
|
|
1532
|
+
|
|
1533
|
+
@on(InputBar.SuggestionSelect)
|
|
1534
|
+
def _on_suggestion_select(self, event: InputBar.SuggestionSelect) -> None:
|
|
1535
|
+
self._suggestions.on_suggestion_select(event.execute)
|
|
1536
|
+
|
|
1537
|
+
@on(SuggestionList.Selected)
|
|
1538
|
+
def _on_suggestion_selected(self, event: SuggestionList.Selected) -> None:
|
|
1539
|
+
self._suggestions.on_suggestion_selected(event.mode, event.text, event.execute, event.kind)
|
|
1540
|
+
|
|
1541
|
+
@on(InputBar.SuggestionDismiss)
|
|
1542
|
+
@on(SuggestionList.Dismissed)
|
|
1543
|
+
def _on_suggestion_dismissed(self, _event: InputBar.SuggestionDismiss | SuggestionList.Dismissed) -> None:
|
|
1544
|
+
self._suggestions.dismiss_suggestions()
|
|
1545
|
+
|
|
1546
|
+
# ------------------------------------------------------------------ #
|
|
1547
|
+
# Input bar action buttons
|
|
1548
|
+
# ------------------------------------------------------------------ #
|
|
1549
|
+
|
|
1550
|
+
@on(InputBar.InterruptRequested)
|
|
1551
|
+
def _on_interrupt_requested(self, _event: InputBar.InterruptRequested) -> None:
|
|
1552
|
+
"""Stop button clicked — interrupt the running agent."""
|
|
1553
|
+
self.action_interrupt()
|
|
1554
|
+
|
|
1555
|
+
@on(InputBar.RetryRequested)
|
|
1556
|
+
def _on_retry_requested(self, event: InputBar.RetryRequested) -> None:
|
|
1557
|
+
"""Retry button clicked — retry from current state.
|
|
1558
|
+
|
|
1559
|
+
``event.text`` carries any note the user typed before clicking
|
|
1560
|
+
Continue/Retry. When non-empty, the engine uses it as the
|
|
1561
|
+
mid-turn continuation prompt.
|
|
1562
|
+
"""
|
|
1563
|
+
if self._workflow.workflow_mode:
|
|
1564
|
+
return
|
|
1565
|
+
if self._model_unconfigured():
|
|
1566
|
+
# A retry re-runs the turn against the model just like a fresh
|
|
1567
|
+
# submit. The composer already consumed the typed note, so hand
|
|
1568
|
+
# it back — unless a draft landed in the meantime, which wins; the
|
|
1569
|
+
# inline status action stays visible for after the user configures
|
|
1570
|
+
# a model.
|
|
1571
|
+
self.query_one(InputBar).restore_draft(event.text)
|
|
1572
|
+
self._show_model_unconfigured_dialog()
|
|
1573
|
+
return
|
|
1574
|
+
self._input_flow.request_retry(event.text)
|
|
1575
|
+
|
|
1576
|
+
@on(ConversationStatusAction.Pressed)
|
|
1577
|
+
def _on_conversation_status_action_pressed(self, event: ConversationStatusAction.Pressed) -> None:
|
|
1578
|
+
"""Retry/continue button clicked from an error or interrupted status.
|
|
1579
|
+
|
|
1580
|
+
Resumes without a continuation prompt: any draft typed in the input
|
|
1581
|
+
bar stays there. Sending the draft as the mid-turn prompt is the
|
|
1582
|
+
input bar's Continue button (``InputBar.RetryRequested``).
|
|
1583
|
+
"""
|
|
1584
|
+
event.stop()
|
|
1585
|
+
input_bar = self.query_one(InputBar)
|
|
1586
|
+
if self._state.run.agent_loading or not input_bar.retry_mode:
|
|
1587
|
+
return
|
|
1588
|
+
if self._model_unconfigured():
|
|
1589
|
+
self._show_model_unconfigured_dialog()
|
|
1590
|
+
return
|
|
1591
|
+
self._input_flow.request_retry("")
|
|
1592
|
+
|
|
1593
|
+
@on(ToolViewRequested)
|
|
1594
|
+
def _on_tool_view_requested(self, event: ToolViewRequested) -> None:
|
|
1595
|
+
"""Open a detailed tool-view modal requested by a tool widget."""
|
|
1596
|
+
from chrys.app.tui.screens.dialogs.tool_view import ToolDetailModal
|
|
1597
|
+
|
|
1598
|
+
event.stop()
|
|
1599
|
+
self.app.push_screen(ToolDetailModal.from_request(event))
|
|
1600
|
+
|
|
1601
|
+
@on(InputBar.NewSessionRequested)
|
|
1602
|
+
def _on_new_session_requested(self, _event: InputBar.NewSessionRequested) -> None:
|
|
1603
|
+
"""New button clicked — start a fresh session."""
|
|
1604
|
+
if self._state.run.agent_running or self._state.run.agent_loading or self._services.execution_busy():
|
|
1605
|
+
return
|
|
1606
|
+
self._create_new_session()
|
|
1607
|
+
|
|
1608
|
+
@on(StatusBar.DetailsClicked)
|
|
1609
|
+
def _on_status_details_clicked(self, _event: StatusBar.DetailsClicked) -> None:
|
|
1610
|
+
"""Open the active runtime details modal."""
|
|
1611
|
+
self.action_runtime_details()
|
|
1612
|
+
|
|
1613
|
+
@work(thread=False)
|
|
1614
|
+
async def _create_new_session(self) -> None:
|
|
1615
|
+
if self._state.run.agent_loading:
|
|
1616
|
+
return
|
|
1617
|
+
self._session_title.set_terminal_title_for_cwd()
|
|
1618
|
+
await self._sessions.create_new_session()
|
|
1619
|
+
|
|
1620
|
+
@work(thread=False)
|
|
1621
|
+
async def _resume_last_session(self) -> None:
|
|
1622
|
+
if self._state.run.agent_loading:
|
|
1623
|
+
return
|
|
1624
|
+
await self._sessions.resume_last_session()
|
|
1625
|
+
|
|
1626
|
+
@work(thread=False)
|
|
1627
|
+
async def _fork_current_session(self) -> None:
|
|
1628
|
+
await self._sessions.fork_current_session()
|
|
1629
|
+
|
|
1630
|
+
def _clear_workspace_marker_for_user_message(self) -> None:
|
|
1631
|
+
self._state.workspace_marker.original_cwd = None
|
|
1632
|
+
|
|
1633
|
+
def _commit_profile_switch_marker(self) -> None:
|
|
1634
|
+
# Once messages are exchanged, the switch indicator becomes permanent.
|
|
1635
|
+
# The next switch will use a new unique key, so the old widget stays
|
|
1636
|
+
# untouched in the chat.
|
|
1637
|
+
if self._state.profile_marker.from_profile is not None:
|
|
1638
|
+
self._state.profile_marker.from_profile = None
|
|
1639
|
+
self._state.profile_marker.to_profile = None
|
|
1640
|
+
|
|
1641
|
+
# ------------------------------------------------------------------ #
|
|
1642
|
+
# Header and status-bar tags, model switching ($)
|
|
1643
|
+
# ------------------------------------------------------------------ #
|
|
1644
|
+
|
|
1645
|
+
@on(AppHeader.ApprovalBadgeClicked)
|
|
1646
|
+
def _on_approval_badge_clicked(self, _event: AppHeader.ApprovalBadgeClicked) -> None:
|
|
1647
|
+
"""Open the approval mode picker modal."""
|
|
1648
|
+
self._config_actions.on_approval_badge_clicked()
|
|
1649
|
+
|
|
1650
|
+
@on(StatusBar.ProfileTagClicked)
|
|
1651
|
+
def _on_profile_tag_clicked(self, _event: StatusBar.ProfileTagClicked) -> None:
|
|
1652
|
+
"""Open the agent picker modal."""
|
|
1653
|
+
self._config_actions.on_profile_tag_clicked()
|
|
1654
|
+
|
|
1655
|
+
@on(StatusBar.ModelTagClicked)
|
|
1656
|
+
def _on_model_tag_clicked(self, event: StatusBar.ModelTagClicked) -> None:
|
|
1657
|
+
"""Route the model tag's semantic action."""
|
|
1658
|
+
if not self._workflow.workflow_mode:
|
|
1659
|
+
self._config_actions.on_model_tag_clicked(event.mode)
|
|
1660
|
+
|
|
1661
|
+
@on(StatusBar.SelectorBusy)
|
|
1662
|
+
def _on_selector_busy(self, event: StatusBar.SelectorBusy) -> None:
|
|
1663
|
+
"""Explain why a selector ignored a click during a run."""
|
|
1664
|
+
if not self._workflow.workflow_mode:
|
|
1665
|
+
self._config_actions.on_selector_busy(event.selector)
|
|
1666
|
+
|
|
1667
|
+
@work(thread=False)
|
|
1668
|
+
async def _switch_model_profile(self, profile_id: str) -> None:
|
|
1669
|
+
"""Commit a Chat model suggestion only while its owner remains active."""
|
|
1670
|
+
if self._workflow.workflow_mode or not self._model_selection_is_committable(profile_id):
|
|
1671
|
+
return
|
|
1672
|
+
await self._config_actions.on_model_picked(profile_id)
|
|
1673
|
+
|
|
1674
|
+
def _model_selection_is_committable(self, profile_id: str) -> bool:
|
|
1675
|
+
"""Re-check every guard the displayed suggestion rows were built on.
|
|
1676
|
+
|
|
1677
|
+
The suggestion popup outlives a pushed screen, so between the rows
|
|
1678
|
+
being built and one being chosen F4 can retire the profile and F2 can
|
|
1679
|
+
hand model ownership to an agent. ``activate_model_profile`` persists
|
|
1680
|
+
whatever id it is handed, so the last word has to be taken here rather
|
|
1681
|
+
than trusted from a row that may have gone stale on screen.
|
|
1682
|
+
"""
|
|
1683
|
+
if self._state.run.agent_running or self._state.run.agent_loading or self._services.execution_busy():
|
|
1684
|
+
return False
|
|
1685
|
+
if is_model_selection_locked(
|
|
1686
|
+
self._state.runtime.details.model,
|
|
1687
|
+
runtime_confirmed=self._state.runtime.details_confirmed,
|
|
1688
|
+
):
|
|
1689
|
+
return False
|
|
1690
|
+
registry = self._services.model_registry
|
|
1691
|
+
profile = None if registry is None else registry.get(profile_id)
|
|
1692
|
+
return profile is not None and is_model_profile_selectable(profile)
|
|
1693
|
+
|
|
1694
|
+
# ------------------------------------------------------------------ #
|
|
1695
|
+
# Model config (/model)
|
|
1696
|
+
# ------------------------------------------------------------------ #
|
|
1697
|
+
|
|
1698
|
+
def _open_model_config(self) -> None:
|
|
1699
|
+
"""Open the model configuration modal."""
|
|
1700
|
+
self._config_actions.open_model_config()
|
|
1701
|
+
|
|
1702
|
+
# ------------------------------------------------------------------ #
|
|
1703
|
+
# Agent config (/agents with optional tab subcommands)
|
|
1704
|
+
# ------------------------------------------------------------------ #
|
|
1705
|
+
|
|
1706
|
+
def action_agents_config(self) -> None:
|
|
1707
|
+
"""Open the agent configuration modal (F2)."""
|
|
1708
|
+
self._open_agent_config()
|
|
1709
|
+
|
|
1710
|
+
def action_models_config(self) -> None:
|
|
1711
|
+
"""Open the model configuration modal (F4)."""
|
|
1712
|
+
self._open_model_config()
|
|
1713
|
+
|
|
1714
|
+
def action_settings(self) -> None:
|
|
1715
|
+
"""Open the Settings dialog (F10)."""
|
|
1716
|
+
self._open_settings()
|
|
1717
|
+
|
|
1718
|
+
def action_runtime_details(self) -> None:
|
|
1719
|
+
"""Open the active runtime details modal."""
|
|
1720
|
+
self._config_actions.open_runtime_details()
|
|
1721
|
+
|
|
1722
|
+
def _open_settings(self, initial_tab: str = GENERAL_TAB_ID) -> None:
|
|
1723
|
+
"""Open the Settings dialog at *initial_tab*."""
|
|
1724
|
+
self._config_actions.open_settings(initial_tab)
|
|
1725
|
+
|
|
1726
|
+
def _schedule_notification_settings_save(self, settings: NotificationSettings) -> None:
|
|
1727
|
+
"""Record the live choice, then persist it after a short debounce."""
|
|
1728
|
+
cast("ChrysApp", self.app).override_notification_settings(settings)
|
|
1729
|
+
self._settings_persistence().schedule(settings.to_settings_patch())
|
|
1730
|
+
|
|
1731
|
+
async def _flush_settings_save(self) -> None:
|
|
1732
|
+
"""Force any debounced settings write to complete now."""
|
|
1733
|
+
await self._settings_persistence().flush()
|
|
1734
|
+
|
|
1735
|
+
def _open_agent_config(self) -> None:
|
|
1736
|
+
"""Open the unified agent configuration modal."""
|
|
1737
|
+
self._config_actions.open_agent_config()
|
|
1738
|
+
|
|
1739
|
+
def _open_agent_config_tab(self, tab: str) -> None:
|
|
1740
|
+
"""Open the agent configuration modal at a specific tab."""
|
|
1741
|
+
self._config_actions.open_agent_config_tab(tab)
|
|
1742
|
+
|
|
1743
|
+
@on(ChatPanel.TitleClicked)
|
|
1744
|
+
def _on_chat_panel_title_clicked(self, _event: ChatPanel.TitleClicked) -> None:
|
|
1745
|
+
"""Open the session title editor when the user clicks the border title."""
|
|
1746
|
+
self._session_title.open_editor()
|
|
1747
|
+
|
|
1748
|
+
@on(WorkflowPanel.WorkingDirClicked)
|
|
1749
|
+
@on(ChatPanel.WorkingDirClicked)
|
|
1750
|
+
def _on_chat_panel_working_dir_clicked(
|
|
1751
|
+
self, _event: ChatPanel.WorkingDirClicked | WorkflowPanel.WorkingDirClicked
|
|
1752
|
+
) -> None:
|
|
1753
|
+
"""Open the file dialog when the user clicks the working directory subtitle."""
|
|
1754
|
+
self._workspace_actions.open_working_dir_picker()
|
|
1755
|
+
|
|
1756
|
+
def _copy_agent_responses(self, arg: str) -> None:
|
|
1757
|
+
"""Handle /copy slash command — copy agent, user, or full-conversation turns.
|
|
1758
|
+
|
|
1759
|
+
Supported forms are ``/copy [N]``, ``/copy agent [N|all]``,
|
|
1760
|
+
``/copy user [N|all]``, and ``/copy all``. The legacy ``/copy N``
|
|
1761
|
+
form remains equivalent to ``/copy agent N``.
|
|
1762
|
+
|
|
1763
|
+
Writes to both the host OS clipboard (pbcopy/clip.exe/xclip) and the terminal's
|
|
1764
|
+
OSC 52 clipboard. OSC 52 is what lets the copy reach the user's actual terminal
|
|
1765
|
+
clipboard in SSH / remote-tty sessions, where the native tools would otherwise
|
|
1766
|
+
copy to the remote host's clipboard (or fail outright).
|
|
1767
|
+
"""
|
|
1768
|
+
self._copy_actions.copy_agent_responses(arg)
|
|
1769
|
+
|
|
1770
|
+
def _toggle_fold(self) -> None:
|
|
1771
|
+
"""Handle /fold slash command — collapse or expand all tool groups."""
|
|
1772
|
+
self._copy_actions.toggle_fold()
|
|
1773
|
+
|
|
1774
|
+
def action_show_rollback(self, arg: str = "") -> None:
|
|
1775
|
+
"""Open the picker or directly execute an explicit target.
|
|
1776
|
+
|
|
1777
|
+
- ``/rollback`` with no argument: open the modal with the last
|
|
1778
|
+
turn pre-selected.
|
|
1779
|
+
- ``/rollback N``: directly discard the most recent ``N`` turns.
|
|
1780
|
+
- ``/rollback to N``: directly keep turns ``1..N`` and discard later turns.
|
|
1781
|
+
- Explicit targets restore eligible file changes by default and run
|
|
1782
|
+
under a blocking loading modal. Use the picker for conversation-only
|
|
1783
|
+
rollback.
|
|
1784
|
+
"""
|
|
1785
|
+
self._rollback_controller.show_rollback(arg)
|
|
1786
|
+
|
|
1787
|
+
def action_show_diff(self) -> None:
|
|
1788
|
+
"""Open the full-screen diff viewer showing file changes per turn."""
|
|
1789
|
+
if self._workflow.workflow_mode:
|
|
1790
|
+
self._diff_controller.show_workflow_diff(
|
|
1791
|
+
self._workflow.session_id, self._workflow.project_cwd or self._workflow.cwd()
|
|
1792
|
+
)
|
|
1793
|
+
else:
|
|
1794
|
+
self._diff_controller.show_diff()
|
|
1795
|
+
|
|
1796
|
+
# ------------------------------------------------------------------ #
|
|
1797
|
+
# Actions (keybindings)
|
|
1798
|
+
# ------------------------------------------------------------------ #
|
|
1799
|
+
|
|
1800
|
+
def _set_agent_running(self, running: bool) -> None:
|
|
1801
|
+
if MainScreen._reactive_state_ready(self):
|
|
1802
|
+
self.agent_running_state = running
|
|
1803
|
+
else:
|
|
1804
|
+
MainScreen._apply_agent_running_state(self, running)
|
|
1805
|
+
|
|
1806
|
+
def _apply_agent_running_state(self, running: bool) -> None:
|
|
1807
|
+
was_running = self._state.run.agent_running
|
|
1808
|
+
self._state.run.agent_running = running
|
|
1809
|
+
if running:
|
|
1810
|
+
self._state.run.turn_end_check_pending = True
|
|
1811
|
+
if not was_running:
|
|
1812
|
+
self._session_title.run_started()
|
|
1813
|
+
self._state.run.generation += 1
|
|
1814
|
+
self._live_diff.reset_for_run_start(
|
|
1815
|
+
LiveDiffOwner(
|
|
1816
|
+
session_id=self._view_adapter.current_chat_session_id(),
|
|
1817
|
+
session_generation=self._services.session_generation(),
|
|
1818
|
+
run_generation=self._state.run.generation,
|
|
1819
|
+
)
|
|
1820
|
+
)
|
|
1821
|
+
else:
|
|
1822
|
+
# Agent tool calls (write_file / edit_file / shell) may have
|
|
1823
|
+
# created or deleted files during the turn; drop the cached
|
|
1824
|
+
# ``@`` suggestion list so the next trigger re-scans. The
|
|
1825
|
+
# scan is lazy — invalidation is cheap; the real work only
|
|
1826
|
+
# runs when the user next types ``@``. Mirrors the
|
|
1827
|
+
# invalidation in ``on_workspace_updated``.
|
|
1828
|
+
self._suggestions.file_cache = None
|
|
1829
|
+
if self._state.run.turn_end_check_pending:
|
|
1830
|
+
self._state.run.turn_end_check_pending = False
|
|
1831
|
+
self._workspace_actions.check_after_turn()
|
|
1832
|
+
input_bar = self.query_one(InputBar)
|
|
1833
|
+
if not running and input_bar.locked:
|
|
1834
|
+
input_bar.unlock_and_keep()
|
|
1835
|
+
self.refresh_bindings()
|
|
1836
|
+
# Auto-dismiss interrupt confirmation when agent stops
|
|
1837
|
+
if not running and self._state.overlays.interrupt_confirm_active:
|
|
1838
|
+
self._navigation.dismiss_interrupt_confirm()
|
|
1839
|
+
self._session_title.sync_activity()
|
|
1840
|
+
|
|
1841
|
+
def _set_agent_loading(self, loading: bool) -> None:
|
|
1842
|
+
if MainScreen._reactive_state_ready(self):
|
|
1843
|
+
self.agent_loading_state = loading
|
|
1844
|
+
else:
|
|
1845
|
+
MainScreen._apply_agent_loading_state(self, loading)
|
|
1846
|
+
|
|
1847
|
+
def _apply_agent_loading_state(self, loading: bool) -> None:
|
|
1848
|
+
self._state.run.agent_loading = loading
|
|
1849
|
+
input_bar = self.query_one(InputBar)
|
|
1850
|
+
if not loading and input_bar.locked and not self._state.run.agent_running:
|
|
1851
|
+
input_bar.unlock_and_keep()
|
|
1852
|
+
|
|
1853
|
+
def _set_has_messages(self, has: bool) -> None:
|
|
1854
|
+
if MainScreen._reactive_state_ready(self):
|
|
1855
|
+
self.has_messages_state = has
|
|
1856
|
+
else:
|
|
1857
|
+
self._state.run.has_messages = has
|
|
1858
|
+
|
|
1859
|
+
def action_show_log_viewer(self) -> None:
|
|
1860
|
+
"""Open the log viewer modal (F6)."""
|
|
1861
|
+
self._navigation.show_log_viewer()
|
|
1862
|
+
|
|
1863
|
+
def action_open_guide(self) -> None:
|
|
1864
|
+
"""Open the user guide (F8), as ``/help`` does."""
|
|
1865
|
+
self._navigation.open_guide()
|
|
1866
|
+
|
|
1867
|
+
def action_copy_text(self) -> None:
|
|
1868
|
+
"""Copy selected text to both terminal and system clipboard."""
|
|
1869
|
+
if not self._copy_selected_text():
|
|
1870
|
+
# Match stock Screen semantics: an empty selection defers the
|
|
1871
|
+
# key to the next namespace (the app-level ctrl+c quit hint).
|
|
1872
|
+
raise SkipAction
|
|
1873
|
+
|
|
1874
|
+
def get_selected_text(self) -> str | None:
|
|
1875
|
+
"""Get selected text, excluding tool-renderer UI chrome and results.
|
|
1876
|
+
|
|
1877
|
+
Chat drags keep ``self.selections`` bounded to the visible slice of
|
|
1878
|
+
the transcript; the full anchored range is extracted here, once, from
|
|
1879
|
+
the logical selection model.
|
|
1880
|
+
"""
|
|
1881
|
+
chat_text = self._chat_selection.extract_text()
|
|
1882
|
+
if chat_text is not None:
|
|
1883
|
+
return chat_text
|
|
1884
|
+
if not self.selections:
|
|
1885
|
+
return None
|
|
1886
|
+
|
|
1887
|
+
widget_text: list[str] = []
|
|
1888
|
+
for widget, selection in self.selections.items():
|
|
1889
|
+
if is_tool_copy_excluded(widget):
|
|
1890
|
+
continue
|
|
1891
|
+
if widget.is_attached and (selected_text_in_widget := widget.get_selection(selection)) is not None:
|
|
1892
|
+
widget_text.extend(selected_text_in_widget)
|
|
1893
|
+
|
|
1894
|
+
if not widget_text:
|
|
1895
|
+
return None
|
|
1896
|
+
return "".join(widget_text).rstrip("\n")
|
|
1897
|
+
|
|
1898
|
+
def action_interrupt(self) -> None:
|
|
1899
|
+
self._publish_interrupt()
|
|
1900
|
+
|
|
1901
|
+
@work(thread=False)
|
|
1902
|
+
async def _publish_interrupt(self) -> None:
|
|
1903
|
+
if self._services.execution().kind == "workflow":
|
|
1904
|
+
self._workflow.run_control.stop()
|
|
1905
|
+
return
|
|
1906
|
+
await self._input_flow.publish_interrupt()
|
|
1907
|
+
|
|
1908
|
+
# -- Per-sub-agent retry/abort bridging ---------------------------
|
|
1909
|
+
#
|
|
1910
|
+
# The :class:`SubAgentToolCall` renderer posts widget-level Textual
|
|
1911
|
+
# ``Message`` subclasses when the user clicks its Retry/Abort buttons.
|
|
1912
|
+
# Those bubble up the DOM to this screen; Textual dispatches them by
|
|
1913
|
+
# snake_case method name (``on_sub_agent_retry_clicked`` matches
|
|
1914
|
+
# ``SubAgentRetryClicked``). The screen then translates them into
|
|
1915
|
+
# bus events so the engine can route by ``invocation_id``.
|
|
1916
|
+
|
|
1917
|
+
async def on_sub_agent_retry_clicked(self, event: SubAgentRetryClicked) -> None:
|
|
1918
|
+
"""Bubbled from :class:`SubAgentToolCall` Retry button."""
|
|
1919
|
+
event.stop()
|
|
1920
|
+
await self._tool_actions.request_sub_agent_retry(event.invocation_id)
|
|
1921
|
+
|
|
1922
|
+
async def on_sub_agent_abort_clicked(self, event: SubAgentAbortClicked) -> None:
|
|
1923
|
+
"""Bubbled from :class:`SubAgentToolCall` Abort button."""
|
|
1924
|
+
event.stop()
|
|
1925
|
+
await self._tool_actions.request_sub_agent_abort(event.invocation_id)
|
|
1926
|
+
|
|
1927
|
+
async def on_sleep_skip_clicked(self, event: SleepSkipClicked) -> None:
|
|
1928
|
+
"""Bubbled from :class:`SleepToolCall` Skip button."""
|
|
1929
|
+
event.stop()
|
|
1930
|
+
await self._tool_actions.skip_sleep(event.call_id)
|
|
1931
|
+
|
|
1932
|
+
async def on_ask_user_inline_submitted(self, event: AskUserInlineSubmitted) -> None:
|
|
1933
|
+
"""Bubbled from :class:`AskUserToolCall` inline response controls."""
|
|
1934
|
+
event.stop()
|
|
1935
|
+
await self._tool_actions.submit_ask_user_inline(event.request_id, event.answers)
|
|
1936
|
+
|
|
1937
|
+
def action_quit(self) -> None:
|
|
1938
|
+
self._navigation.quit()
|
|
1939
|
+
|
|
1940
|
+
def action_sessions(self) -> None:
|
|
1941
|
+
"""Open the sessions modal (Ctrl+N)."""
|
|
1942
|
+
self._navigation.sessions()
|
|
1943
|
+
|
|
1944
|
+
def _clear_current_session(self) -> None:
|
|
1945
|
+
"""/clear — confirm, then delete the current session and start fresh."""
|
|
1946
|
+
self._navigation.clear_session()
|
|
1947
|
+
|
|
1948
|
+
async def _delete_session_and_new(self, session_id: str) -> None:
|
|
1949
|
+
if not self._workflow.workflow_mode:
|
|
1950
|
+
await self._sessions.delete_current_and_new(session_id)
|
|
1951
|
+
return
|
|
1952
|
+
await self._workflow.session_view.delete_current_session(session_id)
|
|
1953
|
+
|
|
1954
|
+
def action_toggle_sidebar(self) -> None:
|
|
1955
|
+
self._navigation.toggle_sidebar()
|
|
1956
|
+
|
|
1957
|
+
def _update_toc(self) -> None:
|
|
1958
|
+
self._navigation.update_toc()
|
|
1959
|
+
|
|
1960
|
+
def action_pick_theme(self) -> None:
|
|
1961
|
+
"""Open the theme picker modal."""
|
|
1962
|
+
if self.theme_editor is not None:
|
|
1963
|
+
self.notify(
|
|
1964
|
+
render_str(self._language_localizer(), THEME_PREVIEW_HINT.bind()), severity="warning", markup=False
|
|
1965
|
+
)
|
|
1966
|
+
self.theme_editor.editor.focus_first_swatch()
|
|
1967
|
+
return
|
|
1968
|
+
self._navigation.pick_theme()
|
|
1969
|
+
|
|
1970
|
+
async def open_theme_editor(
|
|
1971
|
+
self,
|
|
1972
|
+
theme: Theme,
|
|
1973
|
+
store: UserThemeStore,
|
|
1974
|
+
revision: ThemeFileRevision | None = None,
|
|
1975
|
+
) -> None:
|
|
1976
|
+
"""Reserve a left dock before the header, transcript and input bar."""
|
|
1977
|
+
if self.theme_editor is not None:
|
|
1978
|
+
self.theme_editor.editor.focus_first_swatch()
|
|
1979
|
+
return
|
|
1980
|
+
|
|
1981
|
+
def closed() -> None:
|
|
1982
|
+
self.theme_editor = None
|
|
1983
|
+
self._set_theme_editor_compact(False)
|
|
1984
|
+
|
|
1985
|
+
panel = ThemeEditorPanel(theme, store, revision, on_close=closed)
|
|
1986
|
+
self.theme_editor = panel
|
|
1987
|
+
self._set_theme_editor_compact(self.size.width < 128)
|
|
1988
|
+
await self.mount(panel, before=0)
|
|
1989
|
+
|
|
1990
|
+
async def manage_themes(self, *, picker: ThemesScreen | None = None) -> None:
|
|
1991
|
+
"""Open theme management directly in the editor's document selector."""
|
|
1992
|
+
from chrys.app.tui.theme_loader import theme_is_read_only
|
|
1993
|
+
from chrys.app.tui.themes.store import ThemeStoreError, UserThemeStore
|
|
1994
|
+
|
|
1995
|
+
if self.theme_editor is not None:
|
|
1996
|
+
self.theme_editor.editor.focus_first_swatch()
|
|
1997
|
+
return
|
|
1998
|
+
app = cast("ChrysApp", self.app)
|
|
1999
|
+
store = UserThemeStore()
|
|
2000
|
+
theme = app.get_theme(picker.original_theme if picker is not None else app.theme)
|
|
2001
|
+
if theme is None:
|
|
2002
|
+
raise RuntimeError("The selected theme is not registered.")
|
|
2003
|
+
revision = None
|
|
2004
|
+
if not theme_is_read_only(theme.name):
|
|
2005
|
+
try:
|
|
2006
|
+
theme, revision = await asyncio.to_thread(store.load, theme.name)
|
|
2007
|
+
except ThemeStoreError as error:
|
|
2008
|
+
self.notify(render_str(app.locale_controller.localizer, error.display), severity="error", markup=False)
|
|
2009
|
+
return
|
|
2010
|
+
# Keep file reads outside the render batch. The picker may have been
|
|
2011
|
+
# cancelled or covered while awaiting them; do not mount behind a modal.
|
|
2012
|
+
if picker is None:
|
|
2013
|
+
if app.screen is self:
|
|
2014
|
+
await self.open_theme_editor(theme, store, revision)
|
|
2015
|
+
return
|
|
2016
|
+
with app.batch_update():
|
|
2017
|
+
self._theme_editor_opening = True
|
|
2018
|
+
try:
|
|
2019
|
+
restoring_theme = app.theme != picker.original_theme
|
|
2020
|
+
if not picker.dismiss_for_editor():
|
|
2021
|
+
return
|
|
2022
|
+
if restoring_theme:
|
|
2023
|
+
# Finish the queued CSS/signal work before constructing
|
|
2024
|
+
# the dock, so its children receive the final styles once.
|
|
2025
|
+
restored = asyncio.Event()
|
|
2026
|
+
app.call_next(restored.set)
|
|
2027
|
+
await restored.wait()
|
|
2028
|
+
if app.screen is not self:
|
|
2029
|
+
return
|
|
2030
|
+
await self.open_theme_editor(theme, store, revision)
|
|
2031
|
+
# Drain pending mount/layout messages on MainScreen before the
|
|
2032
|
+
# batch's first paint, rather than reflowing again for late ones.
|
|
2033
|
+
mounted = asyncio.Event()
|
|
2034
|
+
if self.call_later(mounted.set):
|
|
2035
|
+
await mounted.wait()
|
|
2036
|
+
finally:
|
|
2037
|
+
self._theme_editor_opening = False
|
|
2038
|
+
|
|
2039
|
+
def action_pick_language(self) -> None:
|
|
2040
|
+
"""Open the no-preview language picker modal."""
|
|
2041
|
+
self._navigation.pick_language()
|
|
2042
|
+
|
|
2043
|
+
def action_set_language(self, requested_locale: str) -> None:
|
|
2044
|
+
"""Confirm a requested locale from a slash-command argument."""
|
|
2045
|
+
self._navigation.set_language(requested_locale)
|
|
2046
|
+
|
|
2047
|
+
def action_toggle_trajectory_dashboard(self) -> None:
|
|
2048
|
+
"""Toggle the in-place trajectory dashboard (F12)."""
|
|
2049
|
+
self._navigation.toggle_trajectory_dashboard()
|
|
2050
|
+
|
|
2051
|
+
def action_escape(self) -> None:
|
|
2052
|
+
"""Unified Esc handler — dismiss overlays, confirm interrupt, or confirm exit."""
|
|
2053
|
+
if self.theme_editor is not None:
|
|
2054
|
+
if self.theme_editor.editor.dismiss_dropdown():
|
|
2055
|
+
return
|
|
2056
|
+
self._navigation.escape(on_idle=self.theme_editor.action_close)
|
|
2057
|
+
return
|
|
2058
|
+
if self._suggestions_active_for_bindings():
|
|
2059
|
+
self._suggestions.dismiss_suggestions()
|
|
2060
|
+
return
|
|
2061
|
+
if not self._dashboard_visible() and self._workflow.back():
|
|
2062
|
+
return
|
|
2063
|
+
self._navigation.escape()
|
|
2064
|
+
|
|
2065
|
+
# ------------------------------------------------------------------ #
|
|
2066
|
+
# Approval response
|
|
2067
|
+
# ------------------------------------------------------------------ #
|
|
2068
|
+
|
|
2069
|
+
@work(thread=False)
|
|
2070
|
+
async def _handle_approval_response(
|
|
2071
|
+
self,
|
|
2072
|
+
request_id: str,
|
|
2073
|
+
approved: bool,
|
|
2074
|
+
reason: str = "",
|
|
2075
|
+
modified_args: dict[str, Any] | None = None,
|
|
2076
|
+
remember_choice: ReuseChoice = "",
|
|
2077
|
+
) -> None:
|
|
2078
|
+
await self._tool_actions.publish_approval_response(request_id, approved, reason, modified_args, remember_choice)
|
|
2079
|
+
|
|
2080
|
+
@work(thread=False)
|
|
2081
|
+
async def _handle_ask_user_response(self, request_id: str, answers: tuple[AskUserAnswer, ...]) -> None:
|
|
2082
|
+
await self._tool_actions.publish_ask_user_response(request_id, answers)
|
|
2083
|
+
|
|
2084
|
+
def _question_inline_preferred(self) -> bool:
|
|
2085
|
+
"""``tools.ask_user.inline`` as the Settings panel shows it right now.
|
|
2086
|
+
|
|
2087
|
+
A panel write lands in the store at once but reaches the in-force
|
|
2088
|
+
settings only after the pending reload (turn end + panel close); a
|
|
2089
|
+
question arriving in between honours what the user just ticked.
|
|
2090
|
+
"""
|
|
2091
|
+
coordinator = self._existing_settings_coordinator()
|
|
2092
|
+
if coordinator is not None:
|
|
2093
|
+
return bool(coordinator.projected_value("tools.ask_user.inline"))
|
|
2094
|
+
return cast("ChrysApp", self.app).settings_handle.settings.ask_user_inline
|
|
2095
|
+
|
|
2096
|
+
# ------------------------------------------------------------------ #
|
|
2097
|
+
# Debug logging helper
|
|
2098
|
+
# ------------------------------------------------------------------ #
|
|
2099
|
+
|
|
2100
|
+
def _debug(self, event_type: str, detail: str = "") -> None:
|
|
2101
|
+
"""Log an event to the debug panel (if mounted)."""
|
|
2102
|
+
try:
|
|
2103
|
+
sp = self.query_one(SidebarPanel)
|
|
2104
|
+
sp.debug_panel.log_event(event_type, detail)
|
|
2105
|
+
except Exception:
|
|
2106
|
+
pass
|
|
2107
|
+
|
|
2108
|
+
@on(ConversationToc.TurnSelected)
|
|
2109
|
+
def _on_toc_turn_selected(self, event: ConversationToc.TurnSelected) -> None:
|
|
2110
|
+
self._navigation.scroll_to_turn(event.turn_id)
|
|
2111
|
+
|
|
2112
|
+
@on(TrajectoryDashboard.StateChanged)
|
|
2113
|
+
def _on_trajectory_dashboard_state_changed(self, event: TrajectoryDashboard.StateChanged) -> None:
|
|
2114
|
+
event.stop()
|
|
2115
|
+
self._view_adapter.sync_main_surface()
|
|
2116
|
+
|
|
2117
|
+
@on(AppHeader.ModeClicked)
|
|
2118
|
+
def _on_mode_clicked(self) -> None:
|
|
2119
|
+
if not self._can_switch_app_mode():
|
|
2120
|
+
# Shell mode is the one block with no run to name; it stays silent.
|
|
2121
|
+
if (notice := self._app_mode_busy_notice()) is not None:
|
|
2122
|
+
self._view_adapter.notify(notice, title=workflow_text.MODE_BUSY_TITLE.bind(), severity="warning")
|
|
2123
|
+
return
|
|
2124
|
+
|
|
2125
|
+
def selected(workflow: bool | None) -> None:
|
|
2126
|
+
if workflow is not None:
|
|
2127
|
+
self._set_workflow_mode(workflow)
|
|
2128
|
+
|
|
2129
|
+
self.app.push_screen(
|
|
2130
|
+
AppModeDialog(self._workflow.workflow_mode, locale_controller=self._locale_controller), selected
|
|
2131
|
+
)
|
|
2132
|
+
|
|
2133
|
+
def action_workflow(self) -> None:
|
|
2134
|
+
if not self._workflow.workflow_mode and not self._can_switch_app_mode():
|
|
2135
|
+
return
|
|
2136
|
+
self._set_workflow_mode(True)
|
|
2137
|
+
self._workflow.browser.enter_selection()
|
|
2138
|
+
|
|
2139
|
+
def _can_switch_app_mode(self) -> bool:
|
|
2140
|
+
"""Keep one presentation mode from submission through execution cleanup."""
|
|
2141
|
+
return not self._state.shell.active and self._app_mode_busy_notice() is None
|
|
2142
|
+
|
|
2143
|
+
def _app_mode_busy_notice(self) -> MessageRef | None:
|
|
2144
|
+
"""Name the run that pins the app mode, or None while nothing runs."""
|
|
2145
|
+
if self._workflow.awaiting_engine or self._services.execution().kind == "workflow":
|
|
2146
|
+
return workflow_text.MODE_WORKFLOW_BUSY.bind()
|
|
2147
|
+
if (
|
|
2148
|
+
self._state.run.agent_loading
|
|
2149
|
+
or self._state.run.agent_running
|
|
2150
|
+
or self._state.submit.active
|
|
2151
|
+
or self._services.execution_busy()
|
|
2152
|
+
):
|
|
2153
|
+
return workflow_text.MODE_AGENT_BUSY.bind()
|
|
2154
|
+
return None
|
|
2155
|
+
|
|
2156
|
+
def _set_workflow_mode(self, workflow: bool) -> None:
|
|
2157
|
+
if workflow == self._workflow.workflow_mode or not self._can_switch_app_mode():
|
|
2158
|
+
return
|
|
2159
|
+
input_bar = self.query_one(InputBar)
|
|
2160
|
+
if self._dashboard_visible():
|
|
2161
|
+
self._view_adapter.hide_trajectory_dashboard()
|
|
2162
|
+
if not workflow:
|
|
2163
|
+
self._workflow.leave()
|
|
2164
|
+
self._workflow.workflow_mode = workflow
|
|
2165
|
+
self.query_one(SidebarPanel).set_workflow_mode(workflow)
|
|
2166
|
+
self._sync_sidebar_tab_strip_focus()
|
|
2167
|
+
self._view_adapter.sync_main_surface()
|
|
2168
|
+
self._suggestions.dismiss_suggestions()
|
|
2169
|
+
self._sync_workflow_chrome()
|
|
2170
|
+
self._sync_execution_chrome()
|
|
2171
|
+
if workflow:
|
|
2172
|
+
self._workflow.enter()
|
|
2173
|
+
else:
|
|
2174
|
+
input_bar.focus_input()
|
|
2175
|
+
|
|
2176
|
+
async def _apply_workflow_cwd(self, cwd: str) -> bool:
|
|
2177
|
+
if not self._workflow.workflow_mode:
|
|
2178
|
+
return False
|
|
2179
|
+
try:
|
|
2180
|
+
await self._workflow.browser.change_draft_workspace(cwd)
|
|
2181
|
+
except (OSError, ValueError) as exc:
|
|
2182
|
+
self._workflow.show_error(str(exc))
|
|
2183
|
+
return True
|
|
2184
|
+
|
|
2185
|
+
def _accept_approval_update(self, event: ApprovalModeUpdated) -> bool:
|
|
2186
|
+
"""The engine's launch policy is authoritative in both app modes."""
|
|
2187
|
+
return event.session_id is None or event.session_id == self.chat_session_id
|
|
2188
|
+
|
|
2189
|
+
def _route_session_error(self, event: Error) -> bool:
|
|
2190
|
+
if not event.session_id or event.session_id != self._workflow.session_id:
|
|
2191
|
+
return False
|
|
2192
|
+
self._workflow.feedback.notify(event.display_message or event.message)
|
|
2193
|
+
self._workflow.request_refresh()
|
|
2194
|
+
return True
|
|
2195
|
+
|
|
2196
|
+
def _sync_workflow_chrome(self) -> None:
|
|
2197
|
+
# The approval badge is the same in both app modes: one mode is chosen per launch.
|
|
2198
|
+
self.query_one(AppHeader).set_workflow_mode(self._workflow.workflow_mode)
|
|
2199
|
+
self._sync_main_bindings()
|
|
2200
|
+
|
|
2201
|
+
async def _on_execution_changed(self, _event: ExecutionChanged) -> None:
|
|
2202
|
+
self._sync_execution_chrome()
|
|
2203
|
+
self._workflow.request_refresh()
|
|
2204
|
+
|
|
2205
|
+
def _sync_execution_chrome(self) -> None:
|
|
2206
|
+
self.query_one(StatusBar).set_execution_busy(self._services.execution_busy())
|
|
2207
|
+
self._sync_main_bindings()
|
|
2208
|
+
|
|
2209
|
+
def _sync_main_bindings(self) -> None:
|
|
2210
|
+
execution_busy = self._services.execution_busy()
|
|
2211
|
+
trajectory_available = self._navigation.can_toggle_trajectory_dashboard()
|
|
2212
|
+
if trajectory_available != self._trajectory_binding_available or execution_busy != self._execution_binding_busy:
|
|
2213
|
+
self._trajectory_binding_available = trajectory_available
|
|
2214
|
+
self._execution_binding_busy = execution_busy
|
|
2215
|
+
self.refresh_bindings()
|
|
2216
|
+
|
|
2217
|
+
def _sync_workflow_timer(self) -> None:
|
|
2218
|
+
visible = self._workflow.workflow_mode and self._workflow_panel.display and self.app.screen is self
|
|
2219
|
+
if visible and self._workflow_timer is None:
|
|
2220
|
+
self._workflow_timer = self.set_interval(0.1, self._workflow.tick)
|
|
2221
|
+
elif not visible and self._workflow_timer is not None:
|
|
2222
|
+
self._workflow_timer.stop()
|
|
2223
|
+
self._workflow_timer = None
|
|
2224
|
+
|
|
2225
|
+
@on(WorkflowPanel.RunSelected)
|
|
2226
|
+
async def _on_workflow_run_selected(self, event: WorkflowPanel.RunSelected) -> None:
|
|
2227
|
+
event.stop()
|
|
2228
|
+
await self._workflow.session_view.select_run(event.run_id)
|
|
2229
|
+
|
|
2230
|
+
@on(WorkflowPanel.StartRequested)
|
|
2231
|
+
def _on_workflow_start(self) -> None:
|
|
2232
|
+
self._workflow.run_control.collect_input()
|
|
2233
|
+
|
|
2234
|
+
@on(WorkflowPanel.NewSessionRequested)
|
|
2235
|
+
def _on_workflow_new(self) -> None:
|
|
2236
|
+
self._workflow.browser.enter_selection(new_session=True)
|
|
2237
|
+
|
|
2238
|
+
@on(WorkflowPanel.StopRequested)
|
|
2239
|
+
def _on_workflow_stop(self) -> None:
|
|
2240
|
+
self._workflow.run_control.stop()
|
|
2241
|
+
|
|
2242
|
+
@on(WorkflowPanel.ResultRequested)
|
|
2243
|
+
def _on_workflow_result(self, event: WorkflowPanel.ResultRequested) -> None:
|
|
2244
|
+
event.stop()
|
|
2245
|
+
self._workflow.session_view.open_result()
|
|
2246
|
+
|
|
2247
|
+
@on(WorkflowGraph.NodeSelected)
|
|
2248
|
+
def _on_workflow_node(self, event: WorkflowGraph.NodeSelected) -> None:
|
|
2249
|
+
event.stop()
|
|
2250
|
+
self._workflow.session_view.open_node(event.node_id)
|
|
2251
|
+
|
|
2252
|
+
@on(WorkflowGraph.RetryRequested)
|
|
2253
|
+
def _on_workflow_retry(self, event: WorkflowGraph.RetryRequested) -> None:
|
|
2254
|
+
event.stop()
|
|
2255
|
+
run = self._workflow.session_view.projector.run(event.target.run_id)
|
|
2256
|
+
attempt = run.nodes.get(event.node_id) if run is not None else None
|
|
2257
|
+
if attempt is not None and (attempt.activation_id, attempt.attempt) == (
|
|
2258
|
+
event.target.activation_id,
|
|
2259
|
+
event.target.attempt,
|
|
2260
|
+
):
|
|
2261
|
+
self._workflow.run_control.retry_node(attempt)
|
|
2262
|
+
|
|
2263
|
+
@on(WorkflowPanel.ViewChanged)
|
|
2264
|
+
def _on_workflow_view_changed(self) -> None:
|
|
2265
|
+
self._workflow.view_changed()
|