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,734 @@
|
|
|
1
|
+
"""Templates screen for browsing and managing templates.
|
|
2
|
+
|
|
3
|
+
This screen provides:
|
|
4
|
+
- Search and browse templates
|
|
5
|
+
- View template content in preview panel
|
|
6
|
+
- Edit templates and create versions
|
|
7
|
+
- Copy template names and content
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
import subprocess
|
|
12
|
+
import tempfile
|
|
13
|
+
from dataclasses import dataclass, field
|
|
14
|
+
from typing import TYPE_CHECKING, Any
|
|
15
|
+
from uuid import UUID
|
|
16
|
+
|
|
17
|
+
from textual import on, work
|
|
18
|
+
from textual.app import ComposeResult
|
|
19
|
+
from textual.binding import Binding
|
|
20
|
+
from textual.containers import Horizontal, Vertical, VerticalScroll
|
|
21
|
+
from textual.widgets import Button, DataTable, Input, Static
|
|
22
|
+
|
|
23
|
+
from alloy_runtime_types.dtos.templates import (
|
|
24
|
+
CreateTemplateVersionRequest,
|
|
25
|
+
RenderTemplateRequest,
|
|
26
|
+
TemplateListItemResponse,
|
|
27
|
+
UpdateTemplateRequest,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
from cli.infrastructure.forms.resolution_modal import ResolutionModal
|
|
31
|
+
from cli.infrastructure.macro_parser import (
|
|
32
|
+
MacroMatch,
|
|
33
|
+
build_jinja_replacement,
|
|
34
|
+
compile_content,
|
|
35
|
+
deduplicate_macros,
|
|
36
|
+
parse_macros,
|
|
37
|
+
)
|
|
38
|
+
from cli.infrastructure.tui.clipboard import copy_to_clipboard
|
|
39
|
+
from cli.infrastructure.tui.formatters import format_datetime, format_tags_list
|
|
40
|
+
from cli.tui.screens.base import BrowserWidget
|
|
41
|
+
from cli.tui.screens.nav_screen import NavScreen
|
|
42
|
+
from cli.tui.widgets.template_create_modal import (
|
|
43
|
+
TemplateCreateModal,
|
|
44
|
+
TemplateCreateResult,
|
|
45
|
+
)
|
|
46
|
+
from cli.tui.widgets.template_update_modal import (
|
|
47
|
+
TemplateUpdateModal,
|
|
48
|
+
TemplateUpdateResult,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
if TYPE_CHECKING:
|
|
52
|
+
from cli.tui.app import AlloyRuntimeApp
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _macro_list_factory() -> list[MacroMatch]:
|
|
56
|
+
return []
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _replacements_factory() -> dict[str, str]:
|
|
60
|
+
return {}
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass
|
|
64
|
+
class PendingEdit:
|
|
65
|
+
"""State for a template edit waiting on macro resolution."""
|
|
66
|
+
|
|
67
|
+
template_id: str
|
|
68
|
+
content: str
|
|
69
|
+
is_new_version: bool
|
|
70
|
+
all_macros: list[MacroMatch] = field(default_factory=_macro_list_factory)
|
|
71
|
+
pending_macros: list[MacroMatch] = field(default_factory=_macro_list_factory)
|
|
72
|
+
replacements: dict[str, str] = field(default_factory=_replacements_factory)
|
|
73
|
+
current_macro_index: int = 0
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class TemplatesWidget(BrowserWidget[TemplateListItemResponse]):
|
|
77
|
+
"""Widget for browsing and managing templates."""
|
|
78
|
+
|
|
79
|
+
app: "AlloyRuntimeApp"
|
|
80
|
+
|
|
81
|
+
TITLE = "Templates"
|
|
82
|
+
TABLE_COLUMNS = ["Type", "Name", "Visibility", "Version", "Tags", "Updated"]
|
|
83
|
+
|
|
84
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
85
|
+
super().__init__(**kwargs)
|
|
86
|
+
self._pending_edit: PendingEdit | None = None
|
|
87
|
+
|
|
88
|
+
def compose(self) -> ComposeResult:
|
|
89
|
+
"""Compose the browser layout with a New Template button."""
|
|
90
|
+
with Vertical(id="browser-root"):
|
|
91
|
+
with Vertical(id="search-container"):
|
|
92
|
+
with Horizontal(id="search-row"):
|
|
93
|
+
yield Input(
|
|
94
|
+
placeholder=f"Search {self.TITLE.lower()}...",
|
|
95
|
+
id="search-input",
|
|
96
|
+
)
|
|
97
|
+
yield Button("+ New", id="new-template-btn", variant="primary")
|
|
98
|
+
yield Static("Loading...", id="status-bar")
|
|
99
|
+
|
|
100
|
+
with Horizontal(id="main-container"):
|
|
101
|
+
with Vertical(id="list-container"):
|
|
102
|
+
yield DataTable(id="data-table", cursor_type="row")
|
|
103
|
+
|
|
104
|
+
with Vertical(id="detail-container"):
|
|
105
|
+
yield Static("Preview", id="preview-title")
|
|
106
|
+
with VerticalScroll(id="preview-scroll"):
|
|
107
|
+
yield Static(
|
|
108
|
+
"Select an item to view details",
|
|
109
|
+
id="preview-content",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
@on(Button.Pressed, "#new-template-btn")
|
|
113
|
+
def _on_new_template_btn_pressed(self, event: Button.Pressed) -> None:
|
|
114
|
+
"""Handle New Template button press."""
|
|
115
|
+
self.open_new_template_modal()
|
|
116
|
+
|
|
117
|
+
async def _fetch_items(
|
|
118
|
+
self, query: str
|
|
119
|
+
) -> tuple[list[TemplateListItemResponse], int]:
|
|
120
|
+
"""Fetch templates from the API."""
|
|
121
|
+
client = await self._ensure_client()
|
|
122
|
+
response = await client.list_templates(
|
|
123
|
+
search=query if query else None,
|
|
124
|
+
limit=50,
|
|
125
|
+
include_content=True,
|
|
126
|
+
)
|
|
127
|
+
return response.templates, response.total_count
|
|
128
|
+
|
|
129
|
+
def _format_row(self, item: TemplateListItemResponse) -> tuple[str, ...]:
|
|
130
|
+
"""Format a template as a table row."""
|
|
131
|
+
tags_dicts = [{"tag_path": t.tag_path} for t in item.tags]
|
|
132
|
+
name = item.name[:27] + "..." if len(item.name) > 30 else item.name
|
|
133
|
+
return (
|
|
134
|
+
item.content_type_id,
|
|
135
|
+
name,
|
|
136
|
+
item.visibility_id,
|
|
137
|
+
f"v{item.latest_version_number}",
|
|
138
|
+
format_tags_list(tags_dicts, max_display=2),
|
|
139
|
+
format_datetime(item.updated_at),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def _update_preview(self, item: TemplateListItemResponse) -> None:
|
|
143
|
+
"""Update the preview pane with template details."""
|
|
144
|
+
preview = self.query_one("#preview-content", Static)
|
|
145
|
+
|
|
146
|
+
tags_display = ", ".join(t.tag_path for t in item.tags) if item.tags else "-"
|
|
147
|
+
vars_display = (
|
|
148
|
+
", ".join(item.required_variables) if item.required_variables else "-"
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
created = format_datetime(item.created_at)
|
|
152
|
+
updated = format_datetime(item.updated_at)
|
|
153
|
+
version_created = format_datetime(item.latest_version_created_at)
|
|
154
|
+
|
|
155
|
+
content_display = item.latest_version_content or "(content not available)"
|
|
156
|
+
max_content_length = 2000
|
|
157
|
+
if len(content_display) > max_content_length:
|
|
158
|
+
content_display = content_display[:max_content_length] + "\n... (truncated)"
|
|
159
|
+
|
|
160
|
+
preview_text = f"""[bold]Template ID:[/] {item.id}
|
|
161
|
+
[bold]Version ID:[/] {item.latest_version_id}
|
|
162
|
+
[bold]Name:[/] {item.name}
|
|
163
|
+
[bold]Type:[/] {item.content_type_id}
|
|
164
|
+
[bold]Visibility:[/] {item.visibility_id}
|
|
165
|
+
[bold]Version:[/] v{item.latest_version_number}
|
|
166
|
+
|
|
167
|
+
[bold]Description:[/]
|
|
168
|
+
{item.description or "(no description)"}
|
|
169
|
+
|
|
170
|
+
[bold]Required Variables:[/] {vars_display}
|
|
171
|
+
[bold]Tags:[/] {tags_display}
|
|
172
|
+
|
|
173
|
+
[bold]Created:[/] {created}
|
|
174
|
+
[bold]Updated:[/] {updated}
|
|
175
|
+
[bold]Version Created:[/] {version_created}
|
|
176
|
+
|
|
177
|
+
[bold]Content:[/]
|
|
178
|
+
{content_display}"""
|
|
179
|
+
|
|
180
|
+
preview.update(preview_text)
|
|
181
|
+
|
|
182
|
+
# =========================================================================
|
|
183
|
+
# Public methods for parent Screen to call
|
|
184
|
+
# =========================================================================
|
|
185
|
+
|
|
186
|
+
def copy_template_name(self) -> None:
|
|
187
|
+
"""Copy the template name to clipboard."""
|
|
188
|
+
template = self.get_selected_item()
|
|
189
|
+
if not template:
|
|
190
|
+
self.app.notify("No template selected", severity="warning")
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
copy_to_clipboard(self.app, template.name, "Template name")
|
|
194
|
+
|
|
195
|
+
def copy_template_content(self) -> None:
|
|
196
|
+
"""Copy the template content to clipboard."""
|
|
197
|
+
template = self.get_selected_item()
|
|
198
|
+
if not template:
|
|
199
|
+
self.app.notify("No template to copy", severity="warning")
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
content = template.latest_version_content
|
|
203
|
+
if content:
|
|
204
|
+
copy_to_clipboard(self.app, content, "Template content")
|
|
205
|
+
else:
|
|
206
|
+
self.app.notify("No content available", severity="warning")
|
|
207
|
+
|
|
208
|
+
def edit_template_content(self) -> None:
|
|
209
|
+
"""Open template content in $EDITOR for editing.
|
|
210
|
+
|
|
211
|
+
For single-version templates, updates the existing version.
|
|
212
|
+
For multi-version templates, automatically creates a new version.
|
|
213
|
+
If content changes and contains macros, opens macro resolution flow before saving.
|
|
214
|
+
"""
|
|
215
|
+
template = self.get_selected_item()
|
|
216
|
+
if not template:
|
|
217
|
+
self.app.notify("No template to edit", severity="warning")
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
# Check if template has multiple versions
|
|
221
|
+
is_multi_version = template.latest_version_number > 1
|
|
222
|
+
|
|
223
|
+
if is_multi_version:
|
|
224
|
+
# For multi-version templates, edit will create a new version
|
|
225
|
+
self.app.notify(
|
|
226
|
+
f"Template has {template.latest_version_number} versions - edit will create v{template.latest_version_number + 1}",
|
|
227
|
+
severity="information",
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
new_content = self._open_editor_for_template(
|
|
231
|
+
template.name,
|
|
232
|
+
template.latest_version_content or "",
|
|
233
|
+
)
|
|
234
|
+
if new_content is None:
|
|
235
|
+
return
|
|
236
|
+
|
|
237
|
+
if new_content == (template.latest_version_content or ""):
|
|
238
|
+
self.app.notify("No changes made", severity="information")
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
self._handle_content_after_edit(
|
|
242
|
+
template_id=str(template.id),
|
|
243
|
+
content=new_content,
|
|
244
|
+
is_new_version=is_multi_version, # Use versioning flow for multi-version templates
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
def create_template_version(self) -> None:
|
|
248
|
+
"""Create a new version of the template in $EDITOR.
|
|
249
|
+
|
|
250
|
+
Opens the current content in the editor. If content changes and
|
|
251
|
+
contains macros, opens macro resolution flow before creating version.
|
|
252
|
+
"""
|
|
253
|
+
template = self.get_selected_item()
|
|
254
|
+
if not template:
|
|
255
|
+
self.app.notify("No template selected", severity="warning")
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
version_hint = f"_v{template.latest_version_number + 1}"
|
|
259
|
+
new_content = self._open_editor_for_template(
|
|
260
|
+
f"{template.name}{version_hint}",
|
|
261
|
+
template.latest_version_content or "",
|
|
262
|
+
)
|
|
263
|
+
if new_content is None:
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
if new_content == (template.latest_version_content or ""):
|
|
267
|
+
self.app.notify(
|
|
268
|
+
"No changes made - version not created", severity="information"
|
|
269
|
+
)
|
|
270
|
+
return
|
|
271
|
+
|
|
272
|
+
self._handle_content_after_edit(
|
|
273
|
+
template_id=str(template.id),
|
|
274
|
+
content=new_content,
|
|
275
|
+
is_new_version=True,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
def _open_editor_for_template(
|
|
279
|
+
self, name_hint: str, initial_content: str
|
|
280
|
+
) -> str | None:
|
|
281
|
+
"""Open external editor with template content.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
name_hint: Name to include in temp file name
|
|
285
|
+
initial_content: Initial content for the editor
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Edited content, or None if editor failed
|
|
289
|
+
"""
|
|
290
|
+
editor = os.environ.get("EDITOR") or os.environ.get("VISUAL") or "vi"
|
|
291
|
+
|
|
292
|
+
try:
|
|
293
|
+
with tempfile.NamedTemporaryFile(
|
|
294
|
+
mode="w",
|
|
295
|
+
suffix=".jinja2",
|
|
296
|
+
prefix=f"template_{name_hint}_",
|
|
297
|
+
delete=False,
|
|
298
|
+
encoding="utf-8",
|
|
299
|
+
) as f:
|
|
300
|
+
f.write(initial_content)
|
|
301
|
+
temp_path = f.name
|
|
302
|
+
|
|
303
|
+
with self.app.suspend():
|
|
304
|
+
subprocess.run([editor, temp_path], check=False)
|
|
305
|
+
|
|
306
|
+
with open(temp_path, encoding="utf-8") as f:
|
|
307
|
+
new_content = f.read()
|
|
308
|
+
|
|
309
|
+
try:
|
|
310
|
+
os.unlink(temp_path)
|
|
311
|
+
except OSError:
|
|
312
|
+
pass
|
|
313
|
+
|
|
314
|
+
return new_content
|
|
315
|
+
|
|
316
|
+
except Exception as e:
|
|
317
|
+
self.app.notify(f"Failed to open editor: {e}", severity="error")
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
def _handle_content_after_edit(
|
|
321
|
+
self, template_id: str, content: str, is_new_version: bool
|
|
322
|
+
) -> None:
|
|
323
|
+
"""Handle content after editor closes - check for macros and save.
|
|
324
|
+
|
|
325
|
+
If the content contains macros, starts the macro resolution flow.
|
|
326
|
+
Otherwise, saves/creates version immediately.
|
|
327
|
+
"""
|
|
328
|
+
all_macros = parse_macros(content)
|
|
329
|
+
|
|
330
|
+
if not all_macros:
|
|
331
|
+
# No macros - save immediately
|
|
332
|
+
if is_new_version:
|
|
333
|
+
self._create_template_version(template_id, content)
|
|
334
|
+
else:
|
|
335
|
+
self._save_template_content(template_id, content)
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
# Has macros - set up resolution flow
|
|
339
|
+
unique_macros = deduplicate_macros(all_macros)
|
|
340
|
+
self._pending_edit = PendingEdit(
|
|
341
|
+
template_id=template_id,
|
|
342
|
+
content=content,
|
|
343
|
+
is_new_version=is_new_version,
|
|
344
|
+
all_macros=all_macros,
|
|
345
|
+
pending_macros=unique_macros,
|
|
346
|
+
replacements={},
|
|
347
|
+
current_macro_index=0,
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
self.app.notify(
|
|
351
|
+
f"Found {len(unique_macros)} macro(s) to resolve",
|
|
352
|
+
severity="information",
|
|
353
|
+
)
|
|
354
|
+
self._start_macro_resolution()
|
|
355
|
+
|
|
356
|
+
def _start_macro_resolution(self) -> None:
|
|
357
|
+
"""Start or continue the macro resolution flow."""
|
|
358
|
+
if self._pending_edit is None:
|
|
359
|
+
return
|
|
360
|
+
|
|
361
|
+
self._process_next_macro()
|
|
362
|
+
|
|
363
|
+
def _process_next_macro(self) -> None:
|
|
364
|
+
"""Process the next macro in the resolution queue."""
|
|
365
|
+
if self._pending_edit is None:
|
|
366
|
+
return
|
|
367
|
+
|
|
368
|
+
if self._pending_edit.current_macro_index >= len(
|
|
369
|
+
self._pending_edit.pending_macros
|
|
370
|
+
):
|
|
371
|
+
# All macros processed - finalize
|
|
372
|
+
self._finalize_edit()
|
|
373
|
+
return
|
|
374
|
+
|
|
375
|
+
macro = self._pending_edit.pending_macros[
|
|
376
|
+
self._pending_edit.current_macro_index
|
|
377
|
+
]
|
|
378
|
+
self._open_resolution_modal_for_macro(macro)
|
|
379
|
+
|
|
380
|
+
@work(exclusive=True)
|
|
381
|
+
async def _open_resolution_modal_for_macro(self, macro: MacroMatch) -> None:
|
|
382
|
+
"""Open the resolution modal for a specific macro."""
|
|
383
|
+
try:
|
|
384
|
+
client = await self._ensure_client()
|
|
385
|
+
self.app.push_screen(
|
|
386
|
+
ResolutionModal(
|
|
387
|
+
client=client,
|
|
388
|
+
macro_type=macro.macro_type,
|
|
389
|
+
query=macro.query,
|
|
390
|
+
),
|
|
391
|
+
self._on_macro_resolved,
|
|
392
|
+
)
|
|
393
|
+
except Exception as e:
|
|
394
|
+
self.app.notify(f"Failed to open resolution modal: {e}", severity="error")
|
|
395
|
+
self._pending_edit = None
|
|
396
|
+
|
|
397
|
+
def _on_macro_resolved(self, result: dict[str, Any] | None) -> None:
|
|
398
|
+
"""Callback when a macro resolution modal is dismissed."""
|
|
399
|
+
if self._pending_edit is None:
|
|
400
|
+
return
|
|
401
|
+
|
|
402
|
+
macro = self._pending_edit.pending_macros[
|
|
403
|
+
self._pending_edit.current_macro_index
|
|
404
|
+
]
|
|
405
|
+
|
|
406
|
+
if result is not None:
|
|
407
|
+
# User selected a value - build jinja replacement
|
|
408
|
+
resolved_value = result["value"]
|
|
409
|
+
jinja_replacement = build_jinja_replacement(
|
|
410
|
+
macro.macro_type, resolved_value
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
# Apply to all matching macros (same type and query)
|
|
414
|
+
matching_count = 0
|
|
415
|
+
for m in self._pending_edit.all_macros:
|
|
416
|
+
if m.macro_type == macro.macro_type and m.query == macro.query:
|
|
417
|
+
self._pending_edit.replacements[m.original_text] = jinja_replacement
|
|
418
|
+
matching_count += 1
|
|
419
|
+
|
|
420
|
+
display = result.get("display", resolved_value)
|
|
421
|
+
if matching_count > 1:
|
|
422
|
+
self.app.notify(
|
|
423
|
+
f"Resolved {matching_count}x @{macro.macro_type}({macro.query}) -> {display}",
|
|
424
|
+
severity="information",
|
|
425
|
+
)
|
|
426
|
+
else:
|
|
427
|
+
self.app.notify(
|
|
428
|
+
f"Resolved @{macro.macro_type}({macro.query}) -> {display}",
|
|
429
|
+
severity="information",
|
|
430
|
+
)
|
|
431
|
+
else:
|
|
432
|
+
# User skipped - macro will remain unchanged
|
|
433
|
+
self.app.notify(
|
|
434
|
+
f"Skipped @{macro.macro_type}({macro.query})",
|
|
435
|
+
severity="warning",
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
# Move to next macro
|
|
439
|
+
self._pending_edit.current_macro_index += 1
|
|
440
|
+
self._process_next_macro()
|
|
441
|
+
|
|
442
|
+
def _finalize_edit(self) -> None:
|
|
443
|
+
"""Compile content with replacements and save."""
|
|
444
|
+
if self._pending_edit is None:
|
|
445
|
+
return
|
|
446
|
+
|
|
447
|
+
# Apply all replacements to get final content
|
|
448
|
+
final_content = compile_content(
|
|
449
|
+
self._pending_edit.content,
|
|
450
|
+
self._pending_edit.replacements,
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
resolved_count = len(
|
|
454
|
+
{
|
|
455
|
+
(m.macro_type, m.query)
|
|
456
|
+
for m in self._pending_edit.all_macros
|
|
457
|
+
if m.original_text in self._pending_edit.replacements
|
|
458
|
+
}
|
|
459
|
+
)
|
|
460
|
+
total_unique = len(self._pending_edit.pending_macros)
|
|
461
|
+
|
|
462
|
+
if resolved_count < total_unique:
|
|
463
|
+
self.app.notify(
|
|
464
|
+
f"Resolved {resolved_count}/{total_unique} macros (skipped macros unchanged)",
|
|
465
|
+
severity="information",
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
# Save based on edit type
|
|
469
|
+
if self._pending_edit.is_new_version:
|
|
470
|
+
self._create_template_version(self._pending_edit.template_id, final_content)
|
|
471
|
+
else:
|
|
472
|
+
self._save_template_content(self._pending_edit.template_id, final_content)
|
|
473
|
+
|
|
474
|
+
# Clear pending state
|
|
475
|
+
self._pending_edit = None
|
|
476
|
+
|
|
477
|
+
def open_new_template_modal(self) -> None:
|
|
478
|
+
"""Open the template creation modal."""
|
|
479
|
+
self._open_template_create_modal()
|
|
480
|
+
|
|
481
|
+
def update_template_metadata(self) -> None:
|
|
482
|
+
"""Open the template update modal for editing metadata."""
|
|
483
|
+
template = self.get_selected_item()
|
|
484
|
+
if not template:
|
|
485
|
+
self.app.notify("No template selected", severity="warning")
|
|
486
|
+
return
|
|
487
|
+
self._open_template_update_modal(template)
|
|
488
|
+
|
|
489
|
+
def render_and_open(self) -> None:
|
|
490
|
+
"""Render the template and open the result in $EDITOR.
|
|
491
|
+
|
|
492
|
+
Renders the selected template with empty variables and opens
|
|
493
|
+
the rendered output in the user's editor. No action is taken
|
|
494
|
+
on editor exit.
|
|
495
|
+
"""
|
|
496
|
+
template = self.get_selected_item()
|
|
497
|
+
if not template:
|
|
498
|
+
self.app.notify("No template selected", severity="warning")
|
|
499
|
+
return
|
|
500
|
+
|
|
501
|
+
# Warn if template has required variables
|
|
502
|
+
if template.required_variables:
|
|
503
|
+
vars_list = ", ".join(template.required_variables)
|
|
504
|
+
self.app.notify(
|
|
505
|
+
f"Note: template has variables ({vars_list}) - rendering with empty values",
|
|
506
|
+
severity="warning",
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
# Delegate to async worker
|
|
510
|
+
self._render_and_open_template(str(template.id), template.name)
|
|
511
|
+
|
|
512
|
+
# =========================================================================
|
|
513
|
+
# Internal async operations
|
|
514
|
+
# =========================================================================
|
|
515
|
+
|
|
516
|
+
@work(exclusive=True)
|
|
517
|
+
async def _save_template_content(self, template_id: str, content: str) -> None:
|
|
518
|
+
"""Save updated template content via API."""
|
|
519
|
+
try:
|
|
520
|
+
client = await self._ensure_client()
|
|
521
|
+
request = UpdateTemplateRequest(content=content)
|
|
522
|
+
await client.update_template(template_id, request)
|
|
523
|
+
|
|
524
|
+
self.app.notify("Template updated successfully", severity="information")
|
|
525
|
+
self._do_search(self._last_search)
|
|
526
|
+
|
|
527
|
+
except Exception as e:
|
|
528
|
+
error_msg = str(e)
|
|
529
|
+
if "multiple versions" in error_msg.lower() or "409" in error_msg:
|
|
530
|
+
self.app.notify(
|
|
531
|
+
"Cannot edit: template has multiple versions. Use Ctrl+V to create new version.",
|
|
532
|
+
severity="error",
|
|
533
|
+
)
|
|
534
|
+
else:
|
|
535
|
+
self.app.notify(f"Failed to update template: {e}", severity="error")
|
|
536
|
+
|
|
537
|
+
@work(exclusive=True)
|
|
538
|
+
async def _create_template_version(self, template_id: str, content: str) -> None:
|
|
539
|
+
"""Create a new template version via API."""
|
|
540
|
+
try:
|
|
541
|
+
client = await self._ensure_client()
|
|
542
|
+
request = CreateTemplateVersionRequest(content=content)
|
|
543
|
+
await client.create_template_version(UUID(template_id), request)
|
|
544
|
+
|
|
545
|
+
self.app.notify("New version created successfully", severity="information")
|
|
546
|
+
self._do_search(self._last_search)
|
|
547
|
+
|
|
548
|
+
except Exception as e:
|
|
549
|
+
self.app.notify(f"Failed to create version: {e}", severity="error")
|
|
550
|
+
|
|
551
|
+
@work(exclusive=True)
|
|
552
|
+
async def _open_template_create_modal(self) -> None:
|
|
553
|
+
"""Open the template creation modal."""
|
|
554
|
+
try:
|
|
555
|
+
client = await self._ensure_client()
|
|
556
|
+
self.app.push_screen(
|
|
557
|
+
TemplateCreateModal(client=client),
|
|
558
|
+
self._on_template_created,
|
|
559
|
+
)
|
|
560
|
+
except Exception as e:
|
|
561
|
+
self.app.notify(f"Failed to open template creator: {e}", severity="error")
|
|
562
|
+
|
|
563
|
+
def _on_template_created(self, result: TemplateCreateResult | None) -> None:
|
|
564
|
+
"""Callback when template creation modal is dismissed."""
|
|
565
|
+
if result is not None:
|
|
566
|
+
self.app.notify(
|
|
567
|
+
f"Created template: {result.template_name} (v{result.version_number})",
|
|
568
|
+
severity="information",
|
|
569
|
+
)
|
|
570
|
+
# Refresh the template list
|
|
571
|
+
self._do_search(self._last_search)
|
|
572
|
+
|
|
573
|
+
@work(exclusive=True)
|
|
574
|
+
async def _open_template_update_modal(
|
|
575
|
+
self, template: TemplateListItemResponse
|
|
576
|
+
) -> None:
|
|
577
|
+
"""Open the template update modal."""
|
|
578
|
+
try:
|
|
579
|
+
client = await self._ensure_client()
|
|
580
|
+
self.app.push_screen(
|
|
581
|
+
TemplateUpdateModal(client=client, template=template),
|
|
582
|
+
self._on_template_updated,
|
|
583
|
+
)
|
|
584
|
+
except Exception as e:
|
|
585
|
+
self.app.notify(f"Failed to open template editor: {e}", severity="error")
|
|
586
|
+
|
|
587
|
+
def _on_template_updated(self, result: TemplateUpdateResult | None) -> None:
|
|
588
|
+
"""Callback when template update modal is dismissed."""
|
|
589
|
+
if result is not None:
|
|
590
|
+
self.app.notify(
|
|
591
|
+
f"Updated template: {result.template_name}",
|
|
592
|
+
severity="information",
|
|
593
|
+
)
|
|
594
|
+
# Refresh the template list
|
|
595
|
+
self._do_search(self._last_search)
|
|
596
|
+
|
|
597
|
+
@work(exclusive=True)
|
|
598
|
+
async def _render_and_open_template(
|
|
599
|
+
self, template_id: str, template_name: str
|
|
600
|
+
) -> None:
|
|
601
|
+
"""Render template and open in $EDITOR."""
|
|
602
|
+
try:
|
|
603
|
+
client = await self._ensure_client()
|
|
604
|
+
request = RenderTemplateRequest(variables={})
|
|
605
|
+
response = await client.render_template(UUID(template_id), request)
|
|
606
|
+
|
|
607
|
+
rendered_text = response.rendered_text
|
|
608
|
+
editor = os.environ.get("EDITOR") or os.environ.get("VISUAL") or "vi"
|
|
609
|
+
|
|
610
|
+
# Create temp file with rendered content
|
|
611
|
+
with tempfile.NamedTemporaryFile(
|
|
612
|
+
mode="w",
|
|
613
|
+
suffix=".txt",
|
|
614
|
+
prefix=f"rendered_{template_name}_",
|
|
615
|
+
delete=False,
|
|
616
|
+
encoding="utf-8",
|
|
617
|
+
) as f:
|
|
618
|
+
f.write(rendered_text)
|
|
619
|
+
temp_path = f.name
|
|
620
|
+
|
|
621
|
+
# Suspend TUI and open editor
|
|
622
|
+
with self.app.suspend():
|
|
623
|
+
subprocess.run([editor, temp_path], check=False)
|
|
624
|
+
|
|
625
|
+
# Clean up temp file (no action on exit)
|
|
626
|
+
try:
|
|
627
|
+
os.unlink(temp_path)
|
|
628
|
+
except OSError:
|
|
629
|
+
pass
|
|
630
|
+
|
|
631
|
+
except Exception as e:
|
|
632
|
+
self.app.notify(f"Failed to render template: {e}", severity="error")
|
|
633
|
+
|
|
634
|
+
|
|
635
|
+
class TemplatesScreen(NavScreen):
|
|
636
|
+
"""Screen for browsing and managing templates.
|
|
637
|
+
|
|
638
|
+
This is a proper Textual Screen that wraps the TemplatesWidget
|
|
639
|
+
and provides keybindings that work regardless of focus.
|
|
640
|
+
"""
|
|
641
|
+
|
|
642
|
+
app: "AlloyRuntimeApp"
|
|
643
|
+
|
|
644
|
+
SCREEN_ID = "templates"
|
|
645
|
+
|
|
646
|
+
DEFAULT_CSS = """
|
|
647
|
+
TemplatesScreen #screen-root {
|
|
648
|
+
height: 1fr;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
TemplatesScreen .browser-widget {
|
|
652
|
+
height: 100%;
|
|
653
|
+
width: 100%;
|
|
654
|
+
}
|
|
655
|
+
"""
|
|
656
|
+
|
|
657
|
+
# Screen-specific bindings (Ctrl+ prefixed)
|
|
658
|
+
BINDINGS = NavScreen.BINDINGS + [
|
|
659
|
+
Binding("ctrl+f", "focus_search", "Search", show=True),
|
|
660
|
+
Binding("ctrl+r", "refresh", "Refresh", show=True),
|
|
661
|
+
Binding("ctrl+y", "copy_name", "Copy Name", show=True),
|
|
662
|
+
Binding("ctrl+shift+y", "copy_content", "Copy Content", show=True),
|
|
663
|
+
Binding("ctrl+e", "edit_content", "Edit", show=True),
|
|
664
|
+
Binding("ctrl+u", "update_metadata", "Edit Metadata", show=True),
|
|
665
|
+
Binding("ctrl+v", "create_version", "New Version", show=True),
|
|
666
|
+
Binding("ctrl+n", "new_template", "New", show=True),
|
|
667
|
+
Binding("ctrl+o", "render_and_open", "Render & Open", show=True),
|
|
668
|
+
Binding("escape", "focus_table", "Focus List", show=False),
|
|
669
|
+
Binding("enter", "copy_name", "Copy Name", show=False),
|
|
670
|
+
]
|
|
671
|
+
|
|
672
|
+
def compose_content(self) -> ComposeResult:
|
|
673
|
+
"""Compose the screen content."""
|
|
674
|
+
yield TemplatesWidget(id="templates-widget")
|
|
675
|
+
|
|
676
|
+
def on_screen_resume(self) -> None:
|
|
677
|
+
"""Focus the data table when screen becomes active."""
|
|
678
|
+
self.call_after_refresh(self._focus_table)
|
|
679
|
+
|
|
680
|
+
def _focus_table(self) -> None:
|
|
681
|
+
"""Focus the data table."""
|
|
682
|
+
try:
|
|
683
|
+
widget = self.query_one("#templates-widget", TemplatesWidget)
|
|
684
|
+
widget.focus_table()
|
|
685
|
+
except Exception:
|
|
686
|
+
pass
|
|
687
|
+
|
|
688
|
+
def _get_widget(self) -> TemplatesWidget:
|
|
689
|
+
"""Get the templates widget."""
|
|
690
|
+
return self.query_one("#templates-widget", TemplatesWidget)
|
|
691
|
+
|
|
692
|
+
# =========================================================================
|
|
693
|
+
# Actions (keybinding handlers)
|
|
694
|
+
# =========================================================================
|
|
695
|
+
|
|
696
|
+
def action_focus_search(self) -> None:
|
|
697
|
+
"""Focus the search input."""
|
|
698
|
+
self._get_widget().focus_search()
|
|
699
|
+
|
|
700
|
+
def action_focus_table(self) -> None:
|
|
701
|
+
"""Focus the data table."""
|
|
702
|
+
self._get_widget().focus_table()
|
|
703
|
+
|
|
704
|
+
def action_refresh(self) -> None:
|
|
705
|
+
"""Refresh the current search results."""
|
|
706
|
+
self._get_widget().refresh_data()
|
|
707
|
+
|
|
708
|
+
def action_copy_name(self) -> None:
|
|
709
|
+
"""Copy the template name to clipboard."""
|
|
710
|
+
self._get_widget().copy_template_name()
|
|
711
|
+
|
|
712
|
+
def action_copy_content(self) -> None:
|
|
713
|
+
"""Copy the template content to clipboard."""
|
|
714
|
+
self._get_widget().copy_template_content()
|
|
715
|
+
|
|
716
|
+
def action_edit_content(self) -> None:
|
|
717
|
+
"""Edit the template content in $EDITOR."""
|
|
718
|
+
self._get_widget().edit_template_content()
|
|
719
|
+
|
|
720
|
+
def action_update_metadata(self) -> None:
|
|
721
|
+
"""Open the template update modal for editing metadata."""
|
|
722
|
+
self._get_widget().update_template_metadata()
|
|
723
|
+
|
|
724
|
+
def action_create_version(self) -> None:
|
|
725
|
+
"""Create a new version of the template."""
|
|
726
|
+
self._get_widget().create_template_version()
|
|
727
|
+
|
|
728
|
+
def action_new_template(self) -> None:
|
|
729
|
+
"""Open the template creation modal."""
|
|
730
|
+
self._get_widget().open_new_template_modal()
|
|
731
|
+
|
|
732
|
+
def action_render_and_open(self) -> None:
|
|
733
|
+
"""Render the template and open in $EDITOR."""
|
|
734
|
+
self._get_widget().render_and_open()
|