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,1262 @@
1
+ """Thread management using LangGraph's built-in checkpoint persistence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ import sqlite3
8
+ from contextlib import asynccontextmanager
9
+ from datetime import datetime
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING, NamedTuple, NotRequired, TypedDict, cast
12
+
13
+ if TYPE_CHECKING:
14
+ from collections.abc import AsyncIterator
15
+
16
+ import aiosqlite
17
+ from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
18
+ from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
19
+
20
+ from docagent_cli.output import OutputFormat
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+ _aiosqlite_patched = False
25
+ _jsonplus_serializer: JsonPlusSerializer | None = None
26
+ _message_count_cache: dict[str, tuple[str | None, int]] = {}
27
+ _MAX_MESSAGE_COUNT_CACHE = 4096
28
+ _initial_prompt_cache: dict[str, tuple[str | None, str | None]] = {}
29
+ _MAX_INITIAL_PROMPT_CACHE = 4096
30
+ _recent_threads_cache: dict[tuple[str | None, int], list[ThreadInfo]] = {}
31
+ _MAX_RECENT_THREADS_CACHE_KEYS = 16
32
+
33
+
34
+ def _patch_aiosqlite() -> None:
35
+ """Patch aiosqlite.Connection with `is_alive()` if missing.
36
+
37
+ Required by langgraph-checkpoint>=2.1.0.
38
+ See: https://github.com/langchain-ai/langgraph/issues/6583
39
+ """
40
+ global _aiosqlite_patched # noqa: PLW0603 # Module-level flag requires global statement
41
+ if _aiosqlite_patched:
42
+ return
43
+
44
+ import aiosqlite as _aiosqlite
45
+
46
+ if not hasattr(_aiosqlite.Connection, "is_alive"):
47
+
48
+ def _is_alive(self: _aiosqlite.Connection) -> bool:
49
+ """Check if the connection is still alive.
50
+
51
+ Returns:
52
+ True if connection is alive, False otherwise.
53
+ """
54
+ return bool(self._running and self._connection is not None)
55
+
56
+ # Dynamically adding a method to aiosqlite.Connection at runtime.
57
+ # Type checkers can't understand this monkey-patch, so we suppress the
58
+ # "attr-defined" error that would otherwise be raised.
59
+ _aiosqlite.Connection.is_alive = _is_alive # type: ignore[attr-defined]
60
+
61
+ _aiosqlite_patched = True
62
+
63
+
64
+ @asynccontextmanager
65
+ async def _connect() -> AsyncIterator[aiosqlite.Connection]:
66
+ """Import aiosqlite, apply the compatibility patch, and connect.
67
+
68
+ Centralizes the deferred import + patch + connect sequence used by every
69
+ database function in this module.
70
+
71
+ Yields:
72
+ An open aiosqlite connection to the sessions database.
73
+ """
74
+ import aiosqlite as _aiosqlite
75
+
76
+ _patch_aiosqlite()
77
+
78
+ async with _aiosqlite.connect(str(get_db_path()), timeout=30.0) as conn:
79
+ yield conn
80
+
81
+
82
+ class ThreadInfo(TypedDict):
83
+ """Thread metadata returned by `list_threads`."""
84
+
85
+ thread_id: str
86
+ """Unique identifier for the thread."""
87
+
88
+ agent_name: str | None
89
+ """Name of the agent that owns the thread."""
90
+
91
+ updated_at: str | None
92
+ """ISO timestamp of the last update."""
93
+
94
+ created_at: NotRequired[str | None]
95
+ """ISO timestamp of thread creation (earliest checkpoint)."""
96
+
97
+ git_branch: NotRequired[str | None]
98
+ """Git branch active when the thread was created."""
99
+
100
+ initial_prompt: NotRequired[str | None]
101
+ """First human message in the thread."""
102
+
103
+ message_count: NotRequired[int]
104
+ """Number of messages in the thread."""
105
+
106
+ latest_checkpoint_id: NotRequired[str | None]
107
+ """Most recent checkpoint ID for cache invalidation."""
108
+
109
+ cwd: NotRequired[str | None]
110
+ """Working directory where the thread was last used."""
111
+
112
+
113
+ class _CheckpointSummary(NamedTuple):
114
+ """Structured data extracted from a thread's latest checkpoint."""
115
+
116
+ message_count: int
117
+ """Number of messages in the latest checkpoint."""
118
+
119
+ initial_prompt: str | None
120
+ """First human prompt recovered from the latest checkpoint."""
121
+
122
+
123
+ def format_timestamp(iso_timestamp: str | None) -> str:
124
+ """Format ISO timestamp for display (e.g., 'Dec 30, 6:10pm').
125
+
126
+ Args:
127
+ iso_timestamp: ISO 8601 timestamp string, or `None`.
128
+
129
+ Returns:
130
+ Formatted timestamp string or empty string if invalid.
131
+ """
132
+ if not iso_timestamp:
133
+ return ""
134
+ try:
135
+ dt = datetime.fromisoformat(iso_timestamp).astimezone()
136
+ return (
137
+ dt.strftime("%b %d, %-I:%M%p")
138
+ .lower()
139
+ .replace("am", "am")
140
+ .replace("pm", "pm")
141
+ )
142
+ except (ValueError, TypeError):
143
+ logger.debug(
144
+ "Failed to parse timestamp %r; displaying as blank",
145
+ iso_timestamp,
146
+ exc_info=True,
147
+ )
148
+ return ""
149
+
150
+
151
+ def format_relative_timestamp(iso_timestamp: str | None) -> str:
152
+ """Format ISO timestamp as relative time (e.g., '5m ago', '2h ago').
153
+
154
+ Args:
155
+ iso_timestamp: ISO 8601 timestamp string, or `None`.
156
+
157
+ Returns:
158
+ Relative time string or empty string if invalid.
159
+ """
160
+ if not iso_timestamp:
161
+ return ""
162
+ try:
163
+ dt = datetime.fromisoformat(iso_timestamp).astimezone()
164
+ except (ValueError, TypeError):
165
+ logger.debug(
166
+ "Failed to parse timestamp %r; displaying as blank",
167
+ iso_timestamp,
168
+ exc_info=True,
169
+ )
170
+ return ""
171
+
172
+ delta = datetime.now(tz=dt.tzinfo) - dt
173
+ seconds = int(delta.total_seconds())
174
+ if seconds < 0:
175
+ return "just now"
176
+ if seconds < 60: # noqa: PLR2004
177
+ return f"{seconds}s ago"
178
+ minutes = seconds // 60
179
+ if minutes < 60: # noqa: PLR2004
180
+ return f"{minutes}m ago"
181
+ hours = minutes // 60
182
+ if hours < 24: # noqa: PLR2004
183
+ return f"{hours}h ago"
184
+ days = hours // 24
185
+ if days < 30: # noqa: PLR2004
186
+ return f"{days}d ago"
187
+ months = days // 30
188
+ if months < 12: # noqa: PLR2004
189
+ return f"{months}mo ago"
190
+ years = days // 365
191
+ return f"{years}y ago"
192
+
193
+
194
+ def format_path(path: str | None) -> str:
195
+ """Format a filesystem path for display.
196
+
197
+ Paths under the user's home directory are shown relative to `~`.
198
+ All other paths are returned as-is.
199
+
200
+ Args:
201
+ path: Absolute filesystem path, or `None`.
202
+
203
+ Returns:
204
+ Formatted path string, or empty string if path is falsy.
205
+ """
206
+ if not path:
207
+ return ""
208
+ try:
209
+ home = str(Path.home())
210
+ if path == home:
211
+ return "~"
212
+ prefix = home + "/"
213
+ if path.startswith(prefix):
214
+ return "~/" + path[len(prefix) :]
215
+ except (RuntimeError, KeyError, OSError):
216
+ logger.debug(
217
+ "Could not resolve home directory for path formatting", exc_info=True
218
+ )
219
+ return path
220
+ else:
221
+ return path
222
+
223
+
224
+ _db_path: Path | None = None
225
+
226
+
227
+ def get_db_path() -> Path:
228
+ """Get path to global database.
229
+
230
+ The result is cached after the first successful call to avoid repeated
231
+ filesystem operations.
232
+
233
+ Returns:
234
+ Path to the SQLite database file.
235
+ """
236
+ global _db_path # noqa: PLW0603 # Module-level cache requires global statement
237
+ if _db_path is not None:
238
+ return _db_path
239
+ db_dir = Path.home() / ".docagent"
240
+ db_dir.mkdir(parents=True, exist_ok=True)
241
+ _db_path = db_dir / "sessions.db"
242
+ return _db_path
243
+
244
+
245
+ def generate_thread_id() -> str:
246
+ """Generate a new thread ID as a full UUID7 string.
247
+
248
+ Returns:
249
+ UUID7 string (time-ordered for natural sort by creation time).
250
+ """
251
+ from uuid_utils import uuid7
252
+
253
+ return str(uuid7())
254
+
255
+
256
+ async def _table_exists(conn: aiosqlite.Connection, table: str) -> bool:
257
+ """Check if a table exists in the database.
258
+
259
+ Returns:
260
+ True if table exists, False otherwise.
261
+ """
262
+ query = "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?"
263
+ async with conn.execute(query, (table,)) as cursor:
264
+ return await cursor.fetchone() is not None
265
+
266
+
267
+ async def list_threads(
268
+ agent_name: str | None = None,
269
+ limit: int = 20,
270
+ include_message_count: bool = False,
271
+ sort_by: str = "updated",
272
+ branch: str | None = None,
273
+ ) -> list[ThreadInfo]:
274
+ """List threads from checkpoints table.
275
+
276
+ Args:
277
+ agent_name: Optional filter by agent name.
278
+ limit: Maximum number of threads to return.
279
+ include_message_count: Whether to include message counts.
280
+ sort_by: Sort field — `"updated"` or `"created"`.
281
+ branch: Optional filter by git branch name.
282
+
283
+ Returns:
284
+ List of `ThreadInfo` dicts with `thread_id`, `agent_name`,
285
+ `updated_at`, `created_at`, `latest_checkpoint_id`, `git_branch`,
286
+ `cwd`, and optionally `message_count`.
287
+
288
+ Raises:
289
+ ValueError: If `sort_by` is not `"updated"` or `"created"`.
290
+ """
291
+ async with _connect() as conn:
292
+ if not await _table_exists(conn, "checkpoints"):
293
+ return []
294
+
295
+ if sort_by not in {"updated", "created"}:
296
+ msg = f"Invalid sort_by {sort_by!r}; expected 'updated' or 'created'"
297
+ raise ValueError(msg)
298
+ order_col = "created_at" if sort_by == "created" else "updated_at"
299
+
300
+ where_clauses: list[str] = []
301
+ params_list: list[str | int] = []
302
+
303
+ if agent_name:
304
+ where_clauses.append("json_extract(metadata, '$.agent_name') = ?")
305
+ params_list.append(agent_name)
306
+ if branch:
307
+ where_clauses.append("json_extract(metadata, '$.git_branch') = ?")
308
+ params_list.append(branch)
309
+
310
+ where_sql = f"WHERE {' AND '.join(where_clauses)}" if where_clauses else ""
311
+
312
+ query = f"""
313
+ SELECT thread_id,
314
+ json_extract(metadata, '$.agent_name') as agent_name,
315
+ MAX(json_extract(metadata, '$.updated_at')) as updated_at,
316
+ MAX(checkpoint_id) as latest_checkpoint_id,
317
+ MIN(json_extract(metadata, '$.updated_at')) as created_at,
318
+ MAX(json_extract(metadata, '$.git_branch')) as git_branch,
319
+ MAX(json_extract(metadata, '$.cwd')) as cwd
320
+ FROM checkpoints
321
+ {where_sql}
322
+ GROUP BY thread_id
323
+ ORDER BY {order_col} DESC
324
+ LIMIT ?
325
+ """ # noqa: S608 # where_sql/order_col derived from controlled internal values; user values use ? placeholders
326
+ params: tuple = (*params_list, limit)
327
+
328
+ async with conn.execute(query, params) as cursor:
329
+ rows = await cursor.fetchall()
330
+ threads: list[ThreadInfo] = [
331
+ ThreadInfo(
332
+ thread_id=r[0],
333
+ agent_name=r[1],
334
+ updated_at=r[2],
335
+ latest_checkpoint_id=r[3],
336
+ created_at=r[4],
337
+ git_branch=r[5],
338
+ cwd=r[6],
339
+ )
340
+ for r in rows
341
+ ]
342
+
343
+ # Fetch message counts if requested
344
+ if include_message_count and threads:
345
+ await _populate_message_counts(conn, threads)
346
+
347
+ # Only cache unfiltered results so the thread selector modal
348
+ # doesn't receive branch-filtered or differently-sorted data.
349
+ if sort_by == "updated" and branch is None:
350
+ _cache_recent_threads(agent_name, limit, threads)
351
+ return threads
352
+
353
+
354
+ async def populate_thread_message_counts(threads: list[ThreadInfo]) -> list[ThreadInfo]:
355
+ """Populate `message_count` for an existing thread list.
356
+
357
+ This is used by the `/threads` modal to render rows quickly, then backfill
358
+ counts in the background without issuing a second thread-list query.
359
+
360
+ Args:
361
+ threads: Thread rows to enrich in place.
362
+
363
+ Returns:
364
+ The same list object with `message_count` values populated.
365
+ """
366
+ if not threads:
367
+ return threads
368
+
369
+ async with _connect() as conn:
370
+ await _populate_message_counts(conn, threads)
371
+ return threads
372
+
373
+
374
+ async def populate_thread_checkpoint_details(
375
+ threads: list[ThreadInfo],
376
+ *,
377
+ include_message_count: bool = True,
378
+ include_initial_prompt: bool = True,
379
+ ) -> list[ThreadInfo]:
380
+ """Populate checkpoint-derived fields for an existing thread list.
381
+
382
+ This is used by the `/threads` modal to enrich rows in one background pass,
383
+ so the latest checkpoint is fetched and deserialized at most once per row.
384
+
385
+ Args:
386
+ threads: Thread rows to enrich in place.
387
+ include_message_count: Whether to populate `message_count`.
388
+ include_initial_prompt: Whether to populate `initial_prompt`.
389
+
390
+ Returns:
391
+ The same list object with missing checkpoint-derived fields populated.
392
+ """
393
+ if not threads or (not include_message_count and not include_initial_prompt):
394
+ return threads
395
+
396
+ async with _connect() as conn:
397
+ await _populate_checkpoint_fields(
398
+ conn,
399
+ threads,
400
+ include_message_count=include_message_count,
401
+ include_initial_prompt=include_initial_prompt,
402
+ )
403
+ return threads
404
+
405
+
406
+ async def prewarm_thread_message_counts(limit: int | None = None) -> None:
407
+ """Prewarm thread selector cache for faster `/threads` open.
408
+
409
+ Fetches a bounded list of recent threads and populates checkpoint-derived
410
+ fields for currently visible columns into the in-memory cache. Intended to
411
+ run in a background worker during app startup.
412
+
413
+ Args:
414
+ limit: Maximum threads to prewarm. Uses `get_thread_limit()` when `None`.
415
+ """
416
+ thread_limit = limit if limit is not None else get_thread_limit()
417
+ if thread_limit < 1:
418
+ return
419
+
420
+ try:
421
+ from docagent_cli.model_config import load_thread_config
422
+
423
+ cfg = load_thread_config()
424
+ threads = await list_threads(limit=thread_limit, include_message_count=False)
425
+ if threads:
426
+ await populate_thread_checkpoint_details(
427
+ threads,
428
+ include_message_count=cfg.columns.get("messages", False),
429
+ include_initial_prompt=cfg.columns.get("initial_prompt", False),
430
+ )
431
+ _cache_recent_threads(None, thread_limit, threads)
432
+ except (OSError, sqlite3.Error):
433
+ logger.debug("Could not prewarm thread selector cache", exc_info=True)
434
+ except Exception:
435
+ logger.warning(
436
+ "Unexpected error while prewarming thread selector cache",
437
+ exc_info=True,
438
+ )
439
+
440
+
441
+ def get_cached_threads(
442
+ agent_name: str | None = None,
443
+ limit: int | None = None,
444
+ ) -> list[ThreadInfo] | None:
445
+ """Get cached recent threads, if available.
446
+
447
+ Args:
448
+ agent_name: Optional agent-name filter key.
449
+ limit: Maximum rows requested. Uses `get_thread_limit()` when `None`.
450
+
451
+ Returns:
452
+ Copy of cached rows when available, otherwise `None`.
453
+ """
454
+
455
+ def _copy_with_cached_counts(rows: list[ThreadInfo]) -> list[ThreadInfo]:
456
+ copied_rows = _copy_threads(rows)
457
+ apply_cached_thread_message_counts(copied_rows)
458
+ apply_cached_thread_initial_prompts(copied_rows)
459
+ return copied_rows
460
+
461
+ thread_limit = limit if limit is not None else get_thread_limit()
462
+ if thread_limit < 1:
463
+ return None
464
+
465
+ exact = _recent_threads_cache.get((agent_name, thread_limit))
466
+ if exact is not None:
467
+ return _copy_with_cached_counts(exact)
468
+
469
+ best_key: tuple[str | None, int] | None = None
470
+ for key in _recent_threads_cache:
471
+ cache_agent, cache_limit = key
472
+ if cache_agent != agent_name or cache_limit < thread_limit:
473
+ continue
474
+ if best_key is None or cache_limit < best_key[1]:
475
+ best_key = key
476
+
477
+ if best_key is None:
478
+ return None
479
+
480
+ return _copy_with_cached_counts(_recent_threads_cache[best_key][:thread_limit])
481
+
482
+
483
+ def apply_cached_thread_message_counts(threads: list[ThreadInfo]) -> int:
484
+ """Apply cached message counts onto thread rows when freshness matches.
485
+
486
+ Args:
487
+ threads: Thread rows to mutate in place.
488
+
489
+ Returns:
490
+ Number of rows that were populated from cache.
491
+ """
492
+ populated = 0
493
+ for thread in threads:
494
+ if "message_count" in thread:
495
+ continue
496
+ thread_id = thread["thread_id"]
497
+ freshness = _thread_freshness(thread)
498
+ cached = _message_count_cache.get(thread_id)
499
+ if cached is None or cached[0] != freshness:
500
+ continue
501
+ thread["message_count"] = cached[1]
502
+ populated += 1
503
+ return populated
504
+
505
+
506
+ def apply_cached_thread_initial_prompts(threads: list[ThreadInfo]) -> int:
507
+ """Apply cached initial prompts onto thread rows when freshness matches.
508
+
509
+ Args:
510
+ threads: Thread rows to mutate in place.
511
+
512
+ Returns:
513
+ Number of rows that were populated from cache.
514
+ """
515
+ populated = 0
516
+ for thread in threads:
517
+ if "initial_prompt" in thread:
518
+ continue
519
+ thread_id = thread["thread_id"]
520
+ freshness = _thread_freshness(thread)
521
+ cached = _initial_prompt_cache.get(thread_id)
522
+ if cached is None or cached[0] != freshness:
523
+ continue
524
+ thread["initial_prompt"] = cached[1]
525
+ populated += 1
526
+ return populated
527
+
528
+
529
+ async def _populate_message_counts(
530
+ conn: aiosqlite.Connection,
531
+ threads: list[ThreadInfo],
532
+ ) -> None:
533
+ """Fill `message_count` on thread rows with cache-aware lookup."""
534
+ await _populate_checkpoint_fields(
535
+ conn,
536
+ threads,
537
+ include_message_count=True,
538
+ include_initial_prompt=False,
539
+ )
540
+
541
+
542
+ async def _get_jsonplus_serializer() -> JsonPlusSerializer:
543
+ """Return a cached JsonPlus serializer, loading it off the UI loop."""
544
+ global _jsonplus_serializer # noqa: PLW0603 # Module-level cache requires global statement
545
+ if _jsonplus_serializer is not None:
546
+ return _jsonplus_serializer
547
+
548
+ loop = asyncio.get_running_loop()
549
+ _jsonplus_serializer = await loop.run_in_executor(None, _create_jsonplus_serializer)
550
+ return _jsonplus_serializer
551
+
552
+
553
+ def _create_jsonplus_serializer() -> JsonPlusSerializer:
554
+ """Import and create a JsonPlus serializer.
555
+
556
+ Returns:
557
+ A ready `JsonPlusSerializer` instance.
558
+ """
559
+ from langgraph.checkpoint.serde.jsonplus import JsonPlusSerializer
560
+
561
+ return JsonPlusSerializer()
562
+
563
+
564
+ def _cache_message_count(thread_id: str, freshness: str | None, count: int) -> None:
565
+ """Cache a thread's message count with a freshness token."""
566
+ if len(_message_count_cache) >= _MAX_MESSAGE_COUNT_CACHE and (
567
+ thread_id not in _message_count_cache
568
+ ):
569
+ oldest = next(iter(_message_count_cache))
570
+ _message_count_cache.pop(oldest, None)
571
+ _message_count_cache[thread_id] = (freshness, count)
572
+
573
+
574
+ def _cache_initial_prompt(
575
+ thread_id: str,
576
+ freshness: str | None,
577
+ initial_prompt: str | None,
578
+ ) -> None:
579
+ """Cache a thread's initial prompt with a freshness token."""
580
+ if len(_initial_prompt_cache) >= _MAX_INITIAL_PROMPT_CACHE and (
581
+ thread_id not in _initial_prompt_cache
582
+ ):
583
+ oldest = next(iter(_initial_prompt_cache))
584
+ _initial_prompt_cache.pop(oldest, None)
585
+ _initial_prompt_cache[thread_id] = (freshness, initial_prompt)
586
+
587
+
588
+ def _thread_freshness(thread: ThreadInfo) -> str | None:
589
+ """Return a cache freshness token for a thread row."""
590
+ return thread.get("latest_checkpoint_id") or thread.get("updated_at")
591
+
592
+
593
+ def _cache_recent_threads(
594
+ agent_name: str | None,
595
+ limit: int,
596
+ threads: list[ThreadInfo],
597
+ ) -> None:
598
+ """Store a copy of recent thread rows for fast selector startup."""
599
+ key = (agent_name, max(1, limit))
600
+ if len(_recent_threads_cache) >= _MAX_RECENT_THREADS_CACHE_KEYS and (
601
+ key not in _recent_threads_cache
602
+ ):
603
+ _recent_threads_cache.clear()
604
+ _recent_threads_cache[key] = _copy_threads(threads)
605
+
606
+
607
+ def _copy_threads(threads: list[ThreadInfo]) -> list[ThreadInfo]:
608
+ """Return shallow-copied thread rows."""
609
+ return [ThreadInfo(**thread) for thread in threads]
610
+
611
+
612
+ async def _count_messages_from_checkpoint(
613
+ conn: aiosqlite.Connection,
614
+ thread_id: str,
615
+ serde: JsonPlusSerializer,
616
+ ) -> int:
617
+ """Count messages from the most recent checkpoint blob.
618
+
619
+ With `durability='exit'`, messages are stored in the checkpoint blob, not in
620
+ the writes table. This function deserializes the checkpoint and counts the
621
+ messages in channel_values.
622
+
623
+ Args:
624
+ conn: Database connection.
625
+ thread_id: The thread ID to count messages for.
626
+ serde: Serializer for decoding checkpoint data.
627
+
628
+ Returns:
629
+ Number of messages in the checkpoint, or 0 if not found.
630
+ """
631
+ return (await _load_latest_checkpoint_summary(conn, thread_id, serde)).message_count
632
+
633
+
634
+ async def _extract_initial_prompt(
635
+ conn: aiosqlite.Connection,
636
+ thread_id: str,
637
+ serde: JsonPlusSerializer,
638
+ ) -> str | None:
639
+ """Extract the first human message from the latest checkpoint.
640
+
641
+ Args:
642
+ conn: Database connection.
643
+ thread_id: The thread ID to extract from.
644
+ serde: Serializer for decoding checkpoint data.
645
+
646
+ Returns:
647
+ First human message content, or None if not found.
648
+ """
649
+ summary = await _load_latest_checkpoint_summary(conn, thread_id, serde)
650
+ return summary.initial_prompt
651
+
652
+
653
+ async def populate_thread_initial_prompts(threads: list[ThreadInfo]) -> None:
654
+ """Populate `initial_prompt` for thread rows in the background.
655
+
656
+ Args:
657
+ threads: Thread rows to enrich in place.
658
+ """
659
+ if not threads:
660
+ return
661
+
662
+ async with _connect() as conn:
663
+ await _populate_checkpoint_fields(
664
+ conn,
665
+ threads,
666
+ include_message_count=False,
667
+ include_initial_prompt=True,
668
+ )
669
+
670
+
671
+ async def _populate_checkpoint_fields(
672
+ conn: aiosqlite.Connection,
673
+ threads: list[ThreadInfo],
674
+ *,
675
+ include_message_count: bool,
676
+ include_initial_prompt: bool,
677
+ ) -> None:
678
+ """Populate checkpoint-derived thread fields with a batched latest-row pass."""
679
+ serde = await _get_jsonplus_serializer()
680
+
681
+ # Phase 1: apply cache hits, collect threads that need DB fetch.
682
+ uncached: list[ThreadInfo] = []
683
+ for thread in threads:
684
+ thread_id = thread["thread_id"]
685
+ freshness = _thread_freshness(thread)
686
+ needs_count = False
687
+ needs_prompt = False
688
+
689
+ if include_message_count:
690
+ cached = _message_count_cache.get(thread_id)
691
+ if cached is not None and cached[0] == freshness:
692
+ thread["message_count"] = cached[1]
693
+ else:
694
+ needs_count = True
695
+
696
+ if include_initial_prompt and "initial_prompt" not in thread:
697
+ cached_prompt = _initial_prompt_cache.get(thread_id)
698
+ if cached_prompt is not None and cached_prompt[0] == freshness:
699
+ thread["initial_prompt"] = cached_prompt[1]
700
+ else:
701
+ needs_prompt = True
702
+
703
+ if needs_count or needs_prompt:
704
+ uncached.append(thread)
705
+
706
+ if not uncached:
707
+ return
708
+
709
+ # Phase 2: batch-fetch all uncached threads.
710
+ uncached_ids = [t["thread_id"] for t in uncached]
711
+ batch_results = await _load_latest_checkpoint_summaries_batch(
712
+ conn, uncached_ids, serde
713
+ )
714
+
715
+ # Phase 3: apply results and update caches.
716
+ for thread in uncached:
717
+ thread_id = thread["thread_id"]
718
+ freshness = _thread_freshness(thread)
719
+ summary = batch_results.get(thread_id, _CheckpointSummary(0, None))
720
+
721
+ if include_message_count and "message_count" not in thread:
722
+ thread["message_count"] = summary.message_count
723
+ _cache_message_count(thread_id, freshness, summary.message_count)
724
+ if include_initial_prompt and "initial_prompt" not in thread:
725
+ thread["initial_prompt"] = summary.initial_prompt
726
+ _cache_initial_prompt(thread_id, freshness, summary.initial_prompt)
727
+
728
+
729
+ _SQLITE_MAX_VARIABLE_NUMBER = 500
730
+ """Max `?` placeholders per SQL query.
731
+
732
+ SQLite limits how many `?` parameters a single query can have (default 999,
733
+ lower on some builds). If a user accumulates hundreds of threads and the
734
+ `/threads` modal fetches them all at once, the `IN (?, ?, ...)` clause could
735
+ exceed that limit. We chunk to this size to stay safe.
736
+ """
737
+
738
+
739
+ async def _load_latest_checkpoint_summaries_batch(
740
+ conn: aiosqlite.Connection,
741
+ thread_ids: list[str],
742
+ serde: JsonPlusSerializer,
743
+ ) -> dict[str, _CheckpointSummary]:
744
+ """Batch-load the latest checkpoint summary for multiple threads.
745
+
746
+ Uses a window function to fetch the latest checkpoint per thread, issuing
747
+ one query per chunk for SQLite variable-limit safety.
748
+
749
+ Args:
750
+ conn: Database connection.
751
+ thread_ids: Thread IDs to look up.
752
+ serde: Serializer for decoding checkpoint blobs.
753
+
754
+ Returns:
755
+ Dict mapping thread IDs to their checkpoint summaries.
756
+ """
757
+ if not thread_ids:
758
+ return {}
759
+
760
+ results: dict[str, _CheckpointSummary] = {}
761
+
762
+ for start in range(0, len(thread_ids), _SQLITE_MAX_VARIABLE_NUMBER):
763
+ chunk = thread_ids[start : start + _SQLITE_MAX_VARIABLE_NUMBER]
764
+ placeholders = ",".join("?" * len(chunk))
765
+ query = f"""
766
+ SELECT thread_id, type, checkpoint FROM (
767
+ SELECT thread_id, type, checkpoint,
768
+ ROW_NUMBER() OVER (
769
+ PARTITION BY thread_id ORDER BY checkpoint_id DESC
770
+ ) AS rn
771
+ FROM checkpoints
772
+ WHERE thread_id IN ({placeholders})
773
+ ) WHERE rn = 1
774
+ """ # noqa: S608 # placeholders built from len(chunk); user values use ? params
775
+ async with conn.execute(query, chunk) as cursor:
776
+ rows = await cursor.fetchall()
777
+
778
+ loop = asyncio.get_running_loop()
779
+ for row in rows:
780
+ tid, type_str, checkpoint_blob = row
781
+ if not type_str or not checkpoint_blob:
782
+ results[tid] = _CheckpointSummary(message_count=0, initial_prompt=None)
783
+ continue
784
+ try:
785
+ data = await loop.run_in_executor(
786
+ None, serde.loads_typed, (type_str, checkpoint_blob)
787
+ )
788
+ results[tid] = _summarize_checkpoint(data)
789
+ except Exception:
790
+ logger.warning(
791
+ "Failed to deserialize checkpoint for thread %s; "
792
+ "message count and initial prompt may be incomplete",
793
+ tid,
794
+ exc_info=True,
795
+ )
796
+ results[tid] = _CheckpointSummary(message_count=0, initial_prompt=None)
797
+
798
+ return results
799
+
800
+
801
+ async def _load_latest_checkpoint_summary(
802
+ conn: aiosqlite.Connection,
803
+ thread_id: str,
804
+ serde: JsonPlusSerializer,
805
+ ) -> _CheckpointSummary:
806
+ """Load checkpoint-derived summary data from the latest checkpoint row.
807
+
808
+ Returns:
809
+ Message-count and prompt data extracted from the latest checkpoint row.
810
+ """
811
+ query = """
812
+ SELECT type, checkpoint
813
+ FROM checkpoints
814
+ WHERE thread_id = ?
815
+ ORDER BY checkpoint_id DESC
816
+ LIMIT 1
817
+ """
818
+ async with conn.execute(query, (thread_id,)) as cursor:
819
+ row = await cursor.fetchone()
820
+ if not row or not row[0] or not row[1]:
821
+ return _CheckpointSummary(message_count=0, initial_prompt=None)
822
+
823
+ type_str, checkpoint_blob = row
824
+ try:
825
+ data = serde.loads_typed((type_str, checkpoint_blob))
826
+ except (ValueError, TypeError, KeyError, AttributeError):
827
+ logger.warning(
828
+ "Failed to deserialize checkpoint for thread %s; "
829
+ "message count and initial prompt may be incomplete",
830
+ thread_id,
831
+ exc_info=True,
832
+ )
833
+ return _CheckpointSummary(message_count=0, initial_prompt=None)
834
+
835
+ return _summarize_checkpoint(data)
836
+
837
+
838
+ def _summarize_checkpoint(data: object) -> _CheckpointSummary:
839
+ """Extract message count and initial human prompt from checkpoint data.
840
+
841
+ Returns:
842
+ Structured summary for the decoded checkpoint payload.
843
+ """
844
+ messages = _checkpoint_messages(data)
845
+ return _CheckpointSummary(
846
+ message_count=len(messages),
847
+ initial_prompt=_initial_prompt_from_messages(messages),
848
+ )
849
+
850
+
851
+ def _checkpoint_messages(data: object) -> list[object]:
852
+ """Return checkpoint messages when the decoded payload has the expected shape."""
853
+ if not isinstance(data, dict):
854
+ return []
855
+
856
+ payload = cast("dict[str, object]", data)
857
+ channel_values = payload.get("channel_values")
858
+ if not isinstance(channel_values, dict):
859
+ return []
860
+
861
+ channel_values_dict = cast("dict[str, object]", channel_values)
862
+ messages = channel_values_dict.get("messages")
863
+ if not isinstance(messages, list):
864
+ return []
865
+
866
+ return cast("list[object]", messages)
867
+
868
+
869
+ def _initial_prompt_from_messages(messages: list[object]) -> str | None:
870
+ """Return the first human message content from a checkpoint message list."""
871
+ for msg in messages:
872
+ if getattr(msg, "type", None) == "human":
873
+ return _coerce_prompt_text(getattr(msg, "content", None))
874
+ return None
875
+
876
+
877
+ def _coerce_prompt_text(content: object) -> str | None:
878
+ """Normalize checkpoint message content into displayable text.
879
+
880
+ Returns:
881
+ Displayable prompt text, or `None` when the content is empty.
882
+ """
883
+ if isinstance(content, str):
884
+ return content
885
+ if isinstance(content, list):
886
+ parts: list[str] = []
887
+ for part in content:
888
+ if isinstance(part, dict):
889
+ part_dict = cast("dict[str, object]", part)
890
+ text = part_dict.get("text")
891
+ parts.append(text if isinstance(text, str) else "")
892
+ else:
893
+ parts.append(str(part))
894
+ joined = " ".join(parts).strip()
895
+ return joined or None
896
+ if content is None:
897
+ return None
898
+ return str(content)
899
+
900
+
901
+ async def get_most_recent(agent_name: str | None = None) -> str | None:
902
+ """Get most recent thread_id, optionally filtered by agent.
903
+
904
+ Returns:
905
+ Most recent thread_id or None if no threads exist.
906
+ """
907
+ async with _connect() as conn:
908
+ if not await _table_exists(conn, "checkpoints"):
909
+ return None
910
+
911
+ if agent_name:
912
+ query = """
913
+ SELECT thread_id FROM checkpoints
914
+ WHERE json_extract(metadata, '$.agent_name') = ?
915
+ ORDER BY checkpoint_id DESC
916
+ LIMIT 1
917
+ """
918
+ params: tuple = (agent_name,)
919
+ else:
920
+ query = (
921
+ "SELECT thread_id FROM checkpoints ORDER BY checkpoint_id DESC LIMIT 1"
922
+ )
923
+ params = ()
924
+
925
+ async with conn.execute(query, params) as cursor:
926
+ row = await cursor.fetchone()
927
+ return row[0] if row else None
928
+
929
+
930
+ async def get_thread_agent(thread_id: str) -> str | None:
931
+ """Get agent_name for a thread.
932
+
933
+ Returns:
934
+ Agent name associated with the thread, or None if not found.
935
+ """
936
+ async with _connect() as conn:
937
+ if not await _table_exists(conn, "checkpoints"):
938
+ return None
939
+
940
+ query = """
941
+ SELECT json_extract(metadata, '$.agent_name')
942
+ FROM checkpoints
943
+ WHERE thread_id = ?
944
+ LIMIT 1
945
+ """
946
+ async with conn.execute(query, (thread_id,)) as cursor:
947
+ row = await cursor.fetchone()
948
+ return row[0] if row else None
949
+
950
+
951
+ async def thread_exists(thread_id: str) -> bool:
952
+ """Check if a thread exists in checkpoints.
953
+
954
+ Returns:
955
+ True if thread exists, False otherwise.
956
+ """
957
+ async with _connect() as conn:
958
+ if not await _table_exists(conn, "checkpoints"):
959
+ return False
960
+
961
+ query = "SELECT 1 FROM checkpoints WHERE thread_id = ? LIMIT 1"
962
+ async with conn.execute(query, (thread_id,)) as cursor:
963
+ row = await cursor.fetchone()
964
+ return row is not None
965
+
966
+
967
+ async def find_similar_threads(thread_id: str, limit: int = 3) -> list[str]:
968
+ """Find threads whose IDs start with the given prefix.
969
+
970
+ Args:
971
+ thread_id: Prefix to match against thread IDs.
972
+ limit: Maximum number of matching threads to return.
973
+
974
+ Returns:
975
+ List of thread IDs that begin with the given prefix.
976
+ """
977
+ async with _connect() as conn:
978
+ if not await _table_exists(conn, "checkpoints"):
979
+ return []
980
+
981
+ query = """
982
+ SELECT DISTINCT thread_id
983
+ FROM checkpoints
984
+ WHERE thread_id LIKE ?
985
+ ORDER BY thread_id
986
+ LIMIT ?
987
+ """
988
+ prefix = thread_id + "%"
989
+ async with conn.execute(query, (prefix, limit)) as cursor:
990
+ rows = await cursor.fetchall()
991
+ return [r[0] for r in rows]
992
+
993
+
994
+ async def delete_thread(thread_id: str) -> bool:
995
+ """Delete thread checkpoints.
996
+
997
+ Returns:
998
+ True if thread was deleted, False if not found.
999
+ """
1000
+ async with _connect() as conn:
1001
+ if not await _table_exists(conn, "checkpoints"):
1002
+ return False
1003
+
1004
+ cursor = await conn.execute(
1005
+ "DELETE FROM checkpoints WHERE thread_id = ?", (thread_id,)
1006
+ )
1007
+ deleted = cursor.rowcount > 0
1008
+ if await _table_exists(conn, "writes"):
1009
+ await conn.execute("DELETE FROM writes WHERE thread_id = ?", (thread_id,))
1010
+ await conn.commit()
1011
+ if deleted:
1012
+ _message_count_cache.pop(thread_id, None)
1013
+ for key, rows in list(_recent_threads_cache.items()):
1014
+ filtered = [row for row in rows if row["thread_id"] != thread_id]
1015
+ _recent_threads_cache[key] = filtered
1016
+ return deleted
1017
+
1018
+
1019
+ @asynccontextmanager
1020
+ async def get_checkpointer() -> AsyncIterator[AsyncSqliteSaver]:
1021
+ """Get AsyncSqliteSaver for the global database.
1022
+
1023
+ Yields:
1024
+ AsyncSqliteSaver instance for checkpoint persistence.
1025
+ """
1026
+ from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver
1027
+
1028
+ _patch_aiosqlite()
1029
+
1030
+ async with AsyncSqliteSaver.from_conn_string(str(get_db_path())) as checkpointer:
1031
+ yield checkpointer
1032
+
1033
+
1034
+ _DEFAULT_THREAD_LIMIT = 20
1035
+
1036
+
1037
+ def get_thread_limit() -> int:
1038
+ """Read the thread listing limit from `DA_CLI_RECENT_THREADS`.
1039
+
1040
+ Falls back to `_DEFAULT_THREAD_LIMIT` when the variable is unset or contains
1041
+ a non-integer value. The result is clamped to a minimum of 1.
1042
+
1043
+ Returns:
1044
+ Number of threads to display.
1045
+ """
1046
+ import os
1047
+
1048
+ raw = os.environ.get("DA_CLI_RECENT_THREADS")
1049
+ if raw is None:
1050
+ return _DEFAULT_THREAD_LIMIT
1051
+ try:
1052
+ return max(1, int(raw))
1053
+ except ValueError:
1054
+ logger.warning(
1055
+ "Invalid DA_CLI_RECENT_THREADS value %r, using default %d",
1056
+ raw,
1057
+ _DEFAULT_THREAD_LIMIT,
1058
+ )
1059
+ return _DEFAULT_THREAD_LIMIT
1060
+
1061
+
1062
+ async def list_threads_command(
1063
+ agent_name: str | None = None,
1064
+ limit: int | None = None,
1065
+ sort_by: str | None = None,
1066
+ branch: str | None = None,
1067
+ verbose: bool = False,
1068
+ relative: bool | None = None,
1069
+ *,
1070
+ output_format: OutputFormat = "text",
1071
+ ) -> None:
1072
+ """CLI handler for `docagent threads list`.
1073
+
1074
+ Fetches and displays a table of recent conversation threads, optionally
1075
+ filtered by agent name or git branch.
1076
+
1077
+ Args:
1078
+ agent_name: Only show threads belonging to this agent.
1079
+
1080
+ When `None`, threads for all agents are shown.
1081
+ limit: Maximum number of threads to display.
1082
+
1083
+ When `None`, reads from `DA_CLI_RECENT_THREADS` or falls back to
1084
+ the default.
1085
+ sort_by: Sort field — `"updated"` or `"created"`.
1086
+
1087
+ When `None`, reads from config (`~/.docagent/config.toml`).
1088
+ branch: Only show threads from this git branch.
1089
+ verbose: When `True`, show all columns (branch, created, prompt).
1090
+ relative: Show timestamps as relative time (e.g., '5m ago').
1091
+
1092
+ When `None`, reads from config (`~/.docagent/config.toml`).
1093
+ output_format: Output format — `'text'` (Rich) or `'json'`.
1094
+ """
1095
+ from docagent_cli.model_config import (
1096
+ load_thread_relative_time,
1097
+ load_thread_sort_order,
1098
+ )
1099
+
1100
+ if sort_by is None:
1101
+ raw = load_thread_sort_order()
1102
+ sort_by = "created" if raw == "created_at" else "updated"
1103
+ if relative is None:
1104
+ relative = load_thread_relative_time()
1105
+
1106
+ fmt_ts = format_relative_timestamp if relative else format_timestamp
1107
+
1108
+ limit = get_thread_limit() if limit is None else max(1, limit)
1109
+
1110
+ threads = await list_threads(
1111
+ agent_name,
1112
+ limit=limit,
1113
+ include_message_count=True,
1114
+ sort_by=sort_by,
1115
+ branch=branch,
1116
+ )
1117
+
1118
+ if verbose and threads:
1119
+ await populate_thread_checkpoint_details(
1120
+ threads, include_message_count=False, include_initial_prompt=True
1121
+ )
1122
+
1123
+ if output_format == "json":
1124
+ from docagent_cli.output import write_json
1125
+
1126
+ write_json("threads list", list(threads))
1127
+ return
1128
+
1129
+ from rich.markup import escape as escape_markup
1130
+ from rich.table import Table
1131
+
1132
+ from docagent_cli import theme
1133
+ from docagent_cli.config import console
1134
+
1135
+ if not threads:
1136
+ filters = []
1137
+ if agent_name:
1138
+ filters.append(f"agent '{escape_markup(agent_name)}'")
1139
+ if branch:
1140
+ filters.append(f"branch '{escape_markup(branch)}'")
1141
+ if filters:
1142
+ console.print(
1143
+ f"[yellow]No threads found for {' and '.join(filters)}.[/yellow]"
1144
+ )
1145
+ else:
1146
+ console.print("[yellow]No threads found.[/yellow]")
1147
+ console.print("[dim]Start a conversation with: docagent[/dim]")
1148
+ return
1149
+
1150
+ title_parts = []
1151
+ if agent_name:
1152
+ title_parts.append(f"agent '{escape_markup(agent_name)}'")
1153
+ if branch:
1154
+ title_parts.append(f"branch '{escape_markup(branch)}'")
1155
+
1156
+ title_filter = f" for {' and '.join(title_parts)}" if title_parts else ""
1157
+ sort_label = "created" if sort_by == "created" else "updated"
1158
+ title = f"Recent Threads{title_filter} (last {limit}, by {sort_label})"
1159
+
1160
+ table = Table(title=title, show_header=True, header_style=f"bold {theme.PRIMARY}")
1161
+ table.add_column("Thread ID", style="bold")
1162
+ table.add_column("Agent")
1163
+ table.add_column("Messages", justify="right")
1164
+ if verbose:
1165
+ table.add_column("Created")
1166
+ table.add_column("Updated" if sort_by == "updated" else "Last Used")
1167
+ if verbose:
1168
+ table.add_column("Branch")
1169
+ table.add_column("Location")
1170
+ table.add_column("Prompt", max_width=40, no_wrap=True)
1171
+
1172
+ prompt_max = 40
1173
+
1174
+ for t in threads:
1175
+ row: list[str] = [
1176
+ t["thread_id"],
1177
+ t["agent_name"] or "unknown",
1178
+ str(t.get("message_count", 0)),
1179
+ ]
1180
+ if verbose:
1181
+ row.append(fmt_ts(t.get("created_at")))
1182
+ row.append(fmt_ts(t.get("updated_at")))
1183
+ if verbose:
1184
+ prompt = " ".join((t.get("initial_prompt") or "").split())
1185
+ if len(prompt) > prompt_max:
1186
+ prompt = prompt[: prompt_max - 3] + "..."
1187
+ row.extend(
1188
+ [
1189
+ t.get("git_branch") or "",
1190
+ format_path(t.get("cwd")),
1191
+ prompt,
1192
+ ]
1193
+ )
1194
+ table.add_row(*row)
1195
+
1196
+ console.print()
1197
+ console.print(table)
1198
+ if len(threads) >= limit:
1199
+ console.print(
1200
+ f"[dim]Showing last {limit} threads. "
1201
+ "Override with -n/--limit or DA_CLI_RECENT_THREADS.[/dim]"
1202
+ )
1203
+ console.print()
1204
+
1205
+
1206
+ async def delete_thread_command(
1207
+ thread_id: str,
1208
+ *,
1209
+ dry_run: bool = False,
1210
+ output_format: OutputFormat = "text",
1211
+ ) -> None:
1212
+ """CLI handler for: docagent threads delete.
1213
+
1214
+ Args:
1215
+ thread_id: ID of the thread to delete.
1216
+ dry_run: If `True`, print what would happen without making changes.
1217
+ output_format: Output format — `'text'` (Rich) or `'json'`.
1218
+ """
1219
+ if dry_run:
1220
+ exists = await thread_exists(thread_id)
1221
+ if output_format == "json":
1222
+ from docagent_cli.output import write_json
1223
+
1224
+ write_json(
1225
+ "threads delete",
1226
+ {"thread_id": thread_id, "exists": exists, "dry_run": True},
1227
+ )
1228
+ return
1229
+
1230
+ from rich.markup import escape as escape_markup
1231
+
1232
+ from docagent_cli.config import console
1233
+
1234
+ escaped_id = escape_markup(thread_id)
1235
+ if exists:
1236
+ console.print(f"Would delete thread '{escaped_id}'.")
1237
+ else:
1238
+ console.print(f"Thread '{escaped_id}' not found. Nothing to delete.")
1239
+ console.print("No changes made.", style="dim")
1240
+ return
1241
+
1242
+ deleted = await delete_thread(thread_id)
1243
+
1244
+ if output_format == "json":
1245
+ from docagent_cli.output import write_json
1246
+
1247
+ write_json("threads delete", {"thread_id": thread_id, "deleted": deleted})
1248
+ return
1249
+
1250
+ from rich.markup import escape as escape_markup
1251
+
1252
+ from docagent_cli import theme
1253
+ from docagent_cli.config import console
1254
+
1255
+ escaped_id = escape_markup(thread_id)
1256
+ if deleted:
1257
+ console.print(f"[green]Thread '{escaped_id}' deleted.[/green]")
1258
+ else:
1259
+ console.print(
1260
+ f"Thread '{escaped_id}' not found or already deleted.",
1261
+ style=theme.MUTED,
1262
+ )