docagent-cli 0.0.35__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.
- docagent_cli/__init__.py +36 -0
- docagent_cli/__main__.py +6 -0
- docagent_cli/_ask_user_types.py +90 -0
- docagent_cli/_cli_context.py +27 -0
- docagent_cli/_debug.py +52 -0
- docagent_cli/_env_vars.py +56 -0
- docagent_cli/_server_config.py +352 -0
- docagent_cli/_session_stats.py +114 -0
- docagent_cli/_testing_models.py +144 -0
- docagent_cli/_version.py +17 -0
- docagent_cli/agent.py +1193 -0
- docagent_cli/app.py +4979 -0
- docagent_cli/app.tcss +283 -0
- docagent_cli/ask_user.py +301 -0
- docagent_cli/built_in_skills/__init__.py +5 -0
- docagent_cli/built_in_skills/doc-coauthoring/SKILL.md +375 -0
- docagent_cli/built_in_skills/docx/LICENSE.txt +30 -0
- docagent_cli/built_in_skills/docx/SKILL.md +590 -0
- docagent_cli/built_in_skills/docx/scripts/__init__.py +1 -0
- docagent_cli/built_in_skills/docx/scripts/accept_changes.py +135 -0
- docagent_cli/built_in_skills/docx/scripts/comment.py +318 -0
- docagent_cli/built_in_skills/docx/scripts/office/helpers/__init__.py +0 -0
- docagent_cli/built_in_skills/docx/scripts/office/helpers/merge_runs.py +199 -0
- docagent_cli/built_in_skills/docx/scripts/office/helpers/simplify_redlines.py +197 -0
- docagent_cli/built_in_skills/docx/scripts/office/pack.py +159 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/mce/mc.xsd +75 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- docagent_cli/built_in_skills/docx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
- docagent_cli/built_in_skills/docx/scripts/office/soffice.py +183 -0
- docagent_cli/built_in_skills/docx/scripts/office/unpack.py +132 -0
- docagent_cli/built_in_skills/docx/scripts/office/validate.py +111 -0
- docagent_cli/built_in_skills/docx/scripts/office/validators/__init__.py +15 -0
- docagent_cli/built_in_skills/docx/scripts/office/validators/base.py +847 -0
- docagent_cli/built_in_skills/docx/scripts/office/validators/docx.py +446 -0
- docagent_cli/built_in_skills/docx/scripts/office/validators/pptx.py +275 -0
- docagent_cli/built_in_skills/docx/scripts/office/validators/redlining.py +247 -0
- docagent_cli/built_in_skills/docx/scripts/templates/comments.xml +3 -0
- docagent_cli/built_in_skills/docx/scripts/templates/commentsExtended.xml +3 -0
- docagent_cli/built_in_skills/docx/scripts/templates/commentsExtensible.xml +3 -0
- docagent_cli/built_in_skills/docx/scripts/templates/commentsIds.xml +3 -0
- docagent_cli/built_in_skills/docx/scripts/templates/people.xml +3 -0
- docagent_cli/built_in_skills/pdf/LICENSE.txt +30 -0
- docagent_cli/built_in_skills/pdf/SKILL.md +314 -0
- docagent_cli/built_in_skills/pdf/forms.md +294 -0
- docagent_cli/built_in_skills/pdf/reference.md +612 -0
- docagent_cli/built_in_skills/pdf/scripts/check_bounding_boxes.py +65 -0
- docagent_cli/built_in_skills/pdf/scripts/check_fillable_fields.py +11 -0
- docagent_cli/built_in_skills/pdf/scripts/convert_pdf_to_images.py +33 -0
- docagent_cli/built_in_skills/pdf/scripts/create_validation_image.py +37 -0
- docagent_cli/built_in_skills/pdf/scripts/extract_form_field_info.py +122 -0
- docagent_cli/built_in_skills/pdf/scripts/extract_form_structure.py +115 -0
- docagent_cli/built_in_skills/pdf/scripts/fill_fillable_fields.py +98 -0
- docagent_cli/built_in_skills/pdf/scripts/fill_pdf_form_with_annotations.py +107 -0
- docagent_cli/built_in_skills/pptx/LICENSE.txt +30 -0
- docagent_cli/built_in_skills/pptx/SKILL.md +232 -0
- docagent_cli/built_in_skills/pptx/editing.md +205 -0
- docagent_cli/built_in_skills/pptx/pptxgenjs.md +420 -0
- docagent_cli/built_in_skills/pptx/scripts/__init__.py +0 -0
- docagent_cli/built_in_skills/pptx/scripts/add_slide.py +195 -0
- docagent_cli/built_in_skills/pptx/scripts/clean.py +286 -0
- docagent_cli/built_in_skills/pptx/scripts/office/helpers/__init__.py +0 -0
- docagent_cli/built_in_skills/pptx/scripts/office/helpers/merge_runs.py +199 -0
- docagent_cli/built_in_skills/pptx/scripts/office/helpers/simplify_redlines.py +197 -0
- docagent_cli/built_in_skills/pptx/scripts/office/pack.py +159 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/mce/mc.xsd +75 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- docagent_cli/built_in_skills/pptx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
- docagent_cli/built_in_skills/pptx/scripts/office/soffice.py +183 -0
- docagent_cli/built_in_skills/pptx/scripts/office/unpack.py +132 -0
- docagent_cli/built_in_skills/pptx/scripts/office/validate.py +111 -0
- docagent_cli/built_in_skills/pptx/scripts/office/validators/__init__.py +15 -0
- docagent_cli/built_in_skills/pptx/scripts/office/validators/base.py +847 -0
- docagent_cli/built_in_skills/pptx/scripts/office/validators/docx.py +446 -0
- docagent_cli/built_in_skills/pptx/scripts/office/validators/pptx.py +275 -0
- docagent_cli/built_in_skills/pptx/scripts/office/validators/redlining.py +247 -0
- docagent_cli/built_in_skills/pptx/scripts/thumbnail.py +289 -0
- docagent_cli/built_in_skills/remember/SKILL.md +118 -0
- docagent_cli/built_in_skills/skill-creator/LICENSE.txt +202 -0
- docagent_cli/built_in_skills/skill-creator/SKILL.md +485 -0
- docagent_cli/built_in_skills/skill-creator/agents/analyzer.md +274 -0
- docagent_cli/built_in_skills/skill-creator/agents/comparator.md +202 -0
- docagent_cli/built_in_skills/skill-creator/agents/grader.md +223 -0
- docagent_cli/built_in_skills/skill-creator/assets/eval_review.html +146 -0
- docagent_cli/built_in_skills/skill-creator/eval-viewer/generate_review.py +471 -0
- docagent_cli/built_in_skills/skill-creator/eval-viewer/viewer.html +1325 -0
- docagent_cli/built_in_skills/skill-creator/references/schemas.md +430 -0
- docagent_cli/built_in_skills/skill-creator/scripts/__init__.py +0 -0
- docagent_cli/built_in_skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- docagent_cli/built_in_skills/skill-creator/scripts/generate_report.py +326 -0
- docagent_cli/built_in_skills/skill-creator/scripts/improve_description.py +247 -0
- docagent_cli/built_in_skills/skill-creator/scripts/package_skill.py +136 -0
- docagent_cli/built_in_skills/skill-creator/scripts/quick_validate.py +103 -0
- docagent_cli/built_in_skills/skill-creator/scripts/run_eval.py +310 -0
- docagent_cli/built_in_skills/skill-creator/scripts/run_loop.py +328 -0
- docagent_cli/built_in_skills/skill-creator/scripts/utils.py +47 -0
- docagent_cli/built_in_skills/theme-factory/LICENSE.txt +202 -0
- docagent_cli/built_in_skills/theme-factory/SKILL.md +59 -0
- docagent_cli/built_in_skills/theme-factory/theme-showcase.pdf +0 -0
- docagent_cli/built_in_skills/theme-factory/themes/arctic-frost.md +19 -0
- docagent_cli/built_in_skills/theme-factory/themes/botanical-garden.md +19 -0
- docagent_cli/built_in_skills/theme-factory/themes/desert-rose.md +19 -0
- docagent_cli/built_in_skills/theme-factory/themes/forest-canopy.md +19 -0
- docagent_cli/built_in_skills/theme-factory/themes/golden-hour.md +19 -0
- docagent_cli/built_in_skills/theme-factory/themes/midnight-galaxy.md +19 -0
- docagent_cli/built_in_skills/theme-factory/themes/modern-minimalist.md +19 -0
- docagent_cli/built_in_skills/theme-factory/themes/ocean-depths.md +19 -0
- docagent_cli/built_in_skills/theme-factory/themes/sunset-boulevard.md +19 -0
- docagent_cli/built_in_skills/theme-factory/themes/tech-innovation.md +19 -0
- docagent_cli/built_in_skills/xlsx/LICENSE.txt +30 -0
- docagent_cli/built_in_skills/xlsx/SKILL.md +292 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/helpers/__init__.py +0 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/helpers/merge_runs.py +199 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/helpers/simplify_redlines.py +197 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/pack.py +159 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/mce/mc.xsd +75 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/soffice.py +183 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/unpack.py +132 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/validate.py +111 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/validators/__init__.py +15 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/validators/base.py +847 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/validators/docx.py +446 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/validators/pptx.py +275 -0
- docagent_cli/built_in_skills/xlsx/scripts/office/validators/redlining.py +247 -0
- docagent_cli/built_in_skills/xlsx/scripts/recalc.py +184 -0
- docagent_cli/clipboard.py +128 -0
- docagent_cli/command_registry.py +284 -0
- docagent_cli/config.py +2418 -0
- docagent_cli/configurable_model.py +162 -0
- docagent_cli/default_agent_prompt.md +12 -0
- docagent_cli/editor.py +142 -0
- docagent_cli/file_ops.py +473 -0
- docagent_cli/formatting.py +28 -0
- docagent_cli/hooks.py +206 -0
- docagent_cli/input.py +787 -0
- docagent_cli/integrations/__init__.py +1 -0
- docagent_cli/integrations/sandbox_factory.py +873 -0
- docagent_cli/integrations/sandbox_provider.py +71 -0
- docagent_cli/local_context.py +718 -0
- docagent_cli/main.py +1641 -0
- docagent_cli/mcp_tools.py +707 -0
- docagent_cli/mcp_trust.py +168 -0
- docagent_cli/media_utils.py +478 -0
- docagent_cli/model_config.py +1620 -0
- docagent_cli/non_interactive.py +948 -0
- docagent_cli/offload.py +371 -0
- docagent_cli/output.py +69 -0
- docagent_cli/project_utils.py +188 -0
- docagent_cli/py.typed +0 -0
- docagent_cli/remote_client.py +515 -0
- docagent_cli/server.py +520 -0
- docagent_cli/server_graph.py +196 -0
- docagent_cli/server_manager.py +365 -0
- docagent_cli/sessions.py +1262 -0
- docagent_cli/skills/__init__.py +18 -0
- docagent_cli/skills/commands.py +1090 -0
- docagent_cli/skills/load.py +192 -0
- docagent_cli/subagents.py +173 -0
- docagent_cli/system_prompt.md +247 -0
- docagent_cli/textual_adapter.py +1352 -0
- docagent_cli/theme.py +842 -0
- docagent_cli/token_state.py +31 -0
- docagent_cli/tool_display.py +298 -0
- docagent_cli/tools.py +236 -0
- docagent_cli/ui.py +420 -0
- docagent_cli/unicode_security.py +516 -0
- docagent_cli/update_check.py +454 -0
- docagent_cli/widgets/__init__.py +9 -0
- docagent_cli/widgets/_links.py +63 -0
- docagent_cli/widgets/approval.py +442 -0
- docagent_cli/widgets/ask_user.py +398 -0
- docagent_cli/widgets/autocomplete.py +691 -0
- docagent_cli/widgets/chat_input.py +1827 -0
- docagent_cli/widgets/diff.py +248 -0
- docagent_cli/widgets/history.py +188 -0
- docagent_cli/widgets/loading.py +177 -0
- docagent_cli/widgets/mcp_viewer.py +362 -0
- docagent_cli/widgets/message_store.py +675 -0
- docagent_cli/widgets/messages.py +1751 -0
- docagent_cli/widgets/model_selector.py +964 -0
- docagent_cli/widgets/status.py +372 -0
- docagent_cli/widgets/theme_selector.py +164 -0
- docagent_cli/widgets/thread_selector.py +1905 -0
- docagent_cli/widgets/tool_renderers.py +148 -0
- docagent_cli/widgets/tool_widgets.py +274 -0
- docagent_cli/widgets/welcome.py +339 -0
- docagent_cli-0.0.35.data/data/docagent_cli/default_agent_prompt.md +12 -0
- docagent_cli-0.0.35.dist-info/METADATA +200 -0
- docagent_cli-0.0.35.dist-info/RECORD +300 -0
- docagent_cli-0.0.35.dist-info/WHEEL +4 -0
- docagent_cli-0.0.35.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,964 @@
|
|
|
1
|
+
"""Interactive model selector screen for /model command."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import logging
|
|
7
|
+
from typing import TYPE_CHECKING, Any, ClassVar
|
|
8
|
+
|
|
9
|
+
from textual.binding import Binding, BindingType
|
|
10
|
+
from textual.containers import Container, Vertical, VerticalScroll
|
|
11
|
+
from textual.content import Content
|
|
12
|
+
from textual.events import (
|
|
13
|
+
Click, # noqa: TC002 - needed at runtime for Textual event dispatch
|
|
14
|
+
)
|
|
15
|
+
from textual.fuzzy import Matcher
|
|
16
|
+
from textual.message import Message
|
|
17
|
+
from textual.screen import ModalScreen
|
|
18
|
+
from textual.widgets import Input, Static
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from collections.abc import Mapping
|
|
22
|
+
|
|
23
|
+
from textual.app import ComposeResult
|
|
24
|
+
|
|
25
|
+
from docagent_cli import theme
|
|
26
|
+
from docagent_cli.config import Glyphs, get_glyphs, is_ascii_mode
|
|
27
|
+
from docagent_cli.model_config import (
|
|
28
|
+
ModelConfig,
|
|
29
|
+
ModelProfileEntry,
|
|
30
|
+
clear_default_model,
|
|
31
|
+
get_available_models,
|
|
32
|
+
get_model_profiles,
|
|
33
|
+
has_provider_credentials,
|
|
34
|
+
save_default_model,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ModelOption(Static):
|
|
41
|
+
"""A clickable model option in the selector."""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
label: str | Content,
|
|
46
|
+
model_spec: str,
|
|
47
|
+
provider: str,
|
|
48
|
+
index: int,
|
|
49
|
+
*,
|
|
50
|
+
has_creds: bool | None = True,
|
|
51
|
+
classes: str = "",
|
|
52
|
+
) -> None:
|
|
53
|
+
"""Initialize a model option.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
label: Display content — a `Content` object (preferred) or a
|
|
57
|
+
plain string that `Static` will parse as markup.
|
|
58
|
+
model_spec: The model specification (provider:model format).
|
|
59
|
+
provider: The provider name.
|
|
60
|
+
index: The index of this option in the filtered list.
|
|
61
|
+
has_creds: Whether the provider has valid credentials. True if
|
|
62
|
+
confirmed, False if missing, None if unknown.
|
|
63
|
+
classes: CSS classes for styling.
|
|
64
|
+
"""
|
|
65
|
+
super().__init__(label, classes=classes)
|
|
66
|
+
self.model_spec = model_spec
|
|
67
|
+
self.provider = provider
|
|
68
|
+
self.index = index
|
|
69
|
+
self.has_creds = has_creds
|
|
70
|
+
|
|
71
|
+
class Clicked(Message):
|
|
72
|
+
"""Message sent when a model option is clicked."""
|
|
73
|
+
|
|
74
|
+
def __init__(self, model_spec: str, provider: str, index: int) -> None:
|
|
75
|
+
"""Initialize the Clicked message.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
model_spec: The model specification.
|
|
79
|
+
provider: The provider name.
|
|
80
|
+
index: The index of the clicked option.
|
|
81
|
+
"""
|
|
82
|
+
super().__init__()
|
|
83
|
+
self.model_spec = model_spec
|
|
84
|
+
self.provider = provider
|
|
85
|
+
self.index = index
|
|
86
|
+
|
|
87
|
+
def on_click(self, event: Click) -> None:
|
|
88
|
+
"""Handle click on this option.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
event: The click event.
|
|
92
|
+
"""
|
|
93
|
+
event.stop()
|
|
94
|
+
self.post_message(self.Clicked(self.model_spec, self.provider, self.index))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class ModelSelectorScreen(ModalScreen[tuple[str, str] | None]):
|
|
98
|
+
"""Full-screen modal for model selection.
|
|
99
|
+
|
|
100
|
+
Displays available models grouped by provider with keyboard navigation
|
|
101
|
+
and search filtering. Current model is highlighted.
|
|
102
|
+
|
|
103
|
+
Returns (model_spec, provider) tuple on selection, or None on cancel.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
BINDINGS: ClassVar[list[BindingType]] = [
|
|
107
|
+
Binding("up", "move_up", "Up", show=False, priority=True),
|
|
108
|
+
Binding("k", "move_up", "Up", show=False, priority=True),
|
|
109
|
+
Binding("down", "move_down", "Down", show=False, priority=True),
|
|
110
|
+
Binding("j", "move_down", "Down", show=False, priority=True),
|
|
111
|
+
Binding("tab", "tab_complete", "Tab complete", show=False, priority=True),
|
|
112
|
+
Binding("pageup", "page_up", "Page up", show=False, priority=True),
|
|
113
|
+
Binding("pagedown", "page_down", "Page down", show=False, priority=True),
|
|
114
|
+
Binding("enter", "select", "Select", show=False, priority=True),
|
|
115
|
+
Binding("ctrl+s", "set_default", "Set default", show=False, priority=True),
|
|
116
|
+
Binding("escape", "cancel", "Cancel", show=False, priority=True),
|
|
117
|
+
]
|
|
118
|
+
|
|
119
|
+
CSS = """
|
|
120
|
+
ModelSelectorScreen {
|
|
121
|
+
align: center middle;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
ModelSelectorScreen > Vertical {
|
|
125
|
+
width: 80;
|
|
126
|
+
max-width: 90%;
|
|
127
|
+
height: 80%;
|
|
128
|
+
background: $surface;
|
|
129
|
+
border: solid $primary;
|
|
130
|
+
padding: 1 2;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
ModelSelectorScreen .model-selector-title {
|
|
134
|
+
text-style: bold;
|
|
135
|
+
color: $primary;
|
|
136
|
+
text-align: center;
|
|
137
|
+
margin-bottom: 1;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
ModelSelectorScreen #model-filter {
|
|
141
|
+
margin-bottom: 1;
|
|
142
|
+
border: solid $primary-lighten-2;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
ModelSelectorScreen #model-filter:focus {
|
|
146
|
+
border: solid $primary;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
ModelSelectorScreen .model-list {
|
|
150
|
+
height: 1fr;
|
|
151
|
+
min-height: 5;
|
|
152
|
+
scrollbar-gutter: stable;
|
|
153
|
+
background: $background;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
ModelSelectorScreen #model-options {
|
|
157
|
+
height: auto;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
ModelSelectorScreen .model-provider-header {
|
|
161
|
+
color: $primary;
|
|
162
|
+
margin-top: 1;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
ModelSelectorScreen #model-options > .model-provider-header:first-child {
|
|
166
|
+
margin-top: 0;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
ModelSelectorScreen .model-option {
|
|
170
|
+
height: 1;
|
|
171
|
+
padding: 0 1;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
ModelSelectorScreen .model-option:hover {
|
|
175
|
+
background: $surface-lighten-1;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
ModelSelectorScreen .model-option-selected {
|
|
179
|
+
background: $primary;
|
|
180
|
+
color: $background;
|
|
181
|
+
text-style: bold;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
ModelSelectorScreen .model-option-selected:hover {
|
|
185
|
+
background: $primary-lighten-1;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
ModelSelectorScreen .model-option-current {
|
|
189
|
+
text-style: italic;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
ModelSelectorScreen .model-selector-help {
|
|
193
|
+
height: 1;
|
|
194
|
+
color: $text-muted;
|
|
195
|
+
text-style: italic;
|
|
196
|
+
margin-top: 1;
|
|
197
|
+
text-align: center;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
ModelSelectorScreen .model-detail-footer {
|
|
201
|
+
height: 4;
|
|
202
|
+
padding: 0 2;
|
|
203
|
+
border-top: solid $primary-lighten-2;
|
|
204
|
+
}
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
def __init__(
|
|
208
|
+
self,
|
|
209
|
+
current_model: str | None = None,
|
|
210
|
+
current_provider: str | None = None,
|
|
211
|
+
cli_profile_override: dict[str, Any] | None = None,
|
|
212
|
+
) -> None:
|
|
213
|
+
"""Initialize the ModelSelectorScreen.
|
|
214
|
+
|
|
215
|
+
Data loading (model discovery, profiles) is deferred to `on_mount`
|
|
216
|
+
so the screen pushes instantly and populates asynchronously.
|
|
217
|
+
|
|
218
|
+
Args:
|
|
219
|
+
current_model: The currently active model name (to highlight).
|
|
220
|
+
current_provider: The provider of the current model.
|
|
221
|
+
cli_profile_override: Extra profile fields from `--profile-override`.
|
|
222
|
+
|
|
223
|
+
Merged on top of upstream + config.toml profiles so that CLI
|
|
224
|
+
overrides appear with `*` markers in the detail footer.
|
|
225
|
+
"""
|
|
226
|
+
super().__init__()
|
|
227
|
+
self._current_model = current_model
|
|
228
|
+
self._current_provider = current_provider
|
|
229
|
+
self._cli_profile_override = cli_profile_override
|
|
230
|
+
|
|
231
|
+
# Model data — populated asynchronously in on_mount via _load_model_data
|
|
232
|
+
self._all_models: list[tuple[str, str]] = []
|
|
233
|
+
self._filtered_models: list[tuple[str, str]] = []
|
|
234
|
+
self._selected_index = 0
|
|
235
|
+
self._options_container: Container | None = None
|
|
236
|
+
self._option_widgets: list[ModelOption] = []
|
|
237
|
+
self._filter_text = ""
|
|
238
|
+
self._current_spec: str | None = None
|
|
239
|
+
if current_model and current_provider:
|
|
240
|
+
self._current_spec = f"{current_provider}:{current_model}"
|
|
241
|
+
self._default_spec: str | None = None
|
|
242
|
+
self._profiles: Mapping[str, ModelProfileEntry] = {}
|
|
243
|
+
self._loaded = False
|
|
244
|
+
|
|
245
|
+
def _find_current_model_index(self) -> int:
|
|
246
|
+
"""Find the index of the current model in the filtered list.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
Index of the current model, or 0 if not found.
|
|
250
|
+
"""
|
|
251
|
+
if not self._current_model or not self._current_provider:
|
|
252
|
+
return 0
|
|
253
|
+
|
|
254
|
+
current_spec = f"{self._current_provider}:{self._current_model}"
|
|
255
|
+
for i, (model_spec, _) in enumerate(self._filtered_models):
|
|
256
|
+
if model_spec == current_spec:
|
|
257
|
+
return i
|
|
258
|
+
return 0
|
|
259
|
+
|
|
260
|
+
def compose(self) -> ComposeResult:
|
|
261
|
+
"""Compose the screen layout.
|
|
262
|
+
|
|
263
|
+
Yields:
|
|
264
|
+
Widgets for the model selector UI.
|
|
265
|
+
"""
|
|
266
|
+
glyphs = get_glyphs()
|
|
267
|
+
|
|
268
|
+
with Vertical():
|
|
269
|
+
# Title with current model in provider:model format
|
|
270
|
+
if self._current_model and self._current_provider:
|
|
271
|
+
current_spec = f"{self._current_provider}:{self._current_model}"
|
|
272
|
+
title = f"Select Model (current: {current_spec})"
|
|
273
|
+
elif self._current_model:
|
|
274
|
+
title = f"Select Model (current: {self._current_model})"
|
|
275
|
+
else:
|
|
276
|
+
title = "Select Model"
|
|
277
|
+
yield Static(title, classes="model-selector-title")
|
|
278
|
+
|
|
279
|
+
# Search input
|
|
280
|
+
yield Input(
|
|
281
|
+
placeholder="Type to filter or enter provider:model...",
|
|
282
|
+
id="model-filter",
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
# Scrollable model list
|
|
286
|
+
with VerticalScroll(classes="model-list"):
|
|
287
|
+
self._options_container = Container(id="model-options")
|
|
288
|
+
yield self._options_container
|
|
289
|
+
|
|
290
|
+
# Model detail footer
|
|
291
|
+
yield Static("", classes="model-detail-footer", id="model-detail-footer")
|
|
292
|
+
|
|
293
|
+
# Help text
|
|
294
|
+
help_text = (
|
|
295
|
+
f"{glyphs.arrow_up}/{glyphs.arrow_down} navigate"
|
|
296
|
+
f" {glyphs.bullet} Enter select"
|
|
297
|
+
f" {glyphs.bullet} Ctrl+S set default"
|
|
298
|
+
f" {glyphs.bullet} Esc cancel"
|
|
299
|
+
)
|
|
300
|
+
yield Static(help_text, classes="model-selector-help")
|
|
301
|
+
|
|
302
|
+
@staticmethod
|
|
303
|
+
def _load_model_data(
|
|
304
|
+
cli_override: dict[str, Any] | None,
|
|
305
|
+
) -> tuple[
|
|
306
|
+
list[tuple[str, str]],
|
|
307
|
+
str | None,
|
|
308
|
+
Mapping[str, ModelProfileEntry],
|
|
309
|
+
]:
|
|
310
|
+
"""Gather model discovery data synchronously.
|
|
311
|
+
|
|
312
|
+
Intended to be called via `asyncio.to_thread` so filesystem I/O in
|
|
313
|
+
`get_available_models` does not block the event loop.
|
|
314
|
+
|
|
315
|
+
Returns:
|
|
316
|
+
Tuple of (all_models, default_spec, profiles) where
|
|
317
|
+
`all_models` is a list of `(provider:model spec, provider)`
|
|
318
|
+
pairs, `default_spec` is the configured default model or
|
|
319
|
+
`None`, and `profiles` maps spec strings to profile entries.
|
|
320
|
+
"""
|
|
321
|
+
all_models: list[tuple[str, str]] = [
|
|
322
|
+
(f"{provider}:{model}", provider)
|
|
323
|
+
for provider, models in get_available_models().items()
|
|
324
|
+
for model in models
|
|
325
|
+
]
|
|
326
|
+
|
|
327
|
+
config = ModelConfig.load()
|
|
328
|
+
profiles = get_model_profiles(cli_override=cli_override)
|
|
329
|
+
return all_models, config.default_model, profiles
|
|
330
|
+
|
|
331
|
+
async def on_mount(self) -> None:
|
|
332
|
+
"""Set up the screen on mount.
|
|
333
|
+
|
|
334
|
+
Loads model data in a background thread so the screen frame renders
|
|
335
|
+
immediately, then populates the model list.
|
|
336
|
+
"""
|
|
337
|
+
if is_ascii_mode():
|
|
338
|
+
colors = theme.get_theme_colors(self)
|
|
339
|
+
container = self.query_one(Vertical)
|
|
340
|
+
container.styles.border = ("ascii", colors.success)
|
|
341
|
+
|
|
342
|
+
# Focus the filter input immediately so the user can start typing
|
|
343
|
+
# while model data loads.
|
|
344
|
+
filter_input = self.query_one("#model-filter", Input)
|
|
345
|
+
filter_input.focus()
|
|
346
|
+
|
|
347
|
+
# Offload to thread because get_available_models does filesystem I/O
|
|
348
|
+
try:
|
|
349
|
+
all_models, default_spec, profiles = await asyncio.to_thread(
|
|
350
|
+
self._load_model_data, self._cli_profile_override
|
|
351
|
+
)
|
|
352
|
+
except Exception:
|
|
353
|
+
logger.exception("Failed to load model data for /model selector")
|
|
354
|
+
self._loaded = True
|
|
355
|
+
if self.is_running:
|
|
356
|
+
self.notify(
|
|
357
|
+
"Could not load model list. "
|
|
358
|
+
"Check provider packages and config.toml.",
|
|
359
|
+
severity="error",
|
|
360
|
+
timeout=10,
|
|
361
|
+
markup=False,
|
|
362
|
+
)
|
|
363
|
+
await self._update_display()
|
|
364
|
+
self._update_footer()
|
|
365
|
+
return
|
|
366
|
+
|
|
367
|
+
# Screen may have been dismissed while the thread was running
|
|
368
|
+
if not self.is_running:
|
|
369
|
+
return
|
|
370
|
+
|
|
371
|
+
self._all_models = all_models
|
|
372
|
+
self._default_spec = default_spec
|
|
373
|
+
self._profiles = profiles
|
|
374
|
+
self._filtered_models = list(self._all_models)
|
|
375
|
+
self._selected_index = self._find_current_model_index()
|
|
376
|
+
self._loaded = True
|
|
377
|
+
|
|
378
|
+
# Re-apply any filter text the user typed while data was loading
|
|
379
|
+
if self._filter_text:
|
|
380
|
+
self._update_filtered_list()
|
|
381
|
+
|
|
382
|
+
await self._update_display()
|
|
383
|
+
self._update_footer()
|
|
384
|
+
|
|
385
|
+
def on_input_changed(self, event: Input.Changed) -> None:
|
|
386
|
+
"""Filter models as user types.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
event: The input changed event.
|
|
390
|
+
"""
|
|
391
|
+
self._filter_text = event.value
|
|
392
|
+
if not self._loaded:
|
|
393
|
+
return # on_mount will re-apply filter after data loads
|
|
394
|
+
self._update_filtered_list()
|
|
395
|
+
self.call_after_refresh(self._update_display)
|
|
396
|
+
|
|
397
|
+
def on_input_submitted(self, event: Input.Submitted) -> None:
|
|
398
|
+
"""Handle Enter key when filter input is focused.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
event: The input submitted event.
|
|
402
|
+
"""
|
|
403
|
+
event.stop()
|
|
404
|
+
self.action_select()
|
|
405
|
+
|
|
406
|
+
def on_model_option_clicked(self, event: ModelOption.Clicked) -> None:
|
|
407
|
+
"""Handle click on a model option.
|
|
408
|
+
|
|
409
|
+
Args:
|
|
410
|
+
event: The click event with model info.
|
|
411
|
+
"""
|
|
412
|
+
self._selected_index = event.index
|
|
413
|
+
self.dismiss((event.model_spec, event.provider))
|
|
414
|
+
|
|
415
|
+
def _update_filtered_list(self) -> None:
|
|
416
|
+
"""Update the filtered models based on search text using fuzzy matching.
|
|
417
|
+
|
|
418
|
+
Results are sorted by match score (best first).
|
|
419
|
+
"""
|
|
420
|
+
query = self._filter_text.strip()
|
|
421
|
+
if not query:
|
|
422
|
+
self._filtered_models = list(self._all_models)
|
|
423
|
+
self._selected_index = self._find_current_model_index()
|
|
424
|
+
return
|
|
425
|
+
|
|
426
|
+
tokens = query.split()
|
|
427
|
+
|
|
428
|
+
try:
|
|
429
|
+
matchers = [Matcher(token, case_sensitive=False) for token in tokens]
|
|
430
|
+
scored: list[tuple[float, str, str]] = []
|
|
431
|
+
for spec, provider in self._all_models:
|
|
432
|
+
scores = [m.match(spec) for m in matchers]
|
|
433
|
+
if all(s > 0 for s in scores):
|
|
434
|
+
scored.append((min(scores), spec, provider))
|
|
435
|
+
except Exception:
|
|
436
|
+
# graceful fallback if Matcher fails on edge-case input
|
|
437
|
+
logger.warning(
|
|
438
|
+
"Fuzzy matcher failed for query %r, falling back to full list",
|
|
439
|
+
query,
|
|
440
|
+
exc_info=True,
|
|
441
|
+
)
|
|
442
|
+
self._filtered_models = list(self._all_models)
|
|
443
|
+
self._selected_index = self._find_current_model_index()
|
|
444
|
+
return
|
|
445
|
+
|
|
446
|
+
self._filtered_models = [
|
|
447
|
+
(spec, provider) for score, spec, provider in sorted(scored, reverse=True)
|
|
448
|
+
]
|
|
449
|
+
self._selected_index = 0
|
|
450
|
+
|
|
451
|
+
async def _update_display(self) -> None:
|
|
452
|
+
"""Render the model list grouped by provider.
|
|
453
|
+
|
|
454
|
+
Performs a full DOM rebuild (removes all children, re-mounts).
|
|
455
|
+
Arrow-key navigation uses `_move_selection` instead to avoid
|
|
456
|
+
the cost of a full rebuild.
|
|
457
|
+
"""
|
|
458
|
+
if not self._options_container:
|
|
459
|
+
return
|
|
460
|
+
|
|
461
|
+
await self._options_container.remove_children()
|
|
462
|
+
self._option_widgets = []
|
|
463
|
+
|
|
464
|
+
if not self._filtered_models:
|
|
465
|
+
msg = "Loading models…" if not self._loaded else "No matching models"
|
|
466
|
+
await self._options_container.mount(Static(Content.styled(msg, "dim")))
|
|
467
|
+
self._update_footer()
|
|
468
|
+
return
|
|
469
|
+
|
|
470
|
+
# Group by provider, preserving insertion order so models from the
|
|
471
|
+
# same provider cluster together in the visual list.
|
|
472
|
+
by_provider: dict[str, list[tuple[str, str]]] = {}
|
|
473
|
+
for model_spec, provider in self._filtered_models:
|
|
474
|
+
by_provider.setdefault(provider, []).append((model_spec, provider))
|
|
475
|
+
|
|
476
|
+
# Rebuild _filtered_models to match the provider-grouped display
|
|
477
|
+
# order. Without this, _filtered_models stays in score-sorted order
|
|
478
|
+
# while _option_widgets follow provider-grouped order, causing
|
|
479
|
+
# _update_footer to look up the wrong model for the highlighted
|
|
480
|
+
# index.
|
|
481
|
+
grouped_order: list[tuple[str, str]] = []
|
|
482
|
+
for entries in by_provider.values():
|
|
483
|
+
grouped_order.extend(entries)
|
|
484
|
+
|
|
485
|
+
# Remap selected_index so the same model stays highlighted.
|
|
486
|
+
old_spec = self._filtered_models[self._selected_index][0]
|
|
487
|
+
self._filtered_models = grouped_order
|
|
488
|
+
self._selected_index = next(
|
|
489
|
+
(i for i, (s, _) in enumerate(grouped_order) if s == old_spec),
|
|
490
|
+
0,
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
glyphs = get_glyphs()
|
|
494
|
+
flat_index = 0
|
|
495
|
+
selected_widget: ModelOption | None = None
|
|
496
|
+
|
|
497
|
+
# Build current model spec for comparison
|
|
498
|
+
current_spec = None
|
|
499
|
+
if self._current_model and self._current_provider:
|
|
500
|
+
current_spec = f"{self._current_provider}:{self._current_model}"
|
|
501
|
+
|
|
502
|
+
# Resolve credentials upfront so the widget-building loop
|
|
503
|
+
# stays focused on layout
|
|
504
|
+
creds = {p: has_provider_credentials(p) for p in by_provider}
|
|
505
|
+
|
|
506
|
+
# Collect all widgets first, then batch-mount once to avoid
|
|
507
|
+
# individual DOM mutations per widget
|
|
508
|
+
all_widgets: list[Static] = []
|
|
509
|
+
|
|
510
|
+
for provider, model_entries in by_provider.items():
|
|
511
|
+
# Provider header with credential indicator
|
|
512
|
+
has_creds = creds[provider]
|
|
513
|
+
if has_creds is True:
|
|
514
|
+
cred_indicator = glyphs.checkmark
|
|
515
|
+
elif has_creds is False:
|
|
516
|
+
cred_indicator = f"{glyphs.warning} missing credentials"
|
|
517
|
+
else:
|
|
518
|
+
cred_indicator = f"{glyphs.question} credentials unknown"
|
|
519
|
+
all_widgets.append(
|
|
520
|
+
Static(
|
|
521
|
+
Content.from_markup(
|
|
522
|
+
"[bold]$provider[/bold] [dim]$cred[/dim]",
|
|
523
|
+
provider=provider,
|
|
524
|
+
cred=cred_indicator,
|
|
525
|
+
),
|
|
526
|
+
classes="model-provider-header",
|
|
527
|
+
)
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
for model_spec, _prov in model_entries:
|
|
531
|
+
is_current = model_spec == current_spec
|
|
532
|
+
is_selected = flat_index == self._selected_index
|
|
533
|
+
|
|
534
|
+
classes = "model-option"
|
|
535
|
+
if is_selected:
|
|
536
|
+
classes += " model-option-selected"
|
|
537
|
+
if is_current:
|
|
538
|
+
classes += " model-option-current"
|
|
539
|
+
|
|
540
|
+
label = self._format_option_label(
|
|
541
|
+
model_spec,
|
|
542
|
+
selected=is_selected,
|
|
543
|
+
current=is_current,
|
|
544
|
+
has_creds=has_creds,
|
|
545
|
+
is_default=model_spec == self._default_spec,
|
|
546
|
+
status=self._get_model_status(model_spec),
|
|
547
|
+
)
|
|
548
|
+
widget = ModelOption(
|
|
549
|
+
label=label,
|
|
550
|
+
model_spec=model_spec,
|
|
551
|
+
provider=provider,
|
|
552
|
+
index=flat_index,
|
|
553
|
+
has_creds=has_creds,
|
|
554
|
+
classes=classes,
|
|
555
|
+
)
|
|
556
|
+
all_widgets.append(widget)
|
|
557
|
+
self._option_widgets.append(widget)
|
|
558
|
+
|
|
559
|
+
if is_selected:
|
|
560
|
+
selected_widget = widget
|
|
561
|
+
|
|
562
|
+
flat_index += 1
|
|
563
|
+
|
|
564
|
+
await self._options_container.mount(*all_widgets)
|
|
565
|
+
|
|
566
|
+
# Scroll the selected item into view without animation so the list
|
|
567
|
+
# appears already scrolled to the current model on first paint.
|
|
568
|
+
if selected_widget:
|
|
569
|
+
if self._selected_index == 0:
|
|
570
|
+
# First item: scroll to top so header is visible
|
|
571
|
+
scroll_container = self.query_one(".model-list", VerticalScroll)
|
|
572
|
+
scroll_container.scroll_home(animate=False)
|
|
573
|
+
else:
|
|
574
|
+
selected_widget.scroll_visible(animate=False)
|
|
575
|
+
|
|
576
|
+
self._update_footer()
|
|
577
|
+
|
|
578
|
+
@staticmethod
|
|
579
|
+
def _format_option_label(
|
|
580
|
+
model_spec: str,
|
|
581
|
+
*,
|
|
582
|
+
selected: bool,
|
|
583
|
+
current: bool,
|
|
584
|
+
has_creds: bool | None,
|
|
585
|
+
is_default: bool = False,
|
|
586
|
+
status: str | None = None,
|
|
587
|
+
) -> Content:
|
|
588
|
+
"""Build the display label for a model option.
|
|
589
|
+
|
|
590
|
+
Args:
|
|
591
|
+
model_spec: The `provider:model` string.
|
|
592
|
+
selected: Whether this option is currently highlighted.
|
|
593
|
+
current: Whether this is the active model.
|
|
594
|
+
has_creds: Credential status (True/False/None).
|
|
595
|
+
is_default: Whether this is the configured default model.
|
|
596
|
+
status: Model status from profile (e.g., `'deprecated'`,
|
|
597
|
+
`'beta'`, `'alpha'`). `'deprecated'` renders in red;
|
|
598
|
+
other non-None values render in yellow.
|
|
599
|
+
|
|
600
|
+
Returns:
|
|
601
|
+
Styled Content label.
|
|
602
|
+
"""
|
|
603
|
+
colors = theme.get_theme_colors()
|
|
604
|
+
glyphs = get_glyphs()
|
|
605
|
+
cursor = f"{glyphs.cursor} " if selected else " "
|
|
606
|
+
if not has_creds:
|
|
607
|
+
spec = Content.styled(model_spec, colors.warning)
|
|
608
|
+
elif is_default:
|
|
609
|
+
spec = Content.styled(model_spec, colors.primary)
|
|
610
|
+
else:
|
|
611
|
+
spec = Content(model_spec)
|
|
612
|
+
suffix = Content.styled(" (current)", "dim") if current else Content("")
|
|
613
|
+
default_suffix = (
|
|
614
|
+
Content.styled(" (default)", colors.primary) if is_default else Content("")
|
|
615
|
+
)
|
|
616
|
+
if status == "deprecated":
|
|
617
|
+
status_suffix = Content.styled(" (deprecated)", colors.error)
|
|
618
|
+
elif status:
|
|
619
|
+
status_suffix = Content.styled(f" ({status})", colors.warning)
|
|
620
|
+
else:
|
|
621
|
+
status_suffix = Content("")
|
|
622
|
+
return Content.assemble(cursor, spec, suffix, default_suffix, status_suffix)
|
|
623
|
+
|
|
624
|
+
@staticmethod
|
|
625
|
+
def _format_footer(
|
|
626
|
+
profile_entry: ModelProfileEntry | None,
|
|
627
|
+
glyphs: Glyphs,
|
|
628
|
+
) -> Content:
|
|
629
|
+
"""Build the detail footer text for the highlighted model.
|
|
630
|
+
|
|
631
|
+
Args:
|
|
632
|
+
profile_entry: Profile data with override tracking, or None.
|
|
633
|
+
glyphs: Glyph set for display characters.
|
|
634
|
+
|
|
635
|
+
Returns:
|
|
636
|
+
Styled `Content` for the 4-line footer.
|
|
637
|
+
"""
|
|
638
|
+
from docagent_cli.textual_adapter import format_token_count
|
|
639
|
+
|
|
640
|
+
if profile_entry is None or not profile_entry["profile"]:
|
|
641
|
+
return Content.styled("Model profile not available :(\n\n\n", "dim")
|
|
642
|
+
|
|
643
|
+
profile = profile_entry["profile"]
|
|
644
|
+
overridden = profile_entry["overridden_keys"]
|
|
645
|
+
|
|
646
|
+
colors = theme.get_theme_colors()
|
|
647
|
+
|
|
648
|
+
def _mark(key: str, text: str) -> Content:
|
|
649
|
+
if key in overridden:
|
|
650
|
+
return Content.styled(f"*{text}", colors.warning)
|
|
651
|
+
return Content(text)
|
|
652
|
+
|
|
653
|
+
def _format_token(key: str, suffix: str) -> Content | None:
|
|
654
|
+
"""Format a token-count profile key, falling back to the raw value.
|
|
655
|
+
|
|
656
|
+
Returns:
|
|
657
|
+
Styled `Content` with override marker, or None if key absent.
|
|
658
|
+
"""
|
|
659
|
+
val = profile.get(key)
|
|
660
|
+
if val is None:
|
|
661
|
+
return None
|
|
662
|
+
try:
|
|
663
|
+
text = f"{format_token_count(int(val))} {suffix}"
|
|
664
|
+
except (ValueError, TypeError, OverflowError):
|
|
665
|
+
text = f"{val} {suffix}"
|
|
666
|
+
return _mark(key, text)
|
|
667
|
+
|
|
668
|
+
def _format_flags(keys: list[tuple[str, str]]) -> list[Content]:
|
|
669
|
+
"""Render boolean profile keys as green (on) or dim (off) labels.
|
|
670
|
+
|
|
671
|
+
Returns:
|
|
672
|
+
List of styled `Content` objects for present keys.
|
|
673
|
+
"""
|
|
674
|
+
parts: list[Content] = []
|
|
675
|
+
for key, label in keys:
|
|
676
|
+
if key in profile:
|
|
677
|
+
base = (
|
|
678
|
+
Content.styled(label, colors.success)
|
|
679
|
+
if profile[key]
|
|
680
|
+
else Content.styled(label, "dim")
|
|
681
|
+
)
|
|
682
|
+
if key in overridden:
|
|
683
|
+
base = Content.assemble(
|
|
684
|
+
Content.styled("*", colors.warning), base
|
|
685
|
+
)
|
|
686
|
+
parts.append(base)
|
|
687
|
+
return parts
|
|
688
|
+
|
|
689
|
+
# Line 1: Context window
|
|
690
|
+
token_keys = [("max_input_tokens", "in"), ("max_output_tokens", "out")]
|
|
691
|
+
ctx_parts = [p for k, s in token_keys if (p := _format_token(k, s)) is not None]
|
|
692
|
+
bullet_sep = Content(f" {glyphs.bullet} ")
|
|
693
|
+
line1 = (
|
|
694
|
+
Content.assemble("Context: ", bullet_sep.join(ctx_parts))
|
|
695
|
+
if ctx_parts
|
|
696
|
+
else Content("")
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
# Line 2: Input modalities
|
|
700
|
+
modality_keys = [
|
|
701
|
+
("text_inputs", "text"),
|
|
702
|
+
("image_inputs", "image"),
|
|
703
|
+
("audio_inputs", "audio"),
|
|
704
|
+
("pdf_inputs", "pdf"),
|
|
705
|
+
("video_inputs", "video"),
|
|
706
|
+
]
|
|
707
|
+
modality_parts = _format_flags(modality_keys)
|
|
708
|
+
space = Content(" ")
|
|
709
|
+
line2 = (
|
|
710
|
+
Content.assemble("Input: ", space.join(modality_parts))
|
|
711
|
+
if modality_parts
|
|
712
|
+
else Content("")
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
# Line 3: Capabilities
|
|
716
|
+
capability_keys = [
|
|
717
|
+
("reasoning_output", "reasoning"),
|
|
718
|
+
("tool_calling", "tool calling"),
|
|
719
|
+
("structured_output", "structured output"),
|
|
720
|
+
]
|
|
721
|
+
cap_parts = _format_flags(capability_keys)
|
|
722
|
+
line3 = (
|
|
723
|
+
Content.assemble("Capabilities: ", space.join(cap_parts))
|
|
724
|
+
if cap_parts
|
|
725
|
+
else Content("")
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
# Line 4: Override notice
|
|
729
|
+
displayed_keys = {k for k, _ in token_keys + modality_keys + capability_keys}
|
|
730
|
+
has_visible_override = bool(overridden & displayed_keys)
|
|
731
|
+
line4 = (
|
|
732
|
+
Content.from_markup("[dim][yellow]*[/yellow] = override[/dim]")
|
|
733
|
+
if has_visible_override
|
|
734
|
+
else Content("")
|
|
735
|
+
)
|
|
736
|
+
|
|
737
|
+
return Content.assemble(line1, "\n", line2, "\n", line3, "\n", line4)
|
|
738
|
+
|
|
739
|
+
def _get_model_status(self, model_spec: str) -> str | None:
|
|
740
|
+
"""Look up the status field for a model from its profile.
|
|
741
|
+
|
|
742
|
+
Args:
|
|
743
|
+
model_spec: The `provider:model` string.
|
|
744
|
+
|
|
745
|
+
Returns:
|
|
746
|
+
Status string (e.g., `'deprecated'`) if the model has a profile
|
|
747
|
+
with a `status` key, otherwise None.
|
|
748
|
+
"""
|
|
749
|
+
entry = self._profiles.get(model_spec)
|
|
750
|
+
if entry is None:
|
|
751
|
+
return None
|
|
752
|
+
profile = entry.get("profile")
|
|
753
|
+
if not profile:
|
|
754
|
+
return None
|
|
755
|
+
return profile.get("status")
|
|
756
|
+
|
|
757
|
+
def _update_footer(self) -> None:
|
|
758
|
+
"""Update the detail footer for the currently highlighted model."""
|
|
759
|
+
footer = self.query_one("#model-detail-footer", Static)
|
|
760
|
+
if not self._filtered_models:
|
|
761
|
+
footer.update(Content.styled("No model selected", "dim"))
|
|
762
|
+
return
|
|
763
|
+
index = min(self._selected_index, len(self._filtered_models) - 1)
|
|
764
|
+
spec, _ = self._filtered_models[index]
|
|
765
|
+
entry = self._profiles.get(spec)
|
|
766
|
+
try:
|
|
767
|
+
text = self._format_footer(entry, get_glyphs())
|
|
768
|
+
except (KeyError, ValueError, TypeError): # Resilient footer rendering
|
|
769
|
+
logger.warning("Failed to format footer for %s", spec, exc_info=True)
|
|
770
|
+
text = Content.styled("Could not load profile details\n\n\n", "dim")
|
|
771
|
+
footer.update(text)
|
|
772
|
+
|
|
773
|
+
def _move_selection(self, delta: int) -> None:
|
|
774
|
+
"""Move selection by delta, updating only the affected widgets.
|
|
775
|
+
|
|
776
|
+
Args:
|
|
777
|
+
delta: Number of positions to move (-1 for up, +1 for down).
|
|
778
|
+
"""
|
|
779
|
+
if not self._filtered_models or not self._option_widgets:
|
|
780
|
+
return
|
|
781
|
+
|
|
782
|
+
count = len(self._filtered_models)
|
|
783
|
+
old_index = self._selected_index
|
|
784
|
+
new_index = (old_index + delta) % count
|
|
785
|
+
self._selected_index = new_index
|
|
786
|
+
|
|
787
|
+
# Update the previously selected widget
|
|
788
|
+
old_widget = self._option_widgets[old_index]
|
|
789
|
+
old_widget.remove_class("model-option-selected")
|
|
790
|
+
old_widget.update(
|
|
791
|
+
self._format_option_label(
|
|
792
|
+
old_widget.model_spec,
|
|
793
|
+
selected=False,
|
|
794
|
+
current=old_widget.model_spec == self._current_spec,
|
|
795
|
+
has_creds=old_widget.has_creds,
|
|
796
|
+
is_default=old_widget.model_spec == self._default_spec,
|
|
797
|
+
status=self._get_model_status(old_widget.model_spec),
|
|
798
|
+
)
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
# Update the newly selected widget
|
|
802
|
+
new_widget = self._option_widgets[new_index]
|
|
803
|
+
new_widget.add_class("model-option-selected")
|
|
804
|
+
new_widget.update(
|
|
805
|
+
self._format_option_label(
|
|
806
|
+
new_widget.model_spec,
|
|
807
|
+
selected=True,
|
|
808
|
+
current=new_widget.model_spec == self._current_spec,
|
|
809
|
+
has_creds=new_widget.has_creds,
|
|
810
|
+
is_default=new_widget.model_spec == self._default_spec,
|
|
811
|
+
status=self._get_model_status(new_widget.model_spec),
|
|
812
|
+
)
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
# Scroll the selected item into view
|
|
816
|
+
if new_index == 0:
|
|
817
|
+
scroll_container = self.query_one(".model-list", VerticalScroll)
|
|
818
|
+
scroll_container.scroll_home(animate=False)
|
|
819
|
+
else:
|
|
820
|
+
new_widget.scroll_visible()
|
|
821
|
+
|
|
822
|
+
self._update_footer()
|
|
823
|
+
|
|
824
|
+
def action_move_up(self) -> None:
|
|
825
|
+
"""Move selection up."""
|
|
826
|
+
self._move_selection(-1)
|
|
827
|
+
|
|
828
|
+
def action_move_down(self) -> None:
|
|
829
|
+
"""Move selection down."""
|
|
830
|
+
self._move_selection(1)
|
|
831
|
+
|
|
832
|
+
def action_tab_complete(self) -> None:
|
|
833
|
+
"""Replace search text with the currently selected model spec."""
|
|
834
|
+
if not self._filtered_models:
|
|
835
|
+
return
|
|
836
|
+
model_spec, _ = self._filtered_models[self._selected_index]
|
|
837
|
+
filter_input = self.query_one("#model-filter", Input)
|
|
838
|
+
filter_input.value = model_spec
|
|
839
|
+
filter_input.cursor_position = len(model_spec)
|
|
840
|
+
|
|
841
|
+
def _visible_page_size(self) -> int:
|
|
842
|
+
"""Return the number of model options that fit in one visual page.
|
|
843
|
+
|
|
844
|
+
Returns:
|
|
845
|
+
Number of model options per page, at least 1.
|
|
846
|
+
"""
|
|
847
|
+
default_page_size = 10
|
|
848
|
+
try:
|
|
849
|
+
scroll = self.query_one(".model-list", VerticalScroll)
|
|
850
|
+
height = scroll.size.height
|
|
851
|
+
except Exception: # noqa: BLE001 # Fallback to default page size on any widget query error
|
|
852
|
+
return default_page_size
|
|
853
|
+
if height <= 0:
|
|
854
|
+
return default_page_size
|
|
855
|
+
|
|
856
|
+
total_models = len(self._filtered_models)
|
|
857
|
+
if total_models == 0:
|
|
858
|
+
return default_page_size
|
|
859
|
+
|
|
860
|
+
# Each provider header = 1 row + margin-top: 1 (first has margin 0)
|
|
861
|
+
num_headers = len(self.query(".model-provider-header"))
|
|
862
|
+
header_rows = max(0, num_headers * 2 - 1) if num_headers else 0
|
|
863
|
+
total_rows = total_models + header_rows
|
|
864
|
+
return max(1, int(height * total_models / total_rows))
|
|
865
|
+
|
|
866
|
+
def action_page_up(self) -> None:
|
|
867
|
+
"""Move selection up by one visible page."""
|
|
868
|
+
if not self._filtered_models:
|
|
869
|
+
return
|
|
870
|
+
page = self._visible_page_size()
|
|
871
|
+
target = max(0, self._selected_index - page)
|
|
872
|
+
delta = target - self._selected_index
|
|
873
|
+
if delta != 0:
|
|
874
|
+
self._move_selection(delta)
|
|
875
|
+
|
|
876
|
+
def action_page_down(self) -> None:
|
|
877
|
+
"""Move selection down by one visible page."""
|
|
878
|
+
if not self._filtered_models:
|
|
879
|
+
return
|
|
880
|
+
count = len(self._filtered_models)
|
|
881
|
+
page = self._visible_page_size()
|
|
882
|
+
target = min(count - 1, self._selected_index + page)
|
|
883
|
+
delta = target - self._selected_index
|
|
884
|
+
if delta != 0:
|
|
885
|
+
self._move_selection(delta)
|
|
886
|
+
|
|
887
|
+
def action_select(self) -> None:
|
|
888
|
+
"""Select the current model."""
|
|
889
|
+
# If there are filtered results, always select the highlighted model
|
|
890
|
+
if self._filtered_models:
|
|
891
|
+
model_spec, provider = self._filtered_models[self._selected_index]
|
|
892
|
+
self.dismiss((model_spec, provider))
|
|
893
|
+
return
|
|
894
|
+
|
|
895
|
+
# No matches - check if user typed a custom provider:model spec
|
|
896
|
+
filter_input = self.query_one("#model-filter", Input)
|
|
897
|
+
custom_input = filter_input.value.strip()
|
|
898
|
+
|
|
899
|
+
if custom_input and ":" in custom_input:
|
|
900
|
+
provider = custom_input.split(":", 1)[0]
|
|
901
|
+
self.dismiss((custom_input, provider))
|
|
902
|
+
elif custom_input:
|
|
903
|
+
self.dismiss((custom_input, ""))
|
|
904
|
+
|
|
905
|
+
async def action_set_default(self) -> None:
|
|
906
|
+
"""Toggle the highlighted model as the default.
|
|
907
|
+
|
|
908
|
+
If the highlighted model is already the default, clears it.
|
|
909
|
+
Otherwise sets it as the new default.
|
|
910
|
+
"""
|
|
911
|
+
if not self._filtered_models or not self._option_widgets:
|
|
912
|
+
return
|
|
913
|
+
|
|
914
|
+
model_spec, _provider = self._filtered_models[self._selected_index]
|
|
915
|
+
help_widget = self.query_one(".model-selector-help", Static)
|
|
916
|
+
|
|
917
|
+
if model_spec == self._default_spec:
|
|
918
|
+
# Already default — clear it
|
|
919
|
+
if await asyncio.to_thread(clear_default_model):
|
|
920
|
+
self._default_spec = None
|
|
921
|
+
self.call_after_refresh(self._update_display)
|
|
922
|
+
help_widget.update(Content.styled("Default cleared", "bold"))
|
|
923
|
+
self.set_timer(3.0, self._restore_help_text)
|
|
924
|
+
else:
|
|
925
|
+
help_widget.update(
|
|
926
|
+
Content.styled(
|
|
927
|
+
"Failed to clear default",
|
|
928
|
+
f"bold {theme.get_theme_colors(self).error}",
|
|
929
|
+
)
|
|
930
|
+
)
|
|
931
|
+
self.set_timer(3.0, self._restore_help_text)
|
|
932
|
+
elif await asyncio.to_thread(save_default_model, model_spec):
|
|
933
|
+
self._default_spec = model_spec
|
|
934
|
+
self.call_after_refresh(self._update_display)
|
|
935
|
+
help_widget.update(
|
|
936
|
+
Content.from_markup(
|
|
937
|
+
"[bold]Default set to $spec[/bold]", spec=model_spec
|
|
938
|
+
)
|
|
939
|
+
)
|
|
940
|
+
self.set_timer(3.0, self._restore_help_text)
|
|
941
|
+
else:
|
|
942
|
+
help_widget.update(
|
|
943
|
+
Content.styled(
|
|
944
|
+
"Failed to save default",
|
|
945
|
+
f"bold {theme.get_theme_colors(self).error}",
|
|
946
|
+
)
|
|
947
|
+
)
|
|
948
|
+
self.set_timer(3.0, self._restore_help_text)
|
|
949
|
+
|
|
950
|
+
def _restore_help_text(self) -> None:
|
|
951
|
+
"""Restore the default help text after a temporary message."""
|
|
952
|
+
glyphs = get_glyphs()
|
|
953
|
+
help_text = (
|
|
954
|
+
f"{glyphs.arrow_up}/{glyphs.arrow_down} navigate"
|
|
955
|
+
f" {glyphs.bullet} Enter select"
|
|
956
|
+
f" {glyphs.bullet} Ctrl+S set default"
|
|
957
|
+
f" {glyphs.bullet} Esc cancel"
|
|
958
|
+
)
|
|
959
|
+
help_widget = self.query_one(".model-selector-help", Static)
|
|
960
|
+
help_widget.update(help_text)
|
|
961
|
+
|
|
962
|
+
def action_cancel(self) -> None:
|
|
963
|
+
"""Cancel the selection."""
|
|
964
|
+
self.dismiss(None)
|