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
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Popup widget for injection autocomplete suggestions.
|
|
2
|
+
|
|
3
|
+
This is a "dumb" display widget - it shows suggestions and highlights one,
|
|
4
|
+
but all keyboard handling is done by ChatInput which controls this popup.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from textual.app import ComposeResult
|
|
8
|
+
from textual.containers import Vertical
|
|
9
|
+
from textual.widget import Widget
|
|
10
|
+
from textual.widgets import OptionList, Static
|
|
11
|
+
from textual.widgets.option_list import Option
|
|
12
|
+
|
|
13
|
+
from cli.tui.chat.services.injection import InjectionCompletion
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class InjectionSuggestionPopup(Widget):
|
|
17
|
+
"""Display-only popup for autocomplete suggestions.
|
|
18
|
+
|
|
19
|
+
This widget is controlled entirely by ChatInput:
|
|
20
|
+
- ChatInput calls show_suggestions() to display options
|
|
21
|
+
- ChatInput calls highlight_next/previous() to change selection
|
|
22
|
+
- ChatInput calls get_selected() when user accepts
|
|
23
|
+
- ChatInput calls hide() to close
|
|
24
|
+
|
|
25
|
+
The popup never takes focus - input stays in the text area.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
DEFAULT_CSS = """
|
|
29
|
+
InjectionSuggestionPopup {
|
|
30
|
+
display: none;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
InjectionSuggestionPopup.visible {
|
|
34
|
+
display: block;
|
|
35
|
+
}
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
# This widget should never take focus
|
|
39
|
+
can_focus = False
|
|
40
|
+
|
|
41
|
+
def __init__(self, id: str | None = None) -> None:
|
|
42
|
+
super().__init__(id=id)
|
|
43
|
+
self._suggestions: list[InjectionCompletion] = []
|
|
44
|
+
self._injection_type: str = ""
|
|
45
|
+
self._filter_text: str = ""
|
|
46
|
+
self._highlighted_index: int = 0
|
|
47
|
+
|
|
48
|
+
def compose(self) -> ComposeResult:
|
|
49
|
+
"""Compose the popup layout."""
|
|
50
|
+
with Vertical():
|
|
51
|
+
yield Static("@fragment()", id="popup-header")
|
|
52
|
+
yield Static(
|
|
53
|
+
"[dim]Type to filter • Tab accept • ↑↓ navigate • Esc dismiss[/]",
|
|
54
|
+
id="popup-hint",
|
|
55
|
+
)
|
|
56
|
+
yield OptionList(id="popup-list")
|
|
57
|
+
yield Static("No matches found", id="empty-hint")
|
|
58
|
+
|
|
59
|
+
def on_mount(self) -> None:
|
|
60
|
+
"""Initialize popup state."""
|
|
61
|
+
self.query_one("#empty-hint", Static).display = False
|
|
62
|
+
# Disable focus on the option list too
|
|
63
|
+
self.query_one("#popup-list", OptionList).can_focus = False
|
|
64
|
+
|
|
65
|
+
def show_suggestions(
|
|
66
|
+
self,
|
|
67
|
+
suggestions: list[InjectionCompletion],
|
|
68
|
+
injection_type: str,
|
|
69
|
+
filter_text: str = "",
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Display suggestions in the popup.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
suggestions: List of completion suggestions.
|
|
75
|
+
injection_type: Type of injection (fragment, text, json, schema).
|
|
76
|
+
filter_text: Current filter/partial identifier typed by user.
|
|
77
|
+
"""
|
|
78
|
+
self._suggestions = suggestions
|
|
79
|
+
self._injection_type = injection_type
|
|
80
|
+
self._filter_text = filter_text
|
|
81
|
+
self._highlighted_index = 0
|
|
82
|
+
|
|
83
|
+
# Update header to show current input state
|
|
84
|
+
header = self.query_one("#popup-header", Static)
|
|
85
|
+
if filter_text:
|
|
86
|
+
header.update(f"@{injection_type}([bold]{filter_text}[/bold])")
|
|
87
|
+
else:
|
|
88
|
+
header.update(f"@{injection_type}()")
|
|
89
|
+
|
|
90
|
+
# Update option list
|
|
91
|
+
option_list = self.query_one("#popup-list", OptionList)
|
|
92
|
+
option_list.clear_options()
|
|
93
|
+
|
|
94
|
+
empty_hint = self.query_one("#empty-hint", Static)
|
|
95
|
+
|
|
96
|
+
if suggestions:
|
|
97
|
+
for i, suggestion in enumerate(suggestions):
|
|
98
|
+
label = (
|
|
99
|
+
f"{suggestion.display_name} [dim]{suggestion.description}[/dim]"
|
|
100
|
+
)
|
|
101
|
+
option_list.add_option(Option(label, id=str(i)))
|
|
102
|
+
|
|
103
|
+
option_list.display = True
|
|
104
|
+
empty_hint.display = False
|
|
105
|
+
option_list.highlighted = 0
|
|
106
|
+
else:
|
|
107
|
+
option_list.display = False
|
|
108
|
+
empty_hint.display = True
|
|
109
|
+
if filter_text:
|
|
110
|
+
empty_hint.update(f"No matches for '{filter_text}'")
|
|
111
|
+
else:
|
|
112
|
+
empty_hint.update("No items available")
|
|
113
|
+
|
|
114
|
+
self.add_class("visible")
|
|
115
|
+
|
|
116
|
+
def hide(self) -> None:
|
|
117
|
+
"""Hide the popup."""
|
|
118
|
+
self.remove_class("visible")
|
|
119
|
+
self._suggestions = []
|
|
120
|
+
self._filter_text = ""
|
|
121
|
+
self._highlighted_index = 0
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def is_visible(self) -> bool:
|
|
125
|
+
"""Check if popup is currently visible."""
|
|
126
|
+
return self.has_class("visible")
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def has_suggestions(self) -> bool:
|
|
130
|
+
"""Check if there are any suggestions to select."""
|
|
131
|
+
return bool(self._suggestions)
|
|
132
|
+
|
|
133
|
+
def highlight_next(self) -> None:
|
|
134
|
+
"""Move highlight to next suggestion."""
|
|
135
|
+
if not self._suggestions:
|
|
136
|
+
return
|
|
137
|
+
if self._highlighted_index < len(self._suggestions) - 1:
|
|
138
|
+
self._highlighted_index += 1
|
|
139
|
+
option_list = self.query_one("#popup-list", OptionList)
|
|
140
|
+
option_list.highlighted = self._highlighted_index
|
|
141
|
+
|
|
142
|
+
def highlight_previous(self) -> None:
|
|
143
|
+
"""Move highlight to previous suggestion."""
|
|
144
|
+
if not self._suggestions:
|
|
145
|
+
return
|
|
146
|
+
if self._highlighted_index > 0:
|
|
147
|
+
self._highlighted_index -= 1
|
|
148
|
+
option_list = self.query_one("#popup-list", OptionList)
|
|
149
|
+
option_list.highlighted = self._highlighted_index
|
|
150
|
+
|
|
151
|
+
def get_selected(self) -> InjectionCompletion | None:
|
|
152
|
+
"""Get the currently highlighted suggestion.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
The selected completion, or None if no suggestions.
|
|
156
|
+
"""
|
|
157
|
+
if not self._suggestions:
|
|
158
|
+
return None
|
|
159
|
+
if 0 <= self._highlighted_index < len(self._suggestions):
|
|
160
|
+
return self._suggestions[self._highlighted_index]
|
|
161
|
+
return None
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
"""Message display widget for rendering chat message history."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Callable
|
|
4
|
+
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.binding import Binding
|
|
7
|
+
from textual.containers import VerticalScroll
|
|
8
|
+
from textual.widget import Widget
|
|
9
|
+
from textual.widgets import Static
|
|
10
|
+
|
|
11
|
+
from cli.tui.chat.messages import CopyToClipboardRequested
|
|
12
|
+
from cli.tui.chat.renderers.markdown import MarkdownRenderer
|
|
13
|
+
from cli.tui.chat.renderers.plain import PlainTextRenderer
|
|
14
|
+
from cli.tui.chat.store import ChatStore
|
|
15
|
+
from cli.tui.chat.types import (
|
|
16
|
+
ChatPhase,
|
|
17
|
+
ChatState,
|
|
18
|
+
MessageRenderer,
|
|
19
|
+
RenderMode,
|
|
20
|
+
extract_message_text,
|
|
21
|
+
)
|
|
22
|
+
from cli.tui.chat.widgets.welcome_screen import WelcomeScreen
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING:
|
|
25
|
+
from cli.tui.app import AlloyRuntimeApp
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class MessageDisplay(Widget):
|
|
29
|
+
"""Widget for displaying chat message history.
|
|
30
|
+
|
|
31
|
+
Features:
|
|
32
|
+
- Renders message history with timestamps
|
|
33
|
+
- Shows streaming content in real-time
|
|
34
|
+
- Auto-scrolls to bottom on new content (unless user scrolls up)
|
|
35
|
+
- Supports copying last response
|
|
36
|
+
- Pluggable renderer for different output formats
|
|
37
|
+
|
|
38
|
+
Posts Messages:
|
|
39
|
+
- CopyToClipboardRequested: When user triggers copy action
|
|
40
|
+
|
|
41
|
+
Auto-scroll behavior:
|
|
42
|
+
- During streaming, auto-scrolls to bottom unless user has scrolled up
|
|
43
|
+
- User scrolling up during streaming disables auto-scroll
|
|
44
|
+
- User scrolling back to bottom (or pressing G) re-enables auto-scroll
|
|
45
|
+
- Auto-scroll is always re-enabled when streaming ends
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
app: "AlloyRuntimeApp"
|
|
49
|
+
|
|
50
|
+
BINDINGS = [
|
|
51
|
+
Binding("ctrl+y", "copy_last", "Copy Last", show=False),
|
|
52
|
+
Binding("ctrl+shift+y", "copy_all", "Copy All", show=False),
|
|
53
|
+
Binding("g", "scroll_top", "Top", show=False),
|
|
54
|
+
Binding("G", "scroll_bottom", "Bottom", show=False),
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
store: ChatStore,
|
|
60
|
+
renderer: MessageRenderer | None = None,
|
|
61
|
+
) -> None:
|
|
62
|
+
"""Initialize the message display.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
store: ChatStore instance for state management.
|
|
66
|
+
renderer: Optional renderer for message formatting. Defaults to MarkdownRenderer.
|
|
67
|
+
"""
|
|
68
|
+
super().__init__()
|
|
69
|
+
self._store = store
|
|
70
|
+
self._custom_renderer: MessageRenderer | None = renderer
|
|
71
|
+
self._markdown_renderer = MarkdownRenderer()
|
|
72
|
+
self._plain_renderer = PlainTextRenderer()
|
|
73
|
+
self._unsubscribe: Callable[[], None] | None = None
|
|
74
|
+
# Auto-scroll is enabled by default; disabled when user scrolls up during streaming
|
|
75
|
+
self._auto_scroll_enabled = True
|
|
76
|
+
# Track whether welcome screen is currently shown
|
|
77
|
+
self._welcome_screen_visible = False
|
|
78
|
+
|
|
79
|
+
def _get_renderer(self, state: ChatState) -> MessageRenderer:
|
|
80
|
+
"""Get the appropriate renderer based on state."""
|
|
81
|
+
if self._custom_renderer:
|
|
82
|
+
return self._custom_renderer
|
|
83
|
+
if state.render_mode == RenderMode.PLAIN:
|
|
84
|
+
return self._plain_renderer
|
|
85
|
+
return self._markdown_renderer
|
|
86
|
+
|
|
87
|
+
def compose(self) -> ComposeResult:
|
|
88
|
+
"""Compose the message display layout."""
|
|
89
|
+
with VerticalScroll(id="message-scroll"):
|
|
90
|
+
# Start with welcome screen visible, messages hidden
|
|
91
|
+
yield WelcomeScreen(id="welcome-screen")
|
|
92
|
+
messages_widget = Static("", id="messages")
|
|
93
|
+
messages_widget.display = False
|
|
94
|
+
yield messages_widget
|
|
95
|
+
self._welcome_screen_visible = True
|
|
96
|
+
|
|
97
|
+
def on_mount(self) -> None:
|
|
98
|
+
"""Subscribe to store on mount."""
|
|
99
|
+
self._unsubscribe = self._store.subscribe(self._on_state_change)
|
|
100
|
+
|
|
101
|
+
def on_unmount(self) -> None:
|
|
102
|
+
"""Unsubscribe from store on unmount."""
|
|
103
|
+
if self._unsubscribe:
|
|
104
|
+
self._unsubscribe()
|
|
105
|
+
|
|
106
|
+
def _on_state_change(self, state: ChatState) -> None:
|
|
107
|
+
"""React to store state changes."""
|
|
108
|
+
self._render_messages(state)
|
|
109
|
+
|
|
110
|
+
def _is_at_bottom(self) -> bool:
|
|
111
|
+
"""Check if scroll is at or near the bottom."""
|
|
112
|
+
scroll = self.query_one("#message-scroll", VerticalScroll)
|
|
113
|
+
# Consider "at bottom" if within 50 pixels of the end
|
|
114
|
+
# Also consider at bottom if max_scroll is 0 (content fits in view)
|
|
115
|
+
return scroll.max_scroll_y <= 0 or scroll.scroll_y >= scroll.max_scroll_y - 50
|
|
116
|
+
|
|
117
|
+
def _render_messages(self, state: ChatState) -> None:
|
|
118
|
+
"""Render messages to the display using the configured renderer."""
|
|
119
|
+
messages_widget = self.query_one("#messages", Static)
|
|
120
|
+
messages = state.messages
|
|
121
|
+
streaming_content = state.streaming_content
|
|
122
|
+
streaming_thinking_content = state.streaming_thinking_content
|
|
123
|
+
pending_user_message = state.pending_user_message
|
|
124
|
+
is_streaming = state.phase == ChatPhase.STREAMING
|
|
125
|
+
|
|
126
|
+
# Check scroll position BEFORE rendering to detect user scroll-up
|
|
127
|
+
# If streaming and not at bottom, user must have scrolled up
|
|
128
|
+
if is_streaming and not self._is_at_bottom():
|
|
129
|
+
self._auto_scroll_enabled = False
|
|
130
|
+
|
|
131
|
+
# Get assistant name from session context
|
|
132
|
+
assistant_name = "Assistant"
|
|
133
|
+
if state.session:
|
|
134
|
+
assistant_name = state.session.display_name
|
|
135
|
+
|
|
136
|
+
# Check if we have anything to show
|
|
137
|
+
has_content = (
|
|
138
|
+
messages
|
|
139
|
+
or streaming_content
|
|
140
|
+
or streaming_thinking_content
|
|
141
|
+
or pending_user_message
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Determine if welcome screen should be visible
|
|
145
|
+
should_show_welcome = state.session is None and not has_content
|
|
146
|
+
|
|
147
|
+
# Switch between welcome screen and messages display
|
|
148
|
+
if should_show_welcome != self._welcome_screen_visible:
|
|
149
|
+
self._toggle_welcome_screen(should_show_welcome)
|
|
150
|
+
|
|
151
|
+
if should_show_welcome:
|
|
152
|
+
# Welcome screen handles its own content
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
if not has_content:
|
|
156
|
+
# Session exists but no messages yet
|
|
157
|
+
messages_widget.update(
|
|
158
|
+
"[dim]No messages yet. Type below and press Ctrl+D to send.[/]"
|
|
159
|
+
)
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
# Delegate rendering to the appropriate renderer based on state
|
|
163
|
+
renderer = self._get_renderer(state)
|
|
164
|
+
rendered_content = renderer.render(
|
|
165
|
+
messages,
|
|
166
|
+
streaming_content=streaming_content,
|
|
167
|
+
streaming_thinking_content=streaming_thinking_content,
|
|
168
|
+
pending_user_message=pending_user_message,
|
|
169
|
+
assistant_name=assistant_name,
|
|
170
|
+
is_streaming=is_streaming,
|
|
171
|
+
thinking_collapsed=state.thinking_collapsed,
|
|
172
|
+
)
|
|
173
|
+
messages_widget.update(rendered_content)
|
|
174
|
+
|
|
175
|
+
# Auto-scroll if enabled
|
|
176
|
+
if self._auto_scroll_enabled:
|
|
177
|
+
self._scroll_to_bottom()
|
|
178
|
+
|
|
179
|
+
# Re-enable auto-scroll when streaming ends
|
|
180
|
+
if not is_streaming:
|
|
181
|
+
self._auto_scroll_enabled = True
|
|
182
|
+
|
|
183
|
+
def _scroll_to_bottom(self) -> None:
|
|
184
|
+
"""Scroll the message view to the bottom."""
|
|
185
|
+
scroll = self.query_one("#message-scroll", VerticalScroll)
|
|
186
|
+
scroll.scroll_end(animate=False)
|
|
187
|
+
|
|
188
|
+
def _toggle_welcome_screen(self, show_welcome: bool) -> None:
|
|
189
|
+
"""Toggle between welcome screen and messages display.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
show_welcome: If True, show welcome screen; if False, show messages.
|
|
193
|
+
"""
|
|
194
|
+
try:
|
|
195
|
+
welcome_screen = self.query_one("#welcome-screen", WelcomeScreen)
|
|
196
|
+
messages_widget = self.query_one("#messages", Static)
|
|
197
|
+
|
|
198
|
+
welcome_screen.display = show_welcome
|
|
199
|
+
messages_widget.display = not show_welcome
|
|
200
|
+
self._welcome_screen_visible = show_welcome
|
|
201
|
+
|
|
202
|
+
# Refresh welcome screen data when showing it
|
|
203
|
+
if show_welcome:
|
|
204
|
+
welcome_screen.refresh_data()
|
|
205
|
+
except Exception:
|
|
206
|
+
# Widgets may not be mounted yet during initial compose
|
|
207
|
+
pass
|
|
208
|
+
|
|
209
|
+
def _get_welcome_message(self) -> str:
|
|
210
|
+
"""Get the welcome message for new sessions."""
|
|
211
|
+
return """[bold]Welcome to Alloy Runtime![/]
|
|
212
|
+
|
|
213
|
+
[dim]Select a session from the sidebar or press Ctrl+N to start a new chat.[/]
|
|
214
|
+
|
|
215
|
+
[bold cyan]Features:[/]
|
|
216
|
+
- Browse and resume previous chat sessions
|
|
217
|
+
- Start new conversations with agents or models
|
|
218
|
+
- Stream responses in real-time
|
|
219
|
+
- Compose messages in your $EDITOR
|
|
220
|
+
|
|
221
|
+
[bold cyan]Keyboard shortcuts:[/]
|
|
222
|
+
Ctrl+D Send message
|
|
223
|
+
Ctrl+N New chat session
|
|
224
|
+
Ctrl+E Open message in $EDITOR
|
|
225
|
+
Ctrl+B Toggle sidebar
|
|
226
|
+
Ctrl+R Regenerate last response
|
|
227
|
+
Ctrl+U Undo last turn
|
|
228
|
+
Ctrl+Y Copy last response
|
|
229
|
+
Ctrl+Shift+Y Copy all messages
|
|
230
|
+
/ Search sessions
|
|
231
|
+
|
|
232
|
+
[bold cyan]Tip:[/] Hold Shift while clicking to use native terminal text selection.
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
def action_copy_last(self) -> None:
|
|
236
|
+
"""Copy the last assistant response to clipboard."""
|
|
237
|
+
messages = self._store.state.messages
|
|
238
|
+
|
|
239
|
+
if not messages:
|
|
240
|
+
self.app.notify("No messages to copy", severity="warning")
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
# Find last assistant message
|
|
244
|
+
for msg in reversed(messages):
|
|
245
|
+
if msg.role == "assistant":
|
|
246
|
+
text = extract_message_text(msg)
|
|
247
|
+
if text:
|
|
248
|
+
self.post_message(CopyToClipboardRequested(text, "Last response"))
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
self.app.notify("No assistant message to copy", severity="warning")
|
|
252
|
+
|
|
253
|
+
def action_copy_all(self) -> None:
|
|
254
|
+
"""Copy all messages to clipboard as plain text."""
|
|
255
|
+
messages = self._store.state.messages
|
|
256
|
+
|
|
257
|
+
if not messages:
|
|
258
|
+
self.app.notify("No messages to copy", severity="warning")
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
# Build plain text version of all messages
|
|
262
|
+
lines: list[str] = []
|
|
263
|
+
for msg in messages:
|
|
264
|
+
role = msg.role.capitalize()
|
|
265
|
+
text = extract_message_text(msg)
|
|
266
|
+
if text:
|
|
267
|
+
lines.append(f"{role}:\n{text}\n")
|
|
268
|
+
|
|
269
|
+
if lines:
|
|
270
|
+
full_text = "\n".join(lines)
|
|
271
|
+
self.post_message(CopyToClipboardRequested(full_text, "All messages"))
|
|
272
|
+
else:
|
|
273
|
+
self.app.notify("No message content to copy", severity="warning")
|
|
274
|
+
|
|
275
|
+
def action_scroll_top(self) -> None:
|
|
276
|
+
"""Scroll to the top of messages."""
|
|
277
|
+
scroll = self.query_one("#message-scroll", VerticalScroll)
|
|
278
|
+
scroll.scroll_home(animate=False)
|
|
279
|
+
|
|
280
|
+
def action_scroll_bottom(self) -> None:
|
|
281
|
+
"""Scroll to the bottom of messages and resume auto-scroll."""
|
|
282
|
+
self._auto_scroll_enabled = True
|
|
283
|
+
self._scroll_to_bottom()
|
|
284
|
+
|
|
285
|
+
def show_welcome(self) -> None:
|
|
286
|
+
"""Show the welcome screen."""
|
|
287
|
+
self._toggle_welcome_screen(show_welcome=True)
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Session sidebar widget for listing and searching chat sessions."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Callable
|
|
4
|
+
|
|
5
|
+
from textual import on, work
|
|
6
|
+
from textual.app import ComposeResult
|
|
7
|
+
from textual.containers import Horizontal, Vertical
|
|
8
|
+
from textual.widget import Widget
|
|
9
|
+
from textual.widgets import Checkbox, DataTable, Input, Static
|
|
10
|
+
|
|
11
|
+
from alloy_runtime_sdk.api_client.client import ApiClient
|
|
12
|
+
from alloy_runtime_types.dtos.sessions import SessionSummary
|
|
13
|
+
from alloy_runtime_sdk.logging.config import get_logger
|
|
14
|
+
|
|
15
|
+
from cli.tui.chat.messages import SessionSelected, SessionsLoaded, SessionsLoadError
|
|
16
|
+
from cli.tui.chat.store import ChatStore
|
|
17
|
+
from cli.tui.chat.types import ChatPhase, ChatState, format_session_time
|
|
18
|
+
|
|
19
|
+
logger = get_logger(__name__)
|
|
20
|
+
|
|
21
|
+
if TYPE_CHECKING:
|
|
22
|
+
from cli.tui.app import AlloyRuntimeApp
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class SessionSidebar(Widget):
|
|
26
|
+
"""Sidebar widget showing session list with search.
|
|
27
|
+
|
|
28
|
+
Features:
|
|
29
|
+
- Search sessions with debounced API calls
|
|
30
|
+
- Display session list in a DataTable
|
|
31
|
+
- Select sessions to load their messages
|
|
32
|
+
- Toggle between named-only and all sessions
|
|
33
|
+
|
|
34
|
+
Posts Messages:
|
|
35
|
+
- SessionSelected: When user selects a session
|
|
36
|
+
- SessionsLoaded: After successfully loading sessions
|
|
37
|
+
- SessionsLoadError: When session loading fails
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
app: "AlloyRuntimeApp"
|
|
41
|
+
|
|
42
|
+
# No bindings on widget - parent Screen handles all keybindings
|
|
43
|
+
# and delegates to public methods on this widget
|
|
44
|
+
|
|
45
|
+
DEFAULT_CSS = """
|
|
46
|
+
SessionSidebar #show-untitled-row {
|
|
47
|
+
height: auto;
|
|
48
|
+
padding: 0 1;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
SessionSidebar #show-untitled {
|
|
52
|
+
height: auto;
|
|
53
|
+
min-height: 1;
|
|
54
|
+
padding: 0;
|
|
55
|
+
margin: 0;
|
|
56
|
+
}
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, store: ChatStore) -> None:
|
|
60
|
+
"""Initialize the session sidebar.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
store: ChatStore instance for state management.
|
|
64
|
+
"""
|
|
65
|
+
super().__init__()
|
|
66
|
+
self._store = store
|
|
67
|
+
self._last_search = ""
|
|
68
|
+
self._show_untitled = False # Default: show only named sessions
|
|
69
|
+
self._unsubscribe: Callable[[], None] | None = None
|
|
70
|
+
# Track visibility to refresh when sidebar is opened
|
|
71
|
+
self._was_visible = False
|
|
72
|
+
# Guard against re-entrant state changes
|
|
73
|
+
self._refreshing = False
|
|
74
|
+
|
|
75
|
+
def compose(self) -> ComposeResult:
|
|
76
|
+
"""Compose the sidebar layout."""
|
|
77
|
+
with Vertical(id="sidebar-root"):
|
|
78
|
+
with Vertical(id="session-header"):
|
|
79
|
+
yield Input(
|
|
80
|
+
placeholder="Search sessions...",
|
|
81
|
+
id="session-search",
|
|
82
|
+
)
|
|
83
|
+
with Horizontal(id="show-untitled-row"):
|
|
84
|
+
yield Checkbox("Show untitled", id="show-untitled", value=False)
|
|
85
|
+
yield Static("Loading...", id="session-status")
|
|
86
|
+
yield DataTable(id="session-table", cursor_type="row")
|
|
87
|
+
|
|
88
|
+
def on_mount(self) -> None:
|
|
89
|
+
"""Initialize table and subscribe to store on mount."""
|
|
90
|
+
table = self.query_one("#session-table", DataTable[str])
|
|
91
|
+
table.add_columns("Session", "Time", "Msgs")
|
|
92
|
+
|
|
93
|
+
# Subscribe to store changes
|
|
94
|
+
self._unsubscribe = self._store.subscribe(self._on_state_change)
|
|
95
|
+
|
|
96
|
+
# Load initial sessions
|
|
97
|
+
self._load_sessions("")
|
|
98
|
+
|
|
99
|
+
def on_unmount(self) -> None:
|
|
100
|
+
"""Unsubscribe from store on unmount."""
|
|
101
|
+
if self._unsubscribe:
|
|
102
|
+
self._unsubscribe()
|
|
103
|
+
|
|
104
|
+
def _on_state_change(self, state: ChatState) -> None:
|
|
105
|
+
"""React to store state changes."""
|
|
106
|
+
# Handle sidebar visibility
|
|
107
|
+
self.display = state.sidebar_visible
|
|
108
|
+
|
|
109
|
+
# Refresh sessions when sidebar becomes visible (with guard against recursion)
|
|
110
|
+
becoming_visible = state.sidebar_visible and not self._was_visible
|
|
111
|
+
self._was_visible = state.sidebar_visible
|
|
112
|
+
|
|
113
|
+
if becoming_visible and not self._refreshing:
|
|
114
|
+
self._refreshing = True
|
|
115
|
+
self._load_sessions(self._last_search)
|
|
116
|
+
|
|
117
|
+
# Update table when sessions change
|
|
118
|
+
self._update_table(state.sessions)
|
|
119
|
+
|
|
120
|
+
# Update status based on phase
|
|
121
|
+
status = self.query_one("#session-status", Static)
|
|
122
|
+
if state.phase == ChatPhase.LOADING_SESSIONS:
|
|
123
|
+
status.update("Loading...")
|
|
124
|
+
elif state.sessions:
|
|
125
|
+
status.update(f"{len(state.sessions)} sessions")
|
|
126
|
+
else:
|
|
127
|
+
status.update("No sessions found")
|
|
128
|
+
|
|
129
|
+
def _update_table(self, sessions: tuple[SessionSummary, ...]) -> None:
|
|
130
|
+
"""Update the DataTable with session data."""
|
|
131
|
+
table = self.query_one("#session-table", DataTable[str])
|
|
132
|
+
table.clear()
|
|
133
|
+
|
|
134
|
+
# Populate table with sessions (filtering is done server-side via named_only)
|
|
135
|
+
for session in sessions:
|
|
136
|
+
name = session.name or "[dim]Untitled[/]"
|
|
137
|
+
if len(name) > 15:
|
|
138
|
+
name = name[:12] + "..."
|
|
139
|
+
table.add_row(
|
|
140
|
+
name,
|
|
141
|
+
format_session_time(session.last_activity_at),
|
|
142
|
+
str(session.message_count),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
@on(Input.Changed, "#session-search")
|
|
146
|
+
def on_search_changed(self, event: Input.Changed) -> None:
|
|
147
|
+
"""Handle search input changes with debouncing."""
|
|
148
|
+
query = event.value.strip()
|
|
149
|
+
if query != self._last_search:
|
|
150
|
+
self._last_search = query
|
|
151
|
+
self._store.set_session_search_query(query)
|
|
152
|
+
self._load_sessions(query)
|
|
153
|
+
|
|
154
|
+
@on(Checkbox.Changed, "#show-untitled")
|
|
155
|
+
def on_show_untitled_changed(self, event: Checkbox.Changed) -> None:
|
|
156
|
+
"""Handle show untitled checkbox toggle - triggers a reload from server."""
|
|
157
|
+
self._show_untitled = event.value
|
|
158
|
+
# Reload sessions from server with new filter setting
|
|
159
|
+
self._load_sessions(self._last_search)
|
|
160
|
+
|
|
161
|
+
@on(DataTable.RowSelected, "#session-table")
|
|
162
|
+
def on_row_selected(self, event: DataTable.RowSelected) -> None:
|
|
163
|
+
"""Handle session selection from table."""
|
|
164
|
+
row_index = event.cursor_row
|
|
165
|
+
sessions = self._store.state.sessions
|
|
166
|
+
if 0 <= row_index < len(sessions):
|
|
167
|
+
session = sessions[row_index]
|
|
168
|
+
logger.debug(
|
|
169
|
+
"sidebar_session_selected",
|
|
170
|
+
session_id=str(session.id),
|
|
171
|
+
session_name=session.name,
|
|
172
|
+
)
|
|
173
|
+
self.post_message(SessionSelected(session.id))
|
|
174
|
+
|
|
175
|
+
@work(exclusive=True)
|
|
176
|
+
async def _load_sessions(self, query: str) -> None:
|
|
177
|
+
"""Load sessions from API."""
|
|
178
|
+
logger.debug("sidebar_loading_sessions", query=query or None)
|
|
179
|
+
self._store.set_phase(ChatPhase.LOADING_SESSIONS)
|
|
180
|
+
status = self.query_one("#session-status", Static)
|
|
181
|
+
status.update("Loading...")
|
|
182
|
+
|
|
183
|
+
try:
|
|
184
|
+
client = await self._get_client()
|
|
185
|
+
response = await client.list_sessions(
|
|
186
|
+
search=query if query else None,
|
|
187
|
+
named_only=not self._show_untitled,
|
|
188
|
+
limit=50,
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
logger.debug("sidebar_sessions_loaded", count=len(response.sessions))
|
|
192
|
+
self._store.set_sessions(list(response.sessions))
|
|
193
|
+
self._store.set_phase(ChatPhase.IDLE)
|
|
194
|
+
self.post_message(SessionsLoaded(list(response.sessions)))
|
|
195
|
+
|
|
196
|
+
except Exception as e:
|
|
197
|
+
logger.warning("sidebar_sessions_load_error", error=str(e))
|
|
198
|
+
self._store.set_phase(ChatPhase.IDLE)
|
|
199
|
+
status.update("[red]Error[/]")
|
|
200
|
+
self.post_message(SessionsLoadError(str(e)))
|
|
201
|
+
finally:
|
|
202
|
+
self._refreshing = False
|
|
203
|
+
|
|
204
|
+
async def _get_client(self) -> ApiClient:
|
|
205
|
+
"""Get the shared server client."""
|
|
206
|
+
return await self.app.store.get_client()
|
|
207
|
+
|
|
208
|
+
def action_focus_search(self) -> None:
|
|
209
|
+
"""Focus the search input."""
|
|
210
|
+
self.query_one("#session-search", Input).focus()
|
|
211
|
+
|
|
212
|
+
def refresh_sessions(self) -> None:
|
|
213
|
+
"""Refresh the session list with current search query."""
|
|
214
|
+
self._load_sessions(self._last_search)
|