alloy-runtime-cli 0.1.0__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.
- alloy_runtime_cli-0.1.0.dist-info/METADATA +61 -0
- alloy_runtime_cli-0.1.0.dist-info/RECORD +451 -0
- alloy_runtime_cli-0.1.0.dist-info/WHEEL +5 -0
- alloy_runtime_cli-0.1.0.dist-info/entry_points.txt +2 -0
- alloy_runtime_cli-0.1.0.dist-info/top_level.txt +1 -0
- cli/__init__.py +0 -0
- cli/commands/__init__.py +0 -0
- cli/commands/admin/__init__.py +0 -0
- cli/commands/admin/bootstrap_command.py +118 -0
- cli/commands/admin/credentials/__init__.py +0 -0
- cli/commands/admin/credentials/create/__init__.py +0 -0
- cli/commands/admin/credentials/create/command.py +148 -0
- cli/commands/admin/credentials/create/presenter.py +16 -0
- cli/commands/admin/credentials/grant/__init__.py +0 -0
- cli/commands/admin/credentials/grant/command.py +119 -0
- cli/commands/admin/credentials/grant/fields.py +33 -0
- cli/commands/admin/credentials/grant/presenter.py +23 -0
- cli/commands/agents/__init__.py +0 -0
- cli/commands/agents/create/__init__.py +0 -0
- cli/commands/agents/create/command.py +475 -0
- cli/commands/agents/create/fields.py +64 -0
- cli/commands/agents/create/presenter.py +68 -0
- cli/commands/agents/delete/__init__.py +0 -0
- cli/commands/agents/delete/command.py +47 -0
- cli/commands/agents/delete/presenter.py +16 -0
- cli/commands/agents/get/command.py +37 -0
- cli/commands/agents/get/presenter.py +32 -0
- cli/commands/agents/list/__init__.py +1 -0
- cli/commands/agents/list/command.py +54 -0
- cli/commands/agents/list/presenter.py +82 -0
- cli/commands/agents/update/__init__.py +0 -0
- cli/commands/agents/update/command.py +435 -0
- cli/commands/agents/update/fields.py +40 -0
- cli/commands/agents/update/presenter.py +68 -0
- cli/commands/audio/__init__.py +0 -0
- cli/commands/audio/transcribe/__init__.py +0 -0
- cli/commands/audio/transcribe/command.py +144 -0
- cli/commands/audio/transcribe/presenter.py +15 -0
- cli/commands/auth/__init__.py +0 -0
- cli/commands/auth/login/__init__.py +0 -0
- cli/commands/auth/login/command.py +80 -0
- cli/commands/auth/signup/__init__.py +0 -0
- cli/commands/auth/signup/command.py +115 -0
- cli/commands/billing/__init__.py +1 -0
- cli/commands/billing/costs/__init__.py +1 -0
- cli/commands/billing/costs/by_agent/__init__.py +1 -0
- cli/commands/billing/costs/by_agent/command.py +57 -0
- cli/commands/billing/costs/by_agent/presenter.py +81 -0
- cli/commands/billing/costs/by_model/__init__.py +1 -0
- cli/commands/billing/costs/by_model/command.py +57 -0
- cli/commands/billing/costs/by_model/presenter.py +80 -0
- cli/commands/billing/costs/daily/__init__.py +1 -0
- cli/commands/billing/costs/daily/command.py +55 -0
- cli/commands/billing/costs/daily/presenter.py +75 -0
- cli/commands/billing/costs/summary/__init__.py +1 -0
- cli/commands/billing/costs/summary/command.py +57 -0
- cli/commands/billing/costs/summary/presenter.py +42 -0
- cli/commands/billing/projects/__init__.py +1 -0
- cli/commands/billing/projects/create/__init__.py +1 -0
- cli/commands/billing/projects/create/command.py +60 -0
- cli/commands/billing/projects/create/presenter.py +26 -0
- cli/commands/billing/projects/get/__init__.py +1 -0
- cli/commands/billing/projects/get/command.py +33 -0
- cli/commands/billing/projects/get/presenter.py +32 -0
- cli/commands/billing/projects/list/__init__.py +1 -0
- cli/commands/billing/projects/list/command.py +40 -0
- cli/commands/billing/projects/list/presenter.py +57 -0
- cli/commands/content/__init__.py +1 -0
- cli/commands/content/delete/__init__.py +0 -0
- cli/commands/content/delete/command.py +49 -0
- cli/commands/content/delete/presenter.py +18 -0
- cli/commands/content/edit/__init__.py +1 -0
- cli/commands/content/edit/command.py +155 -0
- cli/commands/content/edit/editor.py +150 -0
- cli/commands/content/edit/presenter.py +146 -0
- cli/commands/content/get/__init__.py +1 -0
- cli/commands/content/get/command.py +39 -0
- cli/commands/content/get/presenter.py +176 -0
- cli/commands/content/list/__init__.py +1 -0
- cli/commands/content/list/command.py +347 -0
- cli/commands/content/list/export_formatters.py +409 -0
- cli/commands/content/list/export_handler.py +165 -0
- cli/commands/content/list/presenter.py +190 -0
- cli/commands/credentials/__init__.py +0 -0
- cli/commands/credentials/create/__init__.py +0 -0
- cli/commands/credentials/create/command.py +165 -0
- cli/commands/credentials/create/fields.py +38 -0
- cli/commands/credentials/create/presenter.py +20 -0
- cli/commands/credentials/update/__init__.py +0 -0
- cli/commands/credentials/update/command.py +53 -0
- cli/commands/credentials/update/fields.py +71 -0
- cli/commands/credentials/update/presenter.py +16 -0
- cli/commands/flag_utils.py +366 -0
- cli/commands/generate/__init__.py +0 -0
- cli/commands/generate/cancel/__init__.py +1 -0
- cli/commands/generate/cancel/command.py +44 -0
- cli/commands/generate/cancel/presenter.py +26 -0
- cli/commands/generate/status/__init__.py +1 -0
- cli/commands/generate/status/command.py +58 -0
- cli/commands/generate/status/presenter.py +78 -0
- cli/commands/generate/text/__init__.py +0 -0
- cli/commands/generate/text/command.py +1325 -0
- cli/commands/generate/text/concurrent_renderer.py +355 -0
- cli/commands/generate/text/presenter.py +287 -0
- cli/commands/generate/text/stream_renderer.py +129 -0
- cli/commands/knowledge/__init__.py +0 -0
- cli/commands/knowledge/collections/__init__.py +0 -0
- cli/commands/knowledge/collections/cluster/__init__.py +0 -0
- cli/commands/knowledge/collections/cluster/command.py +64 -0
- cli/commands/knowledge/collections/cluster/presenter.py +74 -0
- cli/commands/knowledge/collections/cluster_status/__init__.py +0 -0
- cli/commands/knowledge/collections/cluster_status/command.py +46 -0
- cli/commands/knowledge/collections/cluster_status/presenter.py +10 -0
- cli/commands/knowledge/collections/create/__init__.py +0 -0
- cli/commands/knowledge/collections/create/command.py +137 -0
- cli/commands/knowledge/collections/create/presenter.py +38 -0
- cli/commands/knowledge/collections/delete/__init__.py +1 -0
- cli/commands/knowledge/collections/delete/command.py +47 -0
- cli/commands/knowledge/collections/delete/presenter.py +20 -0
- cli/commands/knowledge/collections/get/__init__.py +1 -0
- cli/commands/knowledge/collections/get/command.py +30 -0
- cli/commands/knowledge/collections/get/presenter.py +44 -0
- cli/commands/knowledge/collections/list/__init__.py +1 -0
- cli/commands/knowledge/collections/list/command.py +41 -0
- cli/commands/knowledge/collections/list/presenter.py +68 -0
- cli/commands/knowledge/collections/update/__init__.py +0 -0
- cli/commands/knowledge/collections/update/command.py +97 -0
- cli/commands/knowledge/collections/update/presenter.py +42 -0
- cli/commands/knowledge/documents/__init__.py +0 -0
- cli/commands/knowledge/documents/bulk_metadata/__init__.py +0 -0
- cli/commands/knowledge/documents/bulk_metadata/command.py +119 -0
- cli/commands/knowledge/documents/bulk_metadata/presenter.py +36 -0
- cli/commands/knowledge/documents/delete/__init__.py +0 -0
- cli/commands/knowledge/documents/delete/command.py +47 -0
- cli/commands/knowledge/documents/delete/presenter.py +20 -0
- cli/commands/knowledge/documents/get/__init__.py +0 -0
- cli/commands/knowledge/documents/get/command.py +39 -0
- cli/commands/knowledge/documents/get/presenter.py +78 -0
- cli/commands/knowledge/documents/ingest/__init__.py +0 -0
- cli/commands/knowledge/documents/ingest/command.py +222 -0
- cli/commands/knowledge/documents/ingest/presenter.py +41 -0
- cli/commands/knowledge/documents/list/__init__.py +0 -0
- cli/commands/knowledge/documents/list/command.py +69 -0
- cli/commands/knowledge/documents/list/presenter.py +86 -0
- cli/commands/knowledge/documents/reingest/__init__.py +0 -0
- cli/commands/knowledge/documents/reingest/command.py +102 -0
- cli/commands/knowledge/documents/reingest/presenter.py +70 -0
- cli/commands/knowledge/documents/update/__init__.py +0 -0
- cli/commands/knowledge/documents/update/command.py +85 -0
- cli/commands/knowledge/documents/update/presenter.py +37 -0
- cli/commands/knowledge/recover/__init__.py +0 -0
- cli/commands/knowledge/recover/command.py +46 -0
- cli/commands/knowledge/recover/presenter.py +79 -0
- cli/commands/knowledge/search/__init__.py +0 -0
- cli/commands/knowledge/search/command.py +218 -0
- cli/commands/knowledge/search/presenter.py +111 -0
- cli/commands/knowledge/synthesis/__init__.py +0 -0
- cli/commands/knowledge/synthesis/create/__init__.py +0 -0
- cli/commands/knowledge/synthesis/create/command.py +127 -0
- cli/commands/knowledge/synthesis/create/presenter.py +33 -0
- cli/commands/knowledge/synthesis/delete/__init__.py +0 -0
- cli/commands/knowledge/synthesis/delete/command.py +53 -0
- cli/commands/knowledge/synthesis/delete/presenter.py +31 -0
- cli/commands/knowledge/synthesis/get/__init__.py +0 -0
- cli/commands/knowledge/synthesis/get/command.py +55 -0
- cli/commands/knowledge/synthesis/get/presenter.py +114 -0
- cli/commands/knowledge/synthesis/list/__init__.py +0 -0
- cli/commands/knowledge/synthesis/list/command.py +132 -0
- cli/commands/knowledge/synthesis/list/presenter.py +84 -0
- cli/commands/knowledge/synthesis/refresh/__init__.py +0 -0
- cli/commands/knowledge/synthesis/refresh/command.py +42 -0
- cli/commands/knowledge/synthesis/refresh/presenter.py +33 -0
- cli/commands/knowledge/synthesis/update/__init__.py +0 -0
- cli/commands/knowledge/synthesis/update/command.py +76 -0
- cli/commands/knowledge/synthesis/update/presenter.py +41 -0
- cli/commands/models/__init__.py +0 -0
- cli/commands/models/list/__init__.py +0 -0
- cli/commands/models/list/command.py +84 -0
- cli/commands/models/list/presenter.py +114 -0
- cli/commands/organizations/__init__.py +0 -0
- cli/commands/organizations/create/command.py +32 -0
- cli/commands/organizations/create/presenter.py +9 -0
- cli/commands/pipelines/__init__.py +1 -0
- cli/commands/pipelines/approvals/__init__.py +1 -0
- cli/commands/pipelines/approvals/decide_command.py +77 -0
- cli/commands/pipelines/approvals/get_command.py +44 -0
- cli/commands/pipelines/approvals/presenter.py +56 -0
- cli/commands/pipelines/costs/__init__.py +1 -0
- cli/commands/pipelines/costs/command.py +57 -0
- cli/commands/pipelines/costs/daily_command.py +54 -0
- cli/commands/pipelines/costs/daily_presenter.py +59 -0
- cli/commands/pipelines/costs/presenter.py +37 -0
- cli/commands/pipelines/create/__init__.py +1 -0
- cli/commands/pipelines/create/command.py +103 -0
- cli/commands/pipelines/create/presenter.py +22 -0
- cli/commands/pipelines/env_vars/__init__.py +1 -0
- cli/commands/pipelines/env_vars/command.py +51 -0
- cli/commands/pipelines/env_vars/presenter.py +16 -0
- cli/commands/pipelines/execute/__init__.py +1 -0
- cli/commands/pipelines/execute/command.py +142 -0
- cli/commands/pipelines/execute/presenter.py +47 -0
- cli/commands/pipelines/executions/__init__.py +1 -0
- cli/commands/pipelines/executions/costs/__init__.py +1 -0
- cli/commands/pipelines/executions/costs/command.py +48 -0
- cli/commands/pipelines/executions/costs/presenter.py +29 -0
- cli/commands/pipelines/executions/costs_by_model/__init__.py +1 -0
- cli/commands/pipelines/executions/costs_by_model/command.py +50 -0
- cli/commands/pipelines/executions/costs_by_model/presenter.py +78 -0
- cli/commands/pipelines/executions/costs_by_step/__init__.py +1 -0
- cli/commands/pipelines/executions/costs_by_step/command.py +50 -0
- cli/commands/pipelines/executions/costs_by_step/presenter.py +72 -0
- cli/commands/pipelines/executions/get_command.py +38 -0
- cli/commands/pipelines/executions/list_command.py +123 -0
- cli/commands/pipelines/executions/presenter.py +131 -0
- cli/commands/pipelines/executions/rerun_command.py +41 -0
- cli/commands/pipelines/executions/update/__init__.py +1 -0
- cli/commands/pipelines/executions/update/command.py +110 -0
- cli/commands/pipelines/executions/update/presenter.py +28 -0
- cli/commands/pipelines/get/__init__.py +1 -0
- cli/commands/pipelines/get/command.py +33 -0
- cli/commands/pipelines/get/presenter.py +48 -0
- cli/commands/pipelines/list/__init__.py +1 -0
- cli/commands/pipelines/list/command.py +53 -0
- cli/commands/pipelines/list/presenter.py +66 -0
- cli/commands/pipelines/schedules/__init__.py +1 -0
- cli/commands/pipelines/schedules/create_command.py +119 -0
- cli/commands/pipelines/schedules/create_presenter.py +35 -0
- cli/commands/pipelines/schedules/delete_command.py +52 -0
- cli/commands/pipelines/schedules/env_vars_command.py +59 -0
- cli/commands/pipelines/schedules/env_vars_presenter.py +16 -0
- cli/commands/pipelines/schedules/get_command.py +38 -0
- cli/commands/pipelines/schedules/list_command.py +33 -0
- cli/commands/pipelines/schedules/once_command.py +90 -0
- cli/commands/pipelines/schedules/once_presenter.py +30 -0
- cli/commands/pipelines/schedules/presenter.py +104 -0
- cli/commands/pipelines/schedules/update_command.py +139 -0
- cli/commands/pipelines/schedules/update_presenter.py +29 -0
- cli/commands/render/__init__.py +0 -0
- cli/commands/render/html_to_image/__init__.py +0 -0
- cli/commands/render/html_to_image/command.py +170 -0
- cli/commands/schemas/__init__.py +0 -0
- cli/commands/schemas/create/__init__.py +0 -0
- cli/commands/schemas/create/command.py +122 -0
- cli/commands/schemas/create/presenter.py +53 -0
- cli/commands/schemas/delete/command.py +45 -0
- cli/commands/schemas/delete/presenter.py +9 -0
- cli/commands/schemas/get/__init__.py +0 -0
- cli/commands/schemas/get/command.py +56 -0
- cli/commands/schemas/get/presenter.py +129 -0
- cli/commands/schemas/list/__init__.py +0 -0
- cli/commands/schemas/list/command.py +64 -0
- cli/commands/schemas/list/presenter.py +133 -0
- cli/commands/schemas/update/__init__.py +0 -0
- cli/commands/schemas/update/command.py +369 -0
- cli/commands/schemas/update/presenter.py +53 -0
- cli/commands/sessions/__init__.py +1 -0
- cli/commands/sessions/delete/__init__.py +1 -0
- cli/commands/sessions/delete/command.py +47 -0
- cli/commands/sessions/delete/presenter.py +10 -0
- cli/commands/sessions/get/__init__.py +1 -0
- cli/commands/sessions/get/command.py +42 -0
- cli/commands/sessions/get/presenter.py +59 -0
- cli/commands/sessions/list/__init__.py +1 -0
- cli/commands/sessions/list/command.py +61 -0
- cli/commands/sessions/list/presenter.py +68 -0
- cli/commands/sessions/messages/__init__.py +1 -0
- cli/commands/sessions/messages/command.py +78 -0
- cli/commands/sessions/messages/presenter.py +79 -0
- cli/commands/shared_flags.py +500 -0
- cli/commands/sync/__init__.py +0 -0
- cli/commands/sync/command.py +45 -0
- cli/commands/sync/presenter.py +49 -0
- cli/commands/tags/__init__.py +1 -0
- cli/commands/tags/create/__init__.py +1 -0
- cli/commands/tags/create/command.py +60 -0
- cli/commands/tags/delete/__init__.py +1 -0
- cli/commands/tags/delete/command.py +47 -0
- cli/commands/tags/delete/presenter.py +10 -0
- cli/commands/tags/get/command.py +31 -0
- cli/commands/tags/get/presenter.py +23 -0
- cli/commands/tags/list/__init__.py +1 -0
- cli/commands/tags/list/command.py +52 -0
- cli/commands/tags/list/presenter.py +49 -0
- cli/commands/tags/update/command.py +64 -0
- cli/commands/tags/update/presenter.py +9 -0
- cli/commands/templates/__init__.py +0 -0
- cli/commands/templates/create/__init__.py +0 -0
- cli/commands/templates/create/command.py +152 -0
- cli/commands/templates/create/presenter.py +86 -0
- cli/commands/templates/delete/__init__.py +0 -0
- cli/commands/templates/delete/command.py +47 -0
- cli/commands/templates/delete/presenter.py +16 -0
- cli/commands/templates/get/__init__.py +0 -0
- cli/commands/templates/get/command.py +52 -0
- cli/commands/templates/get/presenter.py +233 -0
- cli/commands/templates/get_by_version/command.py +32 -0
- cli/commands/templates/get_by_version/presenter.py +30 -0
- cli/commands/templates/list/__init__.py +1 -0
- cli/commands/templates/list/command.py +102 -0
- cli/commands/templates/list/presenter.py +93 -0
- cli/commands/templates/render/__init__.py +0 -0
- cli/commands/templates/render/command.py +115 -0
- cli/commands/templates/render/presenter.py +276 -0
- cli/commands/templates/update/__init__.py +0 -0
- cli/commands/templates/update/command.py +199 -0
- cli/commands/templates/update/presenter.py +94 -0
- cli/commands/templates/version/__init__.py +1 -0
- cli/commands/templates/version/command.py +116 -0
- cli/commands/templates/version/presenter.py +100 -0
- cli/commands/tool_configs/__init__.py +0 -0
- cli/commands/tool_configs/create/__init__.py +0 -0
- cli/commands/tool_configs/create/command.py +118 -0
- cli/commands/tool_configs/create/presenter.py +53 -0
- cli/commands/tool_configs/delete/__init__.py +0 -0
- cli/commands/tool_configs/delete/command.py +47 -0
- cli/commands/tool_configs/delete/presenter.py +18 -0
- cli/commands/tool_configs/get/__init__.py +0 -0
- cli/commands/tool_configs/get/command.py +31 -0
- cli/commands/tool_configs/get/presenter.py +62 -0
- cli/commands/tool_configs/list/__init__.py +0 -0
- cli/commands/tool_configs/list/command.py +59 -0
- cli/commands/tool_configs/list/presenter.py +60 -0
- cli/commands/tool_configs/update/__init__.py +0 -0
- cli/commands/tool_configs/update/command.py +128 -0
- cli/commands/tool_configs/update/presenter.py +53 -0
- cli/commands/tools/__init__.py +1 -0
- cli/commands/tools/get/__init__.py +1 -0
- cli/commands/tools/get/command.py +42 -0
- cli/commands/tools/get/presenter.py +45 -0
- cli/commands/tools/list/__init__.py +1 -0
- cli/commands/tools/list/command.py +56 -0
- cli/commands/tools/list/presenter.py +44 -0
- cli/commands/users/__init__.py +0 -0
- cli/commands/users/create/command.py +53 -0
- cli/commands/users/create/presenter.py +9 -0
- cli/commands/whoami/__init__.py +0 -0
- cli/commands/whoami/command.py +42 -0
- cli/infrastructure/__init__.py +0 -0
- cli/infrastructure/auth_storage.py +71 -0
- cli/infrastructure/client_factory.py +36 -0
- cli/infrastructure/command.py +75 -0
- cli/infrastructure/config.py +188 -0
- cli/infrastructure/console.py +27 -0
- cli/infrastructure/editor.py +138 -0
- cli/infrastructure/error_display.py +178 -0
- cli/infrastructure/field_extractor.py +360 -0
- cli/infrastructure/file_content.py +210 -0
- cli/infrastructure/filter_parser.py +256 -0
- cli/infrastructure/formatters/__init__.py +0 -0
- cli/infrastructure/formatters/base.py +99 -0
- cli/infrastructure/formatters/compact_formatter.py +245 -0
- cli/infrastructure/formatters/json_formatter.py +84 -0
- cli/infrastructure/formatters/lines_formatter.py +102 -0
- cli/infrastructure/formatting/__init__.py +0 -0
- cli/infrastructure/formatting/fields.py +193 -0
- cli/infrastructure/forms/__init__.py +0 -0
- cli/infrastructure/forms/agent_picker.py +123 -0
- cli/infrastructure/forms/agent_tool_editor.py +384 -0
- cli/infrastructure/forms/agent_tools_manager.py +212 -0
- cli/infrastructure/forms/base_picker.py +469 -0
- cli/infrastructure/forms/components.py +126 -0
- cli/infrastructure/forms/json_schema_builder.py +149 -0
- cli/infrastructure/forms/model_picker.py +134 -0
- cli/infrastructure/forms/parsers.py +173 -0
- cli/infrastructure/forms/resolution_modal.py +302 -0
- cli/infrastructure/forms/schema_picker.py +137 -0
- cli/infrastructure/forms/tag_management_modal.py +103 -0
- cli/infrastructure/forms/tag_picker.py +207 -0
- cli/infrastructure/forms/template_picker.py +131 -0
- cli/infrastructure/forms/tool_config_picker.py +130 -0
- cli/infrastructure/forms/tool_picker.py +103 -0
- cli/infrastructure/injection/__init__.py +0 -0
- cli/infrastructure/injection/parser.py +302 -0
- cli/infrastructure/injection/resolver.py +399 -0
- cli/infrastructure/kv_parser.py +130 -0
- cli/infrastructure/local_storage.py +227 -0
- cli/infrastructure/macro_parser.py +215 -0
- cli/infrastructure/output.py +192 -0
- cli/infrastructure/provider_setup.py +81 -0
- cli/infrastructure/renderers/__init__.py +0 -0
- cli/infrastructure/renderers/entity_renderer.py +77 -0
- cli/infrastructure/renderers/list_renderer.py +114 -0
- cli/infrastructure/scope_utils.py +47 -0
- cli/infrastructure/spinner.py +101 -0
- cli/infrastructure/tui/__init__.py +0 -0
- cli/infrastructure/tui/clipboard.py +41 -0
- cli/infrastructure/tui/formatters.py +105 -0
- cli/infrastructure/tui/preview.py +14 -0
- cli/infrastructure/tui/selectable.py +198 -0
- cli/infrastructure/validation/__init__.py +0 -0
- cli/infrastructure/validation/tag_validation.py +74 -0
- cli/main.py +759 -0
- cli/tui/__init__.py +0 -0
- cli/tui/app.py +199 -0
- cli/tui/app_store.py +73 -0
- cli/tui/chat/__init__.py +0 -0
- cli/tui/chat/commands/__init__.py +0 -0
- cli/tui/chat/commands/base.py +65 -0
- cli/tui/chat/commands/create_session.py +135 -0
- cli/tui/chat/commands/load_session.py +119 -0
- cli/tui/chat/commands/regenerate.py +120 -0
- cli/tui/chat/commands/reload_session.py +63 -0
- cli/tui/chat/commands/send_message.py +190 -0
- cli/tui/chat/commands/undo.py +66 -0
- cli/tui/chat/editor.py +71 -0
- cli/tui/chat/messages.py +223 -0
- cli/tui/chat/pane.py +141 -0
- cli/tui/chat/renderers/__init__.py +0 -0
- cli/tui/chat/renderers/base.py +72 -0
- cli/tui/chat/renderers/markdown.py +250 -0
- cli/tui/chat/renderers/plain.py +83 -0
- cli/tui/chat/screen.py +1155 -0
- cli/tui/chat/services/__init__.py +0 -0
- cli/tui/chat/services/injection.py +386 -0
- cli/tui/chat/services/name_generator.py +256 -0
- cli/tui/chat/slash_commands.py +424 -0
- cli/tui/chat/store.py +280 -0
- cli/tui/chat/types.py +220 -0
- cli/tui/chat/widgets/__init__.py +0 -0
- cli/tui/chat/widgets/chat_header.py +75 -0
- cli/tui/chat/widgets/chat_input.py +362 -0
- cli/tui/chat/widgets/injection_popup.py +161 -0
- cli/tui/chat/widgets/message_display.py +287 -0
- cli/tui/chat/widgets/session_sidebar.py +214 -0
- cli/tui/chat/widgets/welcome_screen.py +290 -0
- cli/tui/screens/__init__.py +0 -0
- cli/tui/screens/agents.py +344 -0
- cli/tui/screens/base.py +301 -0
- cli/tui/screens/content.py +508 -0
- cli/tui/screens/dashboard.py +89 -0
- cli/tui/screens/models.py +96 -0
- cli/tui/screens/nav_screen.py +186 -0
- cli/tui/screens/schemas.py +522 -0
- cli/tui/screens/templates.py +734 -0
- cli/tui/screens/tool_configs.py +335 -0
- cli/tui/styles/__init__.py +0 -0
- cli/tui/widgets/__init__.py +0 -0
- cli/tui/widgets/agent_create_modal.py +139 -0
- cli/tui/widgets/agent_form_modal.py +659 -0
- cli/tui/widgets/agent_update_modal.py +299 -0
- cli/tui/widgets/base_form_modal.py +77 -0
- cli/tui/widgets/confirm_modal.py +75 -0
- cli/tui/widgets/help_modal.py +145 -0
- cli/tui/widgets/new_session_modal.py +328 -0
- cli/tui/widgets/schema_create_modal.py +271 -0
- cli/tui/widgets/schema_update_modal.py +188 -0
- cli/tui/widgets/status_footer.py +147 -0
- cli/tui/widgets/template_create_modal.py +502 -0
- cli/tui/widgets/template_update_modal.py +308 -0
- cli/tui/widgets/tool_config_create_modal.py +216 -0
- cli/tui/widgets/tool_config_update_modal.py +208 -0
cli/tui/chat/types.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""Type definitions for the chat module.
|
|
2
|
+
|
|
3
|
+
Contains enums, protocols, and dataclasses used throughout the chat system.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from enum import Enum, auto
|
|
9
|
+
from typing import Protocol, Union
|
|
10
|
+
from uuid import UUID
|
|
11
|
+
|
|
12
|
+
from rich.console import RenderableType
|
|
13
|
+
from alloy_runtime_types.dtos.sessions import MessageResponse, SessionSummary
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ChatPhase(Enum):
|
|
17
|
+
"""Current phase of chat interaction.
|
|
18
|
+
|
|
19
|
+
Used to control UI state and prevent conflicting operations.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
IDLE = auto()
|
|
23
|
+
SENDING = auto()
|
|
24
|
+
STREAMING = auto()
|
|
25
|
+
LOADING_SESSION = auto()
|
|
26
|
+
LOADING_SESSIONS = auto()
|
|
27
|
+
CREATING_SESSION = auto()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class RenderMode(Enum):
|
|
31
|
+
"""Rendering mode for message display."""
|
|
32
|
+
|
|
33
|
+
MARKDOWN = auto()
|
|
34
|
+
PLAIN = auto()
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class SessionContext:
|
|
39
|
+
"""Immutable context for the active chat session.
|
|
40
|
+
|
|
41
|
+
Supports two modes:
|
|
42
|
+
- Agent mode: agent_id and agent_name are set, model comes from agent config
|
|
43
|
+
- Model mode: provider_key and model_name are set, with optional system_instruction
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
session_id: UUID
|
|
47
|
+
session_name: str | None
|
|
48
|
+
agent_id: UUID | None
|
|
49
|
+
agent_name: str | None
|
|
50
|
+
message_count: int
|
|
51
|
+
# Model info for direct model mode (no agent)
|
|
52
|
+
provider_key: str | None = None
|
|
53
|
+
model_name: str | None = None
|
|
54
|
+
system_instruction: str | None = None
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def is_model_mode(self) -> bool:
|
|
58
|
+
"""Check if this session uses direct model mode."""
|
|
59
|
+
return self.provider_key is not None and self.model_name is not None
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def is_agent_mode(self) -> bool:
|
|
63
|
+
"""Check if this session uses agent mode."""
|
|
64
|
+
return self.agent_id is not None
|
|
65
|
+
|
|
66
|
+
@property
|
|
67
|
+
def display_name(self) -> str:
|
|
68
|
+
"""Get the display name for the assistant (agent name or model identifier)."""
|
|
69
|
+
if self.agent_name:
|
|
70
|
+
return self.agent_name
|
|
71
|
+
if self.provider_key and self.model_name:
|
|
72
|
+
return f"{self.provider_key}:{self.model_name}"
|
|
73
|
+
if self.model_name:
|
|
74
|
+
return self.model_name
|
|
75
|
+
return "Assistant"
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class ChatState:
|
|
80
|
+
"""Immutable snapshot of chat state.
|
|
81
|
+
|
|
82
|
+
All fields are immutable (frozen dataclass with tuple for collections).
|
|
83
|
+
State transitions create new instances rather than mutating.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
phase: ChatPhase = ChatPhase.IDLE
|
|
87
|
+
session: SessionContext | None = None
|
|
88
|
+
messages: tuple[MessageResponse, ...] = ()
|
|
89
|
+
streaming_content: str = ""
|
|
90
|
+
streaming_thinking_content: str = "" # Reasoning/thinking content during streaming
|
|
91
|
+
error: str | None = None
|
|
92
|
+
|
|
93
|
+
# Optimistic UI: user message shown before server confirms
|
|
94
|
+
pending_user_message: str | None = None
|
|
95
|
+
|
|
96
|
+
# Session sidebar state
|
|
97
|
+
sessions: tuple[SessionSummary, ...] = ()
|
|
98
|
+
session_search_query: str = ""
|
|
99
|
+
|
|
100
|
+
# UI state
|
|
101
|
+
sidebar_visible: bool = False # Default: hidden for cleaner initial view
|
|
102
|
+
thinking_collapsed: bool = False # Whether thinking blocks are collapsed
|
|
103
|
+
|
|
104
|
+
# Stream cancellation
|
|
105
|
+
cancel_requested: bool = False # Set to True to cancel ongoing stream
|
|
106
|
+
|
|
107
|
+
# Rendering
|
|
108
|
+
render_mode: RenderMode = RenderMode.MARKDOWN
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class MessageRenderer(Protocol):
|
|
112
|
+
"""Protocol for message rendering strategies.
|
|
113
|
+
|
|
114
|
+
Implement this to create custom renderers (markdown, plain text, etc.).
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
def render(
|
|
118
|
+
self,
|
|
119
|
+
messages: tuple[MessageResponse, ...],
|
|
120
|
+
streaming_content: str = "",
|
|
121
|
+
streaming_thinking_content: str = "",
|
|
122
|
+
pending_user_message: str | None = None,
|
|
123
|
+
assistant_name: str = "Assistant",
|
|
124
|
+
is_streaming: bool = False,
|
|
125
|
+
thinking_collapsed: bool = False,
|
|
126
|
+
) -> Union[str, RenderableType]:
|
|
127
|
+
"""Render messages to a displayable format.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
messages: Tuple of message responses to render.
|
|
131
|
+
streaming_content: Partial content being streamed (appended to end).
|
|
132
|
+
streaming_thinking_content: Partial thinking/reasoning content being streamed.
|
|
133
|
+
pending_user_message: User message shown optimistically before server confirms.
|
|
134
|
+
assistant_name: Name to display for assistant (agent name or model identifier).
|
|
135
|
+
is_streaming: Whether currently streaming a response (shows spinner).
|
|
136
|
+
thinking_collapsed: Whether thinking blocks should be collapsed.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
Formatted content suitable for display in a Static widget.
|
|
140
|
+
Can be a string (for plain text) or a Rich renderable (for markdown).
|
|
141
|
+
"""
|
|
142
|
+
...
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def format_time(dt: datetime) -> str:
|
|
146
|
+
"""Format datetime for message display."""
|
|
147
|
+
return dt.strftime("%H:%M")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def format_session_time(dt: datetime) -> str:
|
|
151
|
+
"""Format datetime for session sidebar display."""
|
|
152
|
+
now = datetime.now(dt.tzinfo)
|
|
153
|
+
if dt.date() == now.date():
|
|
154
|
+
return dt.strftime("%H:%M")
|
|
155
|
+
return dt.strftime("%m/%d")
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def extract_message_text(msg: MessageResponse) -> str:
|
|
159
|
+
"""Extract text content from a message's content parts (excludes thinking content)."""
|
|
160
|
+
text_parts: list[str] = []
|
|
161
|
+
for part in msg.content_parts:
|
|
162
|
+
# Skip thinking content - it's extracted separately
|
|
163
|
+
if part.content_type_id == "thinking":
|
|
164
|
+
continue
|
|
165
|
+
if part.content_text:
|
|
166
|
+
text_parts.append(part.content_text)
|
|
167
|
+
return "".join(text_parts)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def extract_message_text_for_display(
|
|
171
|
+
msg: MessageResponse,
|
|
172
|
+
*,
|
|
173
|
+
truncate_threshold: int = 2000,
|
|
174
|
+
) -> tuple[str, bool]:
|
|
175
|
+
"""Extract text content from a message, truncating long content for display.
|
|
176
|
+
|
|
177
|
+
Very long content (e.g., from rendered @fragment() injections) can overwhelm
|
|
178
|
+
the TUI. This function truncates such content while preserving the head and
|
|
179
|
+
tail so users can see what was injected.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
msg: The message to extract text from
|
|
183
|
+
truncate_threshold: Character count above which to truncate each part
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Tuple of (text, was_any_truncated)
|
|
187
|
+
- text: The extracted text with long parts truncated
|
|
188
|
+
- was_any_truncated: True if any content was truncated
|
|
189
|
+
"""
|
|
190
|
+
from cli.infrastructure.tui.formatters import truncate_long_content
|
|
191
|
+
|
|
192
|
+
text_parts: list[str] = []
|
|
193
|
+
any_truncated = False
|
|
194
|
+
|
|
195
|
+
for part in msg.content_parts:
|
|
196
|
+
# Skip thinking content - it's extracted separately
|
|
197
|
+
if part.content_type_id == "thinking":
|
|
198
|
+
continue
|
|
199
|
+
if part.content_text:
|
|
200
|
+
truncated_text, was_truncated = truncate_long_content(
|
|
201
|
+
part.content_text,
|
|
202
|
+
threshold=truncate_threshold,
|
|
203
|
+
)
|
|
204
|
+
text_parts.append(truncated_text)
|
|
205
|
+
if was_truncated:
|
|
206
|
+
any_truncated = True
|
|
207
|
+
|
|
208
|
+
return "".join(text_parts), any_truncated
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def extract_thinking_content(msg: MessageResponse) -> str | None:
|
|
212
|
+
"""Extract thinking/reasoning content from a message's content parts.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
The thinking content if present, None otherwise.
|
|
216
|
+
"""
|
|
217
|
+
for part in msg.content_parts:
|
|
218
|
+
if part.content_type_id == "thinking" and part.content_text:
|
|
219
|
+
return part.content_text
|
|
220
|
+
return None
|
|
File without changes
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Chat header widget for displaying session info."""
|
|
2
|
+
|
|
3
|
+
from typing import Callable
|
|
4
|
+
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.containers import Vertical
|
|
7
|
+
from textual.widget import Widget
|
|
8
|
+
from textual.widgets import Static
|
|
9
|
+
|
|
10
|
+
from cli.tui.chat.store import ChatStore
|
|
11
|
+
from cli.tui.chat.types import ChatState
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ChatHeader(Widget):
|
|
15
|
+
"""Widget for displaying current session information.
|
|
16
|
+
|
|
17
|
+
Shows:
|
|
18
|
+
- Session name
|
|
19
|
+
- Agent name (if configured)
|
|
20
|
+
- Message count
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, store: ChatStore) -> None:
|
|
24
|
+
"""Initialize the chat header.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
store: ChatStore instance for state management.
|
|
28
|
+
"""
|
|
29
|
+
super().__init__()
|
|
30
|
+
self._store = store
|
|
31
|
+
self._unsubscribe: Callable[[], None] | None = None
|
|
32
|
+
|
|
33
|
+
def compose(self) -> ComposeResult:
|
|
34
|
+
"""Compose the header layout."""
|
|
35
|
+
with Vertical(id="chat-header"):
|
|
36
|
+
yield Static(
|
|
37
|
+
"[bold]Select a session[/] or press [cyan]Ctrl+N[/] to start a new chat",
|
|
38
|
+
id="session-info",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def on_mount(self) -> None:
|
|
42
|
+
"""Subscribe to store on mount."""
|
|
43
|
+
self._unsubscribe = self._store.subscribe(self._on_state_change)
|
|
44
|
+
|
|
45
|
+
def on_unmount(self) -> None:
|
|
46
|
+
"""Unsubscribe from store on unmount."""
|
|
47
|
+
if self._unsubscribe:
|
|
48
|
+
self._unsubscribe()
|
|
49
|
+
|
|
50
|
+
def _on_state_change(self, state: ChatState) -> None:
|
|
51
|
+
"""React to store state changes."""
|
|
52
|
+
session_info = self.query_one("#session-info", Static)
|
|
53
|
+
|
|
54
|
+
if state.session is None:
|
|
55
|
+
session_info.update(
|
|
56
|
+
"[bold]Select a session[/] or press [cyan]Ctrl+N[/] to start a new chat"
|
|
57
|
+
)
|
|
58
|
+
else:
|
|
59
|
+
name = state.session.session_name or "Untitled Chat"
|
|
60
|
+
agent_info = (
|
|
61
|
+
f" | Agent: {state.session.agent_name}"
|
|
62
|
+
if state.session.agent_name
|
|
63
|
+
else ""
|
|
64
|
+
)
|
|
65
|
+
msg_count = state.session.message_count
|
|
66
|
+
session_info.update(
|
|
67
|
+
f"[bold]{name}[/]{agent_info} | [dim]{msg_count} messages[/]"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def reset(self) -> None:
|
|
71
|
+
"""Reset header to default state."""
|
|
72
|
+
session_info = self.query_one("#session-info", Static)
|
|
73
|
+
session_info.update(
|
|
74
|
+
"[bold]Select a session[/] or press [cyan]Ctrl+N[/] to start a new chat"
|
|
75
|
+
)
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
"""Chat input widget for composing and sending messages."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Callable
|
|
4
|
+
|
|
5
|
+
from textual import on
|
|
6
|
+
from textual.app import ComposeResult
|
|
7
|
+
from textual.binding import Binding
|
|
8
|
+
from textual.containers import Vertical
|
|
9
|
+
from textual.events import Key
|
|
10
|
+
from textual.widget import Widget
|
|
11
|
+
from textual.widgets import TextArea
|
|
12
|
+
|
|
13
|
+
from cli.infrastructure.injection.parser import detect_partial_injection, has_injections
|
|
14
|
+
from cli.infrastructure.tui.selectable import CopyOnSelectTextArea
|
|
15
|
+
from cli.tui.chat.messages import (
|
|
16
|
+
CopyToClipboardRequested,
|
|
17
|
+
OpenEditorRequested,
|
|
18
|
+
SendMessageRequested,
|
|
19
|
+
SlashCommandExecuted,
|
|
20
|
+
UnknownSlashCommand,
|
|
21
|
+
)
|
|
22
|
+
from cli.tui.chat.services.injection import InjectionService
|
|
23
|
+
from cli.tui.chat.slash_commands import SlashContext, get_default_registry
|
|
24
|
+
from cli.tui.chat.store import ChatStore
|
|
25
|
+
from cli.tui.chat.types import ChatPhase, ChatState
|
|
26
|
+
from cli.tui.chat.widgets.injection_popup import InjectionSuggestionPopup
|
|
27
|
+
from alloy_runtime_sdk.logging.config import get_logger
|
|
28
|
+
|
|
29
|
+
logger = get_logger(__name__)
|
|
30
|
+
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from cli.tui.app import AlloyRuntimeApp
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ChatTextArea(CopyOnSelectTextArea):
|
|
36
|
+
"""Custom TextArea with send key bindings and copy-on-select.
|
|
37
|
+
|
|
38
|
+
Key handling for injection popup is done by the parent ChatInput widget.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
BINDINGS = [
|
|
42
|
+
Binding("ctrl+d", "submit", "Send", show=False, priority=True),
|
|
43
|
+
Binding("ctrl+e", "open_editor", "Editor", show=False, priority=True),
|
|
44
|
+
Binding("ctrl+y", "copy_last", "Copy Last", show=False, priority=True),
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
def action_submit(self) -> None:
|
|
48
|
+
"""Post a submit request to parent."""
|
|
49
|
+
self.post_message(SendMessageRequested(self.text.strip()))
|
|
50
|
+
|
|
51
|
+
def action_open_editor(self) -> None:
|
|
52
|
+
"""Request to open current text in external editor."""
|
|
53
|
+
self.post_message(OpenEditorRequested(self.text))
|
|
54
|
+
|
|
55
|
+
def action_copy_last(self) -> None:
|
|
56
|
+
"""Request to copy last assistant message - bubbles up to ChatScreen."""
|
|
57
|
+
self.post_message(CopyToClipboardRequested("", "_copy_last_request_"))
|
|
58
|
+
|
|
59
|
+
async def _on_key(self, event: Key) -> None:
|
|
60
|
+
"""Handle keys, letting parent intercept for popup control."""
|
|
61
|
+
# Let the parent handle the key first
|
|
62
|
+
await super()._on_key(event)
|
|
63
|
+
# Then ensure cursor is visible
|
|
64
|
+
self.call_after_refresh(self._ensure_cursor_visible)
|
|
65
|
+
|
|
66
|
+
def _ensure_cursor_visible(self) -> None:
|
|
67
|
+
"""Scroll the TextArea to ensure the cursor is visible."""
|
|
68
|
+
self.scroll_cursor_visible(animate=False)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ChatInput(Widget):
|
|
72
|
+
"""Widget for composing chat messages with injection autocomplete.
|
|
73
|
+
|
|
74
|
+
This widget controls the injection popup - the popup is just a display,
|
|
75
|
+
all keyboard handling happens here. Focus stays on the text area.
|
|
76
|
+
|
|
77
|
+
Features:
|
|
78
|
+
- Multi-line text input with auto-grow
|
|
79
|
+
- Send with Ctrl+D
|
|
80
|
+
- Open in external editor with Ctrl+E
|
|
81
|
+
- Slash command parsing (/help, /clear, /copy, etc.)
|
|
82
|
+
- Injection autocomplete (@fragment, @text, @json, @schema)
|
|
83
|
+
- Arrow keys navigate popup while focus stays in input
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
app: "AlloyRuntimeApp"
|
|
87
|
+
|
|
88
|
+
DEFAULT_CSS = """
|
|
89
|
+
ChatInput {
|
|
90
|
+
height: auto;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
ChatInput #input-container {
|
|
94
|
+
height: auto;
|
|
95
|
+
}
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
# Commands that are handled directly in ChatInput (don't need screen)
|
|
99
|
+
_IMMEDIATE_COMMANDS = {
|
|
100
|
+
"clear",
|
|
101
|
+
"c",
|
|
102
|
+
"cls",
|
|
103
|
+
"help",
|
|
104
|
+
"h",
|
|
105
|
+
"?",
|
|
106
|
+
"copy",
|
|
107
|
+
"cp",
|
|
108
|
+
"y",
|
|
109
|
+
"markdown",
|
|
110
|
+
"md",
|
|
111
|
+
"plain", # Render mode toggle
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
def __init__(
|
|
115
|
+
self,
|
|
116
|
+
store: ChatStore,
|
|
117
|
+
injection_service: InjectionService | None = None,
|
|
118
|
+
) -> None:
|
|
119
|
+
"""Initialize the chat input."""
|
|
120
|
+
super().__init__()
|
|
121
|
+
self._store = store
|
|
122
|
+
self._injection_service = injection_service
|
|
123
|
+
self._registry = get_default_registry()
|
|
124
|
+
self._unsubscribe: Callable[[], None] | None = None
|
|
125
|
+
# Track current injection trigger for replacement
|
|
126
|
+
self._current_trigger_start: int = 0
|
|
127
|
+
self._current_trigger_end: int = 0
|
|
128
|
+
# Track if popup was dismissed to prevent re-triggering
|
|
129
|
+
self._popup_dismissed_text: str = ""
|
|
130
|
+
|
|
131
|
+
def compose(self) -> ComposeResult:
|
|
132
|
+
"""Compose the input layout."""
|
|
133
|
+
with Vertical(id="input-container"):
|
|
134
|
+
yield ChatTextArea(id="chat-input")
|
|
135
|
+
|
|
136
|
+
def on_mount(self) -> None:
|
|
137
|
+
"""Subscribe to store on mount."""
|
|
138
|
+
self._unsubscribe = self._store.subscribe(self._on_state_change)
|
|
139
|
+
|
|
140
|
+
def on_unmount(self) -> None:
|
|
141
|
+
"""Unsubscribe from store on unmount."""
|
|
142
|
+
if self._unsubscribe:
|
|
143
|
+
self._unsubscribe()
|
|
144
|
+
|
|
145
|
+
def _on_state_change(self, state: ChatState) -> None:
|
|
146
|
+
"""React to store state changes."""
|
|
147
|
+
# State changes are handled by store subscribers
|
|
148
|
+
# No local status bar to update anymore
|
|
149
|
+
pass
|
|
150
|
+
|
|
151
|
+
def _get_popup(self) -> InjectionSuggestionPopup | None:
|
|
152
|
+
"""Get the injection popup from the parent ChatPane."""
|
|
153
|
+
try:
|
|
154
|
+
return self.screen.query_one("#injection-popup", InjectionSuggestionPopup)
|
|
155
|
+
except Exception:
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
def on_key(self, event: Key) -> None:
|
|
159
|
+
"""Intercept keys when popup is visible for navigation."""
|
|
160
|
+
popup = self._get_popup()
|
|
161
|
+
if not popup or not popup.is_visible:
|
|
162
|
+
return # Let key propagate normally
|
|
163
|
+
|
|
164
|
+
# Handle popup navigation keys
|
|
165
|
+
if event.key == "up":
|
|
166
|
+
event.stop()
|
|
167
|
+
popup.highlight_previous()
|
|
168
|
+
elif event.key == "down":
|
|
169
|
+
event.stop()
|
|
170
|
+
popup.highlight_next()
|
|
171
|
+
elif event.key == "tab" or event.key == "enter":
|
|
172
|
+
event.stop()
|
|
173
|
+
self._accept_suggestion(popup)
|
|
174
|
+
elif event.key == "escape":
|
|
175
|
+
event.stop()
|
|
176
|
+
self._dismiss_popup(popup)
|
|
177
|
+
|
|
178
|
+
def _accept_suggestion(self, popup: InjectionSuggestionPopup) -> None:
|
|
179
|
+
"""Accept the currently highlighted suggestion."""
|
|
180
|
+
completion = popup.get_selected()
|
|
181
|
+
if not completion:
|
|
182
|
+
self._dismiss_popup(popup)
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
text_area = self.query_one("#chat-input", ChatTextArea)
|
|
186
|
+
current_text = text_area.text
|
|
187
|
+
|
|
188
|
+
# Replace the partial injection with the completed one
|
|
189
|
+
new_text = (
|
|
190
|
+
current_text[: self._current_trigger_start]
|
|
191
|
+
+ completion.text
|
|
192
|
+
+ current_text[self._current_trigger_end :]
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
# Update text area
|
|
196
|
+
text_area.text = new_text
|
|
197
|
+
|
|
198
|
+
# Move cursor to end of inserted text
|
|
199
|
+
new_cursor_pos = self._current_trigger_start + len(completion.text)
|
|
200
|
+
lines = new_text.split("\n")
|
|
201
|
+
row = 0
|
|
202
|
+
remaining = new_cursor_pos
|
|
203
|
+
for i, line in enumerate(lines):
|
|
204
|
+
if remaining <= len(line):
|
|
205
|
+
row = i
|
|
206
|
+
break
|
|
207
|
+
remaining -= len(line) + 1
|
|
208
|
+
row = i + 1
|
|
209
|
+
|
|
210
|
+
col = min(remaining, len(lines[row]) if row < len(lines) else 0)
|
|
211
|
+
text_area.move_cursor((row, col))
|
|
212
|
+
|
|
213
|
+
# Hide popup
|
|
214
|
+
popup.hide()
|
|
215
|
+
self._popup_dismissed_text = new_text # Prevent re-trigger
|
|
216
|
+
|
|
217
|
+
def _dismiss_popup(self, popup: InjectionSuggestionPopup) -> None:
|
|
218
|
+
"""Dismiss the popup without accepting."""
|
|
219
|
+
popup.hide()
|
|
220
|
+
text_area = self.query_one("#chat-input", ChatTextArea)
|
|
221
|
+
self._popup_dismissed_text = text_area.text # Prevent re-trigger on same text
|
|
222
|
+
|
|
223
|
+
@on(TextArea.Changed, "#chat-input")
|
|
224
|
+
def _on_text_changed(self, event: TextArea.Changed) -> None:
|
|
225
|
+
"""Check for injection triggers when text changes."""
|
|
226
|
+
text_area = event.text_area
|
|
227
|
+
text = text_area.text
|
|
228
|
+
|
|
229
|
+
# Scroll to keep cursor visible
|
|
230
|
+
text_area.call_after_refresh(
|
|
231
|
+
lambda: text_area.scroll_cursor_visible(animate=False)
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# If text changed significantly, allow popup to trigger again
|
|
235
|
+
if text != self._popup_dismissed_text:
|
|
236
|
+
self._popup_dismissed_text = ""
|
|
237
|
+
|
|
238
|
+
# Check for injection trigger
|
|
239
|
+
if not self._injection_service:
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
# Get cursor position
|
|
243
|
+
cursor_location = text_area.cursor_location
|
|
244
|
+
lines = text.split("\n")
|
|
245
|
+
cursor_pos = sum(len(lines[i]) + 1 for i in range(cursor_location[0]))
|
|
246
|
+
cursor_pos += cursor_location[1]
|
|
247
|
+
|
|
248
|
+
partial = detect_partial_injection(text, cursor_pos)
|
|
249
|
+
popup = self._get_popup()
|
|
250
|
+
|
|
251
|
+
if partial and text != self._popup_dismissed_text:
|
|
252
|
+
# Store trigger position for replacement
|
|
253
|
+
self._current_trigger_start = partial.start
|
|
254
|
+
self._current_trigger_end = partial.end
|
|
255
|
+
|
|
256
|
+
# Get completions
|
|
257
|
+
completions = self._injection_service.get_completions(
|
|
258
|
+
partial.injection_type,
|
|
259
|
+
partial.partial_identifier,
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# Show popup
|
|
263
|
+
if popup:
|
|
264
|
+
popup.show_suggestions(
|
|
265
|
+
completions,
|
|
266
|
+
partial.injection_type,
|
|
267
|
+
filter_text=partial.partial_identifier,
|
|
268
|
+
)
|
|
269
|
+
elif popup and popup.is_visible:
|
|
270
|
+
# No trigger detected, hide popup if visible
|
|
271
|
+
popup.hide()
|
|
272
|
+
|
|
273
|
+
def on_send_message_requested(self, event: SendMessageRequested) -> None:
|
|
274
|
+
"""Handle send request from TextArea - check for slash commands first."""
|
|
275
|
+
content = event.content
|
|
276
|
+
|
|
277
|
+
# Hide popup if visible
|
|
278
|
+
popup = self._get_popup()
|
|
279
|
+
if popup and popup.is_visible:
|
|
280
|
+
popup.hide()
|
|
281
|
+
|
|
282
|
+
# Check for slash command
|
|
283
|
+
parse_result = self._registry.parse(content)
|
|
284
|
+
|
|
285
|
+
if parse_result.is_command:
|
|
286
|
+
event.stop()
|
|
287
|
+
text_area = self.query_one("#chat-input", ChatTextArea)
|
|
288
|
+
text_area.clear()
|
|
289
|
+
|
|
290
|
+
if parse_result.command is None:
|
|
291
|
+
self.post_message(UnknownSlashCommand(content))
|
|
292
|
+
return
|
|
293
|
+
|
|
294
|
+
cmd_name = parse_result.command.name
|
|
295
|
+
is_immediate = cmd_name in self._IMMEDIATE_COMMANDS
|
|
296
|
+
logger.debug(
|
|
297
|
+
"slash_command_parsed",
|
|
298
|
+
command=cmd_name,
|
|
299
|
+
is_immediate=is_immediate,
|
|
300
|
+
args=parse_result.args or None,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
if is_immediate:
|
|
304
|
+
context = SlashContext(store=self._store, app=self.app)
|
|
305
|
+
parse_result.command.execute(parse_result.args, context)
|
|
306
|
+
logger.debug("slash_command_executed_immediate", command=cmd_name)
|
|
307
|
+
else:
|
|
308
|
+
self.post_message(
|
|
309
|
+
SlashCommandExecuted(parse_result.command.name, parse_result.args)
|
|
310
|
+
)
|
|
311
|
+
return
|
|
312
|
+
|
|
313
|
+
# Regular message - validate and process
|
|
314
|
+
state = self._store.state
|
|
315
|
+
session = state.session
|
|
316
|
+
|
|
317
|
+
if state.phase != ChatPhase.IDLE:
|
|
318
|
+
self.app.notify("Please wait for the current operation", severity="warning")
|
|
319
|
+
event.stop()
|
|
320
|
+
return
|
|
321
|
+
|
|
322
|
+
if not session:
|
|
323
|
+
self.app.notify(
|
|
324
|
+
"No active session. Press Ctrl+N to create one.", severity="warning"
|
|
325
|
+
)
|
|
326
|
+
event.stop()
|
|
327
|
+
return
|
|
328
|
+
|
|
329
|
+
if not session.is_agent_mode and not session.is_model_mode:
|
|
330
|
+
self.app.notify(
|
|
331
|
+
"No agent or model configured for this session", severity="warning"
|
|
332
|
+
)
|
|
333
|
+
event.stop()
|
|
334
|
+
return
|
|
335
|
+
|
|
336
|
+
if not content:
|
|
337
|
+
event.stop()
|
|
338
|
+
return
|
|
339
|
+
|
|
340
|
+
# Clear input and let message bubble to parent
|
|
341
|
+
text_area = self.query_one("#chat-input", ChatTextArea)
|
|
342
|
+
text_area.clear()
|
|
343
|
+
|
|
344
|
+
def focus_input(self) -> None:
|
|
345
|
+
"""Focus the text input."""
|
|
346
|
+
self.query_one("#chat-input", ChatTextArea).focus()
|
|
347
|
+
|
|
348
|
+
def clear_input(self) -> None:
|
|
349
|
+
"""Clear the text input."""
|
|
350
|
+
self.query_one("#chat-input", ChatTextArea).clear()
|
|
351
|
+
|
|
352
|
+
def get_text(self) -> str:
|
|
353
|
+
"""Get the current text content."""
|
|
354
|
+
return self.query_one("#chat-input", ChatTextArea).text
|
|
355
|
+
|
|
356
|
+
def set_injection_service(self, service: InjectionService) -> None:
|
|
357
|
+
"""Set the injection service for autocomplete."""
|
|
358
|
+
self._injection_service = service
|
|
359
|
+
|
|
360
|
+
def has_injections(self) -> bool:
|
|
361
|
+
"""Check if current text contains injection patterns."""
|
|
362
|
+
return has_injections(self.get_text())
|