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,1381 @@
|
|
|
1
|
+
# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Rollback modal — compact diff viewer with turn picker + rollback actions.
|
|
4
|
+
|
|
5
|
+
Dismisses with one of:
|
|
6
|
+
|
|
7
|
+
- ``None`` — user cancelled (Esc / Close).
|
|
8
|
+
- ``(target_turn, revert_changes, selected_paths | None)`` — user chose
|
|
9
|
+
a rollback action. ``target_turn`` follows the engine's "keep N
|
|
10
|
+
turns" convention (``0`` = session start / welcome; ``N >= 1`` =
|
|
11
|
+
keep turns ``1..N``). The caller publishes a :class:`UserRollback`
|
|
12
|
+
event.
|
|
13
|
+
|
|
14
|
+
Layout::
|
|
15
|
+
|
|
16
|
+
┌────────────────────────── 80% x 80% ──────────────────────────┐
|
|
17
|
+
│ Title: "Rollback — pick a turn to restore" │
|
|
18
|
+
│ ┌─ Select ─────────────────────────────────────────────────┐ │
|
|
19
|
+
│ │ [▼] Turn N (Current) │ Turn N-1 │ … │ Session start │ │
|
|
20
|
+
│ └──────────────────────────────────────────────────────────┘ │
|
|
21
|
+
│ ┌──────────────────────── DiffPeriodPane ──────────────────────┐│
|
|
22
|
+
│ │ Change list │ DiffView for the selected turn's delta ││
|
|
23
|
+
│ └────────────────────────────────────────────────────────────┘│
|
|
24
|
+
│ [ Discard conversation after Turn N ] [*] Revert … [Close] │
|
|
25
|
+
└───────────────────────────────────────────────────────────────┘
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import asyncio
|
|
31
|
+
import logging
|
|
32
|
+
from contextlib import suppress
|
|
33
|
+
from typing import TYPE_CHECKING, ClassVar, Protocol
|
|
34
|
+
|
|
35
|
+
from rich.text import Text
|
|
36
|
+
from textual import events, on
|
|
37
|
+
from textual.binding import Binding
|
|
38
|
+
from textual.containers import HorizontalGroup, VerticalGroup
|
|
39
|
+
from textual.content import Content
|
|
40
|
+
from textual.strip import Strip
|
|
41
|
+
from textual.visual import Visual
|
|
42
|
+
from textual.widgets import Button, Static, Tree
|
|
43
|
+
|
|
44
|
+
from chrys.app.tui.binding_display import CLOSE_BINDING, TOGGLE_VIEW_BINDING, localized_binding
|
|
45
|
+
from chrys.app.tui.i18n import render_str
|
|
46
|
+
from chrys.app.tui.screens.dialogs.base import BaseDialog
|
|
47
|
+
from chrys.app.tui.screens.diff.screen import DiffPeriodPane, _merge_source, filter_and_dedupe_entries
|
|
48
|
+
from chrys.app.tui.util.diff_entries import DiffFileEntry
|
|
49
|
+
from chrys.app.tui.util.removal import remove_children_shielded, remove_shielded
|
|
50
|
+
from chrys.app.tui.widgets import Checkbox, ChrysLoadingIndicator, Select
|
|
51
|
+
from chrys.foundation.i18n import DisplayBlock, DisplayPath, MessageDef, MessageRef, msg
|
|
52
|
+
from chrys.foundation.i18n.formatting import format_message
|
|
53
|
+
from chrys.foundation.platform.paths import display_relpath
|
|
54
|
+
from chrys.service.mutations.types import (
|
|
55
|
+
FileHashDiff,
|
|
56
|
+
MutationOp,
|
|
57
|
+
MutationProvenance,
|
|
58
|
+
RollbackExclusionReason,
|
|
59
|
+
SnapshotSkipReason,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
if TYPE_CHECKING:
|
|
63
|
+
from collections.abc import Awaitable, Callable
|
|
64
|
+
|
|
65
|
+
from textual.app import ComposeResult
|
|
66
|
+
from textual.css.styles import RulesMap
|
|
67
|
+
from textual.style import Style
|
|
68
|
+
from textual.visual import RenderOptions
|
|
69
|
+
|
|
70
|
+
from chrys.app.tui.i18n import LocaleController
|
|
71
|
+
from chrys.service.mutations.tracker import MutationTracker
|
|
72
|
+
from chrys.service.mutations.types import RollbackPlan, RollbackWarning
|
|
73
|
+
|
|
74
|
+
logger = logging.getLogger(__name__)
|
|
75
|
+
|
|
76
|
+
ROLLBACK_TITLE = msg("tui.rollback.title", fallback="Rollback")
|
|
77
|
+
_CHECK_BINDING = msg("tui.binding.toggle_check", fallback="Check")
|
|
78
|
+
_ROLLBACK_FAILED = msg("tui.rollback.failed", fallback="Rollback failed: {error}")
|
|
79
|
+
_NO_ROLLBACK_SNAPSHOTS = msg(
|
|
80
|
+
"tui.rollback.no_snapshots",
|
|
81
|
+
fallback="No rollback snapshots are available yet.",
|
|
82
|
+
)
|
|
83
|
+
_FILES_EXCLUDED_NOTE = msg(
|
|
84
|
+
"tui.rollback.files_excluded_note",
|
|
85
|
+
fallback="{count} file excluded from revert: {preview}",
|
|
86
|
+
plural_fallback="{count} files excluded from revert: {preview}",
|
|
87
|
+
)
|
|
88
|
+
_REVERT_FILE_CHANGES = msg(
|
|
89
|
+
"tui.rollback.revert_file_changes",
|
|
90
|
+
fallback="Revert {count} file change (WYSIWYG)",
|
|
91
|
+
plural_fallback="Revert {count} file changes (WYSIWYG)",
|
|
92
|
+
)
|
|
93
|
+
_ROLLBACK_TO = msg("tui.rollback.picker.prompt", fallback="Rollback to:")
|
|
94
|
+
_ROLLBACK_CLOSE = msg("tui.rollback.button.close", fallback="Close")
|
|
95
|
+
_NO_FILE_CHANGES = msg("tui.rollback.no_file_changes", fallback="No file changes to revert.")
|
|
96
|
+
_DISCARD_ALL = msg("tui.rollback.button.discard_all", fallback="Discard All")
|
|
97
|
+
_DISCARD_AFTER_TURN = msg(
|
|
98
|
+
"tui.rollback.button.discard_after_turn",
|
|
99
|
+
fallback="Discard conversation after Turn {turn}",
|
|
100
|
+
)
|
|
101
|
+
_TURN_CURRENT = msg("tui.rollback.picker.turn_current", fallback="Turn {turn} (Current)")
|
|
102
|
+
_TURN_CURRENT_PREVIEW = msg(
|
|
103
|
+
"tui.rollback.picker.turn_current_preview",
|
|
104
|
+
fallback="Turn {turn} (Current) - ",
|
|
105
|
+
)
|
|
106
|
+
_TURN = msg("tui.rollback.picker.turn", fallback="Turn {turn}")
|
|
107
|
+
_TURN_PREVIEW = msg("tui.rollback.picker.turn_preview", fallback="Turn {turn} - ")
|
|
108
|
+
_SESSION_START = msg("tui.rollback.picker.session_start", fallback="Session start")
|
|
109
|
+
_DISCARD_ALL_SUFFIX = msg("tui.rollback.picker.discard_all_suffix", fallback=" — discard all")
|
|
110
|
+
_LOAD_PREVIEW_ERROR = msg(
|
|
111
|
+
"tui.rollback.preview.load_error",
|
|
112
|
+
fallback="Error: unable to load rollback preview — {detail}",
|
|
113
|
+
multiline=True,
|
|
114
|
+
)
|
|
115
|
+
_REFRESH_PREVIEW_ERROR = msg(
|
|
116
|
+
"tui.rollback.preview.refresh_error",
|
|
117
|
+
fallback="Error: unable to load this rollback preview — {detail}",
|
|
118
|
+
multiline=True,
|
|
119
|
+
)
|
|
120
|
+
_WARNING_NOTE = msg("tui.rollback.warning_note", fallback="⚠ {message}")
|
|
121
|
+
_MORE_EXCLUSIONS = msg("tui.rollback.exclusions.more", fallback="+{extra_count} more")
|
|
122
|
+
_EXCLUSION_UNRESTORABLE = msg(
|
|
123
|
+
"tui.rollback.exclusion.unrestorable",
|
|
124
|
+
fallback="no restorable backup",
|
|
125
|
+
)
|
|
126
|
+
_EXCLUSION_MOVE_POISONED = msg(
|
|
127
|
+
"tui.rollback.exclusion.move_poisoned",
|
|
128
|
+
fallback="move source unrecoverable",
|
|
129
|
+
)
|
|
130
|
+
_EXCLUSION_FOREIGN = msg(
|
|
131
|
+
"tui.rollback.exclusion.foreign",
|
|
132
|
+
fallback="changed by another session",
|
|
133
|
+
)
|
|
134
|
+
_EXCLUSION_CONTESTED = msg(
|
|
135
|
+
"tui.rollback.exclusion.contested",
|
|
136
|
+
fallback="modified internally and externally",
|
|
137
|
+
)
|
|
138
|
+
_EXCLUSION_PEER_MODIFIED_SINCE = msg(
|
|
139
|
+
"tui.rollback.exclusion.peer_modified_since",
|
|
140
|
+
fallback="changed by another session since",
|
|
141
|
+
)
|
|
142
|
+
EXCLUSION_PREVIEW_ITEM = msg(
|
|
143
|
+
"tui.rollback.exclusion.preview_item",
|
|
144
|
+
fallback="{path} ({reason})",
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
# Dismissal payload: ``(target_turn, revert_changes, selected_paths | None)``
|
|
149
|
+
# — or ``None`` on close/cancel. ``selected_paths`` is ``None`` when the
|
|
150
|
+
# user pressed the plain rollback button (no file revert) and a concrete
|
|
151
|
+
# list of absolute paths when they pressed "& Revert Changes". An empty
|
|
152
|
+
# list means the user un-checked everything before confirming — still a
|
|
153
|
+
# valid outcome (turns are still popped) and treated as "revert nothing".
|
|
154
|
+
RollbackChoice = tuple[int, bool, list[str] | None]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class RollbackModalState(Protocol):
|
|
158
|
+
"""Deferred engine projection consumed after the modal's first paint."""
|
|
159
|
+
|
|
160
|
+
tracker: MutationTracker
|
|
161
|
+
available_turns: list[int]
|
|
162
|
+
current_turn: int
|
|
163
|
+
workspace_cwd: str
|
|
164
|
+
turn_prompts: dict[int, str]
|
|
165
|
+
plan_augment: Callable[[RollbackPlan, str], RollbackPlan] | None
|
|
166
|
+
attribution_refresh: Callable[[], Awaitable[bool]] | None
|
|
167
|
+
projection_acquire: Callable[[], Awaitable[str]] | None
|
|
168
|
+
projection_release: Callable[[str], None] | None
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# Sentinel ``Select`` value used for the "you-are-here" entry at the top
|
|
172
|
+
# of the turn picker. Under the "keep N turns" convention the engine
|
|
173
|
+
# accepts ``target_turn >= 0`` (``0`` is the welcome target), so we use
|
|
174
|
+
# ``-1`` to guarantee no collision with a real rollback target — the
|
|
175
|
+
# entry stays visually present (and dimmed) but semantically inert:
|
|
176
|
+
# selecting it in ``_on_turn_changed`` snaps the Select back to the
|
|
177
|
+
# previous value.
|
|
178
|
+
_LATEST_SENTINEL = -1
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class RollbackLoadCancelled(Exception):
|
|
182
|
+
"""Picker projection was superseded while its loading shell was open."""
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class RollbackProgressModal(BaseDialog[None]):
|
|
186
|
+
"""Non-dismissible loading shell shown until session restore UI takes over."""
|
|
187
|
+
|
|
188
|
+
BINDINGS: ClassVar[list] = [Binding("escape", "noop", show=False, priority=True)]
|
|
189
|
+
|
|
190
|
+
CSS_PATH = "rollback_modal.tcss"
|
|
191
|
+
|
|
192
|
+
def __init__(
|
|
193
|
+
self,
|
|
194
|
+
operation: Callable[[], Awaitable[None]],
|
|
195
|
+
*,
|
|
196
|
+
start_worker: Callable[[Awaitable[None]], object] | None = None,
|
|
197
|
+
locale_controller: LocaleController | None = None,
|
|
198
|
+
) -> None:
|
|
199
|
+
super().__init__(dismiss_on_backdrop=False)
|
|
200
|
+
self._operation = operation
|
|
201
|
+
self._start_worker = start_worker
|
|
202
|
+
self._locale_controller = locale_controller
|
|
203
|
+
self._dismiss_requested = False
|
|
204
|
+
|
|
205
|
+
def compose(self) -> ComposeResult:
|
|
206
|
+
with VerticalGroup(id="rollback-progress-container") as container:
|
|
207
|
+
container.border_title = Text(self._render_message(ROLLBACK_TITLE.bind()))
|
|
208
|
+
with VerticalGroup(id="rollback-progress-state"):
|
|
209
|
+
yield ChrysLoadingIndicator(id="rollback-progress-loading")
|
|
210
|
+
|
|
211
|
+
def on_mount(self) -> None:
|
|
212
|
+
# Do not start the potentially expensive state scan/event dispatch
|
|
213
|
+
# until Textual has painted the blocking shell at least once.
|
|
214
|
+
self.call_after_refresh(self._start_operation)
|
|
215
|
+
|
|
216
|
+
def _start_operation(self) -> None:
|
|
217
|
+
operation = self._run_operation()
|
|
218
|
+
if self._start_worker is None:
|
|
219
|
+
self.run_worker(operation, exclusive=True, group="rollback-progress")
|
|
220
|
+
else:
|
|
221
|
+
self._start_worker(operation)
|
|
222
|
+
|
|
223
|
+
async def _run_operation(self) -> None:
|
|
224
|
+
try:
|
|
225
|
+
await self._operation()
|
|
226
|
+
except Exception as exc:
|
|
227
|
+
logger.exception("Rollback operation failed")
|
|
228
|
+
self.notify(
|
|
229
|
+
self._render_message(_ROLLBACK_FAILED.bind(error=str(exc))),
|
|
230
|
+
title=self._render_message(ROLLBACK_TITLE.bind()),
|
|
231
|
+
severity="error",
|
|
232
|
+
timeout=4,
|
|
233
|
+
markup=False,
|
|
234
|
+
)
|
|
235
|
+
finally:
|
|
236
|
+
self._dismiss_requested = True
|
|
237
|
+
self._dismiss_if_current()
|
|
238
|
+
|
|
239
|
+
def _dismiss_if_current(self) -> None:
|
|
240
|
+
"""Dismiss only after any restore-time loading screen above us closes."""
|
|
241
|
+
if self.is_attached and self.app.screen is self:
|
|
242
|
+
self._dismiss_requested = False
|
|
243
|
+
self.dismiss(None)
|
|
244
|
+
|
|
245
|
+
def handoff_to_session_restore(self) -> None:
|
|
246
|
+
"""Close this shell before the engine's session-restore modal is pushed."""
|
|
247
|
+
self._dismiss_requested = True
|
|
248
|
+
self._dismiss_if_current()
|
|
249
|
+
|
|
250
|
+
def on_screen_resume(self, _event: events.ScreenResume) -> None:
|
|
251
|
+
"""Honor completion deferred while agent restore UI covered this modal."""
|
|
252
|
+
if self._dismiss_requested:
|
|
253
|
+
self._dismiss_if_current()
|
|
254
|
+
|
|
255
|
+
def action_noop(self) -> None:
|
|
256
|
+
"""Consume Escape while rollback owns the active session state."""
|
|
257
|
+
|
|
258
|
+
def _render_message(self, reference: MessageRef) -> str:
|
|
259
|
+
controller = self._locale_controller
|
|
260
|
+
return format_message(reference) if controller is None else render_str(controller.localizer, reference)
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def _build_cumulative_entries(
|
|
264
|
+
tracker: MutationTracker,
|
|
265
|
+
from_turn_id: int,
|
|
266
|
+
cwd: str,
|
|
267
|
+
) -> list[DiffFileEntry]:
|
|
268
|
+
"""Aggregate per-file diffs for all turns with ``turn_id >= from_turn_id``.
|
|
269
|
+
|
|
270
|
+
Matches :func:`_build_session_entries` semantics but filtered to a
|
|
271
|
+
suffix of the turn history: earliest ``before_hash`` wins, latest
|
|
272
|
+
``after_hash`` wins, net-zero churn is filtered out.
|
|
273
|
+
|
|
274
|
+
Which entries are offered mirrors the rollback plan exactly
|
|
275
|
+
(WYSIWYG): only paths present in
|
|
276
|
+
:meth:`MutationTracker.get_rollback_plan_for_turns` for the same
|
|
277
|
+
window are shown, so every plan-side safety filter (restorability,
|
|
278
|
+
move atomicity) is inherited rather than re-implemented. Entries
|
|
279
|
+
whose content backup was withheld by SnapshotPolicy are still
|
|
280
|
+
offered when the plan can act on them (delete a skipped create,
|
|
281
|
+
restore a grown/binary file from its stored before-blob), carrying
|
|
282
|
+
``content_omitted`` so the pane renders a message instead of an
|
|
283
|
+
untruthful empty diff.
|
|
284
|
+
"""
|
|
285
|
+
from chrys.foundation.text.encoding import EncodingDetector, decode_bytes
|
|
286
|
+
|
|
287
|
+
turns = [t for t in tracker.get_all_turns() if t.turn_id >= from_turn_id]
|
|
288
|
+
if not turns:
|
|
289
|
+
return []
|
|
290
|
+
|
|
291
|
+
result: dict[str, FileHashDiff] = {}
|
|
292
|
+
old_paths: dict[str, str | None] = {}
|
|
293
|
+
sources: dict[str, str] = {}
|
|
294
|
+
inferred_paths: set[str] = set()
|
|
295
|
+
|
|
296
|
+
def _set_before_if_first(path: str, turn_id: int) -> tuple[str | None, SnapshotSkipReason | None]:
|
|
297
|
+
if path not in result:
|
|
298
|
+
snap = tracker.get_snapshot(path, turn_id)
|
|
299
|
+
before = snap.content_hash if snap else None
|
|
300
|
+
before_skip = snap.skip_reason if snap else None
|
|
301
|
+
result[path] = FileHashDiff(before=before, after=None, before_skip=before_skip)
|
|
302
|
+
return before, before_skip
|
|
303
|
+
return result[path].before, result[path].before_skip
|
|
304
|
+
|
|
305
|
+
for turn in sorted(turns, key=lambda t: t.turn_id):
|
|
306
|
+
for m in turn.mutations:
|
|
307
|
+
# Foreign rows never fold into "our" endpoints; their paths
|
|
308
|
+
# are excluded from the plan anyway (intersection below).
|
|
309
|
+
if m.provenance is MutationProvenance.FOREIGN:
|
|
310
|
+
continue
|
|
311
|
+
if m.provenance is MutationProvenance.ASSUMED:
|
|
312
|
+
inferred_paths.add(m.path)
|
|
313
|
+
if m.old_path:
|
|
314
|
+
inferred_paths.add(m.old_path)
|
|
315
|
+
before, before_skip = _set_before_if_first(m.path, turn.turn_id)
|
|
316
|
+
result[m.path] = FileHashDiff(
|
|
317
|
+
before=before, after=m.after_hash, before_skip=before_skip, after_skip=m.after_skip
|
|
318
|
+
)
|
|
319
|
+
sources[m.path] = _merge_source(sources.get(m.path, ""), m.source.value)
|
|
320
|
+
if m.old_path:
|
|
321
|
+
old_before, old_before_skip = _set_before_if_first(m.old_path, turn.turn_id)
|
|
322
|
+
result[m.old_path] = FileHashDiff(before=old_before, after=None, before_skip=old_before_skip)
|
|
323
|
+
old_paths[m.path] = m.old_path
|
|
324
|
+
sources[m.old_path] = _merge_source(sources.get(m.old_path, ""), m.source.value)
|
|
325
|
+
|
|
326
|
+
# Offer exactly what the rollback plan will act on — intersecting
|
|
327
|
+
# with the plan inherits its safety filters (non-restorable
|
|
328
|
+
# snapshots, move atomicity, foreign/contested provenance) without
|
|
329
|
+
# duplicating them. On top of that, drop net-zero churn from the
|
|
330
|
+
# display (``is_net_zero`` is skip-aware: a skipped create is
|
|
331
|
+
# absent → exists, not net-zero).
|
|
332
|
+
plan_paths = tracker.get_rollback_plan_for_turns({t.turn_id for t in turns}).paths
|
|
333
|
+
filtered: dict[str, FileHashDiff] = {}
|
|
334
|
+
for path, diff in result.items():
|
|
335
|
+
if path not in plan_paths:
|
|
336
|
+
continue
|
|
337
|
+
if diff.is_net_zero:
|
|
338
|
+
continue
|
|
339
|
+
filtered[path] = diff
|
|
340
|
+
if not filtered:
|
|
341
|
+
return []
|
|
342
|
+
|
|
343
|
+
detector = EncodingDetector()
|
|
344
|
+
store = tracker.store
|
|
345
|
+
entries: list[DiffFileEntry] = []
|
|
346
|
+
for file_path, diff in filtered.items():
|
|
347
|
+
before_bytes = store.read_blob(diff.before) if diff.before else None
|
|
348
|
+
after_bytes = store.read_blob(diff.after) if diff.after else None
|
|
349
|
+
|
|
350
|
+
binary = bool(
|
|
351
|
+
(before_bytes and EncodingDetector.looks_binary(before_bytes))
|
|
352
|
+
or (after_bytes and EncodingDetector.looks_binary(after_bytes))
|
|
353
|
+
)
|
|
354
|
+
before_text = "" if binary or before_bytes is None else decode_bytes(before_bytes)
|
|
355
|
+
after_text = "" if binary or after_bytes is None else decode_bytes(after_bytes)
|
|
356
|
+
|
|
357
|
+
# Skip-aware existence: a side with a withheld backup still
|
|
358
|
+
# *exists* on disk (a skipped create must show as CREATE, not
|
|
359
|
+
# as a hash-less MODIFY/DELETE).
|
|
360
|
+
if not diff.before_exists and diff.after_exists:
|
|
361
|
+
op = MutationOp.CREATE
|
|
362
|
+
elif diff.before_exists and not diff.after_exists:
|
|
363
|
+
op = MutationOp.DELETE
|
|
364
|
+
else:
|
|
365
|
+
op = MutationOp.MODIFY
|
|
366
|
+
|
|
367
|
+
enc = "utf-8"
|
|
368
|
+
content_for_detect = after_bytes or before_bytes
|
|
369
|
+
if content_for_detect and not binary:
|
|
370
|
+
det = detector.detect_from_bytes(content_for_detect)
|
|
371
|
+
enc = det.encoding or "utf-8"
|
|
372
|
+
|
|
373
|
+
rel = display_relpath(file_path, cwd)
|
|
374
|
+
|
|
375
|
+
entries.append(
|
|
376
|
+
DiffFileEntry(
|
|
377
|
+
path=file_path,
|
|
378
|
+
rel_path=rel,
|
|
379
|
+
operation=op,
|
|
380
|
+
old_path=old_paths.get(file_path),
|
|
381
|
+
before_text=before_text,
|
|
382
|
+
after_text=after_text,
|
|
383
|
+
is_binary=binary,
|
|
384
|
+
encoding=enc,
|
|
385
|
+
bytes_changed=True,
|
|
386
|
+
before_hash=diff.before,
|
|
387
|
+
after_hash=diff.after,
|
|
388
|
+
source=sources.get(file_path, ""),
|
|
389
|
+
# Only the after side can be skip-marked here — a
|
|
390
|
+
# before-side skip makes the earliest snapshot
|
|
391
|
+
# non-restorable, which the filter above already drops.
|
|
392
|
+
content_omitted=diff.after_skip.value if diff.after_skip else "",
|
|
393
|
+
inferred=file_path in inferred_paths,
|
|
394
|
+
),
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
entries.sort(key=lambda e: e.rel_path)
|
|
398
|
+
return entries
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
# Human labels for the modal's exclusion note section.
|
|
402
|
+
_EXCLUSION_LABELS: dict[RollbackExclusionReason, MessageDef] = {
|
|
403
|
+
RollbackExclusionReason.UNRESTORABLE: _EXCLUSION_UNRESTORABLE,
|
|
404
|
+
RollbackExclusionReason.MOVE_POISONED: _EXCLUSION_MOVE_POISONED,
|
|
405
|
+
RollbackExclusionReason.FOREIGN: _EXCLUSION_FOREIGN,
|
|
406
|
+
RollbackExclusionReason.CONTESTED: _EXCLUSION_CONTESTED,
|
|
407
|
+
RollbackExclusionReason.PEER_MODIFIED_SINCE: _EXCLUSION_PEER_MODIFIED_SINCE,
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
_EXCLUSION_NOTE_MAX_PATHS = 3
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def exclusion_reason_label(reason: RollbackExclusionReason | str) -> MessageRef | str:
|
|
414
|
+
"""Human label for an exclusion reason — enum or raw ``.value`` string.
|
|
415
|
+
|
|
416
|
+
Event payloads carry reasons as primitive strings (foundation must
|
|
417
|
+
not import the service enum); unknown values render verbatim so a
|
|
418
|
+
newer engine never breaks an older consumer.
|
|
419
|
+
"""
|
|
420
|
+
if isinstance(reason, str):
|
|
421
|
+
try:
|
|
422
|
+
reason = RollbackExclusionReason(reason)
|
|
423
|
+
except ValueError:
|
|
424
|
+
return str(reason)
|
|
425
|
+
definition = _EXCLUSION_LABELS.get(reason)
|
|
426
|
+
return reason.value if definition is None else definition.bind()
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _plan_for_target(tracker: MutationTracker, target_turn: int) -> RollbackPlan | None:
|
|
430
|
+
"""Build the rollback plan for the same turn window ``_entries_for_target`` shows."""
|
|
431
|
+
all_turns = tracker.get_all_turns()
|
|
432
|
+
if target_turn < 0 or not all_turns:
|
|
433
|
+
return None
|
|
434
|
+
turn_ids = {t.turn_id for t in all_turns if target_turn == 0 or t.turn_id > target_turn}
|
|
435
|
+
if not turn_ids:
|
|
436
|
+
return None
|
|
437
|
+
return tracker.get_rollback_plan_for_turns(turn_ids)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def _exclusion_note_text(
|
|
441
|
+
exclusions: list[tuple[str, RollbackExclusionReason]],
|
|
442
|
+
cwd: str,
|
|
443
|
+
locale_controller: LocaleController | None = None,
|
|
444
|
+
) -> str:
|
|
445
|
+
"""One-line summary of plan exclusions for the modal note section."""
|
|
446
|
+
parts: list[str] = []
|
|
447
|
+
for path, reason in exclusions[:_EXCLUSION_NOTE_MAX_PATHS]:
|
|
448
|
+
rel = display_relpath(path, cwd)
|
|
449
|
+
label = exclusion_reason_label(reason)
|
|
450
|
+
rendered_label = (
|
|
451
|
+
label
|
|
452
|
+
if isinstance(label, str)
|
|
453
|
+
else format_message(label)
|
|
454
|
+
if locale_controller is None
|
|
455
|
+
else render_str(locale_controller.localizer, label)
|
|
456
|
+
)
|
|
457
|
+
item_reference = EXCLUSION_PREVIEW_ITEM.bind(path=DisplayPath(rel), reason=rendered_label)
|
|
458
|
+
parts.append(
|
|
459
|
+
format_message(item_reference)
|
|
460
|
+
if locale_controller is None
|
|
461
|
+
else render_str(locale_controller.localizer, item_reference)
|
|
462
|
+
)
|
|
463
|
+
more = len(exclusions) - _EXCLUSION_NOTE_MAX_PATHS
|
|
464
|
+
if more > 0:
|
|
465
|
+
more_reference = _MORE_EXCLUSIONS.bind(extra_count=more)
|
|
466
|
+
parts.append(
|
|
467
|
+
format_message(more_reference)
|
|
468
|
+
if locale_controller is None
|
|
469
|
+
else render_str(locale_controller.localizer, more_reference)
|
|
470
|
+
)
|
|
471
|
+
count = len(exclusions)
|
|
472
|
+
reference = _FILES_EXCLUDED_NOTE.bind(count=count, preview=", ".join(parts))
|
|
473
|
+
return (
|
|
474
|
+
format_message(reference) if locale_controller is None else render_str(locale_controller.localizer, reference)
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def _warning_note_text(
|
|
479
|
+
warnings: list[RollbackWarning],
|
|
480
|
+
locale_controller: LocaleController | None = None,
|
|
481
|
+
) -> str:
|
|
482
|
+
"""Advisory peer warnings for the modal note section.
|
|
483
|
+
|
|
484
|
+
Deduped by message: the coordinator dedupes per peer session, so two
|
|
485
|
+
active peers yield two identical strings — one line suffices.
|
|
486
|
+
"""
|
|
487
|
+
unique = dict.fromkeys(w.message for w in warnings)
|
|
488
|
+
references = (_WARNING_NOTE.bind(message=message) for message in unique)
|
|
489
|
+
if locale_controller is None:
|
|
490
|
+
return "\n".join(format_message(reference) for reference in references)
|
|
491
|
+
return "\n".join(render_str(locale_controller.localizer, reference) for reference in references)
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def _invert_entry(entry: DiffFileEntry) -> DiffFileEntry:
|
|
495
|
+
"""Return the reverse of ``entry`` — what the revert will apply.
|
|
496
|
+
|
|
497
|
+
Swaps ``before_text`` ↔ ``after_text`` and flips the operation:
|
|
498
|
+
|
|
499
|
+
- ``CREATE`` (file was created) → ``DELETE`` (revert will remove it)
|
|
500
|
+
- ``DELETE`` (file was deleted) → ``CREATE`` (revert will restore it)
|
|
501
|
+
- ``MODIFY`` stays ``MODIFY``, but texts are swapped (revert writes
|
|
502
|
+
the pre-range content)
|
|
503
|
+
|
|
504
|
+
``old_path`` is cleared: the reverse of a MOVE A→B is naturally
|
|
505
|
+
represented by the already-emitted pair (``DELETE`` at B plus
|
|
506
|
+
``CREATE`` at A) that :func:`_build_cumulative_entries` produces.
|
|
507
|
+
Carrying the forward-direction ``old_path`` annotation through
|
|
508
|
+
would mis-label those entries.
|
|
509
|
+
"""
|
|
510
|
+
if entry.operation is MutationOp.CREATE:
|
|
511
|
+
new_op = MutationOp.DELETE
|
|
512
|
+
elif entry.operation is MutationOp.DELETE:
|
|
513
|
+
new_op = MutationOp.CREATE
|
|
514
|
+
else:
|
|
515
|
+
new_op = entry.operation
|
|
516
|
+
|
|
517
|
+
return DiffFileEntry(
|
|
518
|
+
path=entry.path,
|
|
519
|
+
rel_path=entry.rel_path,
|
|
520
|
+
operation=new_op,
|
|
521
|
+
old_path=None,
|
|
522
|
+
before_text=entry.after_text,
|
|
523
|
+
after_text=entry.before_text,
|
|
524
|
+
is_binary=entry.is_binary,
|
|
525
|
+
encoding=entry.encoding,
|
|
526
|
+
bytes_changed=entry.bytes_changed,
|
|
527
|
+
before_hash=entry.after_hash,
|
|
528
|
+
after_hash=entry.before_hash,
|
|
529
|
+
source=entry.source,
|
|
530
|
+
content_omitted=entry.content_omitted,
|
|
531
|
+
contested=entry.contested,
|
|
532
|
+
inferred=entry.inferred,
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
def _entries_for_target(
|
|
537
|
+
tracker: MutationTracker,
|
|
538
|
+
target_turn: int,
|
|
539
|
+
cwd: str,
|
|
540
|
+
) -> list[DiffFileEntry]:
|
|
541
|
+
"""Build the diff entries shown when the user previews keeping ``target_turn`` turns.
|
|
542
|
+
|
|
543
|
+
The diff represents **what the workspace will look like AFTER the
|
|
544
|
+
revert** compared to the current state — i.e. the inverse of the
|
|
545
|
+
cumulative changes made across turns with ``tracker_turn_id >
|
|
546
|
+
target_turn``. We reuse the same "All"-tab aggregation (filtered
|
|
547
|
+
to turns that would be discarded) and then invert each entry:
|
|
548
|
+
created files become deletions, deleted files become creations,
|
|
549
|
+
and modifies swap their before/after texts. The effect is that
|
|
550
|
+
"left" in the viewer shows the current on-disk content and "right"
|
|
551
|
+
shows what the file will contain after the revert is applied.
|
|
552
|
+
|
|
553
|
+
Under the "keep N turns" convention, ``target_turn = 0`` aggregates
|
|
554
|
+
every tracker turn (discard everything); ``target_turn = N >= 1``
|
|
555
|
+
aggregates tracker turns with ``turn_id >= target_turn + 1``.
|
|
556
|
+
``turn_id`` matches against tracker entries directly (NOT treated
|
|
557
|
+
as an index into ``get_all_turns()``) — the two are kept in sync
|
|
558
|
+
by the engine (``start_turn(self._turn_number)`` uses the same
|
|
559
|
+
counter that names snapshot files and history turn markers), so a
|
|
560
|
+
``turn_id`` lookup is both correct and gap-safe.
|
|
561
|
+
"""
|
|
562
|
+
all_turns = tracker.get_all_turns()
|
|
563
|
+
if target_turn < 0 or not all_turns:
|
|
564
|
+
return []
|
|
565
|
+
|
|
566
|
+
if target_turn == 0:
|
|
567
|
+
# "Session start" — aggregate every turn the tracker knows
|
|
568
|
+
# about. Use the min turn_id so ``_build_cumulative_entries``
|
|
569
|
+
# doesn't skip pre-history turns that might arrive with
|
|
570
|
+
# turn_id > 1 from a restored session.
|
|
571
|
+
first_turn_id = min(t.turn_id for t in all_turns)
|
|
572
|
+
forward = _build_cumulative_entries(tracker, first_turn_id, cwd)
|
|
573
|
+
else:
|
|
574
|
+
# Keep turns 1..target_turn, discard turns with id > target_turn.
|
|
575
|
+
# Direct turn_id match — gap-safe and not tied to list ordering.
|
|
576
|
+
forward = _build_cumulative_entries(tracker, target_turn + 1, cwd)
|
|
577
|
+
|
|
578
|
+
return [_invert_entry(e) for e in forward]
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
def _build_preview_state(
|
|
582
|
+
tracker: MutationTracker,
|
|
583
|
+
target_turn: int,
|
|
584
|
+
cwd: str,
|
|
585
|
+
) -> tuple[list[DiffFileEntry], RollbackPlan | None]:
|
|
586
|
+
"""Build the expensive rollback entry and plan projection off-loop."""
|
|
587
|
+
entries = filter_and_dedupe_entries(_entries_for_target(tracker, target_turn, cwd))
|
|
588
|
+
return entries, _plan_for_target(tracker, target_turn)
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
class _HangingIndentVisual(Visual):
|
|
592
|
+
"""Render a fixed turn prefix beside a width-aware wrapped message."""
|
|
593
|
+
|
|
594
|
+
def __init__(self, prefix: Content, message: Content) -> None:
|
|
595
|
+
self._prefix = prefix
|
|
596
|
+
self._message = message
|
|
597
|
+
self._combined = prefix + message
|
|
598
|
+
self._prefix_width = prefix.cell_length
|
|
599
|
+
|
|
600
|
+
def get_optimal_width(self, rules: RulesMap, container_width: int) -> int:
|
|
601
|
+
return self._combined.get_optimal_width(rules, container_width)
|
|
602
|
+
|
|
603
|
+
def get_height(self, rules: RulesMap, width: int) -> int:
|
|
604
|
+
if width <= self._prefix_width or not self._message:
|
|
605
|
+
return self._combined.get_height(rules, width)
|
|
606
|
+
return max(1, self._message.get_height(rules, width - self._prefix_width))
|
|
607
|
+
|
|
608
|
+
def render_strips(
|
|
609
|
+
self,
|
|
610
|
+
width: int,
|
|
611
|
+
height: int | None,
|
|
612
|
+
style: Style,
|
|
613
|
+
options: RenderOptions,
|
|
614
|
+
) -> list[Strip]:
|
|
615
|
+
if width <= 0 or height == 0:
|
|
616
|
+
return []
|
|
617
|
+
if width <= self._prefix_width or not self._message:
|
|
618
|
+
return self._combined.render_strips(width, height, style, options)
|
|
619
|
+
|
|
620
|
+
message_width = width - self._prefix_width
|
|
621
|
+
prefix_strips = self._prefix.render_strips(self._prefix_width, 1, style, options)
|
|
622
|
+
message_strips = self._message.render_strips(message_width, height, style, options)
|
|
623
|
+
if not prefix_strips or not message_strips:
|
|
624
|
+
return self._combined.render_strips(width, height, style, options)
|
|
625
|
+
|
|
626
|
+
indent = Strip.blank(self._prefix_width, style.rich_style)
|
|
627
|
+
strips = [
|
|
628
|
+
Strip.join([prefix_strips[0], message_strips[0]]),
|
|
629
|
+
*(Strip.join([indent, message_strip]) for message_strip in message_strips[1:]),
|
|
630
|
+
]
|
|
631
|
+
return strips if height is None else strips[:height]
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
class _TurnOptionText(Text):
|
|
635
|
+
"""Searchable Rich text whose Textual visual uses a hanging indent."""
|
|
636
|
+
|
|
637
|
+
__slots__ = ("_message_text", "_prefix_text")
|
|
638
|
+
|
|
639
|
+
def __init__(
|
|
640
|
+
self,
|
|
641
|
+
prefix: str,
|
|
642
|
+
message: str,
|
|
643
|
+
*,
|
|
644
|
+
prefix_style: str = "",
|
|
645
|
+
message_style: str = "dim",
|
|
646
|
+
) -> None:
|
|
647
|
+
super().__init__()
|
|
648
|
+
self._prefix_text = Text(prefix, style=prefix_style)
|
|
649
|
+
self._message_text = Text(message, style=message_style)
|
|
650
|
+
self.append(self._prefix_text)
|
|
651
|
+
self.append(self._message_text)
|
|
652
|
+
|
|
653
|
+
def visualize(self) -> Visual:
|
|
654
|
+
"""Build the width-aware visual while retaining ``Text.plain`` for search."""
|
|
655
|
+
return _HangingIndentVisual(
|
|
656
|
+
Content.from_rich_text(self._prefix_text),
|
|
657
|
+
Content.from_rich_text(self._message_text),
|
|
658
|
+
)
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
class _RollbackContent(VerticalGroup):
|
|
662
|
+
"""Modal content composed with the shell.
|
|
663
|
+
|
|
664
|
+
The turn picker and footer are cheap to build (constructor data
|
|
665
|
+
only), so they render immediately; the diff area starts as a
|
|
666
|
+
loading indicator occupying the same slot the "No file changes"
|
|
667
|
+
placeholder uses, and :meth:`RollbackModal._render_diff_pane`
|
|
668
|
+
replaces it once the preview projection is built off-loop.
|
|
669
|
+
"""
|
|
670
|
+
|
|
671
|
+
def __init__(
|
|
672
|
+
self,
|
|
673
|
+
options: list[tuple[Text, int]],
|
|
674
|
+
selected_turn: int,
|
|
675
|
+
render_message: Callable[[MessageRef], str],
|
|
676
|
+
) -> None:
|
|
677
|
+
super().__init__(classes="rollback-content")
|
|
678
|
+
self._options = options
|
|
679
|
+
self._selected_turn = selected_turn
|
|
680
|
+
self._render_message = render_message
|
|
681
|
+
|
|
682
|
+
def compose(self) -> ComposeResult:
|
|
683
|
+
select: Select[int] = Select(
|
|
684
|
+
self._options,
|
|
685
|
+
id="rollback-turn-select",
|
|
686
|
+
value=self._selected_turn,
|
|
687
|
+
allow_blank=False,
|
|
688
|
+
prompt=self._render_message(_ROLLBACK_TO.bind()),
|
|
689
|
+
)
|
|
690
|
+
yield select
|
|
691
|
+
|
|
692
|
+
with VerticalGroup(id="rollback-diff-wrapper"):
|
|
693
|
+
yield ChrysLoadingIndicator(id="rollback-loading")
|
|
694
|
+
|
|
695
|
+
with HorizontalGroup(id="rollback-buttons"):
|
|
696
|
+
checkbox = Checkbox(
|
|
697
|
+
self._render_message(_REVERT_FILE_CHANGES.bind(count=0)),
|
|
698
|
+
value=True,
|
|
699
|
+
id="revert-checkbox",
|
|
700
|
+
)
|
|
701
|
+
# Hidden until a preview with actual file changes mounts.
|
|
702
|
+
checkbox.display = False
|
|
703
|
+
yield checkbox
|
|
704
|
+
yield Static("", id="rollback-buttons-spacer")
|
|
705
|
+
yield Button(
|
|
706
|
+
self._render_message(ROLLBACK_TITLE.bind()),
|
|
707
|
+
id="btn-rollback",
|
|
708
|
+
variant="primary",
|
|
709
|
+
flat=True,
|
|
710
|
+
disabled=True,
|
|
711
|
+
)
|
|
712
|
+
yield Button(self._render_message(_ROLLBACK_CLOSE.bind()), id="btn-close", variant="warning", flat=True)
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
class RollbackModal(BaseDialog[RollbackChoice | None]):
|
|
716
|
+
"""Compact modal for choosing a rollback target and action."""
|
|
717
|
+
|
|
718
|
+
BINDINGS: ClassVar[list] = [
|
|
719
|
+
localized_binding("escape", "close", CLOSE_BINDING, priority=True),
|
|
720
|
+
# ``space`` mirrors the full /diff screen: toggles split/unified
|
|
721
|
+
# view for the file currently shown in the DiffView.
|
|
722
|
+
localized_binding("space", "toggle_view", TOGGLE_VIEW_BINDING, priority=True),
|
|
723
|
+
# ``x`` checks/unchecks the file (or folder) under the cursor in
|
|
724
|
+
# the change-list tree, scoping which files are reverted when the
|
|
725
|
+
# user confirms "& Revert Changes".
|
|
726
|
+
localized_binding("x", "toggle_check", _CHECK_BINDING, priority=True),
|
|
727
|
+
]
|
|
728
|
+
_CONTENT_ACTIONS: ClassVar[frozenset[str]] = frozenset({"toggle_view", "toggle_check"})
|
|
729
|
+
|
|
730
|
+
CSS_PATH = "rollback_modal.tcss"
|
|
731
|
+
|
|
732
|
+
def __init__(
|
|
733
|
+
self,
|
|
734
|
+
tracker: MutationTracker | None,
|
|
735
|
+
available_turns: list[int],
|
|
736
|
+
cwd: str,
|
|
737
|
+
turn_prompts: dict[int, str] | None = None,
|
|
738
|
+
current_turn: int | None = None,
|
|
739
|
+
plan_augment: Callable[[RollbackPlan, str], RollbackPlan] | None = None,
|
|
740
|
+
attribution_refresh: Callable[[], Awaitable[bool]] | None = None,
|
|
741
|
+
load_state: Callable[[], Awaitable[RollbackModalState | None]] | None = None,
|
|
742
|
+
locale_controller: LocaleController | None = None,
|
|
743
|
+
) -> None:
|
|
744
|
+
"""
|
|
745
|
+
Args:
|
|
746
|
+
tracker: Live mutation tracker to read turn diffs from.
|
|
747
|
+
available_turns: ``target_turn`` values eligible for
|
|
748
|
+
rollback under the "keep N turns" convention.
|
|
749
|
+
Typically ``[0, 1, ..., N-1]`` where ``N`` is the
|
|
750
|
+
current turn count. ``0`` means "session start".
|
|
751
|
+
cwd: Working directory for computing relative paths.
|
|
752
|
+
turn_prompts: Optional ``{turn_number: user_prompt}`` keyed
|
|
753
|
+
by history turn index (``1``-based). Used to show a
|
|
754
|
+
dimmed preview of each turn's user message in the
|
|
755
|
+
picker. Missing turns render without a preview.
|
|
756
|
+
current_turn: Engine's current turn counter — the index
|
|
757
|
+
of the most-recently-started turn. Drives the
|
|
758
|
+
"Turn N (Current)" sentinel row in the picker.
|
|
759
|
+
When ``None`` (back-compat for tests), falls back to
|
|
760
|
+
``max(available_turns) + 1`` which matches the prior
|
|
761
|
+
behaviour but can mislabel the sentinel when snapshots
|
|
762
|
+
have been pruned/deleted.
|
|
763
|
+
plan_augment: Optional cross-session peer check applied to
|
|
764
|
+
the preview plan (``(plan, cwd) -> plan``), mirroring
|
|
765
|
+
the engine's rollback-time augmentation so the modal
|
|
766
|
+
stays plan-WYSIWYG.
|
|
767
|
+
attribution_refresh: Optional peer reclassification hook
|
|
768
|
+
(``() -> awaitable[changed]``, the engine's refresh
|
|
769
|
+
path) run before each preview build. A peer claim
|
|
770
|
+
published after the modal opened can demote local rows
|
|
771
|
+
(contested/foreign), reshaping the plan itself —
|
|
772
|
+
``plan_augment`` alone cannot surface that.
|
|
773
|
+
load_state: Optional deferred engine projection. When set,
|
|
774
|
+
the modal paints only its loading shell, then awaits this
|
|
775
|
+
callback and keeps the shell visible through preview
|
|
776
|
+
preparation. This is the production path for long
|
|
777
|
+
sessions; direct tracker arguments remain useful for
|
|
778
|
+
already-loaded callers and focused widget tests.
|
|
779
|
+
"""
|
|
780
|
+
super().__init__()
|
|
781
|
+
self._tracker = tracker
|
|
782
|
+
self._available_turns = sorted(available_turns)
|
|
783
|
+
self._cwd = cwd
|
|
784
|
+
self._plan_augment = plan_augment
|
|
785
|
+
self._attribution_refresh = attribution_refresh
|
|
786
|
+
self._projection_acquire: Callable[[], Awaitable[str]] | None = None
|
|
787
|
+
self._projection_release: Callable[[str], None] | None = None
|
|
788
|
+
self._turn_prompts = turn_prompts or {}
|
|
789
|
+
self._current_turn = current_turn
|
|
790
|
+
self._load_state = load_state
|
|
791
|
+
self._locale_controller = locale_controller
|
|
792
|
+
# Default: preview rolling back the most-recent completable turn
|
|
793
|
+
# (largest ``target_turn`` — under "keep N turns" this is the
|
|
794
|
+
# state right after the last completed turn's predecessor).
|
|
795
|
+
self._selected_turn: int = self._available_turns[-1] if self._available_turns else 0
|
|
796
|
+
self._pane: DiffPeriodPane | None = None
|
|
797
|
+
self._content_loading = True
|
|
798
|
+
self._content_ready = False
|
|
799
|
+
self._preview_refreshing = False
|
|
800
|
+
self._preview_failed = False
|
|
801
|
+
|
|
802
|
+
def _render_message(self, reference: MessageRef) -> str:
|
|
803
|
+
controller = self._locale_controller
|
|
804
|
+
return format_message(reference) if controller is None else render_str(controller.localizer, reference)
|
|
805
|
+
|
|
806
|
+
def compose(self) -> ComposeResult:
|
|
807
|
+
# Production starts with a state-loading shell. Already-loaded
|
|
808
|
+
# callers start one stage later with the Select/footer and a preview
|
|
809
|
+
# spinner in the "No file changes" slot. Both remain compact until
|
|
810
|
+
# a real diff expands the modal (``_set_empty_layout``).
|
|
811
|
+
with VerticalGroup(id="rollback-container", classes="empty") as container:
|
|
812
|
+
container.border_title = Text(self._render_message(ROLLBACK_TITLE.bind()))
|
|
813
|
+
if self._load_state is None:
|
|
814
|
+
yield _RollbackContent(self._build_turn_options(), self._selected_turn, self._render_message)
|
|
815
|
+
else:
|
|
816
|
+
with VerticalGroup(id="rollback-initial-loading-state"):
|
|
817
|
+
yield ChrysLoadingIndicator(id="rollback-loading")
|
|
818
|
+
|
|
819
|
+
def on_mount(self) -> None:
|
|
820
|
+
if self._load_state is None:
|
|
821
|
+
self._refresh_button_labels()
|
|
822
|
+
self.call_after_refresh(self._start_initial_load)
|
|
823
|
+
|
|
824
|
+
def _start_initial_load(self) -> None:
|
|
825
|
+
self.run_worker(self._load_and_mount_content(), exclusive=True, group="rollback-modal-content")
|
|
826
|
+
|
|
827
|
+
async def _load_and_mount_content(self) -> None:
|
|
828
|
+
"""Resolve state and preview while the first-painted loading shell remains."""
|
|
829
|
+
deferred_content: _RollbackContent | None = None
|
|
830
|
+
try:
|
|
831
|
+
if self._load_state is not None:
|
|
832
|
+
state = await self._load_state()
|
|
833
|
+
if state is None:
|
|
834
|
+
self.notify(
|
|
835
|
+
self._render_message(_NO_ROLLBACK_SNAPSHOTS.bind()),
|
|
836
|
+
title=self._render_message(ROLLBACK_TITLE.bind()),
|
|
837
|
+
severity="warning",
|
|
838
|
+
timeout=3,
|
|
839
|
+
markup=False,
|
|
840
|
+
)
|
|
841
|
+
self._content_loading = False
|
|
842
|
+
self.dismiss(None)
|
|
843
|
+
return
|
|
844
|
+
self._apply_loaded_state(state)
|
|
845
|
+
deferred_content = _RollbackContent(
|
|
846
|
+
self._build_turn_options(),
|
|
847
|
+
self._selected_turn,
|
|
848
|
+
self._render_message,
|
|
849
|
+
)
|
|
850
|
+
deferred_content.display = False
|
|
851
|
+
await self.query_one("#rollback-container", VerticalGroup).mount(deferred_content)
|
|
852
|
+
self._refresh_button_labels()
|
|
853
|
+
|
|
854
|
+
entries, plan = await self._load_preview_state_guarded()
|
|
855
|
+
await self._render_diff_pane(entries, plan)
|
|
856
|
+
if deferred_content is not None:
|
|
857
|
+
await self._reveal_deferred_content(deferred_content)
|
|
858
|
+
self._content_loading = False
|
|
859
|
+
self._content_ready = True
|
|
860
|
+
# Re-enables the rollback button and refreshes bindings.
|
|
861
|
+
self._set_preview_refreshing(False)
|
|
862
|
+
except RollbackLoadCancelled:
|
|
863
|
+
self._content_loading = False
|
|
864
|
+
self.dismiss(None)
|
|
865
|
+
except Exception as exc:
|
|
866
|
+
self._content_ready = False
|
|
867
|
+
try:
|
|
868
|
+
await self._show_load_error(exc)
|
|
869
|
+
if deferred_content is not None:
|
|
870
|
+
await self._reveal_deferred_content(deferred_content)
|
|
871
|
+
finally:
|
|
872
|
+
self._content_loading = False
|
|
873
|
+
self._preview_refreshing = False
|
|
874
|
+
with suppress(Exception):
|
|
875
|
+
self.refresh_bindings()
|
|
876
|
+
|
|
877
|
+
def _apply_loaded_state(self, state: RollbackModalState) -> None:
|
|
878
|
+
"""Install one immutable-value engine projection for preview rendering."""
|
|
879
|
+
self._tracker = state.tracker
|
|
880
|
+
self._available_turns = sorted(state.available_turns)
|
|
881
|
+
self._turn_prompts = state.turn_prompts
|
|
882
|
+
self._current_turn = state.current_turn
|
|
883
|
+
self._cwd = state.workspace_cwd
|
|
884
|
+
self._plan_augment = state.plan_augment
|
|
885
|
+
self._attribution_refresh = state.attribution_refresh
|
|
886
|
+
self._projection_acquire = state.projection_acquire
|
|
887
|
+
self._projection_release = state.projection_release
|
|
888
|
+
if self._available_turns:
|
|
889
|
+
self._selected_turn = self._available_turns[-1]
|
|
890
|
+
else:
|
|
891
|
+
self._selected_turn = 0
|
|
892
|
+
|
|
893
|
+
async def _reveal_deferred_content(self, content: _RollbackContent) -> None:
|
|
894
|
+
"""Swap fully prepared content for the initial indicator in one event-loop turn."""
|
|
895
|
+
content.display = True
|
|
896
|
+
loading_states = list(self.query("#rollback-initial-loading-state"))
|
|
897
|
+
if loading_states:
|
|
898
|
+
await remove_shielded(loading_states[0])
|
|
899
|
+
|
|
900
|
+
async def _show_load_error(self, exc: Exception) -> None:
|
|
901
|
+
"""Replace the preview slot with a recoverable, fail-closed error state."""
|
|
902
|
+
if not self.is_attached:
|
|
903
|
+
return
|
|
904
|
+
with suppress(Exception):
|
|
905
|
+
self.query_one("#btn-rollback", Button).disabled = True
|
|
906
|
+
self._set_revert_checkbox_visible(False)
|
|
907
|
+
self._set_empty_layout(True)
|
|
908
|
+
message = Static(
|
|
909
|
+
Text(self._render_message(_LOAD_PREVIEW_ERROR.bind(detail=DisplayBlock(str(exc))))),
|
|
910
|
+
id="rollback-load-error",
|
|
911
|
+
)
|
|
912
|
+
wrappers = list(self.query("#rollback-diff-wrapper"))
|
|
913
|
+
if wrappers:
|
|
914
|
+
with suppress(Exception):
|
|
915
|
+
wrapper = wrappers[0]
|
|
916
|
+
await remove_children_shielded(wrapper)
|
|
917
|
+
await wrapper.mount(message)
|
|
918
|
+
return
|
|
919
|
+
# State projection itself failed, before the Select/footer could
|
|
920
|
+
# be built. Keep the modal recoverable by replacing the initial
|
|
921
|
+
# spinner with an error and a normal close affordance.
|
|
922
|
+
with suppress(Exception):
|
|
923
|
+
loading_state = self.query_one("#rollback-initial-loading-state", VerticalGroup)
|
|
924
|
+
await remove_children_shielded(loading_state)
|
|
925
|
+
await loading_state.mount(
|
|
926
|
+
message,
|
|
927
|
+
Button(
|
|
928
|
+
self._render_message(_ROLLBACK_CLOSE.bind()),
|
|
929
|
+
id="btn-close",
|
|
930
|
+
variant="warning",
|
|
931
|
+
flat=True,
|
|
932
|
+
),
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
_PROMPT_PREVIEW_MAX = 160
|
|
936
|
+
|
|
937
|
+
def _build_turn_options(self) -> list[tuple[Text, int]]:
|
|
938
|
+
"""Return ``(label, value)`` pairs for the turn-picker ``Select``.
|
|
939
|
+
|
|
940
|
+
Under the "keep N turns" convention, each ``target_turn = N``
|
|
941
|
+
directly labels itself "Turn N" (= last kept turn). Layout,
|
|
942
|
+
top → bottom:
|
|
943
|
+
|
|
944
|
+
1. ``Turn {current} (Current)`` — disabled sentinel
|
|
945
|
+
communicating "you are here". ``current`` is the engine's
|
|
946
|
+
:attr:`AgentEngine.current_turn_number` when provided, else
|
|
947
|
+
``max(available_turns) + 1`` as a fallback. Using the
|
|
948
|
+
engine counter keeps the sentinel correct even when every
|
|
949
|
+
snapshot on disk has been pruned/deleted (in which case
|
|
950
|
+
``available_turns`` collapses to ``[0]``). Value is
|
|
951
|
+
:data:`_LATEST_SENTINEL` so :meth:`_on_turn_changed` can
|
|
952
|
+
detect accidental selection and snap back.
|
|
953
|
+
2. For each rollback target ``t >= 1`` in ``available_turns``
|
|
954
|
+
(reverse sorted): ``Turn {t} <user prompt preview>``
|
|
955
|
+
(value = ``t``). Preview pulled from
|
|
956
|
+
``self._turn_prompts[t]`` (history turn index = target).
|
|
957
|
+
3. ``Session start — discard all`` (value = ``0``), iff ``0``
|
|
958
|
+
is in ``available_turns``.
|
|
959
|
+
"""
|
|
960
|
+
if not self._available_turns:
|
|
961
|
+
return []
|
|
962
|
+
options: list[tuple[Text, int]] = []
|
|
963
|
+
# Prefer the engine-supplied current turn number — it's
|
|
964
|
+
# authoritative even when snapshots have been deleted (in
|
|
965
|
+
# which case ``available_turns`` may collapse to just ``[0]``
|
|
966
|
+
# and ``max + 1`` would under-report the true current turn).
|
|
967
|
+
current_turn = self._current_turn if self._current_turn is not None else self._available_turns[-1] + 1
|
|
968
|
+
options.append((self._format_latest_entry(current_turn), _LATEST_SENTINEL))
|
|
969
|
+
for t in sorted(self._available_turns, reverse=True):
|
|
970
|
+
if t == 0:
|
|
971
|
+
continue
|
|
972
|
+
options.append((self._format_keep_entry(t), t))
|
|
973
|
+
if 0 in self._available_turns:
|
|
974
|
+
options.append((self._format_session_start_entry(), 0))
|
|
975
|
+
return options
|
|
976
|
+
|
|
977
|
+
def _format_latest_entry(self, latest: int) -> Text:
|
|
978
|
+
"""``Turn N (Current) - <preview>`` — fully dimmed."""
|
|
979
|
+
preview = self._clip_preview(self._turn_prompts.get(latest, ""))
|
|
980
|
+
if preview:
|
|
981
|
+
return _TurnOptionText(
|
|
982
|
+
self._render_message(_TURN_CURRENT_PREVIEW.bind(turn=latest)),
|
|
983
|
+
preview,
|
|
984
|
+
prefix_style="dim",
|
|
985
|
+
message_style="dim italic",
|
|
986
|
+
)
|
|
987
|
+
return Text(self._render_message(_TURN_CURRENT.bind(turn=latest)), style="dim")
|
|
988
|
+
|
|
989
|
+
def _format_keep_entry(self, kept: int) -> Text:
|
|
990
|
+
"""``Turn N - <dim user prompt preview>`` for a selectable target."""
|
|
991
|
+
preview = self._clip_preview(self._turn_prompts.get(kept, ""))
|
|
992
|
+
if preview:
|
|
993
|
+
return _TurnOptionText(self._render_message(_TURN_PREVIEW.bind(turn=kept)), preview)
|
|
994
|
+
return Text(self._render_message(_TURN.bind(turn=kept)))
|
|
995
|
+
|
|
996
|
+
def _format_session_start_entry(self) -> Text:
|
|
997
|
+
"""``Session start — discard all`` bottom row."""
|
|
998
|
+
label = Text(self._render_message(_SESSION_START.bind()))
|
|
999
|
+
label.append(self._render_message(_DISCARD_ALL_SUFFIX.bind()), style="dim")
|
|
1000
|
+
return label
|
|
1001
|
+
|
|
1002
|
+
def _clip_preview(self, raw: str) -> str:
|
|
1003
|
+
"""Collapse whitespace + truncate to :attr:`_PROMPT_PREVIEW_MAX`."""
|
|
1004
|
+
preview = " ".join(raw.split()).strip()
|
|
1005
|
+
if len(preview) > self._PROMPT_PREVIEW_MAX:
|
|
1006
|
+
preview = preview[: self._PROMPT_PREVIEW_MAX - 1] + "…"
|
|
1007
|
+
return preview
|
|
1008
|
+
|
|
1009
|
+
async def _load_preview_state(self) -> tuple[list[DiffFileEntry], RollbackPlan | None]:
|
|
1010
|
+
"""Refresh attribution and build the selected preview without touching the DOM."""
|
|
1011
|
+
if self._attribution_refresh is not None:
|
|
1012
|
+
# Peer claims published after the modal opened can demote
|
|
1013
|
+
# local rows (contested/foreign), reshaping the plan the
|
|
1014
|
+
# entries below are built from — reclassify BEFORE reading
|
|
1015
|
+
# the tracker, matching the engine's execution-time order
|
|
1016
|
+
# (reclassify → build plan → augment). This is the ENGINE
|
|
1017
|
+
# refresh (persists on change); cheap when nothing changed;
|
|
1018
|
+
# failures fall back to the stale view, which the engine
|
|
1019
|
+
# re-checks destructively anyway.
|
|
1020
|
+
try:
|
|
1021
|
+
await self._attribution_refresh()
|
|
1022
|
+
except Exception:
|
|
1023
|
+
logger.debug("Rollback preview attribution refresh failed", exc_info=True)
|
|
1024
|
+
tracker = self._tracker
|
|
1025
|
+
if tracker is None:
|
|
1026
|
+
raise RuntimeError("rollback state was not loaded")
|
|
1027
|
+
entries, plan = await asyncio.to_thread(_build_preview_state, tracker, self._selected_turn, self._cwd)
|
|
1028
|
+
# Paths the plan dropped (non-restorable, move-poisoned, foreign,
|
|
1029
|
+
# …) are surfaced as a non-checkable note, never silently.
|
|
1030
|
+
if plan is not None and self._plan_augment is not None:
|
|
1031
|
+
# Same peer check the engine runs at execution time
|
|
1032
|
+
# (PEER_MODIFIED_SINCE) — the preview must not offer files
|
|
1033
|
+
# the backend will then exclude. Registry I/O failures
|
|
1034
|
+
# leave the un-augmented plan; the engine re-checks anyway.
|
|
1035
|
+
# Executor hop: augmentation scans and reads peer registry
|
|
1036
|
+
# files — like every engine-side call of the same method,
|
|
1037
|
+
# that I/O must not run on the UI event loop.
|
|
1038
|
+
try:
|
|
1039
|
+
plan = await asyncio.to_thread(self._plan_augment, plan, self._cwd)
|
|
1040
|
+
except Exception:
|
|
1041
|
+
logger.debug("Rollback preview peer augmentation failed", exc_info=True)
|
|
1042
|
+
else:
|
|
1043
|
+
# ``entries`` intersected the UN-augmented plan (inside
|
|
1044
|
+
# ``_entries_for_target``); drop what the peer check just
|
|
1045
|
+
# excluded so the tree matches the plan.
|
|
1046
|
+
excluded_paths = {path for path, _reason in plan.exclusions}
|
|
1047
|
+
entries = [e for e in entries if e.path not in excluded_paths]
|
|
1048
|
+
return entries, plan
|
|
1049
|
+
|
|
1050
|
+
async def _load_preview_state_guarded(self) -> tuple[list[DiffFileEntry], RollbackPlan | None]:
|
|
1051
|
+
"""Build one preview while backend prompt admission remains fenced."""
|
|
1052
|
+
acquire = self._projection_acquire
|
|
1053
|
+
release = self._projection_release
|
|
1054
|
+
if acquire is None or release is None:
|
|
1055
|
+
return await self._load_preview_state()
|
|
1056
|
+
|
|
1057
|
+
owner = await acquire()
|
|
1058
|
+
projection_task = asyncio.create_task(self._load_preview_state())
|
|
1059
|
+
release_deferred = False
|
|
1060
|
+
try:
|
|
1061
|
+
return await asyncio.shield(projection_task)
|
|
1062
|
+
except asyncio.CancelledError:
|
|
1063
|
+
release_deferred = True
|
|
1064
|
+
|
|
1065
|
+
def _finish_background_projection(
|
|
1066
|
+
task: asyncio.Task[tuple[list[DiffFileEntry], RollbackPlan | None]],
|
|
1067
|
+
) -> None:
|
|
1068
|
+
try:
|
|
1069
|
+
task.result()
|
|
1070
|
+
except asyncio.CancelledError:
|
|
1071
|
+
pass
|
|
1072
|
+
except Exception:
|
|
1073
|
+
logger.debug("Cancelled rollback preview projection failed", exc_info=True)
|
|
1074
|
+
finally:
|
|
1075
|
+
release(owner)
|
|
1076
|
+
|
|
1077
|
+
projection_task.add_done_callback(_finish_background_projection)
|
|
1078
|
+
raise
|
|
1079
|
+
finally:
|
|
1080
|
+
if not release_deferred:
|
|
1081
|
+
release(owner)
|
|
1082
|
+
|
|
1083
|
+
async def _refresh_diff_pane(self) -> None:
|
|
1084
|
+
"""Load and rebuild the preview for a newly selected turn."""
|
|
1085
|
+
self._preview_failed = False
|
|
1086
|
+
self._set_preview_refreshing(True)
|
|
1087
|
+
try:
|
|
1088
|
+
entries, plan = await self._load_preview_state_guarded()
|
|
1089
|
+
await self._render_diff_pane(entries, plan)
|
|
1090
|
+
except RollbackLoadCancelled:
|
|
1091
|
+
self.dismiss(None)
|
|
1092
|
+
except asyncio.CancelledError:
|
|
1093
|
+
self._preview_failed = True
|
|
1094
|
+
raise
|
|
1095
|
+
except Exception as exc:
|
|
1096
|
+
# The selected target changes before its projection is loaded. A
|
|
1097
|
+
# failure must therefore invalidate the old pane: otherwise its
|
|
1098
|
+
# checked paths could be confirmed against the new target turn.
|
|
1099
|
+
self._preview_failed = True
|
|
1100
|
+
logger.warning("Rollback preview refresh failed", exc_info=True)
|
|
1101
|
+
await self._show_preview_error(exc)
|
|
1102
|
+
finally:
|
|
1103
|
+
self._set_preview_refreshing(False)
|
|
1104
|
+
|
|
1105
|
+
async def _show_preview_error(self, exc: Exception) -> None:
|
|
1106
|
+
"""Replace any prior projection with a non-destructive error state."""
|
|
1107
|
+
self._pane = None
|
|
1108
|
+
self._set_revert_checkbox_visible(False)
|
|
1109
|
+
self._set_empty_layout(True)
|
|
1110
|
+
try:
|
|
1111
|
+
wrapper = self.query_one("#rollback-diff-wrapper", VerticalGroup)
|
|
1112
|
+
await remove_children_shielded(wrapper)
|
|
1113
|
+
await wrapper.mount(
|
|
1114
|
+
Static(
|
|
1115
|
+
Text(self._render_message(_REFRESH_PREVIEW_ERROR.bind(detail=DisplayBlock(str(exc))))),
|
|
1116
|
+
id="rollback-preview-error",
|
|
1117
|
+
)
|
|
1118
|
+
)
|
|
1119
|
+
except Exception:
|
|
1120
|
+
# The safety state is the cleared pane + failed flag. Keep those
|
|
1121
|
+
# authoritative even if teardown races removal of the wrapper.
|
|
1122
|
+
logger.debug("Unable to render rollback preview error", exc_info=True)
|
|
1123
|
+
|
|
1124
|
+
async def _render_diff_pane(self, entries: list[DiffFileEntry], plan: RollbackPlan | None) -> None:
|
|
1125
|
+
"""Replace preview widgets from an already-computed entry/plan projection."""
|
|
1126
|
+
wrapper = self.query_one("#rollback-diff-wrapper", VerticalGroup)
|
|
1127
|
+
# Drop the old pane reference before yielding so a queued confirm
|
|
1128
|
+
# can never act on a preview that is being replaced.
|
|
1129
|
+
self._pane = None
|
|
1130
|
+
await remove_children_shielded(wrapper)
|
|
1131
|
+
has_changes = bool(entries)
|
|
1132
|
+
exclusions = plan.exclusions if plan is not None else []
|
|
1133
|
+
warnings = plan.warnings if plan is not None else []
|
|
1134
|
+
# The revert checkbox has nothing to gate when there are no
|
|
1135
|
+
# file changes at all. Hide it (rather than disable it) so
|
|
1136
|
+
# the footer collapses to a clean "Rollback / Close" pair.
|
|
1137
|
+
self._set_revert_checkbox_visible(has_changes)
|
|
1138
|
+
# Collapse the modal to content height when there is no diff
|
|
1139
|
+
# to show — the big 90% overlay feels empty otherwise. A CSS
|
|
1140
|
+
# class on the container drives the layout swap (see
|
|
1141
|
+
# ``#rollback-container.empty`` in the .tcss); the class stays
|
|
1142
|
+
# absent while a diff is mounted so the container returns to
|
|
1143
|
+
# its full 90%.
|
|
1144
|
+
self._set_empty_layout(not has_changes)
|
|
1145
|
+
if not has_changes:
|
|
1146
|
+
widgets: list[Static] = [
|
|
1147
|
+
Static(Text(self._render_message(_NO_FILE_CHANGES.bind())), id="rollback-diff-placeholder")
|
|
1148
|
+
]
|
|
1149
|
+
if warnings:
|
|
1150
|
+
widgets.append(
|
|
1151
|
+
Static(Text(_warning_note_text(warnings, self._locale_controller)), id="rollback-warnings-note")
|
|
1152
|
+
)
|
|
1153
|
+
if exclusions:
|
|
1154
|
+
# Text() so paths like ``a[bad].py`` render literally
|
|
1155
|
+
# instead of being parsed as Textual markup.
|
|
1156
|
+
widgets.append(
|
|
1157
|
+
Static(
|
|
1158
|
+
Text(_exclusion_note_text(exclusions, self._cwd, self._locale_controller)),
|
|
1159
|
+
id="rollback-exclusions-note",
|
|
1160
|
+
)
|
|
1161
|
+
)
|
|
1162
|
+
await wrapper.mount(*widgets)
|
|
1163
|
+
# Reset checkbox state so the next turn (which may have
|
|
1164
|
+
# changes) starts in a known state.
|
|
1165
|
+
self._sync_revert_checkbox(selected=0, total=0)
|
|
1166
|
+
return
|
|
1167
|
+
# ``checkable=True`` renders [x]/[ ] prefixes on every tree
|
|
1168
|
+
# entry. Defaults to all-checked; rebuilding the pane on turn
|
|
1169
|
+
# change (this method re-runs for every Select change) resets
|
|
1170
|
+
# the selection — satisfies "change turn → re-check everything".
|
|
1171
|
+
pane = DiffPeriodPane(entries, checkable=True, lazy=True)
|
|
1172
|
+
widgets_with_pane: list[Static | DiffPeriodPane] = [pane]
|
|
1173
|
+
if warnings:
|
|
1174
|
+
# The destructive confirm must not be the first place the
|
|
1175
|
+
# user learns a peer is mid-command — surface the advisory
|
|
1176
|
+
# BEFORE the buttons, next to the exclusion note.
|
|
1177
|
+
widgets_with_pane.append(
|
|
1178
|
+
Static(Text(_warning_note_text(warnings, self._locale_controller)), id="rollback-warnings-note")
|
|
1179
|
+
)
|
|
1180
|
+
if exclusions:
|
|
1181
|
+
widgets_with_pane.append(
|
|
1182
|
+
Static(
|
|
1183
|
+
Text(_exclusion_note_text(exclusions, self._cwd, self._locale_controller)),
|
|
1184
|
+
id="rollback-exclusions-note",
|
|
1185
|
+
)
|
|
1186
|
+
)
|
|
1187
|
+
await wrapper.mount(*widgets_with_pane)
|
|
1188
|
+
await pane.ensure_initialized()
|
|
1189
|
+
self._pane = pane
|
|
1190
|
+
# Fresh pane → every entry starts checked.
|
|
1191
|
+
self._sync_revert_checkbox(selected=len(entries), total=len(entries))
|
|
1192
|
+
|
|
1193
|
+
def _set_preview_refreshing(self, refreshing: bool) -> None:
|
|
1194
|
+
"""Disable destructive/preview actions while a turn projection is rebuilt."""
|
|
1195
|
+
self._preview_refreshing = refreshing
|
|
1196
|
+
with suppress(Exception):
|
|
1197
|
+
self.query_one("#btn-rollback", Button).disabled = refreshing or self._preview_failed
|
|
1198
|
+
if self._content_ready:
|
|
1199
|
+
self.refresh_bindings()
|
|
1200
|
+
|
|
1201
|
+
def _set_revert_checkbox_visible(self, visible: bool) -> None:
|
|
1202
|
+
"""Toggle visibility of the footer revert checkbox."""
|
|
1203
|
+
try:
|
|
1204
|
+
cb = self.query_one("#revert-checkbox", Checkbox)
|
|
1205
|
+
except Exception:
|
|
1206
|
+
return
|
|
1207
|
+
cb.display = visible
|
|
1208
|
+
|
|
1209
|
+
def _set_empty_layout(self, empty: bool) -> None:
|
|
1210
|
+
"""Add/remove the ``empty`` class on the container.
|
|
1211
|
+
|
|
1212
|
+
Drives the height collapse/expand in the .tcss: the class
|
|
1213
|
+
swaps ``height: 90%`` for ``height: auto`` on the container
|
|
1214
|
+
and its wrapper children, so the modal shrinks to just the
|
|
1215
|
+
Select + placeholder + button row while loading and when
|
|
1216
|
+
there are no changes to revert. The container composes with
|
|
1217
|
+
the class already set; the first mounted diff flips it off so
|
|
1218
|
+
a turn with changes gets the full overlay.
|
|
1219
|
+
"""
|
|
1220
|
+
try:
|
|
1221
|
+
container = self.query_one("#rollback-container", VerticalGroup)
|
|
1222
|
+
except Exception:
|
|
1223
|
+
return
|
|
1224
|
+
container.set_class(empty, "empty")
|
|
1225
|
+
|
|
1226
|
+
def _sync_revert_checkbox(self, *, selected: int, total: int) -> None:
|
|
1227
|
+
"""Update the revert checkbox's label to reflect the current count.
|
|
1228
|
+
|
|
1229
|
+
Also auto-flips ``value`` at the edges so the visible state
|
|
1230
|
+
matches what the Rollback button will actually do:
|
|
1231
|
+
|
|
1232
|
+
- ``selected == 0`` → uncheck (nothing to revert, revert=False).
|
|
1233
|
+
- ``selected == total`` → check (every file included).
|
|
1234
|
+
|
|
1235
|
+
Programmatic value changes suppress ``Checkbox.Changed`` at the
|
|
1236
|
+
source so they cannot cascade back onto the tree after this method
|
|
1237
|
+
returns (the tree is the source of truth here).
|
|
1238
|
+
"""
|
|
1239
|
+
try:
|
|
1240
|
+
cb = self.query_one("#revert-checkbox", Checkbox)
|
|
1241
|
+
except Exception:
|
|
1242
|
+
return
|
|
1243
|
+
cb.label = self._render_message(_REVERT_FILE_CHANGES.bind(count=selected))
|
|
1244
|
+
with cb.prevent(Checkbox.Changed):
|
|
1245
|
+
if selected == 0 and cb.value:
|
|
1246
|
+
cb.value = False
|
|
1247
|
+
# On turn change (total changed) force-reset value to True so
|
|
1248
|
+
# the user doesn't inherit the previous turn's toggle state.
|
|
1249
|
+
# Heuristic: a new turn always starts with selected == total.
|
|
1250
|
+
if selected == total and total > 0 and not cb.value:
|
|
1251
|
+
cb.value = True
|
|
1252
|
+
|
|
1253
|
+
def _refresh_button_labels(self) -> None:
|
|
1254
|
+
# Under the "keep N turns" convention the selected value *is*
|
|
1255
|
+
# the last kept turn, so the label is a direct substitution
|
|
1256
|
+
# — no gap-aware lookup needed. ``target = 0`` is the welcome
|
|
1257
|
+
# case and collapses to "Discard All".
|
|
1258
|
+
target = self._selected_turn
|
|
1259
|
+
label_plain = (
|
|
1260
|
+
self._render_message(_DISCARD_ALL.bind())
|
|
1261
|
+
if target <= 0
|
|
1262
|
+
else self._render_message(_DISCARD_AFTER_TURN.bind(turn=target))
|
|
1263
|
+
)
|
|
1264
|
+
self.query_one("#btn-rollback", Button).label = label_plain
|
|
1265
|
+
|
|
1266
|
+
@on(Select.Changed, "#rollback-turn-select")
|
|
1267
|
+
async def _on_turn_changed(self, event: Select.Changed) -> None:
|
|
1268
|
+
if event.value is Select.NULL:
|
|
1269
|
+
return
|
|
1270
|
+
new_turn = int(event.value)
|
|
1271
|
+
if new_turn == self._selected_turn:
|
|
1272
|
+
return
|
|
1273
|
+
if new_turn == _LATEST_SENTINEL or not self._content_ready or self._preview_refreshing:
|
|
1274
|
+
# The "you-are-here" sentinel entry is purely informational,
|
|
1275
|
+
# and a change landing before content is ready (or while a
|
|
1276
|
+
# refresh is in flight) cannot be honored. Either way the
|
|
1277
|
+
# Select must not be left displaying a turn that
|
|
1278
|
+
# ``_selected_turn`` doesn't hold — a silent drop here
|
|
1279
|
+
# desyncs the dropdown from the turn a later confirm
|
|
1280
|
+
# dismisses with. Snap back to the prior valid target;
|
|
1281
|
+
# ``prevent`` suppresses the recursive ``Select.Changed``
|
|
1282
|
+
# the reassignment would otherwise emit.
|
|
1283
|
+
with self.prevent(Select.Changed):
|
|
1284
|
+
event.select.value = self._selected_turn
|
|
1285
|
+
return
|
|
1286
|
+
self._selected_turn = new_turn
|
|
1287
|
+
self._refresh_button_labels()
|
|
1288
|
+
await self._refresh_diff_pane()
|
|
1289
|
+
|
|
1290
|
+
@on(Button.Pressed, "#btn-rollback")
|
|
1291
|
+
def _on_rollback(self, _event: Button.Pressed) -> None:
|
|
1292
|
+
"""Confirm handler — decide whether to revert files based on
|
|
1293
|
+
the footer checkbox **and** the tree selection.
|
|
1294
|
+
|
|
1295
|
+
The rule the user asked for: revert iff the checkbox is
|
|
1296
|
+
checked *and* at least one file is selected in the tree.
|
|
1297
|
+
Otherwise fall through to a history-only rollback with no
|
|
1298
|
+
file restoration — matches what the user sees in the
|
|
1299
|
+
checkbox state.
|
|
1300
|
+
"""
|
|
1301
|
+
if not self._content_ready or self._preview_refreshing or self._preview_failed:
|
|
1302
|
+
return
|
|
1303
|
+
paths = self._pane.selected_paths() if self._pane is not None else []
|
|
1304
|
+
try:
|
|
1305
|
+
cb = self.query_one("#revert-checkbox", Checkbox)
|
|
1306
|
+
want_revert = bool(cb.value)
|
|
1307
|
+
except Exception:
|
|
1308
|
+
want_revert = False
|
|
1309
|
+
if want_revert and paths:
|
|
1310
|
+
self.dismiss((self._selected_turn, True, paths))
|
|
1311
|
+
else:
|
|
1312
|
+
# No revert — selected_paths is moot, pass ``None`` so the
|
|
1313
|
+
# backend takes the plain-rollback path unambiguously.
|
|
1314
|
+
self.dismiss((self._selected_turn, False, None))
|
|
1315
|
+
|
|
1316
|
+
@on(Button.Pressed, "#btn-close")
|
|
1317
|
+
def _on_close(self, _event: Button.Pressed) -> None:
|
|
1318
|
+
self.dismiss(None)
|
|
1319
|
+
|
|
1320
|
+
@on(DiffPeriodPane.SelectionChanged)
|
|
1321
|
+
def _on_selection_changed(self, event: DiffPeriodPane.SelectionChanged) -> None:
|
|
1322
|
+
"""Keep the revert-checkbox label in sync with the tree."""
|
|
1323
|
+
event.stop()
|
|
1324
|
+
self._sync_revert_checkbox(selected=event.selected_count, total=event.total_count)
|
|
1325
|
+
|
|
1326
|
+
@on(Checkbox.Changed, "#revert-checkbox")
|
|
1327
|
+
def _on_revert_checkbox_changed(self, event: Checkbox.Changed) -> None:
|
|
1328
|
+
"""Cascade footer checkbox → every leaf in the tree.
|
|
1329
|
+
|
|
1330
|
+
Clicking the "Revert N file changes" checkbox off should clear
|
|
1331
|
+
the tree selection; clicking it back on should re-select
|
|
1332
|
+
everything. Programmatic writes from ``_sync_revert_checkbox``
|
|
1333
|
+
prevent this message at the source because they already reflect the
|
|
1334
|
+
tree state.
|
|
1335
|
+
"""
|
|
1336
|
+
event.stop()
|
|
1337
|
+
if self._pane is None:
|
|
1338
|
+
return
|
|
1339
|
+
self._pane.set_all_checked(bool(event.value))
|
|
1340
|
+
selected, total = self._pane.selection_counts()
|
|
1341
|
+
self._sync_revert_checkbox(selected=selected, total=total)
|
|
1342
|
+
|
|
1343
|
+
def check_action(self, action: str, parameters: tuple[object, ...]) -> bool | None:
|
|
1344
|
+
"""Hide and disable preview actions until interactive content is ready."""
|
|
1345
|
+
if action in self._CONTENT_ACTIONS and (
|
|
1346
|
+
not self._content_ready or self._preview_refreshing or self._preview_failed
|
|
1347
|
+
):
|
|
1348
|
+
return False
|
|
1349
|
+
return super().check_action(action, parameters)
|
|
1350
|
+
|
|
1351
|
+
def _before_dismiss(self, _result: object | None = None) -> None:
|
|
1352
|
+
"""Cancel initial preview work before any Esc, button, or backdrop dismissal."""
|
|
1353
|
+
if self._content_loading and self.is_attached:
|
|
1354
|
+
self.workers.cancel_group(self, "rollback-modal-content")
|
|
1355
|
+
super()._before_dismiss(_result)
|
|
1356
|
+
|
|
1357
|
+
def action_close(self) -> None:
|
|
1358
|
+
self.dismiss(None)
|
|
1359
|
+
|
|
1360
|
+
def action_toggle_view(self) -> None:
|
|
1361
|
+
"""Space binding — delegate split/unified toggle to the pane."""
|
|
1362
|
+
if self._content_ready and not self._preview_refreshing and not self._preview_failed and self._pane is not None:
|
|
1363
|
+
self._pane.toggle_view()
|
|
1364
|
+
|
|
1365
|
+
def action_toggle_check(self) -> None:
|
|
1366
|
+
"""X binding — toggle the check state of the cursor node.
|
|
1367
|
+
|
|
1368
|
+
Operates on whichever ``Tree`` inside the pane currently has
|
|
1369
|
+
focus. Folder cursor → cascades to every descendant leaf
|
|
1370
|
+
(fully-checked folders flip to unchecked, otherwise everything
|
|
1371
|
+
flips on) per the common tri-state convention.
|
|
1372
|
+
"""
|
|
1373
|
+
if not self._content_ready or self._preview_refreshing or self._preview_failed or self._pane is None:
|
|
1374
|
+
return
|
|
1375
|
+
focused = self.focused
|
|
1376
|
+
if not isinstance(focused, Tree):
|
|
1377
|
+
return
|
|
1378
|
+
node = focused.cursor_node
|
|
1379
|
+
if node is None:
|
|
1380
|
+
return
|
|
1381
|
+
self._pane.toggle_check_node(node)
|