alloy-runtime-cli 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- alloy_runtime_cli-0.1.0.dist-info/METADATA +61 -0
- alloy_runtime_cli-0.1.0.dist-info/RECORD +451 -0
- alloy_runtime_cli-0.1.0.dist-info/WHEEL +5 -0
- alloy_runtime_cli-0.1.0.dist-info/entry_points.txt +2 -0
- alloy_runtime_cli-0.1.0.dist-info/top_level.txt +1 -0
- cli/__init__.py +0 -0
- cli/commands/__init__.py +0 -0
- cli/commands/admin/__init__.py +0 -0
- cli/commands/admin/bootstrap_command.py +118 -0
- cli/commands/admin/credentials/__init__.py +0 -0
- cli/commands/admin/credentials/create/__init__.py +0 -0
- cli/commands/admin/credentials/create/command.py +148 -0
- cli/commands/admin/credentials/create/presenter.py +16 -0
- cli/commands/admin/credentials/grant/__init__.py +0 -0
- cli/commands/admin/credentials/grant/command.py +119 -0
- cli/commands/admin/credentials/grant/fields.py +33 -0
- cli/commands/admin/credentials/grant/presenter.py +23 -0
- cli/commands/agents/__init__.py +0 -0
- cli/commands/agents/create/__init__.py +0 -0
- cli/commands/agents/create/command.py +475 -0
- cli/commands/agents/create/fields.py +64 -0
- cli/commands/agents/create/presenter.py +68 -0
- cli/commands/agents/delete/__init__.py +0 -0
- cli/commands/agents/delete/command.py +47 -0
- cli/commands/agents/delete/presenter.py +16 -0
- cli/commands/agents/get/command.py +37 -0
- cli/commands/agents/get/presenter.py +32 -0
- cli/commands/agents/list/__init__.py +1 -0
- cli/commands/agents/list/command.py +54 -0
- cli/commands/agents/list/presenter.py +82 -0
- cli/commands/agents/update/__init__.py +0 -0
- cli/commands/agents/update/command.py +435 -0
- cli/commands/agents/update/fields.py +40 -0
- cli/commands/agents/update/presenter.py +68 -0
- cli/commands/audio/__init__.py +0 -0
- cli/commands/audio/transcribe/__init__.py +0 -0
- cli/commands/audio/transcribe/command.py +144 -0
- cli/commands/audio/transcribe/presenter.py +15 -0
- cli/commands/auth/__init__.py +0 -0
- cli/commands/auth/login/__init__.py +0 -0
- cli/commands/auth/login/command.py +80 -0
- cli/commands/auth/signup/__init__.py +0 -0
- cli/commands/auth/signup/command.py +115 -0
- cli/commands/billing/__init__.py +1 -0
- cli/commands/billing/costs/__init__.py +1 -0
- cli/commands/billing/costs/by_agent/__init__.py +1 -0
- cli/commands/billing/costs/by_agent/command.py +57 -0
- cli/commands/billing/costs/by_agent/presenter.py +81 -0
- cli/commands/billing/costs/by_model/__init__.py +1 -0
- cli/commands/billing/costs/by_model/command.py +57 -0
- cli/commands/billing/costs/by_model/presenter.py +80 -0
- cli/commands/billing/costs/daily/__init__.py +1 -0
- cli/commands/billing/costs/daily/command.py +55 -0
- cli/commands/billing/costs/daily/presenter.py +75 -0
- cli/commands/billing/costs/summary/__init__.py +1 -0
- cli/commands/billing/costs/summary/command.py +57 -0
- cli/commands/billing/costs/summary/presenter.py +42 -0
- cli/commands/billing/projects/__init__.py +1 -0
- cli/commands/billing/projects/create/__init__.py +1 -0
- cli/commands/billing/projects/create/command.py +60 -0
- cli/commands/billing/projects/create/presenter.py +26 -0
- cli/commands/billing/projects/get/__init__.py +1 -0
- cli/commands/billing/projects/get/command.py +33 -0
- cli/commands/billing/projects/get/presenter.py +32 -0
- cli/commands/billing/projects/list/__init__.py +1 -0
- cli/commands/billing/projects/list/command.py +40 -0
- cli/commands/billing/projects/list/presenter.py +57 -0
- cli/commands/content/__init__.py +1 -0
- cli/commands/content/delete/__init__.py +0 -0
- cli/commands/content/delete/command.py +49 -0
- cli/commands/content/delete/presenter.py +18 -0
- cli/commands/content/edit/__init__.py +1 -0
- cli/commands/content/edit/command.py +155 -0
- cli/commands/content/edit/editor.py +150 -0
- cli/commands/content/edit/presenter.py +146 -0
- cli/commands/content/get/__init__.py +1 -0
- cli/commands/content/get/command.py +39 -0
- cli/commands/content/get/presenter.py +176 -0
- cli/commands/content/list/__init__.py +1 -0
- cli/commands/content/list/command.py +347 -0
- cli/commands/content/list/export_formatters.py +409 -0
- cli/commands/content/list/export_handler.py +165 -0
- cli/commands/content/list/presenter.py +190 -0
- cli/commands/credentials/__init__.py +0 -0
- cli/commands/credentials/create/__init__.py +0 -0
- cli/commands/credentials/create/command.py +165 -0
- cli/commands/credentials/create/fields.py +38 -0
- cli/commands/credentials/create/presenter.py +20 -0
- cli/commands/credentials/update/__init__.py +0 -0
- cli/commands/credentials/update/command.py +53 -0
- cli/commands/credentials/update/fields.py +71 -0
- cli/commands/credentials/update/presenter.py +16 -0
- cli/commands/flag_utils.py +366 -0
- cli/commands/generate/__init__.py +0 -0
- cli/commands/generate/cancel/__init__.py +1 -0
- cli/commands/generate/cancel/command.py +44 -0
- cli/commands/generate/cancel/presenter.py +26 -0
- cli/commands/generate/status/__init__.py +1 -0
- cli/commands/generate/status/command.py +58 -0
- cli/commands/generate/status/presenter.py +78 -0
- cli/commands/generate/text/__init__.py +0 -0
- cli/commands/generate/text/command.py +1325 -0
- cli/commands/generate/text/concurrent_renderer.py +355 -0
- cli/commands/generate/text/presenter.py +287 -0
- cli/commands/generate/text/stream_renderer.py +129 -0
- cli/commands/knowledge/__init__.py +0 -0
- cli/commands/knowledge/collections/__init__.py +0 -0
- cli/commands/knowledge/collections/cluster/__init__.py +0 -0
- cli/commands/knowledge/collections/cluster/command.py +64 -0
- cli/commands/knowledge/collections/cluster/presenter.py +74 -0
- cli/commands/knowledge/collections/cluster_status/__init__.py +0 -0
- cli/commands/knowledge/collections/cluster_status/command.py +46 -0
- cli/commands/knowledge/collections/cluster_status/presenter.py +10 -0
- cli/commands/knowledge/collections/create/__init__.py +0 -0
- cli/commands/knowledge/collections/create/command.py +137 -0
- cli/commands/knowledge/collections/create/presenter.py +38 -0
- cli/commands/knowledge/collections/delete/__init__.py +1 -0
- cli/commands/knowledge/collections/delete/command.py +47 -0
- cli/commands/knowledge/collections/delete/presenter.py +20 -0
- cli/commands/knowledge/collections/get/__init__.py +1 -0
- cli/commands/knowledge/collections/get/command.py +30 -0
- cli/commands/knowledge/collections/get/presenter.py +44 -0
- cli/commands/knowledge/collections/list/__init__.py +1 -0
- cli/commands/knowledge/collections/list/command.py +41 -0
- cli/commands/knowledge/collections/list/presenter.py +68 -0
- cli/commands/knowledge/collections/update/__init__.py +0 -0
- cli/commands/knowledge/collections/update/command.py +97 -0
- cli/commands/knowledge/collections/update/presenter.py +42 -0
- cli/commands/knowledge/documents/__init__.py +0 -0
- cli/commands/knowledge/documents/bulk_metadata/__init__.py +0 -0
- cli/commands/knowledge/documents/bulk_metadata/command.py +119 -0
- cli/commands/knowledge/documents/bulk_metadata/presenter.py +36 -0
- cli/commands/knowledge/documents/delete/__init__.py +0 -0
- cli/commands/knowledge/documents/delete/command.py +47 -0
- cli/commands/knowledge/documents/delete/presenter.py +20 -0
- cli/commands/knowledge/documents/get/__init__.py +0 -0
- cli/commands/knowledge/documents/get/command.py +39 -0
- cli/commands/knowledge/documents/get/presenter.py +78 -0
- cli/commands/knowledge/documents/ingest/__init__.py +0 -0
- cli/commands/knowledge/documents/ingest/command.py +222 -0
- cli/commands/knowledge/documents/ingest/presenter.py +41 -0
- cli/commands/knowledge/documents/list/__init__.py +0 -0
- cli/commands/knowledge/documents/list/command.py +69 -0
- cli/commands/knowledge/documents/list/presenter.py +86 -0
- cli/commands/knowledge/documents/reingest/__init__.py +0 -0
- cli/commands/knowledge/documents/reingest/command.py +102 -0
- cli/commands/knowledge/documents/reingest/presenter.py +70 -0
- cli/commands/knowledge/documents/update/__init__.py +0 -0
- cli/commands/knowledge/documents/update/command.py +85 -0
- cli/commands/knowledge/documents/update/presenter.py +37 -0
- cli/commands/knowledge/recover/__init__.py +0 -0
- cli/commands/knowledge/recover/command.py +46 -0
- cli/commands/knowledge/recover/presenter.py +79 -0
- cli/commands/knowledge/search/__init__.py +0 -0
- cli/commands/knowledge/search/command.py +218 -0
- cli/commands/knowledge/search/presenter.py +111 -0
- cli/commands/knowledge/synthesis/__init__.py +0 -0
- cli/commands/knowledge/synthesis/create/__init__.py +0 -0
- cli/commands/knowledge/synthesis/create/command.py +127 -0
- cli/commands/knowledge/synthesis/create/presenter.py +33 -0
- cli/commands/knowledge/synthesis/delete/__init__.py +0 -0
- cli/commands/knowledge/synthesis/delete/command.py +53 -0
- cli/commands/knowledge/synthesis/delete/presenter.py +31 -0
- cli/commands/knowledge/synthesis/get/__init__.py +0 -0
- cli/commands/knowledge/synthesis/get/command.py +55 -0
- cli/commands/knowledge/synthesis/get/presenter.py +114 -0
- cli/commands/knowledge/synthesis/list/__init__.py +0 -0
- cli/commands/knowledge/synthesis/list/command.py +132 -0
- cli/commands/knowledge/synthesis/list/presenter.py +84 -0
- cli/commands/knowledge/synthesis/refresh/__init__.py +0 -0
- cli/commands/knowledge/synthesis/refresh/command.py +42 -0
- cli/commands/knowledge/synthesis/refresh/presenter.py +33 -0
- cli/commands/knowledge/synthesis/update/__init__.py +0 -0
- cli/commands/knowledge/synthesis/update/command.py +76 -0
- cli/commands/knowledge/synthesis/update/presenter.py +41 -0
- cli/commands/models/__init__.py +0 -0
- cli/commands/models/list/__init__.py +0 -0
- cli/commands/models/list/command.py +84 -0
- cli/commands/models/list/presenter.py +114 -0
- cli/commands/organizations/__init__.py +0 -0
- cli/commands/organizations/create/command.py +32 -0
- cli/commands/organizations/create/presenter.py +9 -0
- cli/commands/pipelines/__init__.py +1 -0
- cli/commands/pipelines/approvals/__init__.py +1 -0
- cli/commands/pipelines/approvals/decide_command.py +77 -0
- cli/commands/pipelines/approvals/get_command.py +44 -0
- cli/commands/pipelines/approvals/presenter.py +56 -0
- cli/commands/pipelines/costs/__init__.py +1 -0
- cli/commands/pipelines/costs/command.py +57 -0
- cli/commands/pipelines/costs/daily_command.py +54 -0
- cli/commands/pipelines/costs/daily_presenter.py +59 -0
- cli/commands/pipelines/costs/presenter.py +37 -0
- cli/commands/pipelines/create/__init__.py +1 -0
- cli/commands/pipelines/create/command.py +103 -0
- cli/commands/pipelines/create/presenter.py +22 -0
- cli/commands/pipelines/env_vars/__init__.py +1 -0
- cli/commands/pipelines/env_vars/command.py +51 -0
- cli/commands/pipelines/env_vars/presenter.py +16 -0
- cli/commands/pipelines/execute/__init__.py +1 -0
- cli/commands/pipelines/execute/command.py +142 -0
- cli/commands/pipelines/execute/presenter.py +47 -0
- cli/commands/pipelines/executions/__init__.py +1 -0
- cli/commands/pipelines/executions/costs/__init__.py +1 -0
- cli/commands/pipelines/executions/costs/command.py +48 -0
- cli/commands/pipelines/executions/costs/presenter.py +29 -0
- cli/commands/pipelines/executions/costs_by_model/__init__.py +1 -0
- cli/commands/pipelines/executions/costs_by_model/command.py +50 -0
- cli/commands/pipelines/executions/costs_by_model/presenter.py +78 -0
- cli/commands/pipelines/executions/costs_by_step/__init__.py +1 -0
- cli/commands/pipelines/executions/costs_by_step/command.py +50 -0
- cli/commands/pipelines/executions/costs_by_step/presenter.py +72 -0
- cli/commands/pipelines/executions/get_command.py +38 -0
- cli/commands/pipelines/executions/list_command.py +123 -0
- cli/commands/pipelines/executions/presenter.py +131 -0
- cli/commands/pipelines/executions/rerun_command.py +41 -0
- cli/commands/pipelines/executions/update/__init__.py +1 -0
- cli/commands/pipelines/executions/update/command.py +110 -0
- cli/commands/pipelines/executions/update/presenter.py +28 -0
- cli/commands/pipelines/get/__init__.py +1 -0
- cli/commands/pipelines/get/command.py +33 -0
- cli/commands/pipelines/get/presenter.py +48 -0
- cli/commands/pipelines/list/__init__.py +1 -0
- cli/commands/pipelines/list/command.py +53 -0
- cli/commands/pipelines/list/presenter.py +66 -0
- cli/commands/pipelines/schedules/__init__.py +1 -0
- cli/commands/pipelines/schedules/create_command.py +119 -0
- cli/commands/pipelines/schedules/create_presenter.py +35 -0
- cli/commands/pipelines/schedules/delete_command.py +52 -0
- cli/commands/pipelines/schedules/env_vars_command.py +59 -0
- cli/commands/pipelines/schedules/env_vars_presenter.py +16 -0
- cli/commands/pipelines/schedules/get_command.py +38 -0
- cli/commands/pipelines/schedules/list_command.py +33 -0
- cli/commands/pipelines/schedules/once_command.py +90 -0
- cli/commands/pipelines/schedules/once_presenter.py +30 -0
- cli/commands/pipelines/schedules/presenter.py +104 -0
- cli/commands/pipelines/schedules/update_command.py +139 -0
- cli/commands/pipelines/schedules/update_presenter.py +29 -0
- cli/commands/render/__init__.py +0 -0
- cli/commands/render/html_to_image/__init__.py +0 -0
- cli/commands/render/html_to_image/command.py +170 -0
- cli/commands/schemas/__init__.py +0 -0
- cli/commands/schemas/create/__init__.py +0 -0
- cli/commands/schemas/create/command.py +122 -0
- cli/commands/schemas/create/presenter.py +53 -0
- cli/commands/schemas/delete/command.py +45 -0
- cli/commands/schemas/delete/presenter.py +9 -0
- cli/commands/schemas/get/__init__.py +0 -0
- cli/commands/schemas/get/command.py +56 -0
- cli/commands/schemas/get/presenter.py +129 -0
- cli/commands/schemas/list/__init__.py +0 -0
- cli/commands/schemas/list/command.py +64 -0
- cli/commands/schemas/list/presenter.py +133 -0
- cli/commands/schemas/update/__init__.py +0 -0
- cli/commands/schemas/update/command.py +369 -0
- cli/commands/schemas/update/presenter.py +53 -0
- cli/commands/sessions/__init__.py +1 -0
- cli/commands/sessions/delete/__init__.py +1 -0
- cli/commands/sessions/delete/command.py +47 -0
- cli/commands/sessions/delete/presenter.py +10 -0
- cli/commands/sessions/get/__init__.py +1 -0
- cli/commands/sessions/get/command.py +42 -0
- cli/commands/sessions/get/presenter.py +59 -0
- cli/commands/sessions/list/__init__.py +1 -0
- cli/commands/sessions/list/command.py +61 -0
- cli/commands/sessions/list/presenter.py +68 -0
- cli/commands/sessions/messages/__init__.py +1 -0
- cli/commands/sessions/messages/command.py +78 -0
- cli/commands/sessions/messages/presenter.py +79 -0
- cli/commands/shared_flags.py +500 -0
- cli/commands/sync/__init__.py +0 -0
- cli/commands/sync/command.py +45 -0
- cli/commands/sync/presenter.py +49 -0
- cli/commands/tags/__init__.py +1 -0
- cli/commands/tags/create/__init__.py +1 -0
- cli/commands/tags/create/command.py +60 -0
- cli/commands/tags/delete/__init__.py +1 -0
- cli/commands/tags/delete/command.py +47 -0
- cli/commands/tags/delete/presenter.py +10 -0
- cli/commands/tags/get/command.py +31 -0
- cli/commands/tags/get/presenter.py +23 -0
- cli/commands/tags/list/__init__.py +1 -0
- cli/commands/tags/list/command.py +52 -0
- cli/commands/tags/list/presenter.py +49 -0
- cli/commands/tags/update/command.py +64 -0
- cli/commands/tags/update/presenter.py +9 -0
- cli/commands/templates/__init__.py +0 -0
- cli/commands/templates/create/__init__.py +0 -0
- cli/commands/templates/create/command.py +152 -0
- cli/commands/templates/create/presenter.py +86 -0
- cli/commands/templates/delete/__init__.py +0 -0
- cli/commands/templates/delete/command.py +47 -0
- cli/commands/templates/delete/presenter.py +16 -0
- cli/commands/templates/get/__init__.py +0 -0
- cli/commands/templates/get/command.py +52 -0
- cli/commands/templates/get/presenter.py +233 -0
- cli/commands/templates/get_by_version/command.py +32 -0
- cli/commands/templates/get_by_version/presenter.py +30 -0
- cli/commands/templates/list/__init__.py +1 -0
- cli/commands/templates/list/command.py +102 -0
- cli/commands/templates/list/presenter.py +93 -0
- cli/commands/templates/render/__init__.py +0 -0
- cli/commands/templates/render/command.py +115 -0
- cli/commands/templates/render/presenter.py +276 -0
- cli/commands/templates/update/__init__.py +0 -0
- cli/commands/templates/update/command.py +199 -0
- cli/commands/templates/update/presenter.py +94 -0
- cli/commands/templates/version/__init__.py +1 -0
- cli/commands/templates/version/command.py +116 -0
- cli/commands/templates/version/presenter.py +100 -0
- cli/commands/tool_configs/__init__.py +0 -0
- cli/commands/tool_configs/create/__init__.py +0 -0
- cli/commands/tool_configs/create/command.py +118 -0
- cli/commands/tool_configs/create/presenter.py +53 -0
- cli/commands/tool_configs/delete/__init__.py +0 -0
- cli/commands/tool_configs/delete/command.py +47 -0
- cli/commands/tool_configs/delete/presenter.py +18 -0
- cli/commands/tool_configs/get/__init__.py +0 -0
- cli/commands/tool_configs/get/command.py +31 -0
- cli/commands/tool_configs/get/presenter.py +62 -0
- cli/commands/tool_configs/list/__init__.py +0 -0
- cli/commands/tool_configs/list/command.py +59 -0
- cli/commands/tool_configs/list/presenter.py +60 -0
- cli/commands/tool_configs/update/__init__.py +0 -0
- cli/commands/tool_configs/update/command.py +128 -0
- cli/commands/tool_configs/update/presenter.py +53 -0
- cli/commands/tools/__init__.py +1 -0
- cli/commands/tools/get/__init__.py +1 -0
- cli/commands/tools/get/command.py +42 -0
- cli/commands/tools/get/presenter.py +45 -0
- cli/commands/tools/list/__init__.py +1 -0
- cli/commands/tools/list/command.py +56 -0
- cli/commands/tools/list/presenter.py +44 -0
- cli/commands/users/__init__.py +0 -0
- cli/commands/users/create/command.py +53 -0
- cli/commands/users/create/presenter.py +9 -0
- cli/commands/whoami/__init__.py +0 -0
- cli/commands/whoami/command.py +42 -0
- cli/infrastructure/__init__.py +0 -0
- cli/infrastructure/auth_storage.py +71 -0
- cli/infrastructure/client_factory.py +36 -0
- cli/infrastructure/command.py +75 -0
- cli/infrastructure/config.py +188 -0
- cli/infrastructure/console.py +27 -0
- cli/infrastructure/editor.py +138 -0
- cli/infrastructure/error_display.py +178 -0
- cli/infrastructure/field_extractor.py +360 -0
- cli/infrastructure/file_content.py +210 -0
- cli/infrastructure/filter_parser.py +256 -0
- cli/infrastructure/formatters/__init__.py +0 -0
- cli/infrastructure/formatters/base.py +99 -0
- cli/infrastructure/formatters/compact_formatter.py +245 -0
- cli/infrastructure/formatters/json_formatter.py +84 -0
- cli/infrastructure/formatters/lines_formatter.py +102 -0
- cli/infrastructure/formatting/__init__.py +0 -0
- cli/infrastructure/formatting/fields.py +193 -0
- cli/infrastructure/forms/__init__.py +0 -0
- cli/infrastructure/forms/agent_picker.py +123 -0
- cli/infrastructure/forms/agent_tool_editor.py +384 -0
- cli/infrastructure/forms/agent_tools_manager.py +212 -0
- cli/infrastructure/forms/base_picker.py +469 -0
- cli/infrastructure/forms/components.py +126 -0
- cli/infrastructure/forms/json_schema_builder.py +149 -0
- cli/infrastructure/forms/model_picker.py +134 -0
- cli/infrastructure/forms/parsers.py +173 -0
- cli/infrastructure/forms/resolution_modal.py +302 -0
- cli/infrastructure/forms/schema_picker.py +137 -0
- cli/infrastructure/forms/tag_management_modal.py +103 -0
- cli/infrastructure/forms/tag_picker.py +207 -0
- cli/infrastructure/forms/template_picker.py +131 -0
- cli/infrastructure/forms/tool_config_picker.py +130 -0
- cli/infrastructure/forms/tool_picker.py +103 -0
- cli/infrastructure/injection/__init__.py +0 -0
- cli/infrastructure/injection/parser.py +302 -0
- cli/infrastructure/injection/resolver.py +399 -0
- cli/infrastructure/kv_parser.py +130 -0
- cli/infrastructure/local_storage.py +227 -0
- cli/infrastructure/macro_parser.py +215 -0
- cli/infrastructure/output.py +192 -0
- cli/infrastructure/provider_setup.py +81 -0
- cli/infrastructure/renderers/__init__.py +0 -0
- cli/infrastructure/renderers/entity_renderer.py +77 -0
- cli/infrastructure/renderers/list_renderer.py +114 -0
- cli/infrastructure/scope_utils.py +47 -0
- cli/infrastructure/spinner.py +101 -0
- cli/infrastructure/tui/__init__.py +0 -0
- cli/infrastructure/tui/clipboard.py +41 -0
- cli/infrastructure/tui/formatters.py +105 -0
- cli/infrastructure/tui/preview.py +14 -0
- cli/infrastructure/tui/selectable.py +198 -0
- cli/infrastructure/validation/__init__.py +0 -0
- cli/infrastructure/validation/tag_validation.py +74 -0
- cli/main.py +759 -0
- cli/tui/__init__.py +0 -0
- cli/tui/app.py +199 -0
- cli/tui/app_store.py +73 -0
- cli/tui/chat/__init__.py +0 -0
- cli/tui/chat/commands/__init__.py +0 -0
- cli/tui/chat/commands/base.py +65 -0
- cli/tui/chat/commands/create_session.py +135 -0
- cli/tui/chat/commands/load_session.py +119 -0
- cli/tui/chat/commands/regenerate.py +120 -0
- cli/tui/chat/commands/reload_session.py +63 -0
- cli/tui/chat/commands/send_message.py +190 -0
- cli/tui/chat/commands/undo.py +66 -0
- cli/tui/chat/editor.py +71 -0
- cli/tui/chat/messages.py +223 -0
- cli/tui/chat/pane.py +141 -0
- cli/tui/chat/renderers/__init__.py +0 -0
- cli/tui/chat/renderers/base.py +72 -0
- cli/tui/chat/renderers/markdown.py +250 -0
- cli/tui/chat/renderers/plain.py +83 -0
- cli/tui/chat/screen.py +1155 -0
- cli/tui/chat/services/__init__.py +0 -0
- cli/tui/chat/services/injection.py +386 -0
- cli/tui/chat/services/name_generator.py +256 -0
- cli/tui/chat/slash_commands.py +424 -0
- cli/tui/chat/store.py +280 -0
- cli/tui/chat/types.py +220 -0
- cli/tui/chat/widgets/__init__.py +0 -0
- cli/tui/chat/widgets/chat_header.py +75 -0
- cli/tui/chat/widgets/chat_input.py +362 -0
- cli/tui/chat/widgets/injection_popup.py +161 -0
- cli/tui/chat/widgets/message_display.py +287 -0
- cli/tui/chat/widgets/session_sidebar.py +214 -0
- cli/tui/chat/widgets/welcome_screen.py +290 -0
- cli/tui/screens/__init__.py +0 -0
- cli/tui/screens/agents.py +344 -0
- cli/tui/screens/base.py +301 -0
- cli/tui/screens/content.py +508 -0
- cli/tui/screens/dashboard.py +89 -0
- cli/tui/screens/models.py +96 -0
- cli/tui/screens/nav_screen.py +186 -0
- cli/tui/screens/schemas.py +522 -0
- cli/tui/screens/templates.py +734 -0
- cli/tui/screens/tool_configs.py +335 -0
- cli/tui/styles/__init__.py +0 -0
- cli/tui/widgets/__init__.py +0 -0
- cli/tui/widgets/agent_create_modal.py +139 -0
- cli/tui/widgets/agent_form_modal.py +659 -0
- cli/tui/widgets/agent_update_modal.py +299 -0
- cli/tui/widgets/base_form_modal.py +77 -0
- cli/tui/widgets/confirm_modal.py +75 -0
- cli/tui/widgets/help_modal.py +145 -0
- cli/tui/widgets/new_session_modal.py +328 -0
- cli/tui/widgets/schema_create_modal.py +271 -0
- cli/tui/widgets/schema_update_modal.py +188 -0
- cli/tui/widgets/status_footer.py +147 -0
- cli/tui/widgets/template_create_modal.py +502 -0
- cli/tui/widgets/template_update_modal.py +308 -0
- cli/tui/widgets/tool_config_create_modal.py +216 -0
- cli/tui/widgets/tool_config_update_modal.py +208 -0
cli/tui/chat/messages.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Textual Messages for inter-widget communication in the chat module.
|
|
2
|
+
|
|
3
|
+
These messages enable loose coupling between widgets. Child widgets post
|
|
4
|
+
messages that bubble up to parents, allowing the ChatScreen orchestrator
|
|
5
|
+
to coordinate actions without tight coupling.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from uuid import UUID
|
|
9
|
+
|
|
10
|
+
from textual.message import Message
|
|
11
|
+
|
|
12
|
+
from alloy_runtime_types.dtos.sessions import MessageResponse, SessionSummary
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# =============================================================================
|
|
16
|
+
# Session Events
|
|
17
|
+
# =============================================================================
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SessionSelected(Message):
|
|
21
|
+
"""User selected a session from the sidebar."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, session_id: UUID) -> None:
|
|
24
|
+
self.session_id = session_id
|
|
25
|
+
super().__init__()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SessionCreated(Message):
|
|
29
|
+
"""A new session was successfully created."""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
session_id: UUID,
|
|
34
|
+
agent_id: UUID | None,
|
|
35
|
+
agent_name: str | None,
|
|
36
|
+
) -> None:
|
|
37
|
+
self.session_id = session_id
|
|
38
|
+
self.agent_id = agent_id
|
|
39
|
+
self.agent_name = agent_name
|
|
40
|
+
super().__init__()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class SessionsLoaded(Message):
|
|
44
|
+
"""Sessions list was loaded/refreshed."""
|
|
45
|
+
|
|
46
|
+
def __init__(self, sessions: list[SessionSummary]) -> None:
|
|
47
|
+
self.sessions = sessions
|
|
48
|
+
super().__init__()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class SessionsLoadError(Message):
|
|
52
|
+
"""Failed to load sessions."""
|
|
53
|
+
|
|
54
|
+
def __init__(self, error: str) -> None:
|
|
55
|
+
self.error = error
|
|
56
|
+
super().__init__()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# =============================================================================
|
|
60
|
+
# Message Events
|
|
61
|
+
# =============================================================================
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class MessagesLoaded(Message):
|
|
65
|
+
"""Messages for current session were loaded."""
|
|
66
|
+
|
|
67
|
+
def __init__(self, messages: list[MessageResponse]) -> None:
|
|
68
|
+
self.messages = messages
|
|
69
|
+
super().__init__()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class SendMessageRequested(Message):
|
|
73
|
+
"""User wants to send a message."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, content: str) -> None:
|
|
76
|
+
self.content = content
|
|
77
|
+
super().__init__()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class RegenerateRequested(Message):
|
|
81
|
+
"""User wants to regenerate the last response."""
|
|
82
|
+
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class UndoRequested(Message):
|
|
87
|
+
"""User wants to undo the last conversation turn."""
|
|
88
|
+
|
|
89
|
+
pass
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# =============================================================================
|
|
93
|
+
# Streaming Events
|
|
94
|
+
# =============================================================================
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class StreamingStarted(Message):
|
|
98
|
+
"""Response streaming has begun."""
|
|
99
|
+
|
|
100
|
+
def __init__(self, execution_id: UUID) -> None:
|
|
101
|
+
self.execution_id = execution_id
|
|
102
|
+
super().__init__()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class StreamingChunk(Message):
|
|
106
|
+
"""A chunk of streamed content received."""
|
|
107
|
+
|
|
108
|
+
def __init__(self, content: str, is_reasoning: bool = False) -> None:
|
|
109
|
+
self.content = content
|
|
110
|
+
self.is_reasoning = is_reasoning
|
|
111
|
+
super().__init__()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class StreamingCompleted(Message):
|
|
115
|
+
"""Response streaming finished successfully."""
|
|
116
|
+
|
|
117
|
+
def __init__(self, full_content: str) -> None:
|
|
118
|
+
self.full_content = full_content
|
|
119
|
+
super().__init__()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class StreamingError(Message):
|
|
123
|
+
"""Error occurred during streaming."""
|
|
124
|
+
|
|
125
|
+
def __init__(self, error: str) -> None:
|
|
126
|
+
self.error = error
|
|
127
|
+
super().__init__()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# =============================================================================
|
|
131
|
+
# UI Events
|
|
132
|
+
# =============================================================================
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class FocusInputRequested(Message):
|
|
136
|
+
"""Request to focus the chat input."""
|
|
137
|
+
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class FocusSessionSearchRequested(Message):
|
|
142
|
+
"""Request to focus the session search input."""
|
|
143
|
+
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class CopyToClipboardRequested(Message):
|
|
148
|
+
"""Request to copy content to clipboard."""
|
|
149
|
+
|
|
150
|
+
def __init__(self, content: str, label: str = "Content") -> None:
|
|
151
|
+
self.content = content
|
|
152
|
+
self.label = label
|
|
153
|
+
super().__init__()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class NewSessionRequested(Message):
|
|
157
|
+
"""User wants to create a new session."""
|
|
158
|
+
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class OpenEditorRequested(Message):
|
|
163
|
+
"""User wants to open the current input in $EDITOR."""
|
|
164
|
+
|
|
165
|
+
def __init__(self, current_text: str) -> None:
|
|
166
|
+
self.current_text = current_text
|
|
167
|
+
super().__init__()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class EditorContentReturned(Message):
|
|
171
|
+
"""Content was returned from the external editor."""
|
|
172
|
+
|
|
173
|
+
def __init__(self, content: str) -> None:
|
|
174
|
+
self.content = content
|
|
175
|
+
super().__init__()
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# =============================================================================
|
|
179
|
+
# Slash Command Events
|
|
180
|
+
# =============================================================================
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class SlashCommandExecuted(Message):
|
|
184
|
+
"""A slash command was executed by the user.
|
|
185
|
+
|
|
186
|
+
Some commands are handled directly (e.g., /clear, /copy), while others
|
|
187
|
+
signal actions to be performed by the screen (e.g., /new, /undo).
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
def __init__(self, command_name: str, args: str = "") -> None:
|
|
191
|
+
self.command_name = command_name
|
|
192
|
+
self.args = args
|
|
193
|
+
super().__init__()
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
class UnknownSlashCommand(Message):
|
|
197
|
+
"""User entered an unknown slash command."""
|
|
198
|
+
|
|
199
|
+
def __init__(self, command_text: str) -> None:
|
|
200
|
+
self.command_text = command_text
|
|
201
|
+
super().__init__()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# =============================================================================
|
|
205
|
+
# Welcome Screen Events
|
|
206
|
+
# =============================================================================
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
class AgentQuickSelected(Message):
|
|
210
|
+
"""User selected an agent from quick start list for immediate session creation."""
|
|
211
|
+
|
|
212
|
+
def __init__(self, agent_id: UUID, agent_name: str) -> None:
|
|
213
|
+
self.agent_id = agent_id
|
|
214
|
+
self.agent_name = agent_name
|
|
215
|
+
super().__init__()
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class RecentSessionSelected(Message):
|
|
219
|
+
"""User selected a recent session from the welcome screen."""
|
|
220
|
+
|
|
221
|
+
def __init__(self, session_id: UUID) -> None:
|
|
222
|
+
self.session_id = session_id
|
|
223
|
+
super().__init__()
|
cli/tui/chat/pane.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""ChatPane - an individual chat pane with its own session/model.
|
|
2
|
+
|
|
3
|
+
This widget encapsulates the main chat area (header, messages, input) and can be
|
|
4
|
+
used standalone or in a split-pane layout for comparing different models.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING, Any
|
|
8
|
+
|
|
9
|
+
from textual.app import ComposeResult
|
|
10
|
+
from textual.containers import Vertical
|
|
11
|
+
from textual.widget import Widget
|
|
12
|
+
|
|
13
|
+
from cli.tui.chat.store import ChatStore
|
|
14
|
+
from cli.tui.chat.widgets.chat_header import ChatHeader
|
|
15
|
+
from cli.tui.chat.widgets.chat_input import ChatInput
|
|
16
|
+
from cli.tui.chat.widgets.injection_popup import InjectionSuggestionPopup
|
|
17
|
+
from cli.tui.chat.widgets.message_display import MessageDisplay
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ChatPane(Widget):
|
|
24
|
+
"""Individual chat pane with its own session/model.
|
|
25
|
+
|
|
26
|
+
This widget contains the main chat interface components:
|
|
27
|
+
- ChatHeader: Shows current session/model info
|
|
28
|
+
- MessageDisplay: Shows message history and streaming content
|
|
29
|
+
- ChatInput: Text input for composing messages
|
|
30
|
+
|
|
31
|
+
Each pane has its own ChatStore, allowing independent sessions
|
|
32
|
+
or the same session with different views.
|
|
33
|
+
|
|
34
|
+
Usage:
|
|
35
|
+
# Single pane
|
|
36
|
+
pane = ChatPane(store=my_store, pane_id="main")
|
|
37
|
+
|
|
38
|
+
# Split panes for comparison
|
|
39
|
+
left_pane = ChatPane(store=left_store, pane_id="left")
|
|
40
|
+
right_pane = ChatPane(store=right_store, pane_id="right")
|
|
41
|
+
"""
|
|
42
|
+
|
|
43
|
+
DEFAULT_CSS = """
|
|
44
|
+
ChatPane {
|
|
45
|
+
width: 1fr;
|
|
46
|
+
height: 100%;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
ChatPane.focused-pane {
|
|
50
|
+
border: tall $accent;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
ChatPane.unfocused-pane {
|
|
54
|
+
border: tall $surface-darken-2;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
ChatPane #pane-container {
|
|
58
|
+
height: 100%;
|
|
59
|
+
}
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
store: ChatStore,
|
|
65
|
+
pane_id: str = "main",
|
|
66
|
+
show_border: bool = False,
|
|
67
|
+
**kwargs: Any,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Initialize the chat pane.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
store: ChatStore instance for state management.
|
|
73
|
+
pane_id: Unique identifier for this pane (e.g., "left", "right", "main").
|
|
74
|
+
show_border: Whether to show a border around the pane (for split view).
|
|
75
|
+
"""
|
|
76
|
+
super().__init__(**kwargs)
|
|
77
|
+
self._store = store
|
|
78
|
+
self._pane_id = pane_id
|
|
79
|
+
self._show_border = show_border
|
|
80
|
+
|
|
81
|
+
@property
|
|
82
|
+
def pane_id(self) -> str:
|
|
83
|
+
"""Get the pane identifier."""
|
|
84
|
+
return self._pane_id
|
|
85
|
+
|
|
86
|
+
@property
|
|
87
|
+
def store(self) -> ChatStore:
|
|
88
|
+
"""Get the pane's ChatStore."""
|
|
89
|
+
return self._store
|
|
90
|
+
|
|
91
|
+
def compose(self) -> ComposeResult:
|
|
92
|
+
"""Compose the pane layout."""
|
|
93
|
+
with Vertical(id="pane-container"):
|
|
94
|
+
yield ChatHeader(self._store)
|
|
95
|
+
yield MessageDisplay(self._store)
|
|
96
|
+
# Popup is positioned here (above ChatInput) so it can float over messages
|
|
97
|
+
yield InjectionSuggestionPopup(id="injection-popup")
|
|
98
|
+
yield ChatInput(self._store)
|
|
99
|
+
|
|
100
|
+
def on_mount(self) -> None:
|
|
101
|
+
"""Apply border styling based on configuration."""
|
|
102
|
+
if self._show_border:
|
|
103
|
+
self.add_class("unfocused-pane")
|
|
104
|
+
|
|
105
|
+
def set_focused(self, focused: bool) -> None:
|
|
106
|
+
"""Update the pane's focused state (visual indicator).
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
focused: Whether this pane is the active/focused pane.
|
|
110
|
+
"""
|
|
111
|
+
if not self._show_border:
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
if focused:
|
|
115
|
+
self.remove_class("unfocused-pane")
|
|
116
|
+
self.add_class("focused-pane")
|
|
117
|
+
else:
|
|
118
|
+
self.remove_class("focused-pane")
|
|
119
|
+
self.add_class("unfocused-pane")
|
|
120
|
+
|
|
121
|
+
def set_show_border(self, show_border: bool) -> None:
|
|
122
|
+
"""Enable or disable border styling for the pane."""
|
|
123
|
+
self._show_border = show_border
|
|
124
|
+
if not show_border:
|
|
125
|
+
self.remove_class("focused-pane")
|
|
126
|
+
self.remove_class("unfocused-pane")
|
|
127
|
+
|
|
128
|
+
def focus_input(self) -> None:
|
|
129
|
+
"""Focus the chat input in this pane."""
|
|
130
|
+
chat_input = self.query_one(ChatInput)
|
|
131
|
+
chat_input.focus_input()
|
|
132
|
+
|
|
133
|
+
def clear_input(self) -> None:
|
|
134
|
+
"""Clear the chat input in this pane."""
|
|
135
|
+
chat_input = self.query_one(ChatInput)
|
|
136
|
+
chat_input.clear_input()
|
|
137
|
+
|
|
138
|
+
def get_input_text(self) -> str:
|
|
139
|
+
"""Get the current text from the chat input."""
|
|
140
|
+
chat_input = self.query_one(ChatInput)
|
|
141
|
+
return chat_input.get_text()
|
|
File without changes
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Base utilities for message renderers.
|
|
2
|
+
|
|
3
|
+
Provides common functionality used by all renderer implementations.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from alloy_runtime_types.dtos.sessions import MessageResponse
|
|
7
|
+
|
|
8
|
+
from cli.tui.chat.types import (
|
|
9
|
+
extract_message_text,
|
|
10
|
+
extract_message_text_for_display,
|
|
11
|
+
format_time,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def format_user_header(msg: MessageResponse, *, include_time: bool = True) -> str:
|
|
16
|
+
"""Format a user message header.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
msg: The message to format.
|
|
20
|
+
include_time: Whether to include the timestamp.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Formatted header string.
|
|
24
|
+
"""
|
|
25
|
+
if include_time:
|
|
26
|
+
return f"You [{format_time(msg.created_at)}]:"
|
|
27
|
+
return "You:"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def format_assistant_header(msg: MessageResponse, *, include_time: bool = True) -> str:
|
|
31
|
+
"""Format an assistant message header.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
msg: The message to format.
|
|
35
|
+
include_time: Whether to include the timestamp.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Formatted header string.
|
|
39
|
+
"""
|
|
40
|
+
if include_time:
|
|
41
|
+
return f"Assistant [{format_time(msg.created_at)}]:"
|
|
42
|
+
return "Assistant:"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_message_body(msg: MessageResponse) -> str:
|
|
46
|
+
"""Extract the body text from a message.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
msg: The message to extract text from.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Message body text.
|
|
53
|
+
"""
|
|
54
|
+
return extract_message_text(msg)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_message_body_for_display(msg: MessageResponse) -> tuple[str, bool]:
|
|
58
|
+
"""Extract the body text from a message, truncating long content.
|
|
59
|
+
|
|
60
|
+
Long content (e.g., from @fragment() injections) is truncated to prevent
|
|
61
|
+
overwhelming the TUI. The first/last 50 chars are shown with a truncation
|
|
62
|
+
indicator.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
msg: The message to extract text from.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Tuple of (body_text, was_truncated)
|
|
69
|
+
- body_text: The message body, possibly truncated
|
|
70
|
+
- was_truncated: True if any content was truncated
|
|
71
|
+
"""
|
|
72
|
+
return extract_message_text_for_display(msg)
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Markdown renderer for chat messages.
|
|
2
|
+
|
|
3
|
+
Renders messages with Rich markdown formatting for display in Textual widgets.
|
|
4
|
+
Uses Rich's Markdown class to properly render markdown syntax (headers, code blocks,
|
|
5
|
+
lists, etc.) in assistant responses.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import re
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
|
|
11
|
+
from rich.console import Group, RenderableType
|
|
12
|
+
from rich.markdown import Markdown
|
|
13
|
+
from rich.text import Text
|
|
14
|
+
|
|
15
|
+
from alloy_runtime_types.dtos.sessions import MessageResponse
|
|
16
|
+
|
|
17
|
+
from cli.tui.chat.renderers.base import (
|
|
18
|
+
get_message_body,
|
|
19
|
+
get_message_body_for_display,
|
|
20
|
+
)
|
|
21
|
+
from cli.tui.chat.types import extract_thinking_content, format_time
|
|
22
|
+
|
|
23
|
+
# Spinner frames for streaming indicator
|
|
24
|
+
SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
|
|
25
|
+
|
|
26
|
+
# Color for thinking/reasoning blocks
|
|
27
|
+
THINKING_COLOR = "#bb9af7" # tokyo-night purple accent
|
|
28
|
+
|
|
29
|
+
# Color for truncated content indicator
|
|
30
|
+
TRUNCATED_COLOR = "#f7768e" # tokyo-night red/pink for attention
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class MarkdownRenderer:
|
|
34
|
+
"""Renders messages with Rich markdown formatting.
|
|
35
|
+
|
|
36
|
+
Features:
|
|
37
|
+
- User messages with muted styling
|
|
38
|
+
- Assistant messages rendered as proper markdown (code blocks, headers, etc.)
|
|
39
|
+
- Agent/model name in header
|
|
40
|
+
- Timestamps on each message
|
|
41
|
+
- Spinner during streaming
|
|
42
|
+
- Optimistic user message display
|
|
43
|
+
- Thinking/reasoning blocks with distinctive styling
|
|
44
|
+
|
|
45
|
+
This is the default renderer for the chat interface. It uses Rich's Markdown
|
|
46
|
+
class to properly render markdown syntax in assistant responses.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(self) -> None:
|
|
50
|
+
self._spinner_frame = 0
|
|
51
|
+
|
|
52
|
+
def render(
|
|
53
|
+
self,
|
|
54
|
+
messages: tuple[MessageResponse, ...],
|
|
55
|
+
streaming_content: str = "",
|
|
56
|
+
streaming_thinking_content: str = "",
|
|
57
|
+
pending_user_message: str | None = None,
|
|
58
|
+
assistant_name: str = "Assistant",
|
|
59
|
+
is_streaming: bool = False,
|
|
60
|
+
thinking_collapsed: bool = False,
|
|
61
|
+
) -> Group:
|
|
62
|
+
"""Render messages to a Rich Group with proper markdown formatting.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
messages: Tuple of message responses to render.
|
|
66
|
+
streaming_content: Partial content being streamed (appended to end).
|
|
67
|
+
streaming_thinking_content: Partial thinking/reasoning content being streamed.
|
|
68
|
+
pending_user_message: User message shown optimistically before server confirms.
|
|
69
|
+
assistant_name: Name to display for assistant (agent name or model).
|
|
70
|
+
is_streaming: Whether currently streaming a response.
|
|
71
|
+
thinking_collapsed: Whether thinking blocks should be collapsed.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Rich Group containing formatted renderables suitable for display.
|
|
75
|
+
"""
|
|
76
|
+
renderables: list[RenderableType] = []
|
|
77
|
+
|
|
78
|
+
# Colors: user messages in dim/muted, assistant in accent color
|
|
79
|
+
# Using colors that work well with tokyo-night theme
|
|
80
|
+
user_color = "dim white"
|
|
81
|
+
assistant_color = "#7aa2f7" # tokyo-night blue accent
|
|
82
|
+
|
|
83
|
+
for msg in messages:
|
|
84
|
+
time_str = format_time(msg.created_at)
|
|
85
|
+
|
|
86
|
+
if msg.role == "user":
|
|
87
|
+
# Only truncate user messages (template injections are user-initiated)
|
|
88
|
+
body, was_truncated = get_message_body_for_display(msg)
|
|
89
|
+
header = Text(f"You [{time_str}]:", style=f"bold {user_color}")
|
|
90
|
+
renderables.append(header)
|
|
91
|
+
# Style truncated content differently
|
|
92
|
+
if was_truncated:
|
|
93
|
+
renderables.append(self._style_truncated_content(body))
|
|
94
|
+
else:
|
|
95
|
+
renderables.append(Text(body))
|
|
96
|
+
else:
|
|
97
|
+
# Never truncate assistant messages - show full response
|
|
98
|
+
body = self._get_full_message_body(msg)
|
|
99
|
+
|
|
100
|
+
# For assistant messages, check for thinking content
|
|
101
|
+
thinking_content = extract_thinking_content(msg)
|
|
102
|
+
if thinking_content:
|
|
103
|
+
# Render thinking block (not streaming, use collapsed setting)
|
|
104
|
+
renderables.append(
|
|
105
|
+
self._render_thinking_block_renderable(
|
|
106
|
+
thinking_content,
|
|
107
|
+
is_streaming=False,
|
|
108
|
+
collapsed=thinking_collapsed,
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
renderables.append(Text(""))
|
|
112
|
+
|
|
113
|
+
header = Text(
|
|
114
|
+
f"{assistant_name} [{time_str}]:", style=f"bold {assistant_color}"
|
|
115
|
+
)
|
|
116
|
+
renderables.append(header)
|
|
117
|
+
# Render assistant body as proper markdown
|
|
118
|
+
renderables.append(Markdown(body))
|
|
119
|
+
|
|
120
|
+
renderables.append(Text(""))
|
|
121
|
+
|
|
122
|
+
# Show pending user message (optimistic UI)
|
|
123
|
+
if pending_user_message:
|
|
124
|
+
now_str = datetime.now().strftime("%H:%M")
|
|
125
|
+
header = Text(f"You [{now_str}]:", style=f"bold {user_color}")
|
|
126
|
+
renderables.append(header)
|
|
127
|
+
renderables.append(Text(pending_user_message))
|
|
128
|
+
renderables.append(Text(""))
|
|
129
|
+
|
|
130
|
+
# Show streaming thinking/reasoning block
|
|
131
|
+
if streaming_thinking_content:
|
|
132
|
+
renderables.append(
|
|
133
|
+
self._render_thinking_block_renderable(
|
|
134
|
+
streaming_thinking_content, is_streaming, thinking_collapsed
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
renderables.append(Text(""))
|
|
138
|
+
|
|
139
|
+
# Show streaming response with spinner
|
|
140
|
+
if is_streaming or streaming_content:
|
|
141
|
+
spinner = self._get_spinner() if is_streaming else ""
|
|
142
|
+
if streaming_content:
|
|
143
|
+
header = Text()
|
|
144
|
+
header.append(f"{assistant_name}:", style=f"bold {assistant_color}")
|
|
145
|
+
header.append(f" {spinner}", style="yellow")
|
|
146
|
+
renderables.append(header)
|
|
147
|
+
# Render streaming content as markdown too
|
|
148
|
+
renderables.append(Markdown(streaming_content))
|
|
149
|
+
elif not streaming_thinking_content:
|
|
150
|
+
# Only show "Thinking..." if we don't have actual thinking content
|
|
151
|
+
header = Text()
|
|
152
|
+
header.append(f"{assistant_name}:", style=f"bold {assistant_color}")
|
|
153
|
+
header.append(f" {spinner}", style="yellow")
|
|
154
|
+
renderables.append(header)
|
|
155
|
+
renderables.append(Text("Waiting for response...", style="dim"))
|
|
156
|
+
|
|
157
|
+
return Group(*renderables)
|
|
158
|
+
|
|
159
|
+
def _render_thinking_block_renderable(
|
|
160
|
+
self,
|
|
161
|
+
content: str,
|
|
162
|
+
is_streaming: bool = False,
|
|
163
|
+
collapsed: bool = False,
|
|
164
|
+
) -> Text:
|
|
165
|
+
"""Render a thinking/reasoning block with distinctive styling.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
content: The thinking content to render.
|
|
169
|
+
is_streaming: Whether the thinking is still streaming.
|
|
170
|
+
collapsed: Whether to show collapsed view.
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Rich Text object for the thinking block.
|
|
174
|
+
"""
|
|
175
|
+
spinner = self._get_spinner() if is_streaming else ""
|
|
176
|
+
|
|
177
|
+
if collapsed:
|
|
178
|
+
# Show collapsed indicator with line count
|
|
179
|
+
line_count = content.count("\n") + 1
|
|
180
|
+
result = Text()
|
|
181
|
+
result.append(
|
|
182
|
+
f"◆ Thinking ({line_count} lines)... {spinner}", style=THINKING_COLOR
|
|
183
|
+
)
|
|
184
|
+
result.append(" (expand with /thinking)", style="dim")
|
|
185
|
+
return result
|
|
186
|
+
|
|
187
|
+
# Full thinking block with border styling
|
|
188
|
+
lines: list[str] = []
|
|
189
|
+
lines.append(f"◆ Thinking {spinner}")
|
|
190
|
+
|
|
191
|
+
# Indent and dim the thinking content
|
|
192
|
+
for line in content.split("\n"):
|
|
193
|
+
lines.append(f"│ {line}")
|
|
194
|
+
|
|
195
|
+
result = Text()
|
|
196
|
+
result.append(lines[0] + "\n", style=f"bold {THINKING_COLOR}")
|
|
197
|
+
for line in lines[1:]:
|
|
198
|
+
result.append(line + "\n", style="dim italic")
|
|
199
|
+
|
|
200
|
+
return result
|
|
201
|
+
|
|
202
|
+
def _get_spinner(self) -> str:
|
|
203
|
+
"""Get the current spinner frame and advance to next."""
|
|
204
|
+
frame = SPINNER_FRAMES[self._spinner_frame % len(SPINNER_FRAMES)]
|
|
205
|
+
self._spinner_frame += 1
|
|
206
|
+
return frame
|
|
207
|
+
|
|
208
|
+
def _get_full_message_body(self, msg: MessageResponse) -> str:
|
|
209
|
+
"""Get the full message body without truncation.
|
|
210
|
+
|
|
211
|
+
Used for assistant messages which should never be truncated.
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
msg: The message to extract body from
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Full message body text
|
|
218
|
+
"""
|
|
219
|
+
return get_message_body(msg)
|
|
220
|
+
|
|
221
|
+
def _style_truncated_content(self, body: str) -> Text:
|
|
222
|
+
"""Style content that contains truncation indicators.
|
|
223
|
+
|
|
224
|
+
Highlights the truncation indicator (e.g., "[1,234 chars truncated]")
|
|
225
|
+
in a distinctive color so users can clearly see content was truncated.
|
|
226
|
+
|
|
227
|
+
Args:
|
|
228
|
+
body: The message body with truncation indicator(s)
|
|
229
|
+
|
|
230
|
+
Returns:
|
|
231
|
+
Rich Text object with styled truncation indicators
|
|
232
|
+
"""
|
|
233
|
+
result = Text()
|
|
234
|
+
# Pattern to match truncation indicators like "... [1,234 chars truncated] ..."
|
|
235
|
+
pattern = r"(\.\.\. \[[\d,]+ chars truncated\] \.\.\.)"
|
|
236
|
+
|
|
237
|
+
last_end = 0
|
|
238
|
+
for match in re.finditer(pattern, body):
|
|
239
|
+
# Add text before the match
|
|
240
|
+
if match.start() > last_end:
|
|
241
|
+
result.append(body[last_end : match.start()])
|
|
242
|
+
# Add the truncation indicator with styling
|
|
243
|
+
result.append(match.group(1), style=f"bold {TRUNCATED_COLOR}")
|
|
244
|
+
last_end = match.end()
|
|
245
|
+
|
|
246
|
+
# Add remaining text after last match
|
|
247
|
+
if last_end < len(body):
|
|
248
|
+
result.append(body[last_end:])
|
|
249
|
+
|
|
250
|
+
return result
|