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,424 @@
|
|
|
1
|
+
"""Slash command system for the chat interface.
|
|
2
|
+
|
|
3
|
+
Provides a registry-based system for defining and executing slash commands
|
|
4
|
+
like /clear, /help, /copy, etc. Commands are parsed from user input and
|
|
5
|
+
executed with access to the chat context.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import TYPE_CHECKING, Protocol
|
|
10
|
+
|
|
11
|
+
from cli.tui.chat.store import ChatStore
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from cli.tui.app import AlloyRuntimeApp
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class SlashContext:
|
|
19
|
+
"""Context provided to slash commands during execution.
|
|
20
|
+
|
|
21
|
+
Contains all the resources a command might need to perform its action.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
store: ChatStore
|
|
25
|
+
app: "AlloyRuntimeApp"
|
|
26
|
+
|
|
27
|
+
def notify(self, message: str, severity: str = "information") -> None:
|
|
28
|
+
"""Notify the user with a message.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
message: The message to display.
|
|
32
|
+
severity: Notification severity (information, warning, error).
|
|
33
|
+
"""
|
|
34
|
+
self.app.notify(message, severity=severity) # type: ignore[arg-type]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class SlashCommand(Protocol):
|
|
38
|
+
"""Protocol for slash commands.
|
|
39
|
+
|
|
40
|
+
All slash commands must implement this protocol to be registered.
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def name(self) -> str:
|
|
45
|
+
"""Primary command name (without slash)."""
|
|
46
|
+
...
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def description(self) -> str:
|
|
50
|
+
"""Short description shown in /help."""
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def aliases(self) -> tuple[str, ...]:
|
|
55
|
+
"""Alternative names for the command."""
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
def execute(self, args: str, context: SlashContext) -> None:
|
|
59
|
+
"""Execute the command.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
args: Arguments passed after the command name.
|
|
63
|
+
context: Execution context with store, app, and notify.
|
|
64
|
+
"""
|
|
65
|
+
...
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class ParseResult:
|
|
70
|
+
"""Result of parsing user input for slash commands."""
|
|
71
|
+
|
|
72
|
+
is_command: bool
|
|
73
|
+
command: SlashCommand | None = None
|
|
74
|
+
args: str = ""
|
|
75
|
+
raw_input: str = ""
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class SlashCommandRegistry:
|
|
79
|
+
"""Registry of available slash commands.
|
|
80
|
+
|
|
81
|
+
Manages command registration, lookup by name/alias, and parsing.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self) -> None:
|
|
85
|
+
self._commands: dict[str, SlashCommand] = {}
|
|
86
|
+
self._all_commands: list[SlashCommand] = []
|
|
87
|
+
|
|
88
|
+
def register(self, command: SlashCommand) -> None:
|
|
89
|
+
"""Register a slash command.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
command: The command to register.
|
|
93
|
+
"""
|
|
94
|
+
self._commands[command.name] = command
|
|
95
|
+
for alias in command.aliases:
|
|
96
|
+
self._commands[alias] = command
|
|
97
|
+
self._all_commands.append(command)
|
|
98
|
+
|
|
99
|
+
def get(self, name: str) -> SlashCommand | None:
|
|
100
|
+
"""Get a command by name or alias.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
name: Command name (without slash).
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
The command if found, None otherwise.
|
|
107
|
+
"""
|
|
108
|
+
return self._commands.get(name.lower())
|
|
109
|
+
|
|
110
|
+
def list_commands(self) -> list[SlashCommand]:
|
|
111
|
+
"""Get all registered commands (no duplicates from aliases)."""
|
|
112
|
+
return list(self._all_commands)
|
|
113
|
+
|
|
114
|
+
def parse(self, text: str) -> ParseResult:
|
|
115
|
+
"""Parse user input to detect slash commands.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
text: Raw user input.
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
ParseResult indicating if input is a command and which one.
|
|
122
|
+
"""
|
|
123
|
+
text = text.strip()
|
|
124
|
+
|
|
125
|
+
if not text.startswith("/"):
|
|
126
|
+
return ParseResult(is_command=False, raw_input=text)
|
|
127
|
+
|
|
128
|
+
# Split into command and args
|
|
129
|
+
parts = text[1:].split(maxsplit=1)
|
|
130
|
+
name = parts[0].lower() if parts else ""
|
|
131
|
+
args = parts[1] if len(parts) > 1 else ""
|
|
132
|
+
|
|
133
|
+
command = self.get(name)
|
|
134
|
+
return ParseResult(
|
|
135
|
+
is_command=True,
|
|
136
|
+
command=command,
|
|
137
|
+
args=args,
|
|
138
|
+
raw_input=text,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# =============================================================================
|
|
143
|
+
# Built-in Commands
|
|
144
|
+
# =============================================================================
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@dataclass
|
|
148
|
+
class ClearCommand:
|
|
149
|
+
"""Clear the message display and show welcome."""
|
|
150
|
+
|
|
151
|
+
name: str = field(default="clear", init=False)
|
|
152
|
+
description: str = field(default="Clear the message display", init=False)
|
|
153
|
+
aliases: tuple[str, ...] = field(default=("c", "cls"), init=False)
|
|
154
|
+
|
|
155
|
+
def execute(self, args: str, context: SlashContext) -> None:
|
|
156
|
+
"""Clear display - args are ignored."""
|
|
157
|
+
del args # Unused
|
|
158
|
+
context.store.clear_session()
|
|
159
|
+
context.notify("Display cleared", "information")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass
|
|
163
|
+
class HelpCommand:
|
|
164
|
+
"""Show available slash commands."""
|
|
165
|
+
|
|
166
|
+
name: str = field(default="help", init=False)
|
|
167
|
+
description: str = field(default="Show available commands", init=False)
|
|
168
|
+
aliases: tuple[str, ...] = field(default=("h", "?"), init=False)
|
|
169
|
+
registry: SlashCommandRegistry | None = None
|
|
170
|
+
|
|
171
|
+
def execute(self, args: str, context: SlashContext) -> None:
|
|
172
|
+
"""Display help - args can filter to specific command."""
|
|
173
|
+
del args # Could be used for command-specific help later
|
|
174
|
+
|
|
175
|
+
if not self.registry:
|
|
176
|
+
context.notify("Help unavailable", "warning")
|
|
177
|
+
return
|
|
178
|
+
|
|
179
|
+
# Build help text
|
|
180
|
+
lines = ["[bold]Available Commands:[/]", ""]
|
|
181
|
+
for cmd in self.registry.list_commands():
|
|
182
|
+
alias_str = ""
|
|
183
|
+
if cmd.aliases:
|
|
184
|
+
alias_str = (
|
|
185
|
+
f" [dim](aliases: {', '.join('/' + a for a in cmd.aliases)})[/]"
|
|
186
|
+
)
|
|
187
|
+
lines.append(f" [cyan]/{cmd.name}[/] - {cmd.description}{alias_str}")
|
|
188
|
+
|
|
189
|
+
lines.append("")
|
|
190
|
+
lines.append(
|
|
191
|
+
"[dim]Keyboard shortcuts also available: Ctrl+N (new), Ctrl+R (regenerate), etc.[/]"
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Show as notification - in a real impl could show in a modal
|
|
195
|
+
context.notify("\n".join(lines), "information")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@dataclass
|
|
199
|
+
class CopyCommand:
|
|
200
|
+
"""Copy the last assistant response to clipboard."""
|
|
201
|
+
|
|
202
|
+
name: str = field(default="copy", init=False)
|
|
203
|
+
description: str = field(default="Copy last response to clipboard", init=False)
|
|
204
|
+
aliases: tuple[str, ...] = field(default=("cp", "y"), init=False)
|
|
205
|
+
|
|
206
|
+
def execute(self, args: str, context: SlashContext) -> None:
|
|
207
|
+
"""Copy last assistant message - args are ignored."""
|
|
208
|
+
del args # Unused
|
|
209
|
+
|
|
210
|
+
from cli.tui.chat.types import extract_message_text
|
|
211
|
+
|
|
212
|
+
messages = context.store.state.messages
|
|
213
|
+
if not messages:
|
|
214
|
+
context.notify("No messages to copy", "warning")
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
# Find last assistant message
|
|
218
|
+
for msg in reversed(messages):
|
|
219
|
+
if msg.role == "assistant":
|
|
220
|
+
text = extract_message_text(msg)
|
|
221
|
+
if text:
|
|
222
|
+
from cli.infrastructure.tui.clipboard import copy_to_clipboard
|
|
223
|
+
|
|
224
|
+
copy_to_clipboard(context.app, text, "Last response")
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
context.notify("No assistant message to copy", "warning")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@dataclass
|
|
231
|
+
class NewCommand:
|
|
232
|
+
"""Request a new chat session."""
|
|
233
|
+
|
|
234
|
+
name: str = field(default="new", init=False)
|
|
235
|
+
description: str = field(default="Start a new chat session", init=False)
|
|
236
|
+
aliases: tuple[str, ...] = field(default=("n",), init=False)
|
|
237
|
+
|
|
238
|
+
def execute(self, args: str, context: SlashContext) -> None:
|
|
239
|
+
"""Request new session - signals to screen to open modal."""
|
|
240
|
+
del args, context # Command handled by screen via message
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
@dataclass
|
|
244
|
+
class SessionsCommand:
|
|
245
|
+
"""Focus the session sidebar for browsing."""
|
|
246
|
+
|
|
247
|
+
name: str = field(default="sessions", init=False)
|
|
248
|
+
description: str = field(default="Focus session sidebar", init=False)
|
|
249
|
+
aliases: tuple[str, ...] = field(default=("s", "list"), init=False)
|
|
250
|
+
|
|
251
|
+
def execute(self, args: str, context: SlashContext) -> None:
|
|
252
|
+
"""Focus sessions - signals to screen to focus sidebar."""
|
|
253
|
+
del args, context # Command handled by screen via message
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@dataclass
|
|
257
|
+
class UndoCommand:
|
|
258
|
+
"""Undo the last conversation turn."""
|
|
259
|
+
|
|
260
|
+
name: str = field(default="undo", init=False)
|
|
261
|
+
description: str = field(default="Undo last conversation turn", init=False)
|
|
262
|
+
aliases: tuple[str, ...] = field(default=("u", "z"), init=False)
|
|
263
|
+
|
|
264
|
+
def execute(self, args: str, context: SlashContext) -> None:
|
|
265
|
+
"""Undo last turn - signals to screen to perform undo."""
|
|
266
|
+
del args, context # Command handled by screen via message
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
@dataclass
|
|
270
|
+
class RegenerateCommand:
|
|
271
|
+
"""Regenerate the last AI response."""
|
|
272
|
+
|
|
273
|
+
name: str = field(default="regenerate", init=False)
|
|
274
|
+
description: str = field(default="Regenerate last response", init=False)
|
|
275
|
+
aliases: tuple[str, ...] = field(default=("r", "regen"), init=False)
|
|
276
|
+
|
|
277
|
+
def execute(self, args: str, context: SlashContext) -> None:
|
|
278
|
+
"""Regenerate - signals to screen to perform regeneration."""
|
|
279
|
+
del args, context # Command handled by screen via message
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@dataclass
|
|
283
|
+
class AutoNameCommand:
|
|
284
|
+
"""Toggle automatic session naming."""
|
|
285
|
+
|
|
286
|
+
name: str = field(default="autoname", init=False)
|
|
287
|
+
description: str = field(default="Toggle automatic session naming", init=False)
|
|
288
|
+
aliases: tuple[str, ...] = field(default=("an",), init=False)
|
|
289
|
+
|
|
290
|
+
# Storage key for the preference
|
|
291
|
+
_PREF_KEY: str = field(default="auto_name_sessions", init=False)
|
|
292
|
+
|
|
293
|
+
def execute(self, args: str, context: SlashContext) -> None:
|
|
294
|
+
"""Toggle auto-naming or set to specific value.
|
|
295
|
+
|
|
296
|
+
Args can be:
|
|
297
|
+
- (empty): Toggle the current setting
|
|
298
|
+
- "on" / "true" / "1": Enable auto-naming
|
|
299
|
+
- "off" / "false" / "0": Disable auto-naming
|
|
300
|
+
"""
|
|
301
|
+
from cli.infrastructure.local_storage import get_storage
|
|
302
|
+
|
|
303
|
+
storage = get_storage()
|
|
304
|
+
current = storage.get("preferences", self._PREF_KEY, True)
|
|
305
|
+
|
|
306
|
+
# Parse args to determine new value
|
|
307
|
+
args_lower = args.strip().lower()
|
|
308
|
+
if args_lower in ("on", "true", "1", "enable", "yes"):
|
|
309
|
+
new_value = True
|
|
310
|
+
elif args_lower in ("off", "false", "0", "disable", "no"):
|
|
311
|
+
new_value = False
|
|
312
|
+
else:
|
|
313
|
+
# Toggle
|
|
314
|
+
new_value = not current
|
|
315
|
+
|
|
316
|
+
storage.set("preferences", self._PREF_KEY, new_value)
|
|
317
|
+
|
|
318
|
+
status = "enabled" if new_value else "disabled"
|
|
319
|
+
context.notify(f"Automatic session naming {status}", "information")
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@dataclass
|
|
323
|
+
class SplitCommand:
|
|
324
|
+
"""Toggle split-pane view for comparing models."""
|
|
325
|
+
|
|
326
|
+
name: str = field(default="split", init=False)
|
|
327
|
+
description: str = field(default="Toggle split-pane view", init=False)
|
|
328
|
+
aliases: tuple[str, ...] = field(default=(), init=False)
|
|
329
|
+
|
|
330
|
+
def execute(self, args: str, context: SlashContext) -> None:
|
|
331
|
+
"""Toggle split view - handled by screen via message."""
|
|
332
|
+
del args, context # Command handled by screen via message
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
@dataclass
|
|
336
|
+
class SyncCommand:
|
|
337
|
+
"""Toggle synchronized input mode in split view."""
|
|
338
|
+
|
|
339
|
+
name: str = field(default="sync", init=False)
|
|
340
|
+
description: str = field(
|
|
341
|
+
default="Toggle sync mode (send to both panes)", init=False
|
|
342
|
+
)
|
|
343
|
+
aliases: tuple[str, ...] = field(default=(), init=False)
|
|
344
|
+
|
|
345
|
+
def execute(self, args: str, context: SlashContext) -> None:
|
|
346
|
+
"""Toggle sync mode - handled by screen via message."""
|
|
347
|
+
del args, context # Command handled by screen via message
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
@dataclass
|
|
351
|
+
class SwitchPaneCommand:
|
|
352
|
+
"""Switch focus between panes in split view."""
|
|
353
|
+
|
|
354
|
+
name: str = field(default="switch", init=False)
|
|
355
|
+
description: str = field(default="Switch between panes in split view", init=False)
|
|
356
|
+
aliases: tuple[str, ...] = field(default=("sw",), init=False)
|
|
357
|
+
|
|
358
|
+
def execute(self, args: str, context: SlashContext) -> None:
|
|
359
|
+
"""Switch pane focus - handled by screen via message."""
|
|
360
|
+
del args, context # Command handled by screen via message
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
@dataclass
|
|
364
|
+
class MarkdownToggleCommand:
|
|
365
|
+
"""Toggle markdown rendering on/off."""
|
|
366
|
+
|
|
367
|
+
name: str = field(default="markdown", init=False)
|
|
368
|
+
description: str = field(default="Toggle markdown rendering", init=False)
|
|
369
|
+
aliases: tuple[str, ...] = field(default=("md", "plain"), init=False)
|
|
370
|
+
|
|
371
|
+
def execute(self, args: str, context: SlashContext) -> None:
|
|
372
|
+
"""Toggle render mode between markdown and plain text."""
|
|
373
|
+
del args # Unused
|
|
374
|
+
|
|
375
|
+
from cli.tui.chat.types import RenderMode
|
|
376
|
+
|
|
377
|
+
old_mode = context.store.state.render_mode
|
|
378
|
+
context.store.toggle_render_mode()
|
|
379
|
+
new_mode = context.store.state.render_mode
|
|
380
|
+
mode_name = "Markdown" if new_mode == RenderMode.MARKDOWN else "Plain text"
|
|
381
|
+
context.notify(f"Render mode: {mode_name} (was {old_mode.name})", "information")
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def create_default_registry() -> SlashCommandRegistry:
|
|
385
|
+
"""Create a registry with all built-in commands.
|
|
386
|
+
|
|
387
|
+
Returns:
|
|
388
|
+
SlashCommandRegistry populated with default commands.
|
|
389
|
+
"""
|
|
390
|
+
registry = SlashCommandRegistry()
|
|
391
|
+
|
|
392
|
+
# Create help command with registry reference
|
|
393
|
+
help_cmd = HelpCommand()
|
|
394
|
+
help_cmd.registry = registry
|
|
395
|
+
|
|
396
|
+
# Register all commands
|
|
397
|
+
registry.register(ClearCommand())
|
|
398
|
+
registry.register(help_cmd)
|
|
399
|
+
registry.register(CopyCommand())
|
|
400
|
+
registry.register(NewCommand())
|
|
401
|
+
registry.register(SessionsCommand())
|
|
402
|
+
registry.register(UndoCommand())
|
|
403
|
+
registry.register(RegenerateCommand())
|
|
404
|
+
registry.register(AutoNameCommand())
|
|
405
|
+
# Split view commands
|
|
406
|
+
registry.register(SplitCommand())
|
|
407
|
+
registry.register(SyncCommand())
|
|
408
|
+
registry.register(SwitchPaneCommand())
|
|
409
|
+
# Rendering commands
|
|
410
|
+
registry.register(MarkdownToggleCommand())
|
|
411
|
+
|
|
412
|
+
return registry
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
# Default global registry
|
|
416
|
+
_default_registry: SlashCommandRegistry | None = None
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def get_default_registry() -> SlashCommandRegistry:
|
|
420
|
+
"""Get the default slash command registry (lazy-initialized singleton)."""
|
|
421
|
+
global _default_registry
|
|
422
|
+
if _default_registry is None:
|
|
423
|
+
_default_registry = create_default_registry()
|
|
424
|
+
return _default_registry
|
cli/tui/chat/store.py
ADDED
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
"""Reactive state store for the chat module.
|
|
2
|
+
|
|
3
|
+
Provides centralized state management with subscriber notifications.
|
|
4
|
+
Widgets subscribe to state changes and react accordingly.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import replace
|
|
8
|
+
from typing import Callable
|
|
9
|
+
from uuid import UUID
|
|
10
|
+
|
|
11
|
+
from alloy_runtime_types.dtos.sessions import MessageResponse, SessionSummary
|
|
12
|
+
from alloy_runtime_sdk.logging.config import get_logger
|
|
13
|
+
|
|
14
|
+
from cli.tui.chat.types import ChatPhase, ChatState, RenderMode, SessionContext
|
|
15
|
+
|
|
16
|
+
logger = get_logger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ChatStore:
|
|
20
|
+
"""Reactive state store for chat screen.
|
|
21
|
+
|
|
22
|
+
Follows the observable store pattern:
|
|
23
|
+
- State is accessed via .state property (immutable snapshot)
|
|
24
|
+
- Mutations happen through methods that create new state
|
|
25
|
+
- Subscribers are notified on every state change
|
|
26
|
+
|
|
27
|
+
Usage:
|
|
28
|
+
store = ChatStore()
|
|
29
|
+
|
|
30
|
+
# Subscribe to changes
|
|
31
|
+
def on_change(state: ChatState):
|
|
32
|
+
print(f"Phase: {state.phase}")
|
|
33
|
+
|
|
34
|
+
unsubscribe = store.subscribe(on_change)
|
|
35
|
+
|
|
36
|
+
# Mutate state
|
|
37
|
+
store.set_phase(ChatPhase.STREAMING) # triggers on_change
|
|
38
|
+
|
|
39
|
+
# Unsubscribe when done
|
|
40
|
+
unsubscribe()
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
def __init__(self) -> None:
|
|
44
|
+
"""Initialize with default state."""
|
|
45
|
+
self._state = ChatState()
|
|
46
|
+
self._subscribers: list[Callable[[ChatState], None]] = []
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def state(self) -> ChatState:
|
|
50
|
+
"""Get current immutable state snapshot."""
|
|
51
|
+
return self._state
|
|
52
|
+
|
|
53
|
+
def subscribe(self, callback: Callable[[ChatState], None]) -> Callable[[], None]:
|
|
54
|
+
"""Subscribe to state changes.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
callback: Function called with new state on every change.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Unsubscribe function - call to stop receiving updates.
|
|
61
|
+
"""
|
|
62
|
+
self._subscribers.append(callback)
|
|
63
|
+
return lambda: self._subscribers.remove(callback)
|
|
64
|
+
|
|
65
|
+
def _notify(self) -> None:
|
|
66
|
+
"""Notify all subscribers of state change."""
|
|
67
|
+
for subscriber in self._subscribers:
|
|
68
|
+
subscriber(self._state)
|
|
69
|
+
|
|
70
|
+
def _update(self, **changes: object) -> None:
|
|
71
|
+
"""Update state with changes and notify subscribers.
|
|
72
|
+
|
|
73
|
+
Creates a new ChatState with the given field changes.
|
|
74
|
+
"""
|
|
75
|
+
self._state = replace(self._state, **changes)
|
|
76
|
+
self._notify()
|
|
77
|
+
|
|
78
|
+
# === Phase Management ===
|
|
79
|
+
|
|
80
|
+
def set_phase(self, phase: ChatPhase) -> None:
|
|
81
|
+
"""Set the current chat phase."""
|
|
82
|
+
old_phase = self._state.phase
|
|
83
|
+
if old_phase != phase:
|
|
84
|
+
logger.debug(
|
|
85
|
+
"store_phase_changed", old_phase=old_phase.name, new_phase=phase.name
|
|
86
|
+
)
|
|
87
|
+
self._update(phase=phase)
|
|
88
|
+
|
|
89
|
+
def is_busy(self) -> bool:
|
|
90
|
+
"""Check if the chat is in a busy state (can't accept new actions)."""
|
|
91
|
+
return self._state.phase not in (ChatPhase.IDLE,)
|
|
92
|
+
|
|
93
|
+
# === Session Management ===
|
|
94
|
+
|
|
95
|
+
def set_session(
|
|
96
|
+
self,
|
|
97
|
+
session_id: UUID,
|
|
98
|
+
session_name: str | None,
|
|
99
|
+
agent_id: UUID | None,
|
|
100
|
+
agent_name: str | None,
|
|
101
|
+
message_count: int,
|
|
102
|
+
provider_key: str | None = None,
|
|
103
|
+
model_name: str | None = None,
|
|
104
|
+
system_instruction: str | None = None,
|
|
105
|
+
) -> None:
|
|
106
|
+
"""Set the active session context.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
session_id: The session ID.
|
|
110
|
+
session_name: Optional session name.
|
|
111
|
+
agent_id: Agent ID (for agent mode).
|
|
112
|
+
agent_name: Agent name (for agent mode).
|
|
113
|
+
message_count: Number of messages in session.
|
|
114
|
+
provider_key: Provider key (for direct model mode).
|
|
115
|
+
model_name: Model name (for direct model mode).
|
|
116
|
+
system_instruction: System instruction (for direct model mode).
|
|
117
|
+
"""
|
|
118
|
+
session = SessionContext(
|
|
119
|
+
session_id=session_id,
|
|
120
|
+
session_name=session_name,
|
|
121
|
+
agent_id=agent_id,
|
|
122
|
+
agent_name=agent_name,
|
|
123
|
+
message_count=message_count,
|
|
124
|
+
provider_key=provider_key,
|
|
125
|
+
model_name=model_name,
|
|
126
|
+
system_instruction=system_instruction,
|
|
127
|
+
)
|
|
128
|
+
logger.info(
|
|
129
|
+
"store_session_set",
|
|
130
|
+
session_id=str(session_id),
|
|
131
|
+
session_name=session_name,
|
|
132
|
+
agent_name=agent_name,
|
|
133
|
+
model_name=model_name,
|
|
134
|
+
message_count=message_count,
|
|
135
|
+
)
|
|
136
|
+
self._update(session=session)
|
|
137
|
+
|
|
138
|
+
def clear_session(self) -> None:
|
|
139
|
+
"""Clear the active session."""
|
|
140
|
+
logger.debug("store_session_cleared")
|
|
141
|
+
self._update(session=None, messages=())
|
|
142
|
+
|
|
143
|
+
def set_sessions(self, sessions: list[SessionSummary]) -> None:
|
|
144
|
+
"""Set the session list."""
|
|
145
|
+
self._update(sessions=tuple(sessions))
|
|
146
|
+
|
|
147
|
+
def set_session_search_query(self, query: str) -> None:
|
|
148
|
+
"""Set the session search query."""
|
|
149
|
+
self._update(session_search_query=query)
|
|
150
|
+
|
|
151
|
+
# === Message Management ===
|
|
152
|
+
|
|
153
|
+
def set_messages(self, messages: list[MessageResponse]) -> None:
|
|
154
|
+
"""Set the message list for current session."""
|
|
155
|
+
logger.debug("store_messages_set", message_count=len(messages))
|
|
156
|
+
self._update(messages=tuple(messages))
|
|
157
|
+
|
|
158
|
+
def append_streaming_content(self, chunk: str) -> None:
|
|
159
|
+
"""Append content to streaming buffer."""
|
|
160
|
+
new_content = self._state.streaming_content + chunk
|
|
161
|
+
self._update(streaming_content=new_content)
|
|
162
|
+
|
|
163
|
+
def append_streaming_thinking_content(self, chunk: str) -> None:
|
|
164
|
+
"""Append content to streaming thinking/reasoning buffer."""
|
|
165
|
+
new_content = self._state.streaming_thinking_content + chunk
|
|
166
|
+
self._update(streaming_thinking_content=new_content)
|
|
167
|
+
|
|
168
|
+
def clear_streaming_content(self) -> None:
|
|
169
|
+
"""Clear the streaming content buffer."""
|
|
170
|
+
self._update(streaming_content="", streaming_thinking_content="")
|
|
171
|
+
|
|
172
|
+
# === Error Management ===
|
|
173
|
+
|
|
174
|
+
def set_error(self, error: str | None) -> None:
|
|
175
|
+
"""Set or clear error state."""
|
|
176
|
+
if error:
|
|
177
|
+
logger.warning("store_error_set", error=error)
|
|
178
|
+
self._update(error=error)
|
|
179
|
+
|
|
180
|
+
def clear_error(self) -> None:
|
|
181
|
+
"""Clear error state."""
|
|
182
|
+
self._update(error=None)
|
|
183
|
+
|
|
184
|
+
# === Convenience Methods ===
|
|
185
|
+
|
|
186
|
+
def start_streaming(self, user_message: str | None = None) -> None:
|
|
187
|
+
"""Transition to streaming state with optional pending user message."""
|
|
188
|
+
logger.info(
|
|
189
|
+
"store_streaming_started",
|
|
190
|
+
has_user_message=user_message is not None,
|
|
191
|
+
message_preview=user_message[:50] if user_message else None,
|
|
192
|
+
)
|
|
193
|
+
self._update(
|
|
194
|
+
phase=ChatPhase.STREAMING,
|
|
195
|
+
streaming_content="",
|
|
196
|
+
streaming_thinking_content="",
|
|
197
|
+
error=None,
|
|
198
|
+
pending_user_message=user_message,
|
|
199
|
+
cancel_requested=False, # Reset cancellation flag on new stream
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
def finish_streaming(self, cancelled: bool = False) -> None:
|
|
203
|
+
"""Transition from streaming back to idle.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
cancelled: If True, keep streaming content visible for cancelled streams.
|
|
207
|
+
"""
|
|
208
|
+
content_length = len(self._state.streaming_content)
|
|
209
|
+
logger.info(
|
|
210
|
+
"store_streaming_finished",
|
|
211
|
+
cancelled=cancelled,
|
|
212
|
+
content_length=content_length,
|
|
213
|
+
)
|
|
214
|
+
if cancelled:
|
|
215
|
+
# Keep the partial content visible when cancelled
|
|
216
|
+
self._update(
|
|
217
|
+
phase=ChatPhase.IDLE,
|
|
218
|
+
cancel_requested=False,
|
|
219
|
+
)
|
|
220
|
+
else:
|
|
221
|
+
# Normal completion - clear streaming content
|
|
222
|
+
self._update(
|
|
223
|
+
phase=ChatPhase.IDLE,
|
|
224
|
+
streaming_content="",
|
|
225
|
+
streaming_thinking_content="",
|
|
226
|
+
pending_user_message=None,
|
|
227
|
+
cancel_requested=False,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
def request_cancel(self) -> None:
|
|
231
|
+
"""Request cancellation of current streaming operation."""
|
|
232
|
+
if self._state.phase == ChatPhase.STREAMING:
|
|
233
|
+
logger.info("store_cancel_requested")
|
|
234
|
+
self._update(cancel_requested=True)
|
|
235
|
+
|
|
236
|
+
def toggle_thinking_collapsed(self) -> None:
|
|
237
|
+
"""Toggle whether thinking blocks are collapsed."""
|
|
238
|
+
self._update(thinking_collapsed=not self._state.thinking_collapsed)
|
|
239
|
+
|
|
240
|
+
def set_thinking_collapsed(self, collapsed: bool) -> None:
|
|
241
|
+
"""Set whether thinking blocks are collapsed."""
|
|
242
|
+
self._update(thinking_collapsed=collapsed)
|
|
243
|
+
|
|
244
|
+
def set_pending_user_message(self, message: str | None) -> None:
|
|
245
|
+
"""Set the pending user message (shown optimistically before server confirms)."""
|
|
246
|
+
self._update(pending_user_message=message)
|
|
247
|
+
|
|
248
|
+
def clear_pending_user_message(self) -> None:
|
|
249
|
+
"""Clear the pending user message."""
|
|
250
|
+
self._update(pending_user_message=None)
|
|
251
|
+
|
|
252
|
+
def reset(self) -> None:
|
|
253
|
+
"""Reset to initial state."""
|
|
254
|
+
self._state = ChatState()
|
|
255
|
+
self._notify()
|
|
256
|
+
|
|
257
|
+
# === UI State ===
|
|
258
|
+
|
|
259
|
+
def toggle_sidebar(self) -> None:
|
|
260
|
+
"""Toggle sidebar visibility."""
|
|
261
|
+
self._update(sidebar_visible=not self._state.sidebar_visible)
|
|
262
|
+
|
|
263
|
+
def set_sidebar_visible(self, visible: bool) -> None:
|
|
264
|
+
"""Set sidebar visibility."""
|
|
265
|
+
self._update(sidebar_visible=visible)
|
|
266
|
+
|
|
267
|
+
def toggle_render_mode(self) -> None:
|
|
268
|
+
"""Toggle between markdown and plain text rendering."""
|
|
269
|
+
current = self._state.render_mode
|
|
270
|
+
new_mode = (
|
|
271
|
+
RenderMode.PLAIN if current == RenderMode.MARKDOWN else RenderMode.MARKDOWN
|
|
272
|
+
)
|
|
273
|
+
logger.debug(
|
|
274
|
+
"store_render_mode_toggled", old_mode=current.name, new_mode=new_mode.name
|
|
275
|
+
)
|
|
276
|
+
self._update(render_mode=new_mode)
|
|
277
|
+
|
|
278
|
+
def set_render_mode(self, mode: RenderMode) -> None:
|
|
279
|
+
"""Set the render mode."""
|
|
280
|
+
self._update(render_mode=mode)
|