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.
Files changed (300) hide show
  1. docagent_cli/__init__.py +36 -0
  2. docagent_cli/__main__.py +6 -0
  3. docagent_cli/_ask_user_types.py +90 -0
  4. docagent_cli/_cli_context.py +27 -0
  5. docagent_cli/_debug.py +52 -0
  6. docagent_cli/_env_vars.py +56 -0
  7. docagent_cli/_server_config.py +352 -0
  8. docagent_cli/_session_stats.py +114 -0
  9. docagent_cli/_testing_models.py +144 -0
  10. docagent_cli/_version.py +17 -0
  11. docagent_cli/agent.py +1193 -0
  12. docagent_cli/app.py +4979 -0
  13. docagent_cli/app.tcss +283 -0
  14. docagent_cli/ask_user.py +301 -0
  15. docagent_cli/built_in_skills/__init__.py +5 -0
  16. docagent_cli/built_in_skills/doc-coauthoring/SKILL.md +375 -0
  17. docagent_cli/built_in_skills/docx/LICENSE.txt +30 -0
  18. docagent_cli/built_in_skills/docx/SKILL.md +590 -0
  19. docagent_cli/built_in_skills/docx/scripts/__init__.py +1 -0
  20. docagent_cli/built_in_skills/docx/scripts/accept_changes.py +135 -0
  21. docagent_cli/built_in_skills/docx/scripts/comment.py +318 -0
  22. docagent_cli/built_in_skills/docx/scripts/office/helpers/__init__.py +0 -0
  23. docagent_cli/built_in_skills/docx/scripts/office/helpers/merge_runs.py +199 -0
  24. docagent_cli/built_in_skills/docx/scripts/office/helpers/simplify_redlines.py +197 -0
  25. docagent_cli/built_in_skills/docx/scripts/office/pack.py +159 -0
  26. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  27. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  28. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  29. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  30. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  31. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  32. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  33. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  34. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  35. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  36. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  37. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  38. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  39. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  40. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  41. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  42. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  43. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  44. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  45. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  46. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  47. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  48. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  49. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  50. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  51. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  52. docagent_cli/built_in_skills/docx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  53. docagent_cli/built_in_skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  54. docagent_cli/built_in_skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  55. docagent_cli/built_in_skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  56. docagent_cli/built_in_skills/docx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  57. docagent_cli/built_in_skills/docx/scripts/office/schemas/mce/mc.xsd +75 -0
  58. docagent_cli/built_in_skills/docx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
  59. docagent_cli/built_in_skills/docx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
  60. docagent_cli/built_in_skills/docx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
  61. docagent_cli/built_in_skills/docx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
  62. docagent_cli/built_in_skills/docx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
  63. docagent_cli/built_in_skills/docx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  64. docagent_cli/built_in_skills/docx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
  65. docagent_cli/built_in_skills/docx/scripts/office/soffice.py +183 -0
  66. docagent_cli/built_in_skills/docx/scripts/office/unpack.py +132 -0
  67. docagent_cli/built_in_skills/docx/scripts/office/validate.py +111 -0
  68. docagent_cli/built_in_skills/docx/scripts/office/validators/__init__.py +15 -0
  69. docagent_cli/built_in_skills/docx/scripts/office/validators/base.py +847 -0
  70. docagent_cli/built_in_skills/docx/scripts/office/validators/docx.py +446 -0
  71. docagent_cli/built_in_skills/docx/scripts/office/validators/pptx.py +275 -0
  72. docagent_cli/built_in_skills/docx/scripts/office/validators/redlining.py +247 -0
  73. docagent_cli/built_in_skills/docx/scripts/templates/comments.xml +3 -0
  74. docagent_cli/built_in_skills/docx/scripts/templates/commentsExtended.xml +3 -0
  75. docagent_cli/built_in_skills/docx/scripts/templates/commentsExtensible.xml +3 -0
  76. docagent_cli/built_in_skills/docx/scripts/templates/commentsIds.xml +3 -0
  77. docagent_cli/built_in_skills/docx/scripts/templates/people.xml +3 -0
  78. docagent_cli/built_in_skills/pdf/LICENSE.txt +30 -0
  79. docagent_cli/built_in_skills/pdf/SKILL.md +314 -0
  80. docagent_cli/built_in_skills/pdf/forms.md +294 -0
  81. docagent_cli/built_in_skills/pdf/reference.md +612 -0
  82. docagent_cli/built_in_skills/pdf/scripts/check_bounding_boxes.py +65 -0
  83. docagent_cli/built_in_skills/pdf/scripts/check_fillable_fields.py +11 -0
  84. docagent_cli/built_in_skills/pdf/scripts/convert_pdf_to_images.py +33 -0
  85. docagent_cli/built_in_skills/pdf/scripts/create_validation_image.py +37 -0
  86. docagent_cli/built_in_skills/pdf/scripts/extract_form_field_info.py +122 -0
  87. docagent_cli/built_in_skills/pdf/scripts/extract_form_structure.py +115 -0
  88. docagent_cli/built_in_skills/pdf/scripts/fill_fillable_fields.py +98 -0
  89. docagent_cli/built_in_skills/pdf/scripts/fill_pdf_form_with_annotations.py +107 -0
  90. docagent_cli/built_in_skills/pptx/LICENSE.txt +30 -0
  91. docagent_cli/built_in_skills/pptx/SKILL.md +232 -0
  92. docagent_cli/built_in_skills/pptx/editing.md +205 -0
  93. docagent_cli/built_in_skills/pptx/pptxgenjs.md +420 -0
  94. docagent_cli/built_in_skills/pptx/scripts/__init__.py +0 -0
  95. docagent_cli/built_in_skills/pptx/scripts/add_slide.py +195 -0
  96. docagent_cli/built_in_skills/pptx/scripts/clean.py +286 -0
  97. docagent_cli/built_in_skills/pptx/scripts/office/helpers/__init__.py +0 -0
  98. docagent_cli/built_in_skills/pptx/scripts/office/helpers/merge_runs.py +199 -0
  99. docagent_cli/built_in_skills/pptx/scripts/office/helpers/simplify_redlines.py +197 -0
  100. docagent_cli/built_in_skills/pptx/scripts/office/pack.py +159 -0
  101. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  102. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  103. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  104. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  105. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  106. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  107. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  108. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  109. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  110. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  111. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  112. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  113. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  114. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  115. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  116. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  117. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  118. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  119. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  120. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  121. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  122. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  123. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  124. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  125. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  126. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  127. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  128. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  129. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  130. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  131. docagent_cli/built_in_skills/pptx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  132. docagent_cli/built_in_skills/pptx/scripts/office/schemas/mce/mc.xsd +75 -0
  133. docagent_cli/built_in_skills/pptx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
  134. docagent_cli/built_in_skills/pptx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
  135. docagent_cli/built_in_skills/pptx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
  136. docagent_cli/built_in_skills/pptx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
  137. docagent_cli/built_in_skills/pptx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
  138. docagent_cli/built_in_skills/pptx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  139. docagent_cli/built_in_skills/pptx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
  140. docagent_cli/built_in_skills/pptx/scripts/office/soffice.py +183 -0
  141. docagent_cli/built_in_skills/pptx/scripts/office/unpack.py +132 -0
  142. docagent_cli/built_in_skills/pptx/scripts/office/validate.py +111 -0
  143. docagent_cli/built_in_skills/pptx/scripts/office/validators/__init__.py +15 -0
  144. docagent_cli/built_in_skills/pptx/scripts/office/validators/base.py +847 -0
  145. docagent_cli/built_in_skills/pptx/scripts/office/validators/docx.py +446 -0
  146. docagent_cli/built_in_skills/pptx/scripts/office/validators/pptx.py +275 -0
  147. docagent_cli/built_in_skills/pptx/scripts/office/validators/redlining.py +247 -0
  148. docagent_cli/built_in_skills/pptx/scripts/thumbnail.py +289 -0
  149. docagent_cli/built_in_skills/remember/SKILL.md +118 -0
  150. docagent_cli/built_in_skills/skill-creator/LICENSE.txt +202 -0
  151. docagent_cli/built_in_skills/skill-creator/SKILL.md +485 -0
  152. docagent_cli/built_in_skills/skill-creator/agents/analyzer.md +274 -0
  153. docagent_cli/built_in_skills/skill-creator/agents/comparator.md +202 -0
  154. docagent_cli/built_in_skills/skill-creator/agents/grader.md +223 -0
  155. docagent_cli/built_in_skills/skill-creator/assets/eval_review.html +146 -0
  156. docagent_cli/built_in_skills/skill-creator/eval-viewer/generate_review.py +471 -0
  157. docagent_cli/built_in_skills/skill-creator/eval-viewer/viewer.html +1325 -0
  158. docagent_cli/built_in_skills/skill-creator/references/schemas.md +430 -0
  159. docagent_cli/built_in_skills/skill-creator/scripts/__init__.py +0 -0
  160. docagent_cli/built_in_skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
  161. docagent_cli/built_in_skills/skill-creator/scripts/generate_report.py +326 -0
  162. docagent_cli/built_in_skills/skill-creator/scripts/improve_description.py +247 -0
  163. docagent_cli/built_in_skills/skill-creator/scripts/package_skill.py +136 -0
  164. docagent_cli/built_in_skills/skill-creator/scripts/quick_validate.py +103 -0
  165. docagent_cli/built_in_skills/skill-creator/scripts/run_eval.py +310 -0
  166. docagent_cli/built_in_skills/skill-creator/scripts/run_loop.py +328 -0
  167. docagent_cli/built_in_skills/skill-creator/scripts/utils.py +47 -0
  168. docagent_cli/built_in_skills/theme-factory/LICENSE.txt +202 -0
  169. docagent_cli/built_in_skills/theme-factory/SKILL.md +59 -0
  170. docagent_cli/built_in_skills/theme-factory/theme-showcase.pdf +0 -0
  171. docagent_cli/built_in_skills/theme-factory/themes/arctic-frost.md +19 -0
  172. docagent_cli/built_in_skills/theme-factory/themes/botanical-garden.md +19 -0
  173. docagent_cli/built_in_skills/theme-factory/themes/desert-rose.md +19 -0
  174. docagent_cli/built_in_skills/theme-factory/themes/forest-canopy.md +19 -0
  175. docagent_cli/built_in_skills/theme-factory/themes/golden-hour.md +19 -0
  176. docagent_cli/built_in_skills/theme-factory/themes/midnight-galaxy.md +19 -0
  177. docagent_cli/built_in_skills/theme-factory/themes/modern-minimalist.md +19 -0
  178. docagent_cli/built_in_skills/theme-factory/themes/ocean-depths.md +19 -0
  179. docagent_cli/built_in_skills/theme-factory/themes/sunset-boulevard.md +19 -0
  180. docagent_cli/built_in_skills/theme-factory/themes/tech-innovation.md +19 -0
  181. docagent_cli/built_in_skills/xlsx/LICENSE.txt +30 -0
  182. docagent_cli/built_in_skills/xlsx/SKILL.md +292 -0
  183. docagent_cli/built_in_skills/xlsx/scripts/office/helpers/__init__.py +0 -0
  184. docagent_cli/built_in_skills/xlsx/scripts/office/helpers/merge_runs.py +199 -0
  185. docagent_cli/built_in_skills/xlsx/scripts/office/helpers/simplify_redlines.py +197 -0
  186. docagent_cli/built_in_skills/xlsx/scripts/office/pack.py +159 -0
  187. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chart.xsd +1499 -0
  188. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-chartDrawing.xsd +146 -0
  189. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-diagram.xsd +1085 -0
  190. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-lockedCanvas.xsd +11 -0
  191. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-main.xsd +3081 -0
  192. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-picture.xsd +23 -0
  193. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-spreadsheetDrawing.xsd +185 -0
  194. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/dml-wordprocessingDrawing.xsd +287 -0
  195. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/pml.xsd +1676 -0
  196. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-additionalCharacteristics.xsd +28 -0
  197. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-bibliography.xsd +144 -0
  198. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-commonSimpleTypes.xsd +174 -0
  199. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlDataProperties.xsd +25 -0
  200. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-customXmlSchemaProperties.xsd +18 -0
  201. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesCustom.xsd +59 -0
  202. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesExtended.xsd +56 -0
  203. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-documentPropertiesVariantTypes.xsd +195 -0
  204. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-math.xsd +582 -0
  205. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/shared-relationshipReference.xsd +25 -0
  206. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/sml.xsd +4439 -0
  207. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-main.xsd +570 -0
  208. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-officeDrawing.xsd +509 -0
  209. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-presentationDrawing.xsd +12 -0
  210. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-spreadsheetDrawing.xsd +108 -0
  211. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/vml-wordprocessingDrawing.xsd +96 -0
  212. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/wml.xsd +3646 -0
  213. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ISO-IEC29500-4_2016/xml.xsd +116 -0
  214. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-contentTypes.xsd +42 -0
  215. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-coreProperties.xsd +50 -0
  216. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-digSig.xsd +49 -0
  217. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/ecma/fouth-edition/opc-relationships.xsd +33 -0
  218. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/mce/mc.xsd +75 -0
  219. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/microsoft/wml-2010.xsd +560 -0
  220. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/microsoft/wml-2012.xsd +67 -0
  221. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/microsoft/wml-2018.xsd +14 -0
  222. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/microsoft/wml-cex-2018.xsd +20 -0
  223. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/microsoft/wml-cid-2016.xsd +13 -0
  224. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/microsoft/wml-sdtdatahash-2020.xsd +4 -0
  225. docagent_cli/built_in_skills/xlsx/scripts/office/schemas/microsoft/wml-symex-2015.xsd +8 -0
  226. docagent_cli/built_in_skills/xlsx/scripts/office/soffice.py +183 -0
  227. docagent_cli/built_in_skills/xlsx/scripts/office/unpack.py +132 -0
  228. docagent_cli/built_in_skills/xlsx/scripts/office/validate.py +111 -0
  229. docagent_cli/built_in_skills/xlsx/scripts/office/validators/__init__.py +15 -0
  230. docagent_cli/built_in_skills/xlsx/scripts/office/validators/base.py +847 -0
  231. docagent_cli/built_in_skills/xlsx/scripts/office/validators/docx.py +446 -0
  232. docagent_cli/built_in_skills/xlsx/scripts/office/validators/pptx.py +275 -0
  233. docagent_cli/built_in_skills/xlsx/scripts/office/validators/redlining.py +247 -0
  234. docagent_cli/built_in_skills/xlsx/scripts/recalc.py +184 -0
  235. docagent_cli/clipboard.py +128 -0
  236. docagent_cli/command_registry.py +284 -0
  237. docagent_cli/config.py +2418 -0
  238. docagent_cli/configurable_model.py +162 -0
  239. docagent_cli/default_agent_prompt.md +12 -0
  240. docagent_cli/editor.py +142 -0
  241. docagent_cli/file_ops.py +473 -0
  242. docagent_cli/formatting.py +28 -0
  243. docagent_cli/hooks.py +206 -0
  244. docagent_cli/input.py +787 -0
  245. docagent_cli/integrations/__init__.py +1 -0
  246. docagent_cli/integrations/sandbox_factory.py +873 -0
  247. docagent_cli/integrations/sandbox_provider.py +71 -0
  248. docagent_cli/local_context.py +718 -0
  249. docagent_cli/main.py +1641 -0
  250. docagent_cli/mcp_tools.py +707 -0
  251. docagent_cli/mcp_trust.py +168 -0
  252. docagent_cli/media_utils.py +478 -0
  253. docagent_cli/model_config.py +1620 -0
  254. docagent_cli/non_interactive.py +948 -0
  255. docagent_cli/offload.py +371 -0
  256. docagent_cli/output.py +69 -0
  257. docagent_cli/project_utils.py +188 -0
  258. docagent_cli/py.typed +0 -0
  259. docagent_cli/remote_client.py +515 -0
  260. docagent_cli/server.py +520 -0
  261. docagent_cli/server_graph.py +196 -0
  262. docagent_cli/server_manager.py +365 -0
  263. docagent_cli/sessions.py +1262 -0
  264. docagent_cli/skills/__init__.py +18 -0
  265. docagent_cli/skills/commands.py +1090 -0
  266. docagent_cli/skills/load.py +192 -0
  267. docagent_cli/subagents.py +173 -0
  268. docagent_cli/system_prompt.md +247 -0
  269. docagent_cli/textual_adapter.py +1352 -0
  270. docagent_cli/theme.py +842 -0
  271. docagent_cli/token_state.py +31 -0
  272. docagent_cli/tool_display.py +298 -0
  273. docagent_cli/tools.py +236 -0
  274. docagent_cli/ui.py +420 -0
  275. docagent_cli/unicode_security.py +516 -0
  276. docagent_cli/update_check.py +454 -0
  277. docagent_cli/widgets/__init__.py +9 -0
  278. docagent_cli/widgets/_links.py +63 -0
  279. docagent_cli/widgets/approval.py +442 -0
  280. docagent_cli/widgets/ask_user.py +398 -0
  281. docagent_cli/widgets/autocomplete.py +691 -0
  282. docagent_cli/widgets/chat_input.py +1827 -0
  283. docagent_cli/widgets/diff.py +248 -0
  284. docagent_cli/widgets/history.py +188 -0
  285. docagent_cli/widgets/loading.py +177 -0
  286. docagent_cli/widgets/mcp_viewer.py +362 -0
  287. docagent_cli/widgets/message_store.py +675 -0
  288. docagent_cli/widgets/messages.py +1751 -0
  289. docagent_cli/widgets/model_selector.py +964 -0
  290. docagent_cli/widgets/status.py +372 -0
  291. docagent_cli/widgets/theme_selector.py +164 -0
  292. docagent_cli/widgets/thread_selector.py +1905 -0
  293. docagent_cli/widgets/tool_renderers.py +148 -0
  294. docagent_cli/widgets/tool_widgets.py +274 -0
  295. docagent_cli/widgets/welcome.py +339 -0
  296. docagent_cli-0.0.35.data/data/docagent_cli/default_agent_prompt.md +12 -0
  297. docagent_cli-0.0.35.dist-info/METADATA +200 -0
  298. docagent_cli-0.0.35.dist-info/RECORD +300 -0
  299. docagent_cli-0.0.35.dist-info/WHEEL +4 -0
  300. 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)