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,2478 @@
|
|
|
1
|
+
# Copyright (c) Huawei Technologies Co., Ltd. 2026. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""Tool call widgets — ToolCall (individual) and ToolGroup (collapsible container).
|
|
4
|
+
|
|
5
|
+
ToolGroup groups consecutive tool calls within a single agent turn.
|
|
6
|
+
Parallel tool calls appear together, each with its own spinner/status.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import re
|
|
15
|
+
import time
|
|
16
|
+
from collections.abc import Callable, Sequence
|
|
17
|
+
from contextlib import suppress
|
|
18
|
+
from dataclasses import dataclass, field
|
|
19
|
+
from functools import partial
|
|
20
|
+
from typing import TYPE_CHECKING, Any, ClassVar, Literal, Protocol, cast, runtime_checkable
|
|
21
|
+
|
|
22
|
+
from rich.style import Style
|
|
23
|
+
from rich.text import Text
|
|
24
|
+
from textual.containers import Horizontal, Vertical
|
|
25
|
+
from textual.css.query import NoMatches
|
|
26
|
+
from textual.message import Message
|
|
27
|
+
from textual.reactive import reactive
|
|
28
|
+
from textual.selection import Selection
|
|
29
|
+
from textual.timer import Timer
|
|
30
|
+
from textual.widget import Widget
|
|
31
|
+
from textual.widgets import Static
|
|
32
|
+
|
|
33
|
+
from chrys.app.tui.clipboard import OSC52_COPY_MAX_BYTES, copy_text_to_clipboards, terminal_clipboard_size
|
|
34
|
+
from chrys.app.tui.copy_messages import COPIED_TITLE
|
|
35
|
+
from chrys.app.tui.i18n import render_str, render_text, widget_localizer
|
|
36
|
+
from chrys.app.tui.support.gc_freeze import (
|
|
37
|
+
GcAbsorbReason,
|
|
38
|
+
GcAbsorbRequested,
|
|
39
|
+
GcReclaimReason,
|
|
40
|
+
GcReclaimRequested,
|
|
41
|
+
GcRemovedContent,
|
|
42
|
+
current_gc_freeze_epoch,
|
|
43
|
+
)
|
|
44
|
+
from chrys.app.tui.util.static_update import update_static_in_place
|
|
45
|
+
from chrys.app.tui.util.visibility import is_widget_shown_on_active_screen
|
|
46
|
+
from chrys.app.tui.widgets.chat.file_snapshot import FileSnapshotPayload
|
|
47
|
+
from chrys.app.tui.widgets.chat.tool_view_builders import (
|
|
48
|
+
TOOL_VIEW_EMPTY,
|
|
49
|
+
TOOL_VIEW_OUTPUT,
|
|
50
|
+
ToolViewImage,
|
|
51
|
+
build_code_view,
|
|
52
|
+
build_output_views,
|
|
53
|
+
build_params_view,
|
|
54
|
+
)
|
|
55
|
+
from chrys.app.tui.widgets.click_affordance import ClickAffordance
|
|
56
|
+
from chrys.app.tui.widgets.loading import ChrysLoadingIndicator
|
|
57
|
+
from chrys.foundation.i18n import MessageRef, msg
|
|
58
|
+
from chrys.foundation.i18n.formatting import format_message, sanitize_legacy_block, sanitize_legacy_scalar
|
|
59
|
+
from chrys.foundation.patches.textual_precompose import precompose_tree
|
|
60
|
+
from chrys.foundation.platform.files import surrogate_safe_text
|
|
61
|
+
from chrys.foundation.tool_kinds import (
|
|
62
|
+
KIND_ASK_USER,
|
|
63
|
+
KIND_FILESYSTEM_READ,
|
|
64
|
+
KIND_FILESYSTEM_WRITE,
|
|
65
|
+
KIND_SEARCH,
|
|
66
|
+
KIND_SHELL,
|
|
67
|
+
KIND_SKILL,
|
|
68
|
+
KIND_SUB_AGENT,
|
|
69
|
+
)
|
|
70
|
+
from chrys.foundation.tool_result_metadata import (
|
|
71
|
+
legacy_error_text_applies,
|
|
72
|
+
result_text_exit_code,
|
|
73
|
+
shell_exit_code_from_metadata,
|
|
74
|
+
shell_timed_out_from_metadata,
|
|
75
|
+
tool_result_metadata_failure_state,
|
|
76
|
+
tool_result_metadata_is_rejected,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Protocol attributes expected on any tool widget (BaseToolCard or custom renderer):
|
|
80
|
+
# call_id, tool_name, status, result_text
|
|
81
|
+
# set_complete(result, duration_ms), set_error(error), update_args(args)
|
|
82
|
+
|
|
83
|
+
ToolRenderStatus = Literal["complete", "error", "rejected"]
|
|
84
|
+
logger = logging.getLogger(__name__)
|
|
85
|
+
|
|
86
|
+
_TOOL_DETAILS_COPIED = msg(
|
|
87
|
+
"tui.tool.copy.details",
|
|
88
|
+
fallback="Copied {tool_name} execution details",
|
|
89
|
+
)
|
|
90
|
+
_TOOL_DETAILS_COPIED_TOO_LARGE = msg(
|
|
91
|
+
"tui.tool.copy.details_too_large",
|
|
92
|
+
fallback="Copied {tool_name} execution details (terminal clipboard skipped: payload too large)",
|
|
93
|
+
)
|
|
94
|
+
_TOOL_DETAILS_COPIED_UNAVAILABLE = msg(
|
|
95
|
+
"tui.tool.copy.details_unavailable",
|
|
96
|
+
fallback="Copied {tool_name} execution details (terminal clipboard unavailable)",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
TOOL_CARD_ACTION_VIEW = msg("tui.tool_card.action.view", fallback="view")
|
|
100
|
+
TOOL_CARD_ACTION_COPY = msg("tui.tool_card.action.copy", fallback="copy")
|
|
101
|
+
TOOL_CARD_ACTION_VIEW_TOOLTIP = msg(
|
|
102
|
+
"tui.tool_card.action.view_tooltip",
|
|
103
|
+
fallback="View full tool input and output",
|
|
104
|
+
)
|
|
105
|
+
TOOL_CARD_ACTION_COPY_TOOLTIP = msg(
|
|
106
|
+
"tui.tool_card.action.copy_tooltip",
|
|
107
|
+
fallback="Copy tool execution details",
|
|
108
|
+
)
|
|
109
|
+
TOOL_CARD_RUNNING = msg("tui.tool_card.status.running", fallback="running")
|
|
110
|
+
TOOL_CARD_APPROVED = msg("tui.tool_card.status.approved", fallback="approved")
|
|
111
|
+
TOOL_CARD_REJECTED = msg("tui.tool_card.status.rejected", fallback="Rejected")
|
|
112
|
+
TOOL_CARD_ERRORED = msg("tui.tool_card.status.errored", fallback="Errored")
|
|
113
|
+
TOOL_CARD_ERRORED_WITH_CODE = msg(
|
|
114
|
+
"tui.tool_card.status.errored_with_code",
|
|
115
|
+
fallback="Errored {code}",
|
|
116
|
+
)
|
|
117
|
+
TOOL_CARD_COMPLETED = msg("tui.tool_card.status.completed", fallback="Completed")
|
|
118
|
+
TOOL_CARD_INTERRUPTED = msg("tui.tool_card.status.interrupted", fallback="Interrupted")
|
|
119
|
+
TOOL_CARD_SKIPPED = msg("tui.tool_card.status.skipped", fallback="Skipped")
|
|
120
|
+
_TOOL_CARD_IMAGE_RESOLUTION = msg(
|
|
121
|
+
"tui.tool_card.image.resolution",
|
|
122
|
+
fallback="Resolution: {width}x{height}",
|
|
123
|
+
)
|
|
124
|
+
_TOOL_CARD_IMAGE_TYPE = msg("tui.tool_card.image.type", fallback="Type: {media_type}")
|
|
125
|
+
_TOOL_CARD_GROUP_TITLE = msg(
|
|
126
|
+
"tui.tool_card.group.title",
|
|
127
|
+
fallback="Tools ({done}/{total})",
|
|
128
|
+
)
|
|
129
|
+
_TOOL_CARD_GROUP_TITLE_TIMED = msg(
|
|
130
|
+
"tui.tool_card.group.title_timed",
|
|
131
|
+
fallback="Tools ({done}/{total}, {duration})",
|
|
132
|
+
)
|
|
133
|
+
_TOOL_ACTIVITY_RUNNING = msg("tui.tool_card.group.activity.running", fallback="Running {tool}")
|
|
134
|
+
_TOOL_ACTIVITY_SHELL = msg("tui.tool_card.group.activity.shell", fallback="Running shell command")
|
|
135
|
+
_TOOL_ACTIVITY_READING = msg("tui.tool_card.group.activity.reading", fallback="Reading {path}")
|
|
136
|
+
_TOOL_ACTIVITY_WRITING = msg("tui.tool_card.group.activity.writing", fallback="Writing {path}")
|
|
137
|
+
_TOOL_ACTIVITY_EDITING = msg("tui.tool_card.group.activity.editing", fallback="Editing {path}")
|
|
138
|
+
_TOOL_ACTIVITY_SEARCHING = msg("tui.tool_card.group.activity.searching", fallback="Searching for {pattern}")
|
|
139
|
+
_TOOL_ACTIVITY_FINDING = msg("tui.tool_card.group.activity.finding", fallback="Finding {pattern}")
|
|
140
|
+
_TOOL_ACTIVITY_LOADING_SKILL = msg("tui.tool_card.group.activity.loading_skill", fallback="Loading skill {skill}")
|
|
141
|
+
_TOOL_ACTIVITY_READING_SKILL = msg(
|
|
142
|
+
"tui.tool_card.group.activity.reading_skill",
|
|
143
|
+
fallback="Reading skill resource {resource}",
|
|
144
|
+
)
|
|
145
|
+
_TOOL_ACTIVITY_RUNNING_SKILL = msg(
|
|
146
|
+
"tui.tool_card.group.activity.running_skill",
|
|
147
|
+
fallback="Running skill script {script}",
|
|
148
|
+
)
|
|
149
|
+
_TOOL_ACTIVITY_WAITING_FOR_ANSWER = msg(
|
|
150
|
+
"tui.tool_card.group.activity.waiting_for_answer",
|
|
151
|
+
fallback="Waiting for your answer",
|
|
152
|
+
)
|
|
153
|
+
_TOOL_ACTIVITY_RUNNING_AGENT = msg("tui.tool_card.group.activity.running_agent", fallback="Running {agent}")
|
|
154
|
+
_TOOL_ACTIVITY_AGENT_DETAIL = msg(
|
|
155
|
+
"tui.tool_card.group.activity.agent_detail",
|
|
156
|
+
fallback="Running {agent}: {activity}",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@runtime_checkable
|
|
161
|
+
class _ToolArgsUpdatable(Protocol):
|
|
162
|
+
def update_args(self, args: dict[str, Any]) -> None:
|
|
163
|
+
"""Refresh the displayed arguments for a mounted tool widget."""
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@runtime_checkable
|
|
167
|
+
class _ToolProgressUpdatable(Protocol):
|
|
168
|
+
def update_progress(self, lines: list[str]) -> None:
|
|
169
|
+
"""Refresh the displayed streaming progress for a mounted tool widget."""
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@runtime_checkable
|
|
173
|
+
class _HostedStatusUpdatable(Protocol):
|
|
174
|
+
def update_hosted_status(self, status: str, provider_status: str) -> None:
|
|
175
|
+
"""Refresh provider-hosted lifecycle text."""
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@runtime_checkable
|
|
179
|
+
class _ToolDisplayStateSerializable(Protocol):
|
|
180
|
+
def compact_display_state(self) -> dict[str, Any] | None:
|
|
181
|
+
"""Return compact renderer-specific state needed to rebuild the visible card."""
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@runtime_checkable
|
|
185
|
+
class _ToolDisplayStateRestorable(Protocol):
|
|
186
|
+
def restore_compact_display_state(self, state: dict[str, Any]) -> None:
|
|
187
|
+
"""Restore compact renderer-specific state before terminal rendering."""
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
@runtime_checkable
|
|
191
|
+
class _ToolErrorPayloadRestorable(Protocol):
|
|
192
|
+
def restore_error_payload(
|
|
193
|
+
self,
|
|
194
|
+
error: str,
|
|
195
|
+
*,
|
|
196
|
+
image_contents: list[Any],
|
|
197
|
+
metadata: dict[str, Any] | None,
|
|
198
|
+
artifacts: list[dict[str, Any]],
|
|
199
|
+
) -> None:
|
|
200
|
+
"""Restore structured output retained before a terminal failure."""
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@runtime_checkable
|
|
204
|
+
class _ToolPendingDiffMountable(Protocol):
|
|
205
|
+
async def mount_diff_if_pending(self) -> bool:
|
|
206
|
+
"""Mount an expensive inline diff that was deferred while collapsed."""
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@runtime_checkable
|
|
210
|
+
class _ToolPendingContentMountable(Protocol):
|
|
211
|
+
async def mount_pending_content(self) -> bool:
|
|
212
|
+
"""Mount expensive inline content that was deferred while collapsed."""
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
@runtime_checkable
|
|
216
|
+
class _ToolCollapsedContentReleasable(Protocol):
|
|
217
|
+
def release_collapsed_content(self) -> None:
|
|
218
|
+
"""Release expensive inline content after the parent group collapses."""
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
class _CompletableToolRenderer(Protocol):
|
|
222
|
+
"""Completion surface required from every registered tool renderer."""
|
|
223
|
+
|
|
224
|
+
status: str
|
|
225
|
+
|
|
226
|
+
def set_complete(self, result: str, duration_ms: int = 0, **kwargs: Any) -> None: ...
|
|
227
|
+
|
|
228
|
+
def set_error(self, error: str) -> None: ...
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _completion_renderer(widget: Widget) -> _CompletableToolRenderer:
|
|
232
|
+
"""View a registered Widget through the renderer's trusted structural contract."""
|
|
233
|
+
return cast("_CompletableToolRenderer", widget)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class ToolViewRequested(Message):
|
|
237
|
+
"""Request that the owning screen open a detailed tool-view modal."""
|
|
238
|
+
|
|
239
|
+
def __init__(
|
|
240
|
+
self,
|
|
241
|
+
*,
|
|
242
|
+
title: str,
|
|
243
|
+
input_widgets: list[Widget],
|
|
244
|
+
output_widgets: list[Widget],
|
|
245
|
+
raw_input: str = "",
|
|
246
|
+
raw_output: str = "",
|
|
247
|
+
copy_enabled: bool = True,
|
|
248
|
+
initial_tab: str = "input",
|
|
249
|
+
) -> None:
|
|
250
|
+
super().__init__()
|
|
251
|
+
self.title = title
|
|
252
|
+
self.input_widgets = input_widgets
|
|
253
|
+
self.output_widgets = output_widgets
|
|
254
|
+
self.raw_input = raw_input
|
|
255
|
+
self.raw_output = raw_output
|
|
256
|
+
self.copy_enabled = copy_enabled
|
|
257
|
+
self.initial_tab = initial_tab
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@dataclass
|
|
261
|
+
class _NestedToolActivity:
|
|
262
|
+
"""Latest presentation state for one tool running inside a sub-agent."""
|
|
263
|
+
|
|
264
|
+
tool_name: str
|
|
265
|
+
tool_kind: str
|
|
266
|
+
args: dict[str, Any]
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@dataclass
|
|
270
|
+
class _ToolRecord:
|
|
271
|
+
"""Minimal state needed to rebuild a completed collapsed tool widget."""
|
|
272
|
+
|
|
273
|
+
call_id: str
|
|
274
|
+
tool_name: str
|
|
275
|
+
tool_kind: str
|
|
276
|
+
args_summary: str
|
|
277
|
+
args: dict[str, Any] | None
|
|
278
|
+
status: str = "running"
|
|
279
|
+
result: str = ""
|
|
280
|
+
duration_ms: int = 0
|
|
281
|
+
duration_known: bool = False
|
|
282
|
+
timestamp: str = ""
|
|
283
|
+
replay_timing: bool = False
|
|
284
|
+
file_snapshot: FileSnapshotPayload | None = None
|
|
285
|
+
approval: str | None = None
|
|
286
|
+
metadata: dict[str, Any] | None = None
|
|
287
|
+
image_contents: list[Any] = field(default_factory=list, repr=False, compare=False)
|
|
288
|
+
lazy: bool = False
|
|
289
|
+
display_state: dict[str, Any] | None = None
|
|
290
|
+
completed_via_result: bool = False
|
|
291
|
+
provider_hosted: bool = False
|
|
292
|
+
hosted_family: str = ""
|
|
293
|
+
provider: str = ""
|
|
294
|
+
provider_item_type: str = ""
|
|
295
|
+
provider_status: str = ""
|
|
296
|
+
provider_call_id: str = ""
|
|
297
|
+
canonical_status: str = "running"
|
|
298
|
+
artifacts: list[dict[str, Any]] = field(default_factory=list)
|
|
299
|
+
activity_order: int = 0
|
|
300
|
+
nested_activities: dict[str, _NestedToolActivity] = field(default_factory=dict)
|
|
301
|
+
nested_latest_activity: _ToolActivity | None = None
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _activity_arg(args: dict[str, Any] | None, key: str, *, path: bool = False) -> str:
|
|
305
|
+
"""Return one single-line, markup-safe activity argument."""
|
|
306
|
+
if not args or not isinstance(value := args.get(key), str) or not value:
|
|
307
|
+
return ""
|
|
308
|
+
if path:
|
|
309
|
+
value = surrogate_safe_text(value)
|
|
310
|
+
return sanitize_legacy_scalar(value)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
type _ToolActivity = MessageRef | str
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
_ACTIVITY_ARG_KEYS = frozenset(
|
|
317
|
+
{
|
|
318
|
+
"agent",
|
|
319
|
+
"agent_name",
|
|
320
|
+
"path",
|
|
321
|
+
"pattern",
|
|
322
|
+
"reason",
|
|
323
|
+
"resource_name",
|
|
324
|
+
"script_name",
|
|
325
|
+
"skill_name",
|
|
326
|
+
}
|
|
327
|
+
)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _compact_activity_args(args: dict[str, Any] | None) -> dict[str, Any]:
|
|
331
|
+
"""Keep only string fields that can appear in an activity title."""
|
|
332
|
+
if not args:
|
|
333
|
+
return {}
|
|
334
|
+
return {key: value for key, value in args.items() if key in _ACTIVITY_ARG_KEYS and isinstance(value, str)}
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _tool_activity_reference(tool_name: str, tool_kind: str, args: dict[str, Any] | None) -> _ToolActivity:
|
|
338
|
+
"""Build localized activity prose for a running tool record."""
|
|
339
|
+
safe_name = sanitize_legacy_scalar(tool_name)
|
|
340
|
+
if tool_kind == KIND_SHELL:
|
|
341
|
+
if reason := _activity_arg(args, "reason"):
|
|
342
|
+
return reason
|
|
343
|
+
return _TOOL_ACTIVITY_SHELL.bind()
|
|
344
|
+
if tool_kind == KIND_FILESYSTEM_READ and (path := _activity_arg(args, "path", path=True)):
|
|
345
|
+
return _TOOL_ACTIVITY_READING.bind(path=path)
|
|
346
|
+
if tool_kind == KIND_FILESYSTEM_WRITE and (path := _activity_arg(args, "path", path=True)):
|
|
347
|
+
reference = _TOOL_ACTIVITY_EDITING if tool_name == "edit_file" else _TOOL_ACTIVITY_WRITING
|
|
348
|
+
return reference.bind(path=path)
|
|
349
|
+
if tool_kind == KIND_SEARCH and (pattern := _activity_arg(args, "pattern")):
|
|
350
|
+
reference = _TOOL_ACTIVITY_FINDING if tool_name == "glob" else _TOOL_ACTIVITY_SEARCHING
|
|
351
|
+
return reference.bind(pattern=pattern)
|
|
352
|
+
if tool_kind == KIND_SKILL:
|
|
353
|
+
skill = _activity_arg(args, "skill_name")
|
|
354
|
+
if tool_name == "load_skill" and skill:
|
|
355
|
+
return _TOOL_ACTIVITY_LOADING_SKILL.bind(skill=skill)
|
|
356
|
+
if tool_name == "read_skill_resource":
|
|
357
|
+
resource = _activity_arg(args, "resource_name")
|
|
358
|
+
if skill and resource:
|
|
359
|
+
resource = f"{skill}/{resource}"
|
|
360
|
+
if resource := resource or skill:
|
|
361
|
+
return _TOOL_ACTIVITY_READING_SKILL.bind(resource=resource)
|
|
362
|
+
if tool_name == "run_skill_script":
|
|
363
|
+
script = _activity_arg(args, "script_name")
|
|
364
|
+
if skill and script:
|
|
365
|
+
script = f"{skill}/{script}"
|
|
366
|
+
if script := script or skill:
|
|
367
|
+
return _TOOL_ACTIVITY_RUNNING_SKILL.bind(script=script)
|
|
368
|
+
if tool_kind == KIND_ASK_USER:
|
|
369
|
+
return _TOOL_ACTIVITY_WAITING_FOR_ANSWER.bind()
|
|
370
|
+
if tool_kind == KIND_SUB_AGENT:
|
|
371
|
+
agent = _activity_arg(args, "agent_name") or _activity_arg(args, "agent") or safe_name
|
|
372
|
+
return _TOOL_ACTIVITY_RUNNING_AGENT.bind(agent=agent)
|
|
373
|
+
return _TOOL_ACTIVITY_RUNNING.bind(tool=safe_name)
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def tool_activity_reference(tool_name: str, tool_kind: str, args: dict[str, Any] | None) -> _ToolActivity:
|
|
377
|
+
"""Return the shared semantic activity used by group and nested-agent summaries."""
|
|
378
|
+
return _tool_activity_reference(tool_name, tool_kind, args)
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _record_activity_reference(record: _ToolRecord, render_reference: Callable[[MessageRef], str]) -> _ToolActivity:
|
|
382
|
+
"""Return the most specific current activity for a top-level record."""
|
|
383
|
+
if record.tool_kind == KIND_SUB_AGENT and record.nested_latest_activity is not None:
|
|
384
|
+
nested_activity = record.nested_latest_activity
|
|
385
|
+
detail = nested_activity if isinstance(nested_activity, str) else render_reference(nested_activity)
|
|
386
|
+
agent = (
|
|
387
|
+
_activity_arg(record.args, "agent_name")
|
|
388
|
+
or _activity_arg(record.args, "agent")
|
|
389
|
+
or sanitize_legacy_scalar(record.tool_name)
|
|
390
|
+
)
|
|
391
|
+
return _TOOL_ACTIVITY_AGENT_DETAIL.bind(agent=agent, activity=detail)
|
|
392
|
+
return _tool_activity_reference(record.tool_name, record.tool_kind, record.args)
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _compact_tool_metadata(metadata: dict[str, Any] | None) -> dict[str, Any] | None:
|
|
396
|
+
"""Drop bulky snapshot payloads from metadata retained by collapsed tool records."""
|
|
397
|
+
if not metadata:
|
|
398
|
+
return metadata
|
|
399
|
+
compacted = dict(metadata)
|
|
400
|
+
compacted.pop("file_snapshot", None)
|
|
401
|
+
compacted.pop("shell_file_snapshots", None)
|
|
402
|
+
return compacted
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def fmt_duration(ms: int) -> str:
|
|
406
|
+
"""Format milliseconds as a compact human string: ``120ms``, ``25s``, ``1m 25s``, ``2h 3m``."""
|
|
407
|
+
if ms < 1000:
|
|
408
|
+
return f"{ms}ms"
|
|
409
|
+
seconds = ms // 1000
|
|
410
|
+
if seconds < 60:
|
|
411
|
+
return f"{seconds}s"
|
|
412
|
+
m, s = divmod(seconds, 60)
|
|
413
|
+
if m < 60:
|
|
414
|
+
return f"{m}m {s}s" if s else f"{m}m"
|
|
415
|
+
h, m = divmod(m, 60)
|
|
416
|
+
if m == 0:
|
|
417
|
+
return f"{h}h"
|
|
418
|
+
return f"{h}h {m}m"
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
_ARGS_DISPLAY_MAX = 120
|
|
422
|
+
"""Max characters for tool call arguments display."""
|
|
423
|
+
|
|
424
|
+
_RESULT_DISPLAY_MAX = 250
|
|
425
|
+
"""Max characters for tool call result display."""
|
|
426
|
+
|
|
427
|
+
_IMAGE_PLACEHOLDER_RE = re.compile(r"^\[image/[^]\s]+ image\]$")
|
|
428
|
+
|
|
429
|
+
_TERMINAL_TOOL_STATUSES = frozenset({"complete", "error", "rejected"})
|
|
430
|
+
_TERMINAL_CANONICAL_STATUSES = frozenset({"completed", "failed", "interrupted"})
|
|
431
|
+
|
|
432
|
+
_PENDING_CONTENT_MOUNT_CONCURRENCY = 4
|
|
433
|
+
"""Maximum deferred tool contents prepared concurrently after expanding a group."""
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
if TYPE_CHECKING:
|
|
437
|
+
from textual.app import ComposeResult, RenderResult
|
|
438
|
+
from textual.events import Click, Leave, MouseMove
|
|
439
|
+
from textual.geometry import Offset
|
|
440
|
+
from textual.visual import VisualType
|
|
441
|
+
|
|
442
|
+
_ToolCopyExcludedBase = Widget
|
|
443
|
+
else:
|
|
444
|
+
_ToolCopyExcludedBase = object
|
|
445
|
+
|
|
446
|
+
TOOL_COPY_EXCLUDED_CLASS = "-copy-excluded"
|
|
447
|
+
"""CSS class marking tool-renderer widgets whose text should not be copied."""
|
|
448
|
+
|
|
449
|
+
TOOL_COPY_INCLUDED_CLASS = "-copy-included"
|
|
450
|
+
"""CSS class opting a subtree back into copy inside an excluded tool card."""
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def _content_additional_properties(content: Any) -> dict[str, Any]:
|
|
454
|
+
if isinstance(content, dict):
|
|
455
|
+
extra = content.get("additional_properties")
|
|
456
|
+
else:
|
|
457
|
+
# Kernel Content is an external boundary for the TUI.
|
|
458
|
+
extra = getattr(content, "additional_properties", None)
|
|
459
|
+
return extra if isinstance(extra, dict) else {}
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def _content_attr(content: Any, attr: str) -> Any:
|
|
463
|
+
if isinstance(content, dict):
|
|
464
|
+
return content.get(attr)
|
|
465
|
+
# Kernel Content is an external boundary for the TUI.
|
|
466
|
+
return getattr(content, attr, None)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def _positive_int(value: Any) -> int | None:
|
|
470
|
+
if isinstance(value, bool):
|
|
471
|
+
return None
|
|
472
|
+
if isinstance(value, int) and value > 0:
|
|
473
|
+
return value
|
|
474
|
+
return None
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _image_metadata_value(content: Any, key: str) -> Any:
|
|
478
|
+
extra = _content_additional_properties(content)
|
|
479
|
+
return extra.get(key) if key in extra else _content_attr(content, key)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
def _image_result_display(result: str, image_contents: Sequence[Any] | None) -> str:
|
|
483
|
+
if not image_contents or not result:
|
|
484
|
+
return result
|
|
485
|
+
|
|
486
|
+
lines: list[str] = []
|
|
487
|
+
for line in result.splitlines():
|
|
488
|
+
stripped = line.strip()
|
|
489
|
+
if _IMAGE_PLACEHOLDER_RE.match(stripped):
|
|
490
|
+
continue
|
|
491
|
+
lines.append(line)
|
|
492
|
+
return "\n".join(lines).strip()
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def result_text_is_error(result: str, *, exit_code_authoritative: bool = False) -> bool:
|
|
496
|
+
"""Return true for tool result text that follows the UI error convention."""
|
|
497
|
+
if exit_code_authoritative:
|
|
498
|
+
exit_code = result_text_exit_code(result)
|
|
499
|
+
if exit_code is not None:
|
|
500
|
+
return exit_code != 0
|
|
501
|
+
return result.lstrip().startswith("Error:")
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def tool_result_is_error(
|
|
505
|
+
result: str,
|
|
506
|
+
approval: str | None,
|
|
507
|
+
tool_kind: str = "",
|
|
508
|
+
metadata: dict[str, Any] | None = None,
|
|
509
|
+
tool_name: str = "",
|
|
510
|
+
) -> bool:
|
|
511
|
+
return _tool_result_error_state(result, approval, tool_kind, metadata, tool_name)
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def tool_result_render_status(
|
|
515
|
+
result: str,
|
|
516
|
+
approval: str | None,
|
|
517
|
+
tool_kind: str = "",
|
|
518
|
+
metadata: dict[str, Any] | None = None,
|
|
519
|
+
tool_name: str = "",
|
|
520
|
+
*,
|
|
521
|
+
parsed_error: bool = False,
|
|
522
|
+
parsed_error_overrides_structured_success: bool = False,
|
|
523
|
+
) -> ToolRenderStatus:
|
|
524
|
+
"""Return the terminal display status for a completed tool result."""
|
|
525
|
+
if tool_result_is_rejected(approval, metadata):
|
|
526
|
+
return "rejected"
|
|
527
|
+
if _tool_result_error_state(
|
|
528
|
+
result,
|
|
529
|
+
approval,
|
|
530
|
+
tool_kind,
|
|
531
|
+
metadata,
|
|
532
|
+
tool_name,
|
|
533
|
+
parsed_error=parsed_error,
|
|
534
|
+
parsed_error_overrides_structured_success=parsed_error_overrides_structured_success,
|
|
535
|
+
):
|
|
536
|
+
return "error"
|
|
537
|
+
return "complete"
|
|
538
|
+
|
|
539
|
+
|
|
540
|
+
def _tool_result_error_state(
|
|
541
|
+
result: str,
|
|
542
|
+
approval: str | None,
|
|
543
|
+
tool_kind: str = "",
|
|
544
|
+
metadata: dict[str, Any] | None = None,
|
|
545
|
+
tool_name: str = "",
|
|
546
|
+
*,
|
|
547
|
+
parsed_error: bool = False,
|
|
548
|
+
parsed_error_overrides_structured_success: bool = False,
|
|
549
|
+
) -> bool:
|
|
550
|
+
if tool_result_is_rejected(approval, metadata):
|
|
551
|
+
return False
|
|
552
|
+
metadata_state = tool_result_metadata_failure_state(metadata)
|
|
553
|
+
if metadata_state is True:
|
|
554
|
+
return True
|
|
555
|
+
if metadata_state is False:
|
|
556
|
+
return parsed_error and parsed_error_overrides_structured_success
|
|
557
|
+
if parsed_error:
|
|
558
|
+
return True
|
|
559
|
+
if tool_kind == KIND_SHELL:
|
|
560
|
+
exit_code = shell_exit_code_from_metadata(metadata)
|
|
561
|
+
if exit_code is not None:
|
|
562
|
+
return exit_code != 0
|
|
563
|
+
if shell_timed_out_from_metadata(metadata):
|
|
564
|
+
return True
|
|
565
|
+
if not legacy_error_text_applies(tool_kind, tool_name):
|
|
566
|
+
return False
|
|
567
|
+
return result_text_is_error(result, exit_code_authoritative=tool_kind == KIND_SHELL)
|
|
568
|
+
|
|
569
|
+
|
|
570
|
+
def tool_result_is_rejected(approval: str | None, metadata: dict[str, Any] | None = None) -> bool:
|
|
571
|
+
"""Return true when a tool result should render with rejected styling."""
|
|
572
|
+
if approval == "user_rejected":
|
|
573
|
+
return True
|
|
574
|
+
return tool_result_metadata_is_rejected(metadata)
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
def _image_border_subtitle(
|
|
578
|
+
image_contents: list[Any] | None,
|
|
579
|
+
fallback_size: tuple[int, int] | None,
|
|
580
|
+
*,
|
|
581
|
+
render_message: Callable[[MessageRef], str] = format_message,
|
|
582
|
+
) -> str:
|
|
583
|
+
if not image_contents:
|
|
584
|
+
return ""
|
|
585
|
+
first = image_contents[0]
|
|
586
|
+
width = _positive_int(_image_metadata_value(first, "width"))
|
|
587
|
+
height = _positive_int(_image_metadata_value(first, "height"))
|
|
588
|
+
if (width is None or height is None) and fallback_size is not None:
|
|
589
|
+
width, height = fallback_size
|
|
590
|
+
media_type = _image_metadata_value(first, "media_type") or _content_attr(first, "media_type")
|
|
591
|
+
if not isinstance(media_type, str):
|
|
592
|
+
media_type = ""
|
|
593
|
+
|
|
594
|
+
parts: list[str] = []
|
|
595
|
+
if width is not None and height is not None:
|
|
596
|
+
parts.append(render_message(_TOOL_CARD_IMAGE_RESOLUTION.bind(width=width, height=height)))
|
|
597
|
+
if media_type:
|
|
598
|
+
parts.append(render_message(_TOOL_CARD_IMAGE_TYPE.bind(media_type=sanitize_legacy_scalar(media_type))))
|
|
599
|
+
return " · ".join(parts)
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
class ToolCopyExcludedMixin(_ToolCopyExcludedBase):
|
|
603
|
+
"""Mixin for tool UI that should be omitted from screen-level copy."""
|
|
604
|
+
|
|
605
|
+
ALLOW_SELECT: ClassVar[bool] = False
|
|
606
|
+
image_contents: tuple[Any, ...] | list[Any] = ()
|
|
607
|
+
|
|
608
|
+
if TYPE_CHECKING:
|
|
609
|
+
approval: str | None
|
|
610
|
+
args: dict[str, Any]
|
|
611
|
+
args_summary: str
|
|
612
|
+
call_id: str
|
|
613
|
+
duration_ms: int
|
|
614
|
+
metadata: dict[str, Any] | None
|
|
615
|
+
result_text: str
|
|
616
|
+
status: str
|
|
617
|
+
tool_kind: str
|
|
618
|
+
tool_name: str
|
|
619
|
+
|
|
620
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
621
|
+
super().__init__(*args, **kwargs)
|
|
622
|
+
self.add_class(TOOL_COPY_EXCLUDED_CLASS)
|
|
623
|
+
|
|
624
|
+
def get_selection(self, selection: Selection) -> tuple[str, str] | None:
|
|
625
|
+
"""Tool renderer chrome/results are visual context, not transcript text."""
|
|
626
|
+
return None
|
|
627
|
+
|
|
628
|
+
def _show_tool_copy_button(self) -> None:
|
|
629
|
+
"""Reveal the copy and view affordances after a terminal state."""
|
|
630
|
+
with suppress(Exception):
|
|
631
|
+
self.query_one(ToolCardHeader).show_actions()
|
|
632
|
+
|
|
633
|
+
def _render_message(self, reference: MessageRef) -> str:
|
|
634
|
+
return render_str(widget_localizer(self), reference)
|
|
635
|
+
|
|
636
|
+
def _defer_tool_copy(self) -> bool:
|
|
637
|
+
"""Return true when a subclass has scheduled copy handling itself."""
|
|
638
|
+
return False
|
|
639
|
+
|
|
640
|
+
def _defer_tool_view(self) -> bool:
|
|
641
|
+
"""Return true when a subclass has scheduled view handling itself."""
|
|
642
|
+
return False
|
|
643
|
+
|
|
644
|
+
def _tool_copy_status(self) -> str:
|
|
645
|
+
"""Return a stable terminal status label for the copy payload."""
|
|
646
|
+
result = self.result_text.strip()
|
|
647
|
+
if self.has_class("-rejected"):
|
|
648
|
+
return "rejected"
|
|
649
|
+
if result.lower() == "cancelled":
|
|
650
|
+
return "cancelled"
|
|
651
|
+
if self.status == "error" or self.has_class("-error"):
|
|
652
|
+
return "error"
|
|
653
|
+
if tool_result_is_error(result, self.approval, self.tool_kind, self.metadata, self.tool_name):
|
|
654
|
+
return "error"
|
|
655
|
+
if self.status == "complete" or self.has_class("-success") or self.has_class("-complete"):
|
|
656
|
+
return "completed"
|
|
657
|
+
return self.status
|
|
658
|
+
|
|
659
|
+
def _tool_copy_input(self) -> tuple[str, str]:
|
|
660
|
+
"""Return ``(language, text)`` for the tool input section."""
|
|
661
|
+
if self.args:
|
|
662
|
+
return "json", json.dumps(self.args, indent=2, ensure_ascii=False, default=str)
|
|
663
|
+
if self.args_summary:
|
|
664
|
+
return "text", self.args_summary
|
|
665
|
+
return "json", "{}"
|
|
666
|
+
|
|
667
|
+
def _tool_input_args(self) -> dict[str, Any]:
|
|
668
|
+
"""Return structured input args from live state or replay summary."""
|
|
669
|
+
if self.args:
|
|
670
|
+
return self.args
|
|
671
|
+
if not self.args_summary:
|
|
672
|
+
return {}
|
|
673
|
+
try:
|
|
674
|
+
parsed = json.loads(self.args_summary)
|
|
675
|
+
except TypeError, ValueError:
|
|
676
|
+
return {}
|
|
677
|
+
return parsed if isinstance(parsed, dict) else {}
|
|
678
|
+
|
|
679
|
+
def _tool_copy_sections(self) -> list[tuple[str, str, str]]:
|
|
680
|
+
"""Return ``(title, language, text)`` output sections for the copy payload."""
|
|
681
|
+
return [
|
|
682
|
+
(
|
|
683
|
+
self._render_message(TOOL_VIEW_OUTPUT.bind()),
|
|
684
|
+
"text",
|
|
685
|
+
self.result_text or self._render_message(TOOL_VIEW_EMPTY.bind()),
|
|
686
|
+
)
|
|
687
|
+
]
|
|
688
|
+
|
|
689
|
+
def format_tool_execution_copy(self) -> str:
|
|
690
|
+
"""Format this tool invocation as a paste-friendly Markdown transcript."""
|
|
691
|
+
input_lang, input_text = self._tool_copy_input()
|
|
692
|
+
lines = [
|
|
693
|
+
f"# {self.tool_name}",
|
|
694
|
+
"",
|
|
695
|
+
f"- **Status:** `{self._tool_copy_status()}`",
|
|
696
|
+
]
|
|
697
|
+
if self.duration_ms:
|
|
698
|
+
lines.append(f"- **Duration:** `{fmt_duration(self.duration_ms)}`")
|
|
699
|
+
if self.call_id:
|
|
700
|
+
lines.append(f"- **Call ID:** `{self.call_id}`")
|
|
701
|
+
|
|
702
|
+
lines.extend(["", "## Input", _fenced_block(input_lang, input_text)])
|
|
703
|
+
for title, language, text in self._tool_copy_sections():
|
|
704
|
+
lines.extend(
|
|
705
|
+
["", f"## {title}", _fenced_block(language, text or self._render_message(TOOL_VIEW_EMPTY.bind()))]
|
|
706
|
+
)
|
|
707
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
708
|
+
|
|
709
|
+
def copy_tool_execution(self) -> None:
|
|
710
|
+
"""Copy full tool input/output to the OS clipboard, and OSC52 when small."""
|
|
711
|
+
payload = self.format_tool_execution_copy()
|
|
712
|
+
|
|
713
|
+
terminal_payload_too_large = terminal_clipboard_size(payload) > OSC52_COPY_MAX_BYTES
|
|
714
|
+
localizer = widget_localizer(self)
|
|
715
|
+
if copy_text_to_clipboards(self.app, payload, max_terminal_bytes=OSC52_COPY_MAX_BYTES):
|
|
716
|
+
self.notify(
|
|
717
|
+
render_str(localizer, _TOOL_DETAILS_COPIED.bind(tool_name=self.tool_name)),
|
|
718
|
+
title=render_str(localizer, COPIED_TITLE.bind()),
|
|
719
|
+
timeout=2,
|
|
720
|
+
markup=False,
|
|
721
|
+
)
|
|
722
|
+
elif terminal_payload_too_large:
|
|
723
|
+
self.notify(
|
|
724
|
+
render_str(localizer, _TOOL_DETAILS_COPIED_TOO_LARGE.bind(tool_name=self.tool_name)),
|
|
725
|
+
title=render_str(localizer, COPIED_TITLE.bind()),
|
|
726
|
+
timeout=3,
|
|
727
|
+
markup=False,
|
|
728
|
+
)
|
|
729
|
+
else:
|
|
730
|
+
self.notify(
|
|
731
|
+
render_str(localizer, _TOOL_DETAILS_COPIED_UNAVAILABLE.bind(tool_name=self.tool_name)),
|
|
732
|
+
title=render_str(localizer, COPIED_TITLE.bind()),
|
|
733
|
+
timeout=3,
|
|
734
|
+
markup=False,
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
def on_tool_copy_button_clicked(self, event: ToolCopyButton.Clicked) -> None:
|
|
738
|
+
"""Handle clicks from the header copy affordance."""
|
|
739
|
+
event.stop()
|
|
740
|
+
if self._defer_tool_copy():
|
|
741
|
+
return
|
|
742
|
+
self.copy_tool_execution()
|
|
743
|
+
|
|
744
|
+
# --- View modal ---
|
|
745
|
+
|
|
746
|
+
def _tool_view_title(self) -> str:
|
|
747
|
+
"""Return the modal title for the detailed view."""
|
|
748
|
+
return self.tool_name
|
|
749
|
+
|
|
750
|
+
def _view_dark(self) -> bool:
|
|
751
|
+
"""Whether the active Textual theme is dark — drives syntax styling."""
|
|
752
|
+
try:
|
|
753
|
+
return self.app.current_theme.dark
|
|
754
|
+
except Exception:
|
|
755
|
+
return True
|
|
756
|
+
|
|
757
|
+
def _tool_view_input_widgets(self) -> list[Widget]:
|
|
758
|
+
"""Build the Input tab content. Subclasses may override for richer views."""
|
|
759
|
+
dark = self._view_dark()
|
|
760
|
+
if self.args:
|
|
761
|
+
return build_params_view(self.args, dark=dark, render_message=self._render_message)
|
|
762
|
+
language, text = self._tool_copy_input()
|
|
763
|
+
return [build_code_view(language, text, dark=dark, render_message=self._render_message)]
|
|
764
|
+
|
|
765
|
+
def _tool_view_output_widgets(self) -> list[Widget]:
|
|
766
|
+
"""Build the Output tab content. Subclasses may override for richer views."""
|
|
767
|
+
if self.image_contents:
|
|
768
|
+
widgets: list[Widget] = [ToolViewImage(self.image_contents)]
|
|
769
|
+
display = _image_result_display(self.result_text, self.image_contents)
|
|
770
|
+
if display:
|
|
771
|
+
widgets.extend(
|
|
772
|
+
build_output_views(
|
|
773
|
+
[(self._render_message(TOOL_VIEW_OUTPUT.bind()), "text", display)],
|
|
774
|
+
dark=self._view_dark(),
|
|
775
|
+
render_message=self._render_message,
|
|
776
|
+
)
|
|
777
|
+
)
|
|
778
|
+
return widgets
|
|
779
|
+
return build_output_views(
|
|
780
|
+
self._tool_copy_sections(),
|
|
781
|
+
dark=self._view_dark(),
|
|
782
|
+
render_message=self._render_message,
|
|
783
|
+
)
|
|
784
|
+
|
|
785
|
+
def _tool_view_raw_input(self) -> str:
|
|
786
|
+
"""Return the raw Input payload to copy (JSON for structured args)."""
|
|
787
|
+
args = self._tool_input_args()
|
|
788
|
+
if args:
|
|
789
|
+
return json.dumps(args, indent=2, ensure_ascii=False, default=str)
|
|
790
|
+
return self._tool_copy_input()[1]
|
|
791
|
+
|
|
792
|
+
def _tool_view_raw_output(self) -> str:
|
|
793
|
+
"""Return the raw Output payload to copy (concatenated section text)."""
|
|
794
|
+
sections = self._tool_copy_sections()
|
|
795
|
+
if len(sections) == 1:
|
|
796
|
+
return sections[0][2] or self._render_message(TOOL_VIEW_EMPTY.bind())
|
|
797
|
+
empty = self._render_message(TOOL_VIEW_EMPTY.bind())
|
|
798
|
+
return "\n\n".join(f"{title}\n{text or empty}" for title, _, text in sections)
|
|
799
|
+
|
|
800
|
+
def _tool_view_copy_enabled(self) -> bool:
|
|
801
|
+
"""Whether the detail modal should expose its whole-payload copy binding."""
|
|
802
|
+
return True
|
|
803
|
+
|
|
804
|
+
def open_tool_view(self) -> None:
|
|
805
|
+
"""Request a modal showing the full, untruncated tool input and output."""
|
|
806
|
+
self.post_message(
|
|
807
|
+
ToolViewRequested(
|
|
808
|
+
title=self._tool_view_title(),
|
|
809
|
+
input_widgets=self._tool_view_input_widgets(),
|
|
810
|
+
output_widgets=self._tool_view_output_widgets(),
|
|
811
|
+
raw_input=self._tool_view_raw_input(),
|
|
812
|
+
raw_output=self._tool_view_raw_output(),
|
|
813
|
+
copy_enabled=self._tool_view_copy_enabled(),
|
|
814
|
+
)
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
def on_tool_view_button_clicked(self, event: ToolViewButton.Clicked) -> None:
|
|
818
|
+
"""Handle clicks from the header view affordance."""
|
|
819
|
+
event.stop()
|
|
820
|
+
if self._defer_tool_view():
|
|
821
|
+
return
|
|
822
|
+
self.open_tool_view()
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
class ToolViewButton(ClickAffordance):
|
|
826
|
+
"""Legacy view affordance widget; retained as the ``Clicked`` message namespace.
|
|
827
|
+
|
|
828
|
+
:class:`ToolCardHeader` now renders the affordance text itself and posts
|
|
829
|
+
``ToolViewButton.Clicked`` so existing ``on_tool_view_button_clicked``
|
|
830
|
+
handlers keep working unchanged.
|
|
831
|
+
"""
|
|
832
|
+
|
|
833
|
+
class Clicked(Message):
|
|
834
|
+
"""Posted when the view affordance is clicked."""
|
|
835
|
+
|
|
836
|
+
CLICK_MESSAGE = Clicked
|
|
837
|
+
|
|
838
|
+
def __init__(self) -> None:
|
|
839
|
+
super().__init__("view", classes="tool-view-btn")
|
|
840
|
+
|
|
841
|
+
def on_mount(self) -> None:
|
|
842
|
+
localizer = widget_localizer(self)
|
|
843
|
+
self.update(render_text(localizer, TOOL_CARD_ACTION_VIEW.bind()))
|
|
844
|
+
self.tooltip = render_str(localizer, TOOL_CARD_ACTION_VIEW_TOOLTIP.bind())
|
|
845
|
+
|
|
846
|
+
|
|
847
|
+
class ToolCopyButton(ClickAffordance):
|
|
848
|
+
"""Legacy copy affordance widget; retained as the ``Clicked`` message namespace.
|
|
849
|
+
|
|
850
|
+
See :class:`ToolViewButton` — :class:`ToolCardHeader` posts
|
|
851
|
+
``ToolCopyButton.Clicked`` for clicks on its copy zone.
|
|
852
|
+
"""
|
|
853
|
+
|
|
854
|
+
class Clicked(Message):
|
|
855
|
+
"""Posted when the copy affordance is clicked."""
|
|
856
|
+
|
|
857
|
+
CLICK_MESSAGE = Clicked
|
|
858
|
+
|
|
859
|
+
def __init__(self, *, tooltip_message: MessageRef | None = None) -> None:
|
|
860
|
+
super().__init__("copy", classes="tool-copy-btn")
|
|
861
|
+
self._tooltip_message = tooltip_message
|
|
862
|
+
|
|
863
|
+
def on_mount(self) -> None:
|
|
864
|
+
localizer = widget_localizer(self)
|
|
865
|
+
self.update(render_text(localizer, TOOL_CARD_ACTION_COPY.bind()))
|
|
866
|
+
tooltip = self._tooltip_message or TOOL_CARD_ACTION_COPY_TOOLTIP.bind()
|
|
867
|
+
self.tooltip = render_str(localizer, tooltip)
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
class ToolCardHeader(Static):
|
|
871
|
+
"""Single-widget tool header: label plus right-aligned view/copy affordances.
|
|
872
|
+
|
|
873
|
+
Replaces the former five-widget row (``ToolHeader`` container, label
|
|
874
|
+
``Static``, ``ToolViewButton``, ``ToolActionSeparator``,
|
|
875
|
+
``ToolCopyButton``). Tool cards dominate long transcripts, and every
|
|
876
|
+
mounted widget pays per-frame arrange/compositor cost while visible, so
|
|
877
|
+
the header renders the whole row itself and reimplements the affordance
|
|
878
|
+
behaviors: zone hit-testing for clicks, per-zone hover styling and
|
|
879
|
+
tooltip, and a pointer cursor over the actions.
|
|
880
|
+
|
|
881
|
+
The widget subclasses ``Static`` and treats its content as the label, so
|
|
882
|
+
renderer code that does ``query_one("#x-label", Static).update(...)``
|
|
883
|
+
keeps working unchanged. Markup parsing is disabled: labels are Rich
|
|
884
|
+
``Text`` (styling travels in spans), and a plain string — which may quote
|
|
885
|
+
model output containing ``[...]`` — must render literally instead of
|
|
886
|
+
raising ``MarkupError``.
|
|
887
|
+
"""
|
|
888
|
+
|
|
889
|
+
ALLOW_SELECT = False
|
|
890
|
+
|
|
891
|
+
COMPONENT_CLASSES: ClassVar[set[str]] = {
|
|
892
|
+
"toolcardheader--action",
|
|
893
|
+
"toolcardheader--action-hover",
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
DEFAULT_CSS = """
|
|
897
|
+
ToolCardHeader {
|
|
898
|
+
width: 100%;
|
|
899
|
+
height: auto;
|
|
900
|
+
}
|
|
901
|
+
ToolCardHeader > .toolcardheader--action {
|
|
902
|
+
color: $text-muted;
|
|
903
|
+
text-style: dim not bold;
|
|
904
|
+
}
|
|
905
|
+
ToolCardHeader > .toolcardheader--action-hover {
|
|
906
|
+
color: $accent;
|
|
907
|
+
text-style: underline not dim not bold;
|
|
908
|
+
}
|
|
909
|
+
"""
|
|
910
|
+
|
|
911
|
+
# Rendered actions cell: " view / copy " — 1-cell pads mirror the CSS
|
|
912
|
+
# padding of the legacy affordance widgets.
|
|
913
|
+
_ACTION_ZONES: ClassVar[tuple[tuple[str, int, int], ...]] = (("view", 1, 5), ("copy", 8, 12))
|
|
914
|
+
_ACTIONS_WIDTH: ClassVar[int] = 13
|
|
915
|
+
_ZONE_TOOLTIPS: ClassVar[dict[str, MessageRef]] = {
|
|
916
|
+
"view": TOOL_CARD_ACTION_VIEW_TOOLTIP.bind(),
|
|
917
|
+
"copy": TOOL_CARD_ACTION_COPY_TOOLTIP.bind(),
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
def __init__(
|
|
921
|
+
self,
|
|
922
|
+
label: Text | str = "",
|
|
923
|
+
*,
|
|
924
|
+
id: str | None = None,
|
|
925
|
+
view_only_label: MessageRef | None = None,
|
|
926
|
+
) -> None:
|
|
927
|
+
super().__init__(label, id=id, markup=False)
|
|
928
|
+
self.add_class(TOOL_COPY_EXCLUDED_CLASS)
|
|
929
|
+
self._action_mode: Literal["hidden", "view_only", "full"] = (
|
|
930
|
+
"view_only" if view_only_label is not None else "hidden"
|
|
931
|
+
)
|
|
932
|
+
self._hover_zone: str | None = None
|
|
933
|
+
self._view_label = TOOL_CARD_ACTION_VIEW.fallback
|
|
934
|
+
self._copy_label = TOOL_CARD_ACTION_COPY.fallback
|
|
935
|
+
self._view_only_message = view_only_label
|
|
936
|
+
self._view_only_label = format_message(view_only_label) if view_only_label is not None else ""
|
|
937
|
+
|
|
938
|
+
def on_mount(self) -> None:
|
|
939
|
+
self._resolve_action_labels()
|
|
940
|
+
|
|
941
|
+
def update(self, content: VisualType = "", *, layout: bool = True) -> None:
|
|
942
|
+
"""Replace the label; relayout only when the new label changes the header's size.
|
|
943
|
+
|
|
944
|
+
Running cards rewrite their label with the elapsed time every second, and a
|
|
945
|
+
same-size label needs a repaint, not a reflow of the whole transcript. An explicit
|
|
946
|
+
``layout=False`` still skips the layout unconditionally.
|
|
947
|
+
"""
|
|
948
|
+
if layout:
|
|
949
|
+
update_static_in_place(self, content)
|
|
950
|
+
else:
|
|
951
|
+
super().update(content, layout=False)
|
|
952
|
+
|
|
953
|
+
def _resolve_action_labels(self) -> None:
|
|
954
|
+
localizer = widget_localizer(self)
|
|
955
|
+
self._view_label = render_str(localizer, TOOL_CARD_ACTION_VIEW.bind())
|
|
956
|
+
self._copy_label = render_str(localizer, TOOL_CARD_ACTION_COPY.bind())
|
|
957
|
+
if self._view_only_message is not None:
|
|
958
|
+
self._view_only_label = render_str(localizer, self._view_only_message)
|
|
959
|
+
|
|
960
|
+
def get_selection(self, selection: Selection) -> tuple[str, str] | None:
|
|
961
|
+
"""Header chrome is visual context, not transcript text."""
|
|
962
|
+
return None
|
|
963
|
+
|
|
964
|
+
@property
|
|
965
|
+
def actions_visible(self) -> bool:
|
|
966
|
+
"""Whether the view/copy affordances are currently shown."""
|
|
967
|
+
return self._action_mode != "hidden"
|
|
968
|
+
|
|
969
|
+
@property
|
|
970
|
+
def copy_action_visible(self) -> bool:
|
|
971
|
+
"""Whether the terminal copy affordance is currently shown."""
|
|
972
|
+
return self._action_mode == "full"
|
|
973
|
+
|
|
974
|
+
def show_actions(self) -> None:
|
|
975
|
+
"""Reveal the view/copy affordances after a terminal tool state."""
|
|
976
|
+
if self._action_mode == "full":
|
|
977
|
+
return
|
|
978
|
+
self._action_mode = "full"
|
|
979
|
+
self._hover_zone = None
|
|
980
|
+
if self.is_mounted:
|
|
981
|
+
self._resolve_action_labels()
|
|
982
|
+
# Showing the actions narrows the label column, which can rewrap a
|
|
983
|
+
# long label — a layout refresh, but only once per card completion.
|
|
984
|
+
self.clear_cached_dimensions()
|
|
985
|
+
self.refresh(layout=True)
|
|
986
|
+
|
|
987
|
+
def append_replay_timing(
|
|
988
|
+
self,
|
|
989
|
+
*,
|
|
990
|
+
timestamp: str,
|
|
991
|
+
duration_ms: int,
|
|
992
|
+
show_duration: bool,
|
|
993
|
+
) -> None:
|
|
994
|
+
"""Append replay-only timing omitted by the renderer's normal label."""
|
|
995
|
+
if not timestamp and not show_duration:
|
|
996
|
+
return
|
|
997
|
+
label = self._label_renderable().copy()
|
|
998
|
+
if show_duration:
|
|
999
|
+
label.append(f" ({fmt_duration(duration_ms)})", style="dim")
|
|
1000
|
+
if timestamp:
|
|
1001
|
+
label.append(f" {timestamp}", style="dim")
|
|
1002
|
+
self.update(label)
|
|
1003
|
+
|
|
1004
|
+
def _label_renderable(self) -> Text:
|
|
1005
|
+
content = self.content
|
|
1006
|
+
if isinstance(content, Text):
|
|
1007
|
+
return content
|
|
1008
|
+
return Text(str(content))
|
|
1009
|
+
|
|
1010
|
+
def _zone_style(self, name: str) -> Style:
|
|
1011
|
+
# Component classes resolve theme variables like ``$text-muted``
|
|
1012
|
+
# ("auto 60%") through the stylesheet. Never feed
|
|
1013
|
+
# ``app.get_css_variables()`` values into Rich style strings: Rich
|
|
1014
|
+
# cannot parse tokens like "auto 60%" and silently drops the whole
|
|
1015
|
+
# style, leaving the span to inherit ancestor colors. The full
|
|
1016
|
+
# (non-partial) style is needed for the alpha blend against the
|
|
1017
|
+
# background (partial resolution yields unblended #ffffff), but its
|
|
1018
|
+
# stamped background color must be stripped so spans stay transparent
|
|
1019
|
+
# on ANSI passthrough themes.
|
|
1020
|
+
full = self.get_component_rich_style(name)
|
|
1021
|
+
return full.without_color + Style.from_color(full.color)
|
|
1022
|
+
|
|
1023
|
+
def _actions_text(self) -> Text:
|
|
1024
|
+
base = self._zone_style("toolcardheader--action")
|
|
1025
|
+
hover = self._zone_style("toolcardheader--action-hover")
|
|
1026
|
+
actions = Text(no_wrap=True)
|
|
1027
|
+
actions.append(" ")
|
|
1028
|
+
view_label = self._view_only_label if self._action_mode == "view_only" else self._view_label
|
|
1029
|
+
actions.append(view_label, style=hover if self._hover_zone == "view" else base)
|
|
1030
|
+
if self._action_mode == "full":
|
|
1031
|
+
actions.append(" / ", style=base)
|
|
1032
|
+
actions.append(self._copy_label, style=hover if self._hover_zone == "copy" else base)
|
|
1033
|
+
actions.append(" ")
|
|
1034
|
+
return actions
|
|
1035
|
+
|
|
1036
|
+
def _actions_width(self) -> int:
|
|
1037
|
+
"""Return the rendered action-cell width for the current mode."""
|
|
1038
|
+
if self._action_mode == "view_only":
|
|
1039
|
+
return Text(self._view_only_label).cell_len + 2
|
|
1040
|
+
return self._ACTIONS_WIDTH
|
|
1041
|
+
|
|
1042
|
+
def render(self) -> RenderResult:
|
|
1043
|
+
if not self.actions_visible:
|
|
1044
|
+
return self.visual
|
|
1045
|
+
from rich.table import Table
|
|
1046
|
+
|
|
1047
|
+
row = Table.grid(expand=True)
|
|
1048
|
+
row.add_column(ratio=1)
|
|
1049
|
+
row.add_column(width=self._actions_width())
|
|
1050
|
+
row.add_row(self._label_renderable(), self._actions_text())
|
|
1051
|
+
return row
|
|
1052
|
+
|
|
1053
|
+
def _zone_at(self, offset: Offset) -> str | None:
|
|
1054
|
+
"""Map a content-area offset to ``"view"``/``"copy"``, or ``None``."""
|
|
1055
|
+
if not self.actions_visible or offset.y != 0:
|
|
1056
|
+
return None
|
|
1057
|
+
actions_width = self._actions_width()
|
|
1058
|
+
actions_x = self.content_size.width - actions_width
|
|
1059
|
+
if actions_x < 0:
|
|
1060
|
+
# Header narrower than the actions cell: the rendered grid is
|
|
1061
|
+
# squeezed, so zone geometry no longer matches what's on screen.
|
|
1062
|
+
return None
|
|
1063
|
+
x = offset.x - actions_x
|
|
1064
|
+
if self._action_mode == "view_only":
|
|
1065
|
+
return "view" if 1 <= x < actions_width - 1 else None
|
|
1066
|
+
for zone, start, end in self._ACTION_ZONES:
|
|
1067
|
+
if start <= x < end:
|
|
1068
|
+
return zone
|
|
1069
|
+
return None
|
|
1070
|
+
|
|
1071
|
+
def _set_hover_zone(self, zone: str | None) -> None:
|
|
1072
|
+
if zone == self._hover_zone:
|
|
1073
|
+
return
|
|
1074
|
+
self._hover_zone = zone
|
|
1075
|
+
reference = self._ZONE_TOOLTIPS.get(zone) if zone else None
|
|
1076
|
+
self.tooltip = render_str(widget_localizer(self), reference) if reference is not None else None
|
|
1077
|
+
self.styles.pointer = "pointer" if zone else "default"
|
|
1078
|
+
self.refresh()
|
|
1079
|
+
|
|
1080
|
+
def on_mouse_move(self, event: MouseMove) -> None:
|
|
1081
|
+
self._set_hover_zone(self._zone_at(event.get_content_offset_capture(self)))
|
|
1082
|
+
|
|
1083
|
+
def on_leave(self, event: Leave) -> None:
|
|
1084
|
+
self._set_hover_zone(None)
|
|
1085
|
+
|
|
1086
|
+
def on_click(self, event: Click) -> None:
|
|
1087
|
+
zone = self._zone_at(event.get_content_offset_capture(self))
|
|
1088
|
+
if zone is None:
|
|
1089
|
+
return
|
|
1090
|
+
event.prevent_default()
|
|
1091
|
+
event.stop()
|
|
1092
|
+
if zone == "view":
|
|
1093
|
+
self.post_message(ToolViewButton.Clicked())
|
|
1094
|
+
else:
|
|
1095
|
+
self.post_message(ToolCopyButton.Clicked())
|
|
1096
|
+
|
|
1097
|
+
|
|
1098
|
+
def is_tool_copy_excluded(widget: Widget) -> bool:
|
|
1099
|
+
"""Return true when *widget* is inside tool UI excluded from copying.
|
|
1100
|
+
|
|
1101
|
+
Walked nearest-ancestor-first so the closest marker wins: a subtree
|
|
1102
|
+
carrying ``TOOL_COPY_INCLUDED_CLASS`` can opt back into selection/copy
|
|
1103
|
+
inside an otherwise excluded card.
|
|
1104
|
+
"""
|
|
1105
|
+
for ancestor in widget.ancestors_with_self:
|
|
1106
|
+
if ancestor.has_class(TOOL_COPY_INCLUDED_CLASS):
|
|
1107
|
+
return False
|
|
1108
|
+
if ancestor.has_class(TOOL_COPY_EXCLUDED_CLASS):
|
|
1109
|
+
return True
|
|
1110
|
+
return False
|
|
1111
|
+
|
|
1112
|
+
|
|
1113
|
+
def _fenced_block(language: str, text: str) -> str:
|
|
1114
|
+
"""Return a Markdown tilde fence long enough for *text*."""
|
|
1115
|
+
text = _sanitize_copy_text(text)
|
|
1116
|
+
max_run = max((len(match.group(0)) for match in re.finditer(r"~+", text)), default=0)
|
|
1117
|
+
fence = "~" * max(3, max_run + 1)
|
|
1118
|
+
lang = language.strip()
|
|
1119
|
+
opener = f"{fence}{lang}" if lang else fence
|
|
1120
|
+
return f"{opener}\n{text}\n{fence}"
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
def _sanitize_copy_text(text: str) -> str:
|
|
1124
|
+
"""Escape terminal control characters in copied tool payloads."""
|
|
1125
|
+
sanitized: list[str] = []
|
|
1126
|
+
changed = False
|
|
1127
|
+
for char in text:
|
|
1128
|
+
codepoint = ord(char)
|
|
1129
|
+
if (codepoint < 0x20 and char not in {"\n", "\t"}) or 0x7F <= codepoint <= 0x9F:
|
|
1130
|
+
sanitized.append(f"\\x{codepoint:02x}")
|
|
1131
|
+
changed = True
|
|
1132
|
+
else:
|
|
1133
|
+
sanitized.append(char)
|
|
1134
|
+
if not changed:
|
|
1135
|
+
return text
|
|
1136
|
+
return "".join(sanitized)
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
# ---------------------------------------------------------------------------
|
|
1140
|
+
# BaseToolCard / ToolCall — single tool invocation
|
|
1141
|
+
# ---------------------------------------------------------------------------
|
|
1142
|
+
|
|
1143
|
+
|
|
1144
|
+
class BaseToolCard(ToolCopyExcludedMixin, Widget):
|
|
1145
|
+
"""Shared state and copy/view behavior for tool renderer cards."""
|
|
1146
|
+
|
|
1147
|
+
status: reactive[str] = reactive("running")
|
|
1148
|
+
|
|
1149
|
+
def __init__(
|
|
1150
|
+
self,
|
|
1151
|
+
call_id: str,
|
|
1152
|
+
tool_name: str,
|
|
1153
|
+
args_summary: str = "",
|
|
1154
|
+
args: dict[str, Any] | None = None,
|
|
1155
|
+
) -> None:
|
|
1156
|
+
super().__init__()
|
|
1157
|
+
self.call_id = call_id
|
|
1158
|
+
self.tool_name = tool_name
|
|
1159
|
+
self.args_summary = args_summary
|
|
1160
|
+
self.args = args or {}
|
|
1161
|
+
self.tool_kind = ""
|
|
1162
|
+
self.result_text = ""
|
|
1163
|
+
self.metadata: dict[str, Any] = {}
|
|
1164
|
+
self.image_contents: list[Any] = []
|
|
1165
|
+
self.duration_ms = 0
|
|
1166
|
+
self.approval: str | None = None
|
|
1167
|
+
self.provider_hosted = False
|
|
1168
|
+
self.hosted_family = ""
|
|
1169
|
+
self.provider = ""
|
|
1170
|
+
self.provider_item_type = ""
|
|
1171
|
+
self.provider_status = ""
|
|
1172
|
+
self.provider_call_id = ""
|
|
1173
|
+
self.canonical_status = "running"
|
|
1174
|
+
self.artifacts: list[dict[str, Any]] = []
|
|
1175
|
+
|
|
1176
|
+
def configure_hosted(
|
|
1177
|
+
self,
|
|
1178
|
+
*,
|
|
1179
|
+
provider_hosted: bool,
|
|
1180
|
+
hosted_family: str,
|
|
1181
|
+
provider: str,
|
|
1182
|
+
provider_item_type: str,
|
|
1183
|
+
provider_status: str,
|
|
1184
|
+
provider_call_id: str,
|
|
1185
|
+
canonical_status: str,
|
|
1186
|
+
artifacts: list[dict[str, Any]] | None = None,
|
|
1187
|
+
) -> None:
|
|
1188
|
+
"""Install provider-hosted presentation metadata before mount."""
|
|
1189
|
+
self.provider_hosted = provider_hosted
|
|
1190
|
+
self.hosted_family = hosted_family
|
|
1191
|
+
self.provider = provider
|
|
1192
|
+
self.provider_item_type = provider_item_type
|
|
1193
|
+
self.provider_status = provider_status
|
|
1194
|
+
self.provider_call_id = provider_call_id
|
|
1195
|
+
self.canonical_status = canonical_status
|
|
1196
|
+
self.artifacts = list(artifacts or [])
|
|
1197
|
+
|
|
1198
|
+
|
|
1199
|
+
class ToolCall(BaseToolCard):
|
|
1200
|
+
"""Single tool call with animated spinner, name, args, result, and duration."""
|
|
1201
|
+
|
|
1202
|
+
DEFAULT_CSS = """
|
|
1203
|
+
ToolCall {
|
|
1204
|
+
padding: 0 0 0 2;
|
|
1205
|
+
height: auto;
|
|
1206
|
+
margin: 0 0 1 0;
|
|
1207
|
+
}
|
|
1208
|
+
ToolCall #tc-label {
|
|
1209
|
+
height: auto;
|
|
1210
|
+
}
|
|
1211
|
+
ToolCall #tc-body {
|
|
1212
|
+
height: auto;
|
|
1213
|
+
color: $text-muted;
|
|
1214
|
+
}
|
|
1215
|
+
ToolCall > #tc-panel {
|
|
1216
|
+
height: auto;
|
|
1217
|
+
margin: 0 0 0 2;
|
|
1218
|
+
border: round $tui-border-primary 50%;
|
|
1219
|
+
border-title-color: $text-error;
|
|
1220
|
+
border-title-style: bold;
|
|
1221
|
+
border-subtitle-align: right;
|
|
1222
|
+
border-subtitle-color: $text-muted;
|
|
1223
|
+
padding: 0 1;
|
|
1224
|
+
}
|
|
1225
|
+
ToolCall.-success > #tc-panel {
|
|
1226
|
+
border: round $tui-border-success 30%;
|
|
1227
|
+
border-title-color: $success;
|
|
1228
|
+
border-subtitle-color: $success;
|
|
1229
|
+
border-title-style: not bold;
|
|
1230
|
+
}
|
|
1231
|
+
ToolCall.-error > #tc-panel {
|
|
1232
|
+
border: round $tui-border-error 50%;
|
|
1233
|
+
border-title-color: $text-error;
|
|
1234
|
+
border-subtitle-color: $text-error;
|
|
1235
|
+
border-title-style: not bold;
|
|
1236
|
+
}
|
|
1237
|
+
ToolCall.-rejected > #tc-panel {
|
|
1238
|
+
border: round $tui-border-warning 50%;
|
|
1239
|
+
border-title-color: $warning;
|
|
1240
|
+
border-subtitle-color: $warning;
|
|
1241
|
+
border-title-style: not bold;
|
|
1242
|
+
}
|
|
1243
|
+
"""
|
|
1244
|
+
|
|
1245
|
+
_SPINNERS: ClassVar[str] = "◐◓◑◒"
|
|
1246
|
+
|
|
1247
|
+
def __init__(
|
|
1248
|
+
self,
|
|
1249
|
+
call_id: str,
|
|
1250
|
+
tool_name: str,
|
|
1251
|
+
args_summary: str = "",
|
|
1252
|
+
args: dict[str, Any] | None = None,
|
|
1253
|
+
) -> None:
|
|
1254
|
+
super().__init__(call_id, tool_name, args_summary, args=args)
|
|
1255
|
+
self._spin_idx = 0
|
|
1256
|
+
self._timer: Timer | None = None
|
|
1257
|
+
# Progress snapshots may claim the body while the card is still
|
|
1258
|
+
# running; the spinner timer must not repaint over them.
|
|
1259
|
+
self._body_pinned = False
|
|
1260
|
+
|
|
1261
|
+
def _label_text(self, duration_ms: int = 0) -> Text:
|
|
1262
|
+
t = Text()
|
|
1263
|
+
t.append("• ", style="bold")
|
|
1264
|
+
t.append(self.tool_name, style="bold")
|
|
1265
|
+
if duration_ms:
|
|
1266
|
+
t.append(f" ({fmt_duration(duration_ms)})", style="dim")
|
|
1267
|
+
return t
|
|
1268
|
+
|
|
1269
|
+
@staticmethod
|
|
1270
|
+
def _format_args_title(args_summary: str) -> str:
|
|
1271
|
+
"""Format JSON args as ``key: value key2: value2`` for border title."""
|
|
1272
|
+
try:
|
|
1273
|
+
parsed = json.loads(args_summary)
|
|
1274
|
+
except Exception:
|
|
1275
|
+
return sanitize_legacy_scalar(args_summary)
|
|
1276
|
+
if not isinstance(parsed, dict):
|
|
1277
|
+
return sanitize_legacy_scalar(args_summary)
|
|
1278
|
+
parts: list[str] = []
|
|
1279
|
+
for k, v in parsed.items():
|
|
1280
|
+
if isinstance(v, str):
|
|
1281
|
+
parts.append(f'{k}: "{sanitize_legacy_scalar(v)}"')
|
|
1282
|
+
else:
|
|
1283
|
+
parts.append(f"{k}: {sanitize_legacy_scalar(json.dumps(v, ensure_ascii=False))}")
|
|
1284
|
+
return " ".join(parts)
|
|
1285
|
+
|
|
1286
|
+
def _border_title_text(self) -> str:
|
|
1287
|
+
title = self.tool_name
|
|
1288
|
+
if self.args_summary:
|
|
1289
|
+
title = self._format_args_title(self.args_summary) or self.tool_name
|
|
1290
|
+
if len(title) > _ARGS_DISPLAY_MAX:
|
|
1291
|
+
title = title[:_ARGS_DISPLAY_MAX] + "..."
|
|
1292
|
+
return title
|
|
1293
|
+
|
|
1294
|
+
def compose(self) -> ComposeResult:
|
|
1295
|
+
yield ToolCardHeader(self._label_text(), id="tc-label")
|
|
1296
|
+
panel = Widget(id="tc-panel")
|
|
1297
|
+
panel.border_title = Text(self._border_title_text())
|
|
1298
|
+
yield panel
|
|
1299
|
+
|
|
1300
|
+
def update_args(self, args: dict[str, Any]) -> None:
|
|
1301
|
+
"""Refresh the displayed arguments after an approval edit."""
|
|
1302
|
+
self.args = args
|
|
1303
|
+
self.args_summary = json.dumps(args, ensure_ascii=False) if args else ""
|
|
1304
|
+
with suppress(Exception):
|
|
1305
|
+
self.query_one("#tc-panel").border_title = Text(self._border_title_text())
|
|
1306
|
+
|
|
1307
|
+
def on_mount(self) -> None:
|
|
1308
|
+
panel = self.query_one("#tc-panel")
|
|
1309
|
+
# One body Static hosts the spinner while running and the result text
|
|
1310
|
+
# after completion — content swaps instead of paired display toggles.
|
|
1311
|
+
panel.mount(Static(self._render_spinner(), id="tc-body"))
|
|
1312
|
+
self._timer = self.set_interval(0.12, self._spin)
|
|
1313
|
+
|
|
1314
|
+
def on_unmount(self) -> None:
|
|
1315
|
+
if self._timer is not None:
|
|
1316
|
+
self._timer.stop()
|
|
1317
|
+
|
|
1318
|
+
def _spin(self) -> None:
|
|
1319
|
+
if self.status == "running" and not self._body_pinned:
|
|
1320
|
+
self._spin_idx = (self._spin_idx + 1) % len(self._SPINNERS)
|
|
1321
|
+
# Only a shown card repaints: repainting a widget outside the visible cut (a collapsed
|
|
1322
|
+
# group, scrolled away, under a modal) makes the next geometry read rebuild the
|
|
1323
|
+
# compositor map for the whole transcript. Every tick renders the card's current
|
|
1324
|
+
# state, so the first tick after the card shows paints it.
|
|
1325
|
+
if not is_widget_shown_on_active_screen(self):
|
|
1326
|
+
return
|
|
1327
|
+
from contextlib import suppress
|
|
1328
|
+
|
|
1329
|
+
with suppress(Exception):
|
|
1330
|
+
update_static_in_place(self.query_one("#tc-body", Static), self._render_spinner())
|
|
1331
|
+
|
|
1332
|
+
def _render_spinner(self) -> Text:
|
|
1333
|
+
t = Text()
|
|
1334
|
+
t.append(f"{self._SPINNERS[self._spin_idx]} ", style="yellow")
|
|
1335
|
+
t.append(render_str(widget_localizer(self), TOOL_CARD_RUNNING.bind()), style="yellow")
|
|
1336
|
+
return t
|
|
1337
|
+
|
|
1338
|
+
def _truncate_result(self, text: str) -> str:
|
|
1339
|
+
"""Truncate result text for display. Subclasses may override."""
|
|
1340
|
+
if len(text) > _RESULT_DISPLAY_MAX:
|
|
1341
|
+
return text[:_RESULT_DISPLAY_MAX] + "..."
|
|
1342
|
+
return text
|
|
1343
|
+
|
|
1344
|
+
def _render_image_contents(self, image_contents: list[Any] | None) -> None:
|
|
1345
|
+
from chrys.app.tui.widgets.chat.image_preview import ImagePreviewGrid, extract_image_previews
|
|
1346
|
+
|
|
1347
|
+
previews = extract_image_previews(image_contents)
|
|
1348
|
+
existing: Static | None = None
|
|
1349
|
+
with suppress(Exception):
|
|
1350
|
+
existing = self.query_one("#tc-images", Static)
|
|
1351
|
+
if not previews:
|
|
1352
|
+
if existing is not None:
|
|
1353
|
+
existing.remove()
|
|
1354
|
+
return
|
|
1355
|
+
max_width = self.content_size.width or self.size.width or 80
|
|
1356
|
+
grid = ImagePreviewGrid(previews, max_width=max_width)
|
|
1357
|
+
try:
|
|
1358
|
+
panel = self.query_one("#tc-panel")
|
|
1359
|
+
except NoMatches:
|
|
1360
|
+
return
|
|
1361
|
+
subtitle = _image_border_subtitle(
|
|
1362
|
+
image_contents,
|
|
1363
|
+
previews[0].image.size if len(previews) == 1 else None,
|
|
1364
|
+
render_message=lambda reference: render_str(widget_localizer(self), reference),
|
|
1365
|
+
)
|
|
1366
|
+
if subtitle:
|
|
1367
|
+
panel.border_subtitle = Text(subtitle)
|
|
1368
|
+
if existing is not None:
|
|
1369
|
+
existing.update(grid)
|
|
1370
|
+
else:
|
|
1371
|
+
with suppress(Exception):
|
|
1372
|
+
panel.mount(Static(grid, id="tc-images"))
|
|
1373
|
+
|
|
1374
|
+
def set_complete(self, result: str, duration_ms: int = 0, **kwargs: Any) -> None:
|
|
1375
|
+
"""Mark this tool call as complete."""
|
|
1376
|
+
from contextlib import suppress
|
|
1377
|
+
|
|
1378
|
+
self.result_text = result
|
|
1379
|
+
self.duration_ms = duration_ms
|
|
1380
|
+
self.approval = kwargs.get("approval")
|
|
1381
|
+
metadata = kwargs.get("metadata")
|
|
1382
|
+
self.metadata = metadata if isinstance(metadata, dict) else {}
|
|
1383
|
+
render_status = tool_result_render_status(result, self.approval, self.tool_kind, self.metadata, self.tool_name)
|
|
1384
|
+
self.status = render_status
|
|
1385
|
+
if self._timer is not None:
|
|
1386
|
+
self._timer.stop()
|
|
1387
|
+
|
|
1388
|
+
with suppress(Exception):
|
|
1389
|
+
self.query_one("#tc-label", Static).update(self._label_text(duration_ms))
|
|
1390
|
+
|
|
1391
|
+
panel: Widget | None = None
|
|
1392
|
+
with suppress(NoMatches):
|
|
1393
|
+
panel = self.query_one("#tc-panel")
|
|
1394
|
+
approval = self.approval
|
|
1395
|
+
if approval == "user_approved" and panel is not None:
|
|
1396
|
+
panel.border_subtitle = render_text(widget_localizer(self), TOOL_CARD_APPROVED.bind())
|
|
1397
|
+
elif render_status == "rejected" and panel is not None:
|
|
1398
|
+
panel.border_subtitle = render_text(widget_localizer(self), TOOL_CARD_REJECTED.bind())
|
|
1399
|
+
|
|
1400
|
+
if render_status == "rejected":
|
|
1401
|
+
self.add_class("-rejected")
|
|
1402
|
+
elif render_status == "error":
|
|
1403
|
+
if panel is not None:
|
|
1404
|
+
panel.border_subtitle = render_text(widget_localizer(self), TOOL_CARD_ERRORED.bind())
|
|
1405
|
+
self.add_class("-error")
|
|
1406
|
+
else:
|
|
1407
|
+
self.add_class("-success")
|
|
1408
|
+
image_contents = kwargs.get("image_contents")
|
|
1409
|
+
self.image_contents = list(image_contents or [])
|
|
1410
|
+
display = _image_result_display(result, image_contents)
|
|
1411
|
+
display = self._truncate_result(display) if display else ""
|
|
1412
|
+
with suppress(Exception):
|
|
1413
|
+
self.query_one("#tc-body", Static).update(Text(sanitize_legacy_block(display)))
|
|
1414
|
+
self._render_image_contents(image_contents)
|
|
1415
|
+
self.add_class("-done")
|
|
1416
|
+
self._show_tool_copy_button()
|
|
1417
|
+
|
|
1418
|
+
def set_error(self, error: str) -> None:
|
|
1419
|
+
"""Mark this tool call as failed."""
|
|
1420
|
+
from contextlib import suppress
|
|
1421
|
+
|
|
1422
|
+
self.result_text = error
|
|
1423
|
+
self.status = "error"
|
|
1424
|
+
if self._timer is not None:
|
|
1425
|
+
self._timer.stop()
|
|
1426
|
+
|
|
1427
|
+
self.add_class("-error")
|
|
1428
|
+
with suppress(Exception):
|
|
1429
|
+
self.query_one("#tc-body", Static).update(Text(sanitize_legacy_block(error)))
|
|
1430
|
+
self.add_class("-done")
|
|
1431
|
+
self._show_tool_copy_button()
|
|
1432
|
+
|
|
1433
|
+
|
|
1434
|
+
# ---------------------------------------------------------------------------
|
|
1435
|
+
# ToolGroupTitle — clickable header that toggles collapse
|
|
1436
|
+
# ---------------------------------------------------------------------------
|
|
1437
|
+
|
|
1438
|
+
|
|
1439
|
+
class ToolGroupTitle(ToolCopyExcludedMixin, ClickAffordance):
|
|
1440
|
+
"""Clickable title for ToolGroup — posts Clicked message on click."""
|
|
1441
|
+
|
|
1442
|
+
DEFAULT_CSS = """
|
|
1443
|
+
ToolGroupTitle {
|
|
1444
|
+
color: $tui-tool-group-title;
|
|
1445
|
+
text-style: bold;
|
|
1446
|
+
height: auto;
|
|
1447
|
+
}
|
|
1448
|
+
"""
|
|
1449
|
+
|
|
1450
|
+
class Clicked(Message):
|
|
1451
|
+
"""Posted when the title is clicked."""
|
|
1452
|
+
|
|
1453
|
+
CLICK_MESSAGE = Clicked
|
|
1454
|
+
|
|
1455
|
+
def __init__(self, title: str | Text) -> None:
|
|
1456
|
+
self._full_title = title.copy() if isinstance(title, Text) else Text(title)
|
|
1457
|
+
super().__init__(self._full_title)
|
|
1458
|
+
|
|
1459
|
+
def set_title(self, title: Text) -> None:
|
|
1460
|
+
"""Store the unabridged title and refresh its fitted rendering."""
|
|
1461
|
+
self._full_title = title.copy()
|
|
1462
|
+
self.refresh(layout=False)
|
|
1463
|
+
|
|
1464
|
+
def render(self) -> Text:
|
|
1465
|
+
"""Render a single-line title with a visible narrow-width ellipsis."""
|
|
1466
|
+
title = self._full_title.copy()
|
|
1467
|
+
available = self.content_size.width or self.size.width
|
|
1468
|
+
if available > 0:
|
|
1469
|
+
title.truncate(available, overflow="ellipsis")
|
|
1470
|
+
return title
|
|
1471
|
+
|
|
1472
|
+
|
|
1473
|
+
# ---------------------------------------------------------------------------
|
|
1474
|
+
# ToolGroup — collapsible container of ToolCall widgets
|
|
1475
|
+
# ---------------------------------------------------------------------------
|
|
1476
|
+
|
|
1477
|
+
|
|
1478
|
+
class ToolGroup(ToolCopyExcludedMixin, Widget):
|
|
1479
|
+
"""Collapsible group of tool calls within a single agent turn.
|
|
1480
|
+
|
|
1481
|
+
Consecutive tool starts are grouped together. The title shows progress
|
|
1482
|
+
(done/total) and cumulative duration.
|
|
1483
|
+
"""
|
|
1484
|
+
|
|
1485
|
+
DEFAULT_CSS = """
|
|
1486
|
+
ToolGroup {
|
|
1487
|
+
margin: 1 0;
|
|
1488
|
+
padding: 0 1;
|
|
1489
|
+
border-left: thick $tui-border-warning $border-opacity;
|
|
1490
|
+
height: auto;
|
|
1491
|
+
}
|
|
1492
|
+
ToolGroup > #tg-header {
|
|
1493
|
+
height: 1;
|
|
1494
|
+
width: 100%;
|
|
1495
|
+
}
|
|
1496
|
+
ToolGroup > #tg-header > ToolGroupTitle {
|
|
1497
|
+
height: 1;
|
|
1498
|
+
width: 1fr;
|
|
1499
|
+
overflow: hidden;
|
|
1500
|
+
}
|
|
1501
|
+
ToolGroup > #tg-header > ChrysLoadingIndicator {
|
|
1502
|
+
display: none;
|
|
1503
|
+
color: $tui-tool-group-title;
|
|
1504
|
+
height: 1;
|
|
1505
|
+
width: 6;
|
|
1506
|
+
min-width: 6;
|
|
1507
|
+
max-width: 6;
|
|
1508
|
+
}
|
|
1509
|
+
ToolGroup > Vertical {
|
|
1510
|
+
height: auto;
|
|
1511
|
+
}
|
|
1512
|
+
"""
|
|
1513
|
+
|
|
1514
|
+
collapsed: reactive[bool] = reactive(False)
|
|
1515
|
+
|
|
1516
|
+
def __init__(
|
|
1517
|
+
self,
|
|
1518
|
+
*,
|
|
1519
|
+
live: bool = True,
|
|
1520
|
+
tool_widget_configurator: Callable[[Widget], None] | None = None,
|
|
1521
|
+
) -> None:
|
|
1522
|
+
self._collapse_lock_call_ids: set[str] = set()
|
|
1523
|
+
super().__init__()
|
|
1524
|
+
self._live = live
|
|
1525
|
+
self._tool_widget_configurator = tool_widget_configurator
|
|
1526
|
+
self._tools: dict[str, Widget] = {}
|
|
1527
|
+
self._tool_records: dict[str, _ToolRecord] = {}
|
|
1528
|
+
self._activity_sequence = 0
|
|
1529
|
+
self._activity_header_finalized = not live
|
|
1530
|
+
self._retained_activity_text = ""
|
|
1531
|
+
self._last_title_plain = ""
|
|
1532
|
+
self._done = 0
|
|
1533
|
+
self._start_time = time.monotonic()
|
|
1534
|
+
self._end_time: float | None = None
|
|
1535
|
+
self._timer: Timer | None = None
|
|
1536
|
+
self._pending_content_worker: Any | None = None
|
|
1537
|
+
self._content_structure_lock = asyncio.Lock()
|
|
1538
|
+
self._content_mounted = True
|
|
1539
|
+
self._content_freeze_epoch: int | None = None
|
|
1540
|
+
"""GC freeze epoch read before the oldest mounted tool widget was built; None while none is."""
|
|
1541
|
+
self.display = False # hidden until first tool is added
|
|
1542
|
+
|
|
1543
|
+
def compose(self) -> ComposeResult:
|
|
1544
|
+
with Horizontal(id="tg-header"):
|
|
1545
|
+
if self._live:
|
|
1546
|
+
yield ChrysLoadingIndicator(id="tg-activity-indicator")
|
|
1547
|
+
yield ToolGroupTitle(self._title_text())
|
|
1548
|
+
content = Vertical(id="tg-content")
|
|
1549
|
+
content.display = not self.collapsed
|
|
1550
|
+
yield content
|
|
1551
|
+
|
|
1552
|
+
def on_mount(self) -> None:
|
|
1553
|
+
# Bulk replay can populate a completed logical group before Textual
|
|
1554
|
+
# attaches it. Do not start an elapsed-time timer that completion has
|
|
1555
|
+
# already conceptually stopped.
|
|
1556
|
+
if not self.all_complete:
|
|
1557
|
+
self._timer = self.set_interval(1.0, self._tick)
|
|
1558
|
+
self._sync_activity_indicator()
|
|
1559
|
+
|
|
1560
|
+
def on_unmount(self) -> None:
|
|
1561
|
+
if self._timer is not None:
|
|
1562
|
+
self._timer.stop()
|
|
1563
|
+
self._timer = None
|
|
1564
|
+
self._cancel_pending_content_mounts()
|
|
1565
|
+
|
|
1566
|
+
def _accepts_tool_mounts(self) -> bool:
|
|
1567
|
+
"""Whether renderer widgets may still be attached below this group."""
|
|
1568
|
+
return self.is_attached and not self._closing and not self._pruning
|
|
1569
|
+
|
|
1570
|
+
def _tick(self) -> None:
|
|
1571
|
+
"""Update title with live elapsed time while tools are running."""
|
|
1572
|
+
if self._end_time is None and self._tools:
|
|
1573
|
+
from contextlib import suppress
|
|
1574
|
+
|
|
1575
|
+
with suppress(Exception):
|
|
1576
|
+
self._update_title()
|
|
1577
|
+
|
|
1578
|
+
def get_tool(self, call_id: str) -> Widget | None:
|
|
1579
|
+
"""Return the tool widget for ``call_id``, or ``None`` if not in this group.
|
|
1580
|
+
|
|
1581
|
+
Public accessor used by :class:`ChatPanel` to resolve the widget
|
|
1582
|
+
mounted for a given tool call without reaching into the internal
|
|
1583
|
+
``_tools`` map.
|
|
1584
|
+
"""
|
|
1585
|
+
return self._tools.get(call_id)
|
|
1586
|
+
|
|
1587
|
+
def reveal_tool(self, call_id: str) -> Widget | None:
|
|
1588
|
+
"""Expand the group and return the mounted widget for ``call_id`` if available."""
|
|
1589
|
+
if self.collapsed:
|
|
1590
|
+
self.collapsed = False
|
|
1591
|
+
if not self._content_mounted:
|
|
1592
|
+
return None
|
|
1593
|
+
return self._tools.get(call_id)
|
|
1594
|
+
|
|
1595
|
+
def lock_collapse_for(self, call_id: str) -> None:
|
|
1596
|
+
"""Keep this group expanded while a live inline control is active."""
|
|
1597
|
+
if not call_id:
|
|
1598
|
+
return
|
|
1599
|
+
self._collapse_lock_call_ids.add(call_id)
|
|
1600
|
+
if self.collapsed:
|
|
1601
|
+
self.collapsed = False
|
|
1602
|
+
|
|
1603
|
+
def unlock_collapse_for(self, call_id: str) -> None:
|
|
1604
|
+
"""Release a live inline control's collapse lock."""
|
|
1605
|
+
self._collapse_lock_call_ids.discard(call_id)
|
|
1606
|
+
|
|
1607
|
+
@property
|
|
1608
|
+
def collapse_locked(self) -> bool:
|
|
1609
|
+
"""True while live inline controls require this group to stay expanded."""
|
|
1610
|
+
return bool(self._collapse_lock_call_ids)
|
|
1611
|
+
|
|
1612
|
+
def validate_collapsed(self, collapsed: bool) -> bool:
|
|
1613
|
+
"""Prevent any caller from hiding live inline controls."""
|
|
1614
|
+
if collapsed and self.collapse_locked:
|
|
1615
|
+
return False
|
|
1616
|
+
return collapsed
|
|
1617
|
+
|
|
1618
|
+
def is_tool_running(self, call_id: str) -> bool:
|
|
1619
|
+
"""Return whether the logical tool record for ``call_id`` is still running."""
|
|
1620
|
+
record = self._tool_records.get(call_id)
|
|
1621
|
+
return record is not None and record.status == "running"
|
|
1622
|
+
|
|
1623
|
+
def _next_activity_order(self) -> int:
|
|
1624
|
+
self._activity_sequence += 1
|
|
1625
|
+
return self._activity_sequence
|
|
1626
|
+
|
|
1627
|
+
def finalize_activity_header(self) -> None:
|
|
1628
|
+
"""Switch to the count title once this group reaches a non-tool boundary.
|
|
1629
|
+
|
|
1630
|
+
A still-running record continues to win until it actually finishes;
|
|
1631
|
+
this matters for sub-agents whose group can close while their work is
|
|
1632
|
+
still routed to the detached card.
|
|
1633
|
+
"""
|
|
1634
|
+
if self._activity_header_finalized:
|
|
1635
|
+
return
|
|
1636
|
+
self._activity_header_finalized = True
|
|
1637
|
+
self._update_title()
|
|
1638
|
+
|
|
1639
|
+
def clear_ask_user_inline_prompts(self) -> None:
|
|
1640
|
+
"""Remove inline ask_user controls mounted in this group."""
|
|
1641
|
+
from chrys.app.tui.widgets.chat.renderers.ask_user import AskUserToolCall
|
|
1642
|
+
|
|
1643
|
+
for tc in self._tools.values():
|
|
1644
|
+
if isinstance(tc, AskUserToolCall):
|
|
1645
|
+
tc.clear_inline_prompt()
|
|
1646
|
+
self._collapse_lock_call_ids.clear()
|
|
1647
|
+
|
|
1648
|
+
async def add_tool(
|
|
1649
|
+
self,
|
|
1650
|
+
call_id: str,
|
|
1651
|
+
tool_name: str,
|
|
1652
|
+
tool_kind: str,
|
|
1653
|
+
args_summary: str = "",
|
|
1654
|
+
args: dict[str, Any] | None = None,
|
|
1655
|
+
*,
|
|
1656
|
+
provider_hosted: bool = False,
|
|
1657
|
+
hosted_family: str = "",
|
|
1658
|
+
provider: str = "",
|
|
1659
|
+
provider_item_type: str = "",
|
|
1660
|
+
provider_status: str = "",
|
|
1661
|
+
provider_call_id: str = "",
|
|
1662
|
+
canonical_status: str = "running",
|
|
1663
|
+
) -> None:
|
|
1664
|
+
"""Add a running tool call to this group."""
|
|
1665
|
+
from chrys.app.tui.widgets.chat.tool_renderers import create_tool_widget
|
|
1666
|
+
|
|
1667
|
+
if not self._accepts_tool_mounts():
|
|
1668
|
+
return
|
|
1669
|
+
self._tool_records[call_id] = _ToolRecord(
|
|
1670
|
+
call_id=call_id,
|
|
1671
|
+
tool_name=tool_name,
|
|
1672
|
+
tool_kind=tool_kind,
|
|
1673
|
+
args_summary=args_summary,
|
|
1674
|
+
args=args,
|
|
1675
|
+
provider_hosted=provider_hosted,
|
|
1676
|
+
hosted_family=hosted_family,
|
|
1677
|
+
provider=provider,
|
|
1678
|
+
provider_item_type=provider_item_type,
|
|
1679
|
+
provider_status=provider_status,
|
|
1680
|
+
provider_call_id=provider_call_id,
|
|
1681
|
+
canonical_status=canonical_status,
|
|
1682
|
+
activity_order=self._next_activity_order(),
|
|
1683
|
+
)
|
|
1684
|
+
if not self.display:
|
|
1685
|
+
self.display = True
|
|
1686
|
+
self._end_time = None
|
|
1687
|
+
self._ensure_timer()
|
|
1688
|
+
async with self._content_structure_lock:
|
|
1689
|
+
# The structure lock may have yielded behind a collapse restore or
|
|
1690
|
+
# prune. Revalidate before constructing a renderer: Textual keeps
|
|
1691
|
+
# composed descendants queryable briefly even when their ancestor
|
|
1692
|
+
# can no longer accept mounts.
|
|
1693
|
+
if not self._accepts_tool_mounts():
|
|
1694
|
+
self._tool_records.pop(call_id, None)
|
|
1695
|
+
self._finish_if_all_complete()
|
|
1696
|
+
return
|
|
1697
|
+
if existing := self._tools.get(call_id):
|
|
1698
|
+
if record := self._tool_records.get(call_id):
|
|
1699
|
+
self._apply_tool_record_state(record, existing)
|
|
1700
|
+
self._update_title()
|
|
1701
|
+
return
|
|
1702
|
+
if not self._content_mounted:
|
|
1703
|
+
await self._restore_completed_tool_widgets_locked(allow_collapsed=True)
|
|
1704
|
+
self._update_title()
|
|
1705
|
+
return
|
|
1706
|
+
|
|
1707
|
+
self._note_content_build_epoch()
|
|
1708
|
+
if provider_hosted:
|
|
1709
|
+
tc = create_tool_widget(
|
|
1710
|
+
call_id,
|
|
1711
|
+
tool_name,
|
|
1712
|
+
tool_kind,
|
|
1713
|
+
args_summary,
|
|
1714
|
+
args=args,
|
|
1715
|
+
provider_hosted=True,
|
|
1716
|
+
hosted_family=hosted_family,
|
|
1717
|
+
provider=provider,
|
|
1718
|
+
provider_item_type=provider_item_type,
|
|
1719
|
+
provider_status=provider_status,
|
|
1720
|
+
provider_call_id=provider_call_id,
|
|
1721
|
+
canonical_status=canonical_status,
|
|
1722
|
+
)
|
|
1723
|
+
else:
|
|
1724
|
+
tc = create_tool_widget(call_id, tool_name, tool_kind, args_summary, args=args)
|
|
1725
|
+
self._configure_tool_widget(tc)
|
|
1726
|
+
self._tools[call_id] = tc
|
|
1727
|
+
# EventBus delivery normally leaves a scheduler turn between start
|
|
1728
|
+
# and result. Nested transcript journals can replay both back to
|
|
1729
|
+
# back, so compose the renderer tree before mounting its root; a
|
|
1730
|
+
# terminal update may then safely query renderer descendants as
|
|
1731
|
+
# soon as ``add_tool`` returns.
|
|
1732
|
+
precompose_tree([tc])
|
|
1733
|
+
await self.query_one("#tg-content").mount(tc)
|
|
1734
|
+
if record := self._tool_records.get(call_id):
|
|
1735
|
+
self._apply_tool_record_state(record, tc)
|
|
1736
|
+
self._content_mounted = True
|
|
1737
|
+
self._update_title()
|
|
1738
|
+
|
|
1739
|
+
async def add_collapsed_replay_tool(
|
|
1740
|
+
self,
|
|
1741
|
+
call_id: str,
|
|
1742
|
+
tool_name: str,
|
|
1743
|
+
tool_kind: str,
|
|
1744
|
+
args_summary: str = "",
|
|
1745
|
+
args: dict[str, Any] | None = None,
|
|
1746
|
+
*,
|
|
1747
|
+
result: str = "",
|
|
1748
|
+
duration_ms: int = 0,
|
|
1749
|
+
duration_known: bool = False,
|
|
1750
|
+
timestamp: str = "",
|
|
1751
|
+
file_snapshot: FileSnapshotPayload | None = None,
|
|
1752
|
+
approval: str | None = None,
|
|
1753
|
+
metadata: dict[str, Any] | None = None,
|
|
1754
|
+
image_contents: list[Any] | None = None,
|
|
1755
|
+
artifacts: list[dict[str, Any]] | None = None,
|
|
1756
|
+
provider_hosted: bool = False,
|
|
1757
|
+
hosted_family: str = "",
|
|
1758
|
+
provider: str = "",
|
|
1759
|
+
provider_item_type: str = "",
|
|
1760
|
+
provider_status: str = "",
|
|
1761
|
+
provider_call_id: str = "",
|
|
1762
|
+
canonical_status: str = "completed",
|
|
1763
|
+
lazy: bool = False,
|
|
1764
|
+
) -> None:
|
|
1765
|
+
"""Add a completed replay tool without constructing its renderer widget."""
|
|
1766
|
+
async with self._content_structure_lock:
|
|
1767
|
+
render_status = (
|
|
1768
|
+
"error"
|
|
1769
|
+
if canonical_status in {"failed", "interrupted"}
|
|
1770
|
+
else tool_result_render_status(result, approval, tool_kind, metadata, tool_name)
|
|
1771
|
+
)
|
|
1772
|
+
self._tool_records[call_id] = _ToolRecord(
|
|
1773
|
+
call_id=call_id,
|
|
1774
|
+
tool_name=tool_name,
|
|
1775
|
+
tool_kind=tool_kind,
|
|
1776
|
+
args_summary=args_summary,
|
|
1777
|
+
args=args,
|
|
1778
|
+
status=render_status,
|
|
1779
|
+
result=result,
|
|
1780
|
+
duration_ms=duration_ms,
|
|
1781
|
+
duration_known=duration_known or duration_ms != 0,
|
|
1782
|
+
timestamp=timestamp,
|
|
1783
|
+
replay_timing=True,
|
|
1784
|
+
file_snapshot=file_snapshot,
|
|
1785
|
+
approval=approval,
|
|
1786
|
+
metadata=_compact_tool_metadata(metadata),
|
|
1787
|
+
image_contents=list(image_contents or []),
|
|
1788
|
+
lazy=lazy,
|
|
1789
|
+
completed_via_result=not provider_hosted or canonical_status == "completed",
|
|
1790
|
+
provider_hosted=provider_hosted,
|
|
1791
|
+
hosted_family=hosted_family,
|
|
1792
|
+
provider=provider,
|
|
1793
|
+
provider_item_type=provider_item_type,
|
|
1794
|
+
provider_status=provider_status,
|
|
1795
|
+
provider_call_id=provider_call_id,
|
|
1796
|
+
canonical_status=canonical_status,
|
|
1797
|
+
artifacts=list(artifacts or []),
|
|
1798
|
+
activity_order=self._next_activity_order(),
|
|
1799
|
+
)
|
|
1800
|
+
self._tools.pop(call_id, None)
|
|
1801
|
+
self.display = True
|
|
1802
|
+
self._content_mounted = False
|
|
1803
|
+
self._reconcile_done_count()
|
|
1804
|
+
if self.all_complete:
|
|
1805
|
+
self._end_time = self._start_time
|
|
1806
|
+
self._stop_timer()
|
|
1807
|
+
if not self.collapsed:
|
|
1808
|
+
self.collapsed = True
|
|
1809
|
+
else:
|
|
1810
|
+
self._update_title()
|
|
1811
|
+
|
|
1812
|
+
def update_tool_args(self, call_id: str, args: dict[str, Any]) -> None:
|
|
1813
|
+
"""Refresh arguments for a running tool card."""
|
|
1814
|
+
record = self._tool_records.get(call_id)
|
|
1815
|
+
if (
|
|
1816
|
+
record is not None
|
|
1817
|
+
and record.canonical_status in _TERMINAL_CANONICAL_STATUSES
|
|
1818
|
+
and (record.completed_via_result or record.canonical_status == "interrupted")
|
|
1819
|
+
):
|
|
1820
|
+
# Same window as complete_tool: a status-only terminal transition
|
|
1821
|
+
# still awaits the real result item, which may carry the arguments.
|
|
1822
|
+
logger.debug("Ignoring late arguments for terminal tool call %s", call_id)
|
|
1823
|
+
return
|
|
1824
|
+
tc = self._tools.get(call_id)
|
|
1825
|
+
if isinstance(tc, _ToolArgsUpdatable):
|
|
1826
|
+
tc.update_args(args)
|
|
1827
|
+
if record is not None:
|
|
1828
|
+
record.args = args
|
|
1829
|
+
record.args_summary = json.dumps(args, ensure_ascii=False) if args else ""
|
|
1830
|
+
self._update_title()
|
|
1831
|
+
|
|
1832
|
+
def update_nested_tool_activity(
|
|
1833
|
+
self,
|
|
1834
|
+
parent_call_id: str,
|
|
1835
|
+
call_id: str,
|
|
1836
|
+
*,
|
|
1837
|
+
tool_name: str | None = None,
|
|
1838
|
+
tool_kind: str | None = None,
|
|
1839
|
+
args: dict[str, Any] | None = None,
|
|
1840
|
+
status: str | None = None,
|
|
1841
|
+
) -> None:
|
|
1842
|
+
"""Update presentation-only state for a tool running inside a sub-agent."""
|
|
1843
|
+
record = self._tool_records.get(parent_call_id)
|
|
1844
|
+
if record is None or record.status != "running":
|
|
1845
|
+
return
|
|
1846
|
+
if status in _TERMINAL_CANONICAL_STATUSES:
|
|
1847
|
+
if record.nested_activities.pop(call_id, None) is not None:
|
|
1848
|
+
self._update_title()
|
|
1849
|
+
return
|
|
1850
|
+
activity = record.nested_activities.get(call_id)
|
|
1851
|
+
if activity is None:
|
|
1852
|
+
if tool_name is None:
|
|
1853
|
+
return
|
|
1854
|
+
activity = _NestedToolActivity(
|
|
1855
|
+
tool_name=tool_name,
|
|
1856
|
+
tool_kind=tool_kind or "",
|
|
1857
|
+
args=_compact_activity_args(args),
|
|
1858
|
+
)
|
|
1859
|
+
record.nested_activities[call_id] = activity
|
|
1860
|
+
else:
|
|
1861
|
+
if tool_name is not None:
|
|
1862
|
+
activity.tool_name = tool_name
|
|
1863
|
+
if tool_kind is not None:
|
|
1864
|
+
activity.tool_kind = tool_kind
|
|
1865
|
+
if args is not None:
|
|
1866
|
+
activity.args = _compact_activity_args(args)
|
|
1867
|
+
order = self._next_activity_order()
|
|
1868
|
+
record.activity_order = order
|
|
1869
|
+
record.nested_latest_activity = _tool_activity_reference(
|
|
1870
|
+
activity.tool_name,
|
|
1871
|
+
activity.tool_kind,
|
|
1872
|
+
activity.args,
|
|
1873
|
+
)
|
|
1874
|
+
self._update_title()
|
|
1875
|
+
|
|
1876
|
+
def update_sub_agent_activity(self, parent_call_id: str, activity: _ToolActivity) -> None:
|
|
1877
|
+
"""Replace a running sub-agent's latest assistant/tool activity."""
|
|
1878
|
+
record = self._tool_records.get(parent_call_id)
|
|
1879
|
+
if record is None or record.status != "running" or record.tool_kind != KIND_SUB_AGENT:
|
|
1880
|
+
return
|
|
1881
|
+
record.nested_latest_activity = activity
|
|
1882
|
+
record.activity_order = self._next_activity_order()
|
|
1883
|
+
self._update_title()
|
|
1884
|
+
|
|
1885
|
+
def update_sub_agent_display_name(self, parent_call_id: str, agent_name: str) -> None:
|
|
1886
|
+
"""Apply the runtime display name without replacing the tool arguments."""
|
|
1887
|
+
if not agent_name:
|
|
1888
|
+
return
|
|
1889
|
+
record = self._tool_records.get(parent_call_id)
|
|
1890
|
+
if record is None or record.status != "running" or record.tool_kind != KIND_SUB_AGENT:
|
|
1891
|
+
return
|
|
1892
|
+
record.args = {**(record.args or {}), "agent_name": agent_name}
|
|
1893
|
+
record.activity_order = self._next_activity_order()
|
|
1894
|
+
self._update_title()
|
|
1895
|
+
|
|
1896
|
+
def _capture_tool_display_state(self, call_id: str, tc: Widget | None) -> None:
|
|
1897
|
+
"""Store compact renderer display state on the logical tool record."""
|
|
1898
|
+
record = self._tool_records.get(call_id)
|
|
1899
|
+
if record is not None and isinstance(tc, _ToolDisplayStateSerializable):
|
|
1900
|
+
record.display_state = tc.compact_display_state()
|
|
1901
|
+
|
|
1902
|
+
def complete_tool(
|
|
1903
|
+
self,
|
|
1904
|
+
call_id: str,
|
|
1905
|
+
result: str,
|
|
1906
|
+
duration_ms: int = 0,
|
|
1907
|
+
*,
|
|
1908
|
+
image_contents: list[Any] | None = None,
|
|
1909
|
+
file_snapshot: FileSnapshotPayload | None = None,
|
|
1910
|
+
approval: str | None = None,
|
|
1911
|
+
metadata: dict[str, Any] | None = None,
|
|
1912
|
+
artifacts: list[dict[str, Any]] | None = None,
|
|
1913
|
+
provider_status: str = "",
|
|
1914
|
+
canonical_status: str = "completed",
|
|
1915
|
+
lazy: bool = False,
|
|
1916
|
+
) -> None:
|
|
1917
|
+
"""Mark a tool call as complete.
|
|
1918
|
+
|
|
1919
|
+
If *lazy* is True, the tool widget defers expensive diff
|
|
1920
|
+
construction until the group is first expanded (used during replay).
|
|
1921
|
+
"""
|
|
1922
|
+
record = self._tool_records.get(call_id)
|
|
1923
|
+
if (
|
|
1924
|
+
record is not None
|
|
1925
|
+
and record.canonical_status in _TERMINAL_CANONICAL_STATUSES
|
|
1926
|
+
and (record.completed_via_result or record.canonical_status == "interrupted")
|
|
1927
|
+
):
|
|
1928
|
+
logger.debug("Ignoring late result for terminal tool call %s", call_id)
|
|
1929
|
+
return
|
|
1930
|
+
was_terminal = record.status in _TERMINAL_TOOL_STATUSES if record is not None else False
|
|
1931
|
+
if record is not None:
|
|
1932
|
+
record.status = (
|
|
1933
|
+
"error"
|
|
1934
|
+
if canonical_status == "interrupted"
|
|
1935
|
+
else tool_result_render_status(
|
|
1936
|
+
result,
|
|
1937
|
+
approval,
|
|
1938
|
+
record.tool_kind,
|
|
1939
|
+
metadata,
|
|
1940
|
+
record.tool_name,
|
|
1941
|
+
)
|
|
1942
|
+
)
|
|
1943
|
+
record.result = result
|
|
1944
|
+
record.duration_ms = duration_ms
|
|
1945
|
+
record.duration_known = True
|
|
1946
|
+
record.file_snapshot = file_snapshot
|
|
1947
|
+
record.approval = approval
|
|
1948
|
+
record.metadata = _compact_tool_metadata(metadata)
|
|
1949
|
+
record.image_contents = list(image_contents or [])
|
|
1950
|
+
record.artifacts = list(artifacts or [])
|
|
1951
|
+
record.provider_status = provider_status or record.provider_status
|
|
1952
|
+
record.canonical_status = canonical_status
|
|
1953
|
+
record.lazy = lazy
|
|
1954
|
+
record.completed_via_result = True
|
|
1955
|
+
record.nested_activities.clear()
|
|
1956
|
+
tc = self._tools.get(call_id)
|
|
1957
|
+
if tc is not None:
|
|
1958
|
+
if isinstance(tc, BaseToolCard):
|
|
1959
|
+
tc.canonical_status = canonical_status
|
|
1960
|
+
tc.provider_status = provider_status or tc.provider_status
|
|
1961
|
+
tc.artifacts = list(artifacts or [])
|
|
1962
|
+
tc.metadata = dict(metadata or {})
|
|
1963
|
+
renderer = _completion_renderer(tc)
|
|
1964
|
+
if canonical_status == "interrupted":
|
|
1965
|
+
renderer.set_error(result or "interrupted")
|
|
1966
|
+
else:
|
|
1967
|
+
renderer.set_complete(
|
|
1968
|
+
result,
|
|
1969
|
+
duration_ms,
|
|
1970
|
+
image_contents=image_contents,
|
|
1971
|
+
file_snapshot=file_snapshot,
|
|
1972
|
+
approval=approval,
|
|
1973
|
+
metadata=metadata,
|
|
1974
|
+
artifacts=artifacts,
|
|
1975
|
+
lazy=lazy,
|
|
1976
|
+
)
|
|
1977
|
+
self._capture_tool_display_state(call_id, tc)
|
|
1978
|
+
if record is not None or tc is not None:
|
|
1979
|
+
if not was_terminal:
|
|
1980
|
+
self._done += 1
|
|
1981
|
+
self.unlock_collapse_for(call_id)
|
|
1982
|
+
self._finish_if_all_complete()
|
|
1983
|
+
self._update_title()
|
|
1984
|
+
if self.all_complete and self.collapsed:
|
|
1985
|
+
self.call_later(self._release_completed_tool_widgets)
|
|
1986
|
+
|
|
1987
|
+
def _ensure_timer(self) -> None:
|
|
1988
|
+
"""Ensure elapsed-time title updates are active while the group is running."""
|
|
1989
|
+
if self._timer is None:
|
|
1990
|
+
self._timer = self.set_interval(1.0, self._tick)
|
|
1991
|
+
|
|
1992
|
+
def _stop_timer(self) -> None:
|
|
1993
|
+
"""Stop elapsed-time title updates."""
|
|
1994
|
+
if self._timer is not None:
|
|
1995
|
+
self._timer.stop()
|
|
1996
|
+
self._timer = None
|
|
1997
|
+
|
|
1998
|
+
def _terminal_record_count(self) -> int:
|
|
1999
|
+
"""Return how many logical tool records are already terminal."""
|
|
2000
|
+
return sum(1 for record in self._tool_records.values() if record.status in _TERMINAL_TOOL_STATUSES)
|
|
2001
|
+
|
|
2002
|
+
def _reconcile_done_count(self) -> None:
|
|
2003
|
+
"""Keep done count consistent after hidden-record state transitions."""
|
|
2004
|
+
if self._tool_records:
|
|
2005
|
+
self._done = self._terminal_record_count()
|
|
2006
|
+
|
|
2007
|
+
def _finish_if_all_complete(self) -> None:
|
|
2008
|
+
"""Stop the group timer once every logical tool record is terminal."""
|
|
2009
|
+
if self.all_complete:
|
|
2010
|
+
self._end_time = time.monotonic()
|
|
2011
|
+
self._stop_timer()
|
|
2012
|
+
|
|
2013
|
+
def _mark_record_error(self, call_id: str, error: str) -> None:
|
|
2014
|
+
"""Mark one logical tool record as errored."""
|
|
2015
|
+
if record := self._tool_records.get(call_id):
|
|
2016
|
+
record.status = "error"
|
|
2017
|
+
record.result = error
|
|
2018
|
+
record.canonical_status = "interrupted" if error == "cancelled" else "failed"
|
|
2019
|
+
record.completed_via_result = False
|
|
2020
|
+
record.nested_activities.clear()
|
|
2021
|
+
|
|
2022
|
+
def cancel_running(self) -> None:
|
|
2023
|
+
"""Mark all still-running tool calls as cancelled."""
|
|
2024
|
+
for call_id, tc in self._tools.items():
|
|
2025
|
+
renderer = _completion_renderer(tc)
|
|
2026
|
+
if renderer.status == "running":
|
|
2027
|
+
renderer.set_error("cancelled")
|
|
2028
|
+
self._mark_record_error(call_id, "cancelled")
|
|
2029
|
+
for call_id, record in self._tool_records.items():
|
|
2030
|
+
if record.status == "running":
|
|
2031
|
+
self._mark_record_error(call_id, "cancelled")
|
|
2032
|
+
self._collapse_lock_call_ids.clear()
|
|
2033
|
+
self._reconcile_done_count()
|
|
2034
|
+
self._finish_if_all_complete()
|
|
2035
|
+
self._update_title()
|
|
2036
|
+
|
|
2037
|
+
@property
|
|
2038
|
+
def all_complete(self) -> bool:
|
|
2039
|
+
"""True when all tool calls in this group have finished."""
|
|
2040
|
+
total = len(self._tool_records) if self._tool_records else len(self._tools)
|
|
2041
|
+
return self._done == total and total > 0
|
|
2042
|
+
|
|
2043
|
+
def _elapsed_ms(self) -> int:
|
|
2044
|
+
end = self._end_time if self._end_time is not None else time.monotonic()
|
|
2045
|
+
return int((end - self._start_time) * 1000)
|
|
2046
|
+
|
|
2047
|
+
def _title_text(self) -> Text:
|
|
2048
|
+
arrow = "▶" if self.collapsed else "▼"
|
|
2049
|
+
running = [
|
|
2050
|
+
record for record in self._tool_records.values() if record is not None and record.status == "running"
|
|
2051
|
+
]
|
|
2052
|
+
if running:
|
|
2053
|
+
record = max(running, key=lambda candidate: candidate.activity_order)
|
|
2054
|
+
localizer = widget_localizer(self)
|
|
2055
|
+
activity = _record_activity_reference(record, lambda reference: render_str(localizer, reference))
|
|
2056
|
+
title = activity if isinstance(activity, str) else render_str(localizer, activity)
|
|
2057
|
+
self._retained_activity_text = title
|
|
2058
|
+
return Text(title, no_wrap=True, overflow="ellipsis")
|
|
2059
|
+
if not self._activity_header_finalized and self._retained_activity_text:
|
|
2060
|
+
return Text(
|
|
2061
|
+
self._retained_activity_text,
|
|
2062
|
+
no_wrap=True,
|
|
2063
|
+
overflow="ellipsis",
|
|
2064
|
+
)
|
|
2065
|
+
n = len(self._tool_records) if self._tool_records else len(self._tools)
|
|
2066
|
+
elapsed = self._elapsed_ms()
|
|
2067
|
+
if elapsed >= 1000:
|
|
2068
|
+
reference = _TOOL_CARD_GROUP_TITLE_TIMED.bind(done=self._done, total=n, duration=fmt_duration(elapsed))
|
|
2069
|
+
else:
|
|
2070
|
+
reference = _TOOL_CARD_GROUP_TITLE.bind(done=self._done, total=n)
|
|
2071
|
+
title = render_str(widget_localizer(self), reference)
|
|
2072
|
+
return Text(f"{arrow} {title}", no_wrap=True, overflow="ellipsis")
|
|
2073
|
+
|
|
2074
|
+
def _update_title(self) -> None:
|
|
2075
|
+
title = self._title_text()
|
|
2076
|
+
self._sync_activity_indicator()
|
|
2077
|
+
if title.plain == self._last_title_plain:
|
|
2078
|
+
return
|
|
2079
|
+
try:
|
|
2080
|
+
title_widget = self.query_one(ToolGroupTitle)
|
|
2081
|
+
except NoMatches:
|
|
2082
|
+
return
|
|
2083
|
+
title_widget.set_title(title)
|
|
2084
|
+
self._last_title_plain = title.plain
|
|
2085
|
+
|
|
2086
|
+
def _sync_activity_indicator(self) -> None:
|
|
2087
|
+
"""Animate through the open tool phase and any detached running work."""
|
|
2088
|
+
if not self._live:
|
|
2089
|
+
return
|
|
2090
|
+
with suppress(NoMatches):
|
|
2091
|
+
indicator = self.query_one("#tg-activity-indicator", ChrysLoadingIndicator)
|
|
2092
|
+
has_running_tool = any(
|
|
2093
|
+
record is not None and record.status == "running" for record in self._tool_records.values()
|
|
2094
|
+
)
|
|
2095
|
+
active = not self._activity_header_finalized or has_running_tool
|
|
2096
|
+
indicator.display = active
|
|
2097
|
+
if active:
|
|
2098
|
+
indicator.resume_animation()
|
|
2099
|
+
else:
|
|
2100
|
+
indicator.pause_animation()
|
|
2101
|
+
|
|
2102
|
+
def watch_collapsed(self, collapsed: bool) -> None:
|
|
2103
|
+
try:
|
|
2104
|
+
content = self.query_one("#tg-content")
|
|
2105
|
+
except NoMatches:
|
|
2106
|
+
return
|
|
2107
|
+
content.display = not collapsed
|
|
2108
|
+
self._update_title()
|
|
2109
|
+
if collapsed:
|
|
2110
|
+
self._cancel_pending_content_mounts()
|
|
2111
|
+
self._release_expensive_tool_content()
|
|
2112
|
+
self.call_later(self._release_completed_tool_widgets)
|
|
2113
|
+
else:
|
|
2114
|
+
if self._content_mounted:
|
|
2115
|
+
# On first expand, trigger lazy diff mounting for any tool
|
|
2116
|
+
# widgets that deferred their diff construction during replay.
|
|
2117
|
+
self._schedule_pending_content_mounts()
|
|
2118
|
+
else:
|
|
2119
|
+
self.call_later(self._restore_completed_tool_widgets)
|
|
2120
|
+
|
|
2121
|
+
def _note_content_build_epoch(self) -> None:
|
|
2122
|
+
"""Record the GC freeze epoch before building the first mounted tool widget.
|
|
2123
|
+
|
|
2124
|
+
Read before any widget of the subtree exists, it bounds the whole subtree: an
|
|
2125
|
+
unchanged epoch at prune time proves no freeze captured any part of it.
|
|
2126
|
+
"""
|
|
2127
|
+
if self._content_freeze_epoch is None:
|
|
2128
|
+
self._content_freeze_epoch = current_gc_freeze_epoch()
|
|
2129
|
+
|
|
2130
|
+
def _young_content_watch(self, content: Widget) -> GcRemovedContent | None:
|
|
2131
|
+
"""Describe the mounted content about to be pruned if no freeze has captured it.
|
|
2132
|
+
|
|
2133
|
+
Returns None when the epoch moved since the oldest child was built (or was never
|
|
2134
|
+
recorded), so the prune keeps its idle full reclaim. Otherwise the coordinator
|
|
2135
|
+
weakly watches every node of the subtree and reclaims only if one outlives the next
|
|
2136
|
+
freezing action's collection.
|
|
2137
|
+
"""
|
|
2138
|
+
built_at_epoch = self._content_freeze_epoch
|
|
2139
|
+
if built_at_epoch is None or built_at_epoch != current_gc_freeze_epoch():
|
|
2140
|
+
return None
|
|
2141
|
+
return GcRemovedContent.watch(
|
|
2142
|
+
built_at_epoch=built_at_epoch,
|
|
2143
|
+
nodes=(node for child in content.children for node in child.walk_children(with_self=True)),
|
|
2144
|
+
)
|
|
2145
|
+
|
|
2146
|
+
def _release_expensive_tool_content(self) -> None:
|
|
2147
|
+
"""Let child tool widgets drop expensive mounted content before pruning."""
|
|
2148
|
+
for tc in self._tools.values():
|
|
2149
|
+
if isinstance(tc, _ToolCollapsedContentReleasable):
|
|
2150
|
+
tc.release_collapsed_content()
|
|
2151
|
+
|
|
2152
|
+
async def _release_completed_tool_widgets(self) -> None:
|
|
2153
|
+
"""Prune completed tool widget subtrees while the group is collapsed."""
|
|
2154
|
+
async with self._content_structure_lock:
|
|
2155
|
+
await self._release_completed_tool_widgets_locked()
|
|
2156
|
+
|
|
2157
|
+
def _completed_widget_pruning_supported(self) -> bool:
|
|
2158
|
+
"""Return whether every renderer in this group has restorable state."""
|
|
2159
|
+
return not any(
|
|
2160
|
+
record is not None and record.tool_kind == KIND_SUB_AGENT for record in self._tool_records.values()
|
|
2161
|
+
)
|
|
2162
|
+
|
|
2163
|
+
async def _release_completed_tool_widgets_locked(self) -> None:
|
|
2164
|
+
"""Prune completed widgets while holding the structure lock."""
|
|
2165
|
+
if not self.collapsed or not self.all_complete or not self._content_mounted:
|
|
2166
|
+
return
|
|
2167
|
+
# SubAgentToolCall owns presentation state that _ToolRecord does not
|
|
2168
|
+
# snapshot (progress totals, compaction counts, and retry/pause
|
|
2169
|
+
# history). Pruning is currently all-or-nothing for the content
|
|
2170
|
+
# subtree, so retain the whole mixed group until sub-agent cards gain
|
|
2171
|
+
# a complete compact-display-state protocol.
|
|
2172
|
+
if not self._completed_widget_pruning_supported():
|
|
2173
|
+
return
|
|
2174
|
+
with suppress(Exception):
|
|
2175
|
+
self._release_expensive_tool_content()
|
|
2176
|
+
content = self.query_one("#tg-content")
|
|
2177
|
+
if not content.children:
|
|
2178
|
+
self._content_mounted = False
|
|
2179
|
+
self._tools = {}
|
|
2180
|
+
self._content_freeze_epoch = None
|
|
2181
|
+
return
|
|
2182
|
+
young_content = self._young_content_watch(content)
|
|
2183
|
+
await content.remove_children()
|
|
2184
|
+
# Textual's FIFO arrangement cache keeps prior child placements
|
|
2185
|
+
# across node-version changes. Drop those detached placements before
|
|
2186
|
+
# reporting the removal so the removed subtree is no longer
|
|
2187
|
+
# reachable from this permanently mounted container.
|
|
2188
|
+
content._clear_arrangement_cache()
|
|
2189
|
+
# Refresh NodeList's cached displayed-child view. A hidden
|
|
2190
|
+
# container is not laid out again, so Textual would otherwise keep
|
|
2191
|
+
# the removed widget list reachable until a later expansion.
|
|
2192
|
+
_ = content.displayed_children
|
|
2193
|
+
self._content_mounted = False
|
|
2194
|
+
self._tools = {}
|
|
2195
|
+
self._content_freeze_epoch = None
|
|
2196
|
+
# A subtree no freeze has captured is young garbage: the coordinator reclaims
|
|
2197
|
+
# only if it outlives the next freezing action's collection, instead of
|
|
2198
|
+
# rescanning the whole heap for it.
|
|
2199
|
+
self.post_message(
|
|
2200
|
+
GcReclaimRequested(GcReclaimReason.STABLE_CONTENT_REMOVED, prompt=False, removed=young_content)
|
|
2201
|
+
)
|
|
2202
|
+
if not self.collapsed or not self.all_complete:
|
|
2203
|
+
await self._restore_completed_tool_widgets_locked(allow_collapsed=not self.all_complete)
|
|
2204
|
+
|
|
2205
|
+
async def _restore_completed_tool_widgets(self, *, allow_collapsed: bool = False) -> None:
|
|
2206
|
+
"""Rebuild completed tool widgets when a collapsed group is expanded."""
|
|
2207
|
+
async with self._content_structure_lock:
|
|
2208
|
+
await self._restore_completed_tool_widgets_locked(allow_collapsed=allow_collapsed)
|
|
2209
|
+
|
|
2210
|
+
async def _restore_completed_tool_widgets_locked(self, *, allow_collapsed: bool = False) -> None:
|
|
2211
|
+
"""Rebuild tool widgets while holding the structure lock."""
|
|
2212
|
+
if not self._accepts_tool_mounts() or (self.collapsed and not allow_collapsed) or self._content_mounted:
|
|
2213
|
+
return
|
|
2214
|
+
from chrys.app.tui.widgets.chat.tool_renderers import create_tool_widget
|
|
2215
|
+
|
|
2216
|
+
content = self.query_one("#tg-content")
|
|
2217
|
+
rebuilt: dict[str, Widget] = {}
|
|
2218
|
+
records = list(self._tool_records.values())
|
|
2219
|
+
if records:
|
|
2220
|
+
self._note_content_build_epoch()
|
|
2221
|
+
widgets: list[Widget] = []
|
|
2222
|
+
for record in records:
|
|
2223
|
+
if record.provider_hosted:
|
|
2224
|
+
tc = create_tool_widget(
|
|
2225
|
+
record.call_id,
|
|
2226
|
+
record.tool_name,
|
|
2227
|
+
record.tool_kind,
|
|
2228
|
+
record.args_summary,
|
|
2229
|
+
args=record.args,
|
|
2230
|
+
provider_hosted=True,
|
|
2231
|
+
hosted_family=record.hosted_family,
|
|
2232
|
+
provider=record.provider,
|
|
2233
|
+
provider_item_type=record.provider_item_type,
|
|
2234
|
+
provider_status=record.provider_status,
|
|
2235
|
+
provider_call_id=record.provider_call_id,
|
|
2236
|
+
canonical_status=record.canonical_status,
|
|
2237
|
+
artifacts=record.artifacts,
|
|
2238
|
+
)
|
|
2239
|
+
else:
|
|
2240
|
+
tc = create_tool_widget(
|
|
2241
|
+
record.call_id,
|
|
2242
|
+
record.tool_name,
|
|
2243
|
+
record.tool_kind,
|
|
2244
|
+
record.args_summary,
|
|
2245
|
+
args=record.args,
|
|
2246
|
+
)
|
|
2247
|
+
self._configure_tool_widget(tc)
|
|
2248
|
+
rebuilt[record.call_id] = tc
|
|
2249
|
+
widgets.append(tc)
|
|
2250
|
+
if widgets:
|
|
2251
|
+
pending_mount = content.mount(*widgets)
|
|
2252
|
+
# Textual registers every widget synchronously before returning the
|
|
2253
|
+
# awaitable. Publish the matching map/state in the same critical
|
|
2254
|
+
# section so no caller can start a second full restore.
|
|
2255
|
+
self._tools = rebuilt
|
|
2256
|
+
self._content_mounted = True
|
|
2257
|
+
await pending_mount
|
|
2258
|
+
else:
|
|
2259
|
+
self._tools = rebuilt
|
|
2260
|
+
self._content_mounted = True
|
|
2261
|
+
for record in records:
|
|
2262
|
+
tc = rebuilt[record.call_id]
|
|
2263
|
+
self._apply_tool_record_state(record, tc)
|
|
2264
|
+
if self.collapsed:
|
|
2265
|
+
await self._release_completed_tool_widgets_locked()
|
|
2266
|
+
return
|
|
2267
|
+
self._schedule_pending_content_mounts(base_subtree_restored=True)
|
|
2268
|
+
if self.collapsed:
|
|
2269
|
+
await self._release_completed_tool_widgets_locked()
|
|
2270
|
+
|
|
2271
|
+
def _configure_tool_widget(self, widget: Widget) -> None:
|
|
2272
|
+
if self._tool_widget_configurator is not None:
|
|
2273
|
+
self._tool_widget_configurator(widget)
|
|
2274
|
+
|
|
2275
|
+
def _apply_tool_record_state(self, record: _ToolRecord, tc: Widget) -> None:
|
|
2276
|
+
"""Apply authoritative logical state to a newly mounted renderer."""
|
|
2277
|
+
if isinstance(tc, BaseToolCard):
|
|
2278
|
+
tc.metadata = dict(record.metadata or {})
|
|
2279
|
+
if record.display_state is not None and isinstance(tc, _ToolDisplayStateRestorable):
|
|
2280
|
+
tc.restore_compact_display_state(record.display_state)
|
|
2281
|
+
renderer = _completion_renderer(tc)
|
|
2282
|
+
duration_rendered = False
|
|
2283
|
+
# canonical_status covers status-only hosted completions: their
|
|
2284
|
+
# completed_via_result flag is deliberately reset to let a late
|
|
2285
|
+
# terminal result still land, so it cannot carry rebuild state.
|
|
2286
|
+
if record.completed_via_result or record.canonical_status == "completed":
|
|
2287
|
+
renderer.set_complete(
|
|
2288
|
+
record.result,
|
|
2289
|
+
record.duration_ms,
|
|
2290
|
+
image_contents=record.image_contents,
|
|
2291
|
+
file_snapshot=record.file_snapshot,
|
|
2292
|
+
approval=record.approval,
|
|
2293
|
+
metadata=record.metadata,
|
|
2294
|
+
artifacts=record.artifacts,
|
|
2295
|
+
lazy=record.lazy,
|
|
2296
|
+
)
|
|
2297
|
+
# Completion renderers include non-zero durations in their own
|
|
2298
|
+
# labels. Zero-duration spans still need the replay-only suffix.
|
|
2299
|
+
duration_rendered = record.duration_ms != 0
|
|
2300
|
+
elif record.status == "error":
|
|
2301
|
+
if isinstance(tc, _ToolErrorPayloadRestorable):
|
|
2302
|
+
tc.restore_error_payload(
|
|
2303
|
+
record.result,
|
|
2304
|
+
image_contents=record.image_contents,
|
|
2305
|
+
metadata=record.metadata,
|
|
2306
|
+
artifacts=record.artifacts,
|
|
2307
|
+
)
|
|
2308
|
+
else:
|
|
2309
|
+
renderer.set_error(record.result)
|
|
2310
|
+
if record.replay_timing and (record.timestamp or record.duration_known):
|
|
2311
|
+
with suppress(NoMatches):
|
|
2312
|
+
tc.query_one(ToolCardHeader).append_replay_timing(
|
|
2313
|
+
timestamp=record.timestamp,
|
|
2314
|
+
duration_ms=record.duration_ms,
|
|
2315
|
+
show_duration=record.duration_known and not duration_rendered,
|
|
2316
|
+
)
|
|
2317
|
+
|
|
2318
|
+
def _cancel_pending_content_mounts(self) -> None:
|
|
2319
|
+
"""Cancel deferred content work for this group."""
|
|
2320
|
+
worker = self._pending_content_worker
|
|
2321
|
+
if worker is None:
|
|
2322
|
+
return
|
|
2323
|
+
self._pending_content_worker = None
|
|
2324
|
+
with suppress(Exception):
|
|
2325
|
+
worker.cancel()
|
|
2326
|
+
|
|
2327
|
+
def _schedule_pending_content_mounts(self, *, base_subtree_restored: bool = False) -> None:
|
|
2328
|
+
"""Start deferred tool content mounting outside the expand callback."""
|
|
2329
|
+
if not self._accepts_tool_mounts() or self.collapsed or not self._content_mounted:
|
|
2330
|
+
return
|
|
2331
|
+
self._cancel_pending_content_mounts()
|
|
2332
|
+
# A factory, not a coroutine: an exclusive group cancels a worker that never
|
|
2333
|
+
# started, and only a coroutine created at start leaves nothing unawaited.
|
|
2334
|
+
self._pending_content_worker = self.run_worker(
|
|
2335
|
+
partial(self._mount_pending_diffs, base_subtree_restored=base_subtree_restored),
|
|
2336
|
+
group=f"tool-group-{id(self)}-pending-content",
|
|
2337
|
+
exclusive=True,
|
|
2338
|
+
exit_on_error=False,
|
|
2339
|
+
)
|
|
2340
|
+
|
|
2341
|
+
async def _mount_pending_diffs(self, *, base_subtree_restored: bool = False) -> None:
|
|
2342
|
+
"""Mount deferred expensive content for tools that used lazy=True."""
|
|
2343
|
+
if not self._accepts_tool_mounts():
|
|
2344
|
+
return
|
|
2345
|
+
pending_tools = [
|
|
2346
|
+
tc
|
|
2347
|
+
for tc in list(self._tools.values())
|
|
2348
|
+
if isinstance(tc, _ToolPendingContentMountable | _ToolPendingDiffMountable)
|
|
2349
|
+
]
|
|
2350
|
+
if not pending_tools:
|
|
2351
|
+
if base_subtree_restored and not self.collapsed and self._content_mounted:
|
|
2352
|
+
self.post_message(GcAbsorbRequested(GcAbsorbReason.STABLE_CONTENT_MOUNTED))
|
|
2353
|
+
return
|
|
2354
|
+
|
|
2355
|
+
semaphore = asyncio.Semaphore(_PENDING_CONTENT_MOUNT_CONCURRENCY)
|
|
2356
|
+
|
|
2357
|
+
async def mount_one(tc: Widget) -> bool:
|
|
2358
|
+
async with semaphore:
|
|
2359
|
+
if not self._accepts_tool_mounts() or self.collapsed or not self._content_mounted:
|
|
2360
|
+
return False
|
|
2361
|
+
if isinstance(tc, _ToolPendingContentMountable):
|
|
2362
|
+
return await tc.mount_pending_content()
|
|
2363
|
+
if isinstance(tc, _ToolPendingDiffMountable):
|
|
2364
|
+
return await tc.mount_diff_if_pending()
|
|
2365
|
+
return False
|
|
2366
|
+
|
|
2367
|
+
tasks = [asyncio.create_task(mount_one(tc)) for tc in pending_tools]
|
|
2368
|
+
mounted_content = False
|
|
2369
|
+
try:
|
|
2370
|
+
for task in asyncio.as_completed(tasks):
|
|
2371
|
+
mounted_content = await task or mounted_content
|
|
2372
|
+
if self.collapsed or not self._content_mounted:
|
|
2373
|
+
for pending in tasks:
|
|
2374
|
+
if not pending.done():
|
|
2375
|
+
pending.cancel()
|
|
2376
|
+
break
|
|
2377
|
+
except asyncio.CancelledError:
|
|
2378
|
+
for task in tasks:
|
|
2379
|
+
task.cancel()
|
|
2380
|
+
raise
|
|
2381
|
+
finally:
|
|
2382
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
2383
|
+
if (base_subtree_restored or mounted_content) and not self.collapsed and self._content_mounted:
|
|
2384
|
+
self.post_message(GcAbsorbRequested(GcAbsorbReason.STABLE_CONTENT_MOUNTED))
|
|
2385
|
+
|
|
2386
|
+
def update_tool_progress(
|
|
2387
|
+
self,
|
|
2388
|
+
call_id: str,
|
|
2389
|
+
lines: list[str],
|
|
2390
|
+
*,
|
|
2391
|
+
image_contents: list[Any] | None = None,
|
|
2392
|
+
snapshot_metadata: dict[str, Any] | None = None,
|
|
2393
|
+
provider_status: str = "",
|
|
2394
|
+
) -> None:
|
|
2395
|
+
"""Forward streaming progress lines to a running tool widget."""
|
|
2396
|
+
record = self._tool_records.get(call_id)
|
|
2397
|
+
if record is not None:
|
|
2398
|
+
if record.canonical_status in _TERMINAL_CANONICAL_STATUSES:
|
|
2399
|
+
logger.debug("Ignoring late progress for terminal tool call %s", call_id)
|
|
2400
|
+
return
|
|
2401
|
+
record.image_contents = list(image_contents or record.image_contents)
|
|
2402
|
+
record.metadata = {**(record.metadata or {}), **(snapshot_metadata or {})}
|
|
2403
|
+
record.provider_status = provider_status or record.provider_status
|
|
2404
|
+
tc = self._tools.get(call_id)
|
|
2405
|
+
if isinstance(tc, _ToolProgressUpdatable):
|
|
2406
|
+
if isinstance(tc, BaseToolCard):
|
|
2407
|
+
tc.image_contents = list(image_contents or tc.image_contents)
|
|
2408
|
+
tc.metadata = {**(tc.metadata or {}), **(snapshot_metadata or {})}
|
|
2409
|
+
tc.provider_status = provider_status or tc.provider_status
|
|
2410
|
+
tc.update_progress(lines)
|
|
2411
|
+
self._capture_tool_display_state(call_id, tc)
|
|
2412
|
+
|
|
2413
|
+
def update_tool_status(
|
|
2414
|
+
self,
|
|
2415
|
+
call_id: str,
|
|
2416
|
+
status: str,
|
|
2417
|
+
*,
|
|
2418
|
+
provider_status: str = "",
|
|
2419
|
+
metadata: dict[str, Any] | None = None,
|
|
2420
|
+
) -> None:
|
|
2421
|
+
"""Apply one monotonic canonical lifecycle transition."""
|
|
2422
|
+
record = self._tool_records.get(call_id)
|
|
2423
|
+
if record is None:
|
|
2424
|
+
return
|
|
2425
|
+
if record.canonical_status in _TERMINAL_CANONICAL_STATUSES:
|
|
2426
|
+
logger.debug("Ignoring late status %s for terminal tool call %s", status, call_id)
|
|
2427
|
+
return
|
|
2428
|
+
if record.canonical_status == "running" and status == "pending":
|
|
2429
|
+
logger.debug("Ignoring regressive pending status for running tool call %s", call_id)
|
|
2430
|
+
return
|
|
2431
|
+
record.provider_status = provider_status or record.provider_status
|
|
2432
|
+
record.metadata = {**(record.metadata or {}), **(metadata or {})}
|
|
2433
|
+
tc = self._tools.get(call_id)
|
|
2434
|
+
if isinstance(tc, BaseToolCard):
|
|
2435
|
+
tc.provider_status = record.provider_status
|
|
2436
|
+
tc.metadata = dict(record.metadata or {})
|
|
2437
|
+
if status not in _TERMINAL_CANONICAL_STATUSES:
|
|
2438
|
+
record.canonical_status = status
|
|
2439
|
+
if isinstance(tc, BaseToolCard):
|
|
2440
|
+
tc.canonical_status = status
|
|
2441
|
+
if isinstance(tc, _HostedStatusUpdatable):
|
|
2442
|
+
tc.update_hosted_status(status, record.provider_status)
|
|
2443
|
+
if tc is not None:
|
|
2444
|
+
tc.refresh()
|
|
2445
|
+
return
|
|
2446
|
+
result_text = str((metadata or {}).get("result_text", ""))
|
|
2447
|
+
if status == "completed":
|
|
2448
|
+
self.complete_tool(
|
|
2449
|
+
call_id,
|
|
2450
|
+
result_text,
|
|
2451
|
+
image_contents=record.image_contents,
|
|
2452
|
+
metadata=record.metadata,
|
|
2453
|
+
artifacts=record.artifacts,
|
|
2454
|
+
provider_status=record.provider_status,
|
|
2455
|
+
canonical_status=status,
|
|
2456
|
+
)
|
|
2457
|
+
record.completed_via_result = False
|
|
2458
|
+
else:
|
|
2459
|
+
error = result_text or ("interrupted" if status == "interrupted" else "Error: Provider tool failed")
|
|
2460
|
+
record.canonical_status = status
|
|
2461
|
+
record.status = "error"
|
|
2462
|
+
record.result = error
|
|
2463
|
+
record.completed_via_result = False
|
|
2464
|
+
record.nested_activities.clear()
|
|
2465
|
+
if tc is not None:
|
|
2466
|
+
if isinstance(tc, BaseToolCard):
|
|
2467
|
+
tc.canonical_status = status
|
|
2468
|
+
if isinstance(tc, _HostedStatusUpdatable):
|
|
2469
|
+
tc.update_hosted_status(status, record.provider_status)
|
|
2470
|
+
_completion_renderer(tc).set_error(error)
|
|
2471
|
+
self._done += 1
|
|
2472
|
+
self._finish_if_all_complete()
|
|
2473
|
+
self._update_title()
|
|
2474
|
+
|
|
2475
|
+
def on_tool_group_title_clicked(self) -> None:
|
|
2476
|
+
if self.collapse_locked:
|
|
2477
|
+
return
|
|
2478
|
+
self.collapsed = not self.collapsed
|