shotgun-sh 0.2.7.dev1__tar.gz → 0.2.8.dev1__tar.gz

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.

Potentially problematic release.


This version of shotgun-sh might be problematic. Click here for more details.

Files changed (158) hide show
  1. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/PKG-INFO +3 -1
  2. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/README.md +76 -0
  3. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/pyproject.toml +3 -1
  4. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/agent_manager.py +191 -13
  5. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/common.py +42 -17
  6. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/tools/file_management.py +55 -9
  7. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/main.py +54 -10
  8. shotgun_sh-0.2.8.dev1/src/shotgun/prompts/agents/specify.j2 +318 -0
  9. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/tui/app.py +116 -0
  10. shotgun_sh-0.2.7.dev1/src/shotgun/prompts/agents/specify.j2 +0 -51
  11. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/.gitignore +0 -0
  12. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/LICENSE +0 -0
  13. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/README_PYPI.md +0 -0
  14. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/hatch_build.py +0 -0
  15. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/__init__.py +0 -0
  16. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/__init__.py +0 -0
  17. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/config/__init__.py +0 -0
  18. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/config/constants.py +0 -0
  19. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/config/manager.py +0 -0
  20. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/config/models.py +0 -0
  21. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/config/provider.py +0 -0
  22. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/conversation_history.py +0 -0
  23. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/conversation_manager.py +0 -0
  24. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/export.py +0 -0
  25. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/history/__init__.py +0 -0
  26. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/history/compaction.py +0 -0
  27. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/history/constants.py +0 -0
  28. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/history/context_extraction.py +0 -0
  29. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/history/history_building.py +0 -0
  30. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/history/history_processors.py +0 -0
  31. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/history/message_utils.py +0 -0
  32. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/history/token_counting/__init__.py +0 -0
  33. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/history/token_counting/anthropic.py +0 -0
  34. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/history/token_counting/base.py +0 -0
  35. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/history/token_counting/openai.py +0 -0
  36. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/history/token_counting/sentencepiece_counter.py +0 -0
  37. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/history/token_counting/tokenizer_cache.py +0 -0
  38. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/history/token_counting/utils.py +0 -0
  39. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/history/token_estimation.py +0 -0
  40. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/llm.py +0 -0
  41. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/messages.py +0 -0
  42. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/models.py +0 -0
  43. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/plan.py +0 -0
  44. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/research.py +0 -0
  45. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/specify.py +0 -0
  46. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/tasks.py +0 -0
  47. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/tools/__init__.py +0 -0
  48. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/tools/codebase/__init__.py +0 -0
  49. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/tools/codebase/codebase_shell.py +0 -0
  50. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/tools/codebase/directory_lister.py +0 -0
  51. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/tools/codebase/file_read.py +0 -0
  52. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/tools/codebase/models.py +0 -0
  53. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/tools/codebase/query_graph.py +0 -0
  54. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/tools/codebase/retrieve_code.py +0 -0
  55. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/tools/web_search/__init__.py +0 -0
  56. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/tools/web_search/anthropic.py +0 -0
  57. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/tools/web_search/gemini.py +0 -0
  58. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/tools/web_search/openai.py +0 -0
  59. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/tools/web_search/utils.py +0 -0
  60. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/agents/usage_manager.py +0 -0
  61. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/api_endpoints.py +0 -0
  62. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/build_constants.py +0 -0
  63. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/cli/__init__.py +0 -0
  64. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/cli/codebase/__init__.py +0 -0
  65. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/cli/codebase/commands.py +0 -0
  66. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/cli/codebase/models.py +0 -0
  67. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/cli/config.py +0 -0
  68. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/cli/export.py +0 -0
  69. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/cli/feedback.py +0 -0
  70. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/cli/models.py +0 -0
  71. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/cli/plan.py +0 -0
  72. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/cli/research.py +0 -0
  73. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/cli/specify.py +0 -0
  74. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/cli/tasks.py +0 -0
  75. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/cli/update.py +0 -0
  76. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/cli/utils.py +0 -0
  77. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/codebase/__init__.py +0 -0
  78. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/codebase/core/__init__.py +0 -0
  79. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/codebase/core/change_detector.py +0 -0
  80. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/codebase/core/code_retrieval.py +0 -0
  81. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/codebase/core/cypher_models.py +0 -0
  82. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/codebase/core/ingestor.py +0 -0
  83. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/codebase/core/language_config.py +0 -0
  84. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/codebase/core/manager.py +0 -0
  85. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/codebase/core/nl_query.py +0 -0
  86. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/codebase/core/parser_loader.py +0 -0
  87. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/codebase/models.py +0 -0
  88. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/codebase/service.py +0 -0
  89. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/llm_proxy/__init__.py +0 -0
  90. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/llm_proxy/clients.py +0 -0
  91. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/llm_proxy/constants.py +0 -0
  92. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/logging_config.py +0 -0
  93. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/posthog_telemetry.py +0 -0
  94. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/prompts/__init__.py +0 -0
  95. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/prompts/agents/__init__.py +0 -0
  96. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/prompts/agents/export.j2 +0 -0
  97. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/prompts/agents/partials/codebase_understanding.j2 +0 -0
  98. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +0 -0
  99. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/prompts/agents/partials/content_formatting.j2 +0 -0
  100. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/prompts/agents/partials/interactive_mode.j2 +0 -0
  101. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/prompts/agents/plan.j2 +0 -0
  102. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/prompts/agents/research.j2 +0 -0
  103. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +0 -0
  104. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/prompts/agents/state/system_state.j2 +0 -0
  105. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/prompts/agents/tasks.j2 +0 -0
  106. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/prompts/codebase/__init__.py +0 -0
  107. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/prompts/codebase/cypher_query_patterns.j2 +0 -0
  108. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/prompts/codebase/cypher_system.j2 +0 -0
  109. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/prompts/codebase/enhanced_query_context.j2 +0 -0
  110. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/prompts/codebase/partials/cypher_rules.j2 +0 -0
  111. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/prompts/codebase/partials/graph_schema.j2 +0 -0
  112. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/prompts/codebase/partials/temporal_context.j2 +0 -0
  113. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/prompts/history/__init__.py +0 -0
  114. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/prompts/history/incremental_summarization.j2 +0 -0
  115. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/prompts/history/summarization.j2 +0 -0
  116. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/prompts/loader.py +0 -0
  117. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/prompts/tools/web_search.j2 +0 -0
  118. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/py.typed +0 -0
  119. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/sdk/__init__.py +0 -0
  120. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/sdk/codebase.py +0 -0
  121. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/sdk/exceptions.py +0 -0
  122. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/sdk/models.py +0 -0
  123. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/sdk/services.py +0 -0
  124. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/sentry_telemetry.py +0 -0
  125. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/shotgun_web/__init__.py +0 -0
  126. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/shotgun_web/client.py +0 -0
  127. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/shotgun_web/constants.py +0 -0
  128. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/shotgun_web/models.py +0 -0
  129. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/telemetry.py +0 -0
  130. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/tui/__init__.py +0 -0
  131. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/tui/commands/__init__.py +0 -0
  132. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/tui/components/prompt_input.py +0 -0
  133. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/tui/components/spinner.py +0 -0
  134. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/tui/components/splash.py +0 -0
  135. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/tui/components/vertical_tail.py +0 -0
  136. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/tui/filtered_codebase_service.py +0 -0
  137. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/tui/screens/chat.py +0 -0
  138. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/tui/screens/chat.tcss +0 -0
  139. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/tui/screens/chat_screen/__init__.py +0 -0
  140. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/tui/screens/chat_screen/command_providers.py +0 -0
  141. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/tui/screens/chat_screen/hint_message.py +0 -0
  142. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/tui/screens/chat_screen/history.py +0 -0
  143. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/tui/screens/directory_setup.py +0 -0
  144. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/tui/screens/feedback.py +0 -0
  145. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/tui/screens/model_picker.py +0 -0
  146. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/tui/screens/provider_config.py +0 -0
  147. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/tui/screens/shotgun_auth.py +0 -0
  148. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/tui/screens/splash.py +0 -0
  149. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/tui/screens/welcome.py +0 -0
  150. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/tui/styles.tcss +0 -0
  151. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/tui/utils/__init__.py +0 -0
  152. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/tui/utils/mode_progress.py +0 -0
  153. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/utils/__init__.py +0 -0
  154. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/utils/datetime_utils.py +0 -0
  155. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/utils/env_utils.py +0 -0
  156. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/utils/file_system_utils.py +0 -0
  157. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/utils/source_detection.py +0 -0
  158. {shotgun_sh-0.2.7.dev1 → shotgun_sh-0.2.8.dev1}/src/shotgun/utils/update_checker.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shotgun-sh
3
- Version: 0.2.7.dev1
3
+ Version: 0.2.8.dev1
4
4
  Summary: AI-powered research, planning, and task management CLI tool
5
5
  Project-URL: Homepage, https://shotgun.sh/
6
6
  Project-URL: Repository, https://github.com/shotgun-sh/shotgun
@@ -34,7 +34,9 @@ Requires-Dist: pydantic-ai>=0.0.14
34
34
  Requires-Dist: rich>=13.0.0
35
35
  Requires-Dist: sentencepiece>=0.2.0
36
36
  Requires-Dist: sentry-sdk[pure-eval]>=2.0.0
37
+ Requires-Dist: tenacity>=8.0.0
37
38
  Requires-Dist: textual-dev>=1.7.0
39
+ Requires-Dist: textual-serve>=0.1.0
38
40
  Requires-Dist: textual>=6.1.0
39
41
  Requires-Dist: tiktoken>=0.7.0
40
42
  Requires-Dist: tree-sitter-go>=0.23.0
@@ -408,6 +408,82 @@ export SENTRY_DSN=your-sentry-dsn
408
408
  - **Opt-in for development**: Telemetry requires explicit environment variables
409
409
  - **Automatic in production**: Production builds include telemetry for error tracking
410
410
 
411
+ ## Docker
412
+
413
+ Run Shotgun in a Docker container with web access.
414
+
415
+ ### Using Pre-built Images (Recommended)
416
+
417
+ Pull the official image from GitHub Container Registry:
418
+
419
+ ```bash
420
+ # Pull latest stable version
421
+ docker pull ghcr.io/shotgun-sh/shotgun:latest
422
+
423
+ # Or pull a specific version
424
+ docker pull ghcr.io/shotgun-sh/shotgun:v0.1.0
425
+
426
+ # Or pull development version
427
+ docker pull ghcr.io/shotgun-sh/shotgun:dev
428
+ ```
429
+
430
+ Then run:
431
+
432
+ ```bash
433
+ docker run -p 8000:8000 \
434
+ -v $(pwd):/workspace \
435
+ -v ~/.shotgun-sh:/home/shotgun/.shotgun-sh \
436
+ ghcr.io/shotgun-sh/shotgun:latest --no-update-check
437
+ ```
438
+
439
+ ### Building from Source (Optional)
440
+
441
+ If you prefer to build the image yourself:
442
+
443
+ ```bash
444
+ docker build -t shotgun:latest .
445
+ ```
446
+
447
+ ### Run the Container
448
+
449
+ The container requires two volume mounts:
450
+ 1. Your codebase/workspace directory (mounted to `/workspace`)
451
+ 2. Config directory for API keys and settings (mounted to `/home/shotgun/.shotgun-sh`)
452
+
453
+ ```bash
454
+ # Basic usage (serves on port 8000)
455
+ docker run -p 8000:8000 \
456
+ -v $(pwd):/workspace \
457
+ -v ~/.shotgun-sh:/home/shotgun/.shotgun-sh \
458
+ ghcr.io/shotgun-sh/shotgun:latest --no-update-check
459
+
460
+ # Custom port
461
+ docker run -p 3000:3000 \
462
+ -v $(pwd):/workspace \
463
+ -v ~/.shotgun-sh:/home/shotgun/.shotgun-sh \
464
+ ghcr.io/shotgun-sh/shotgun:latest --no-update-check --port 3000
465
+
466
+ # Different codebase directory
467
+ docker run -p 8000:8000 \
468
+ -v /path/to/your/project:/workspace \
469
+ -v ~/.shotgun-sh:/home/shotgun/.shotgun-sh \
470
+ ghcr.io/shotgun-sh/shotgun:latest --no-update-check
471
+
472
+ # Run in background with auto-restart
473
+ docker run -d --restart unless-stopped \
474
+ --name shotgun-web \
475
+ -p 8000:8000 \
476
+ -v $(pwd):/workspace \
477
+ -v ~/.shotgun-sh:/home/shotgun/.shotgun-sh \
478
+ ghcr.io/shotgun-sh/shotgun:latest --no-update-check
479
+ ```
480
+
481
+ ### Configuration
482
+
483
+ On first run, configure your API keys through the web UI. The configuration will persist in the mounted `~/.shotgun-sh` directory.
484
+
485
+ Access the web interface at `http://localhost:8000` (or your custom port).
486
+
411
487
  ## Support
412
488
 
413
489
  Join our discord https://discord.gg/5RmY6J2N7s
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "shotgun-sh"
3
- version = "0.2.7.dev1"
3
+ version = "0.2.8.dev1"
4
4
  description = "AI-powered research, planning, and task management CLI tool"
5
5
  readme = "README_PYPI.md"
6
6
  license = { text = "MIT" }
@@ -32,6 +32,7 @@ dependencies = [
32
32
  "posthog>=3.0.0",
33
33
  "textual>=6.1.0",
34
34
  "textual-dev>=1.7.0",
35
+ "textual-serve>=0.1.0",
35
36
  "kuzu>=0.7.0",
36
37
  "tree-sitter>=0.21.0",
37
38
  "tree-sitter-python>=0.23.0",
@@ -46,6 +47,7 @@ dependencies = [
46
47
  "sentencepiece>=0.2.0",
47
48
  "packaging>=23.0",
48
49
  "genai-prices>=0.0.27",
50
+ "tenacity>=8.0.0",
49
51
  ]
50
52
 
51
53
  [project.urls]
@@ -4,9 +4,17 @@ import json
4
4
  import logging
5
5
  from collections.abc import AsyncIterable, Sequence
6
6
  from dataclasses import dataclass, field, is_dataclass, replace
7
+ from pathlib import Path
7
8
  from typing import TYPE_CHECKING, Any, cast
8
9
 
9
10
  import logfire
11
+ from tenacity import (
12
+ before_sleep_log,
13
+ retry,
14
+ retry_if_exception,
15
+ stop_after_attempt,
16
+ wait_exponential,
17
+ )
10
18
 
11
19
  if TYPE_CHECKING:
12
20
  from shotgun.agents.conversation_history import ConversationState
@@ -55,6 +63,35 @@ from .tasks import create_tasks_agent
55
63
  logger = logging.getLogger(__name__)
56
64
 
57
65
 
66
+ def _is_retryable_error(exception: BaseException) -> bool:
67
+ """Check if exception should trigger a retry.
68
+
69
+ Args:
70
+ exception: The exception to check.
71
+
72
+ Returns:
73
+ True if the exception is a transient error that should be retried.
74
+ """
75
+ # ValueError for truncated/incomplete JSON
76
+ if isinstance(exception, ValueError):
77
+ error_str = str(exception)
78
+ return "EOF while parsing" in error_str or (
79
+ "JSON" in error_str and "parsing" in error_str
80
+ )
81
+
82
+ # API errors (overload, rate limits)
83
+ exception_name = type(exception).__name__
84
+ if "APIStatusError" in exception_name:
85
+ error_str = str(exception)
86
+ return "overload" in error_str.lower() or "rate" in error_str.lower()
87
+
88
+ # Network errors
89
+ if "ConnectionError" in exception_name or "TimeoutError" in exception_name:
90
+ return True
91
+
92
+ return False
93
+
94
+
58
95
  class MessageHistoryUpdated(Message):
59
96
  """Event posted when the message history is updated."""
60
97
 
@@ -268,6 +305,49 @@ class AgentManager(Widget):
268
305
  f"Invalid agent type: {agent_type}. Must be one of: {', '.join(e.value for e in AgentType)}"
269
306
  ) from None
270
307
 
308
+ @retry(
309
+ stop=stop_after_attempt(3),
310
+ wait=wait_exponential(multiplier=1, min=1, max=8),
311
+ retry=retry_if_exception(_is_retryable_error),
312
+ before_sleep=before_sleep_log(logger, logging.WARNING),
313
+ reraise=True,
314
+ )
315
+ async def _run_agent_with_retry(
316
+ self,
317
+ agent: Agent[AgentDeps, AgentResponse],
318
+ prompt: str | None,
319
+ deps: AgentDeps,
320
+ usage_limits: UsageLimits | None,
321
+ message_history: list[ModelMessage],
322
+ event_stream_handler: Any,
323
+ **kwargs: Any,
324
+ ) -> AgentRunResult[AgentResponse]:
325
+ """Run agent with automatic retry on transient errors.
326
+
327
+ Args:
328
+ agent: The agent to run.
329
+ prompt: Optional prompt to send to the agent.
330
+ deps: Agent dependencies.
331
+ usage_limits: Optional usage limits.
332
+ message_history: Message history to provide to agent.
333
+ event_stream_handler: Event handler for streaming.
334
+ **kwargs: Additional keyword arguments.
335
+
336
+ Returns:
337
+ The agent run result.
338
+
339
+ Raises:
340
+ Various exceptions if all retries fail.
341
+ """
342
+ return await agent.run(
343
+ prompt,
344
+ deps=deps,
345
+ usage_limits=usage_limits,
346
+ message_history=message_history,
347
+ event_stream_handler=event_stream_handler,
348
+ **kwargs,
349
+ )
350
+
271
351
  async def run(
272
352
  self,
273
353
  prompt: str | None = None,
@@ -394,8 +474,9 @@ class AgentManager(Widget):
394
474
  )
395
475
 
396
476
  try:
397
- result: AgentRunResult[AgentResponse] = await self.current_agent.run(
398
- prompt,
477
+ result: AgentRunResult[AgentResponse] = await self._run_agent_with_retry(
478
+ agent=self.current_agent,
479
+ prompt=prompt,
399
480
  deps=deps,
400
481
  usage_limits=usage_limits,
401
482
  message_history=message_history,
@@ -404,6 +485,36 @@ class AgentManager(Widget):
404
485
  else None,
405
486
  **kwargs,
406
487
  )
488
+ except ValueError as e:
489
+ # Handle truncated/incomplete JSON in tool calls specifically
490
+ error_str = str(e)
491
+ if "EOF while parsing" in error_str or (
492
+ "JSON" in error_str and "parsing" in error_str
493
+ ):
494
+ logger.error(
495
+ "Tool call with truncated/incomplete JSON arguments detected",
496
+ extra={
497
+ "agent_mode": self._current_agent_type.value,
498
+ "model_name": model_name,
499
+ "error": error_str,
500
+ },
501
+ )
502
+ logfire.error(
503
+ "Tool call with truncated JSON arguments",
504
+ agent_mode=self._current_agent_type.value,
505
+ model_name=model_name,
506
+ error=error_str,
507
+ )
508
+ # Add helpful hint message for the user
509
+ self.ui_message_history.append(
510
+ HintMessage(
511
+ message="⚠️ The agent attempted an operation with arguments that were too large (truncated JSON). "
512
+ "Try breaking your request into smaller steps or more focused contracts."
513
+ )
514
+ )
515
+ self._post_messages_updated()
516
+ # Re-raise to maintain error visibility
517
+ raise
407
518
  except Exception as e:
408
519
  # Log the error with full stack trace to shotgun.log and Logfire
409
520
  logger.exception(
@@ -427,13 +538,40 @@ class AgentManager(Widget):
427
538
 
428
539
  # Agent ALWAYS returns AgentResponse with structured output
429
540
  agent_response = result.output
430
- logger.debug("Agent returned structured AgentResponse")
541
+ logger.debug(
542
+ "Agent returned structured AgentResponse",
543
+ extra={
544
+ "has_response": agent_response.response is not None,
545
+ "response_length": len(agent_response.response)
546
+ if agent_response.response
547
+ else 0,
548
+ "response_preview": agent_response.response[:100] + "..."
549
+ if agent_response.response and len(agent_response.response) > 100
550
+ else agent_response.response or "(empty)",
551
+ "has_clarifying_questions": bool(agent_response.clarifying_questions),
552
+ "num_clarifying_questions": len(agent_response.clarifying_questions)
553
+ if agent_response.clarifying_questions
554
+ else 0,
555
+ },
556
+ )
431
557
 
432
558
  # Always add the agent's response messages to maintain conversation history
433
559
  self.ui_message_history = original_messages + cast(
434
560
  list[ModelRequest | ModelResponse | HintMessage], result.new_messages()
435
561
  )
436
562
 
563
+ # Get file operations early so we can use them for contextual messages
564
+ file_operations = deps.file_tracker.operations.copy()
565
+ self.recently_change_files = file_operations
566
+
567
+ logger.debug(
568
+ "File operations tracked",
569
+ extra={
570
+ "num_file_operations": len(file_operations),
571
+ "operation_files": [Path(op.file_path).name for op in file_operations],
572
+ },
573
+ )
574
+
437
575
  # Check if there are clarifying questions
438
576
  if agent_response.clarifying_questions:
439
577
  logger.info(
@@ -480,12 +618,50 @@ class AgentManager(Widget):
480
618
  response_text=agent_response.response,
481
619
  )
482
620
  )
621
+
622
+ # Post UI update with hint messages and file operations
623
+ logger.debug(
624
+ "Posting UI update for Q&A mode with hint messages and file operations"
625
+ )
626
+ self._post_messages_updated(file_operations)
483
627
  else:
484
- # No clarifying questions - just show the response if present
628
+ # No clarifying questions - show the response or a default success message
485
629
  if agent_response.response and agent_response.response.strip():
630
+ logger.debug(
631
+ "Adding agent response as hint",
632
+ extra={
633
+ "response_preview": agent_response.response[:100] + "..."
634
+ if len(agent_response.response) > 100
635
+ else agent_response.response,
636
+ "has_file_operations": len(file_operations) > 0,
637
+ },
638
+ )
486
639
  self.ui_message_history.append(
487
640
  HintMessage(message=agent_response.response)
488
641
  )
642
+ else:
643
+ # Fallback: response is empty or whitespace
644
+ logger.debug(
645
+ "Agent response was empty, using fallback completion message",
646
+ extra={"has_file_operations": len(file_operations) > 0},
647
+ )
648
+ # Show contextual message based on whether files were modified
649
+ if file_operations:
650
+ self.ui_message_history.append(
651
+ HintMessage(
652
+ message="✅ Task completed - files have been modified"
653
+ )
654
+ )
655
+ else:
656
+ self.ui_message_history.append(
657
+ HintMessage(message="✅ Task completed")
658
+ )
659
+
660
+ # Post UI update immediately so user sees the response without delay
661
+ logger.debug(
662
+ "Posting immediate UI update with hint message and file operations"
663
+ )
664
+ self._post_messages_updated(file_operations)
489
665
 
490
666
  # Apply compaction to persistent message history to prevent cascading growth
491
667
  all_messages = result.all_messages()
@@ -517,16 +693,18 @@ class AgentManager(Widget):
517
693
  self.message_history = all_messages
518
694
 
519
695
  usage = result.usage()
520
- deps.usage_manager.add_usage(
521
- usage, model_name=deps.llm_model.name, provider=deps.llm_model.provider
522
- )
523
-
524
- # Log file operations summary if any files were modified
525
- file_operations = deps.file_tracker.operations.copy()
526
- self.recently_change_files = file_operations
696
+ if hasattr(deps, "llm_model") and deps.llm_model is not None:
697
+ deps.usage_manager.add_usage(
698
+ usage, model_name=deps.llm_model.name, provider=deps.llm_model.provider
699
+ )
700
+ else:
701
+ logger.warning(
702
+ "llm_model is None, skipping usage tracking",
703
+ extra={"agent_mode": self._current_agent_type.value},
704
+ )
527
705
 
528
- # Post message history update (hints are now added synchronously above)
529
- self._post_messages_updated(file_operations)
706
+ # UI updates are now posted immediately in each branch (Q&A or non-Q&A)
707
+ # before compaction, so no duplicate posting needed here
530
708
 
531
709
  return result
532
710
 
@@ -384,23 +384,48 @@ def get_agent_existing_files(agent_mode: AgentType | None = None) -> list[str]:
384
384
  relative_path = file_path.relative_to(base_path)
385
385
  existing_files.append(str(relative_path))
386
386
  else:
387
- # For other agents, check both .md file and directory with same name
388
- allowed_file = AGENT_DIRECTORIES[agent_mode]
389
-
390
- # Check for the .md file
391
- md_file_path = base_path / allowed_file
392
- if md_file_path.exists():
393
- existing_files.append(allowed_file)
394
-
395
- # Check for directory with same base name (e.g., research/ for research.md)
396
- base_name = allowed_file.replace(".md", "")
397
- dir_path = base_path / base_name
398
- if dir_path.exists() and dir_path.is_dir():
399
- # List all files in the directory
400
- for file_path in dir_path.rglob("*"):
401
- if file_path.is_file():
402
- relative_path = file_path.relative_to(base_path)
403
- existing_files.append(str(relative_path))
387
+ # For other agents, check files/directories they have access to
388
+ allowed_paths_raw = AGENT_DIRECTORIES[agent_mode]
389
+
390
+ # Convert single Path/string to list of Paths for uniform handling
391
+ if isinstance(allowed_paths_raw, str):
392
+ # Special case: "*" means export agent (shouldn't reach here but handle it)
393
+ allowed_paths = (
394
+ [Path(allowed_paths_raw)] if allowed_paths_raw != "*" else []
395
+ )
396
+ elif isinstance(allowed_paths_raw, Path):
397
+ allowed_paths = [allowed_paths_raw]
398
+ else:
399
+ # Already a list
400
+ allowed_paths = allowed_paths_raw
401
+
402
+ # Check each allowed path
403
+ for allowed_path in allowed_paths:
404
+ allowed_str = str(allowed_path)
405
+
406
+ # Check if it's a directory (no .md suffix)
407
+ if not allowed_path.suffix or not allowed_str.endswith(".md"):
408
+ # It's a directory - list all files within it
409
+ dir_path = base_path / allowed_str
410
+ if dir_path.exists() and dir_path.is_dir():
411
+ for file_path in dir_path.rglob("*"):
412
+ if file_path.is_file():
413
+ relative_path = file_path.relative_to(base_path)
414
+ existing_files.append(str(relative_path))
415
+ else:
416
+ # It's a file - check if it exists
417
+ file_path = base_path / allowed_str
418
+ if file_path.exists():
419
+ existing_files.append(allowed_str)
420
+
421
+ # Also check for associated directory (e.g., research/ for research.md)
422
+ base_name = allowed_str.replace(".md", "")
423
+ dir_path = base_path / base_name
424
+ if dir_path.exists() and dir_path.is_dir():
425
+ for file_path in dir_path.rglob("*"):
426
+ if file_path.is_file():
427
+ relative_path = file_path.relative_to(base_path)
428
+ existing_files.append(str(relative_path))
404
429
 
405
430
  return existing_files
406
431
 
@@ -15,11 +15,18 @@ from shotgun.utils.file_system_utils import get_shotgun_base_path
15
15
  logger = get_logger(__name__)
16
16
 
17
17
  # Map agent modes to their allowed directories/files (in workflow order)
18
- AGENT_DIRECTORIES = {
19
- AgentType.RESEARCH: "research.md",
20
- AgentType.SPECIFY: "specification.md",
21
- AgentType.PLAN: "plan.md",
22
- AgentType.TASKS: "tasks.md",
18
+ # Values can be:
19
+ # - A Path: exact file (e.g., Path("research.md"))
20
+ # - A list of Paths: multiple allowed files/directories (e.g., [Path("specification.md"), Path("contracts")])
21
+ # - "*": any file except protected files (for export agent)
22
+ AGENT_DIRECTORIES: dict[AgentType, str | Path | list[Path]] = {
23
+ AgentType.RESEARCH: Path("research.md"),
24
+ AgentType.SPECIFY: [
25
+ Path("specification.md"),
26
+ Path("contracts"),
27
+ ], # Specify can write specs and contract files
28
+ AgentType.PLAN: Path("plan.md"),
29
+ AgentType.TASKS: Path("tasks.md"),
23
30
  AgentType.EXPORT: "*", # Export agent can write anywhere except protected files
24
31
  }
25
32
 
@@ -60,13 +67,52 @@ def _validate_agent_scoped_path(filename: str, agent_mode: AgentType | None) ->
60
67
  # Allow writing anywhere else in .shotgun directory
61
68
  full_path = (base_path / filename).resolve()
62
69
  else:
63
- # For other agents, only allow writing to their specific file
64
- allowed_file = AGENT_DIRECTORIES[agent_mode]
65
- if filename != allowed_file:
70
+ # For other agents, check if they have access to the requested file
71
+ allowed_paths_raw = AGENT_DIRECTORIES[agent_mode]
72
+
73
+ # Convert single Path/string to list of Paths for uniform handling
74
+ if isinstance(allowed_paths_raw, str):
75
+ # Special case: "*" means export agent
76
+ allowed_paths = (
77
+ [Path(allowed_paths_raw)] if allowed_paths_raw != "*" else []
78
+ )
79
+ elif isinstance(allowed_paths_raw, Path):
80
+ allowed_paths = [allowed_paths_raw]
81
+ else:
82
+ # Already a list
83
+ allowed_paths = allowed_paths_raw
84
+
85
+ # Check if filename matches any allowed path
86
+ is_allowed = False
87
+ for allowed_path in allowed_paths:
88
+ allowed_str = str(allowed_path)
89
+
90
+ # Check if it's a directory (no .md extension or suffix)
91
+ # Directories: Path("contracts") has no suffix, files: Path("spec.md") has .md suffix
92
+ if not allowed_path.suffix or (
93
+ allowed_path.suffix and not allowed_str.endswith(".md")
94
+ ):
95
+ # Directory - allow any file within this directory
96
+ # Check both "contracts/file.py" and "contracts" prefix
97
+ if (
98
+ filename.startswith(allowed_str + "/")
99
+ or filename == allowed_str
100
+ ):
101
+ is_allowed = True
102
+ break
103
+ else:
104
+ # Exact file match
105
+ if filename == allowed_str:
106
+ is_allowed = True
107
+ break
108
+
109
+ if not is_allowed:
110
+ allowed_display = ", ".join(f"'{p}'" for p in allowed_paths)
66
111
  raise ValueError(
67
- f"{agent_mode.value.capitalize()} agent can only write to '{allowed_file}'. "
112
+ f"{agent_mode.value.capitalize()} agent can only write to {allowed_display}. "
68
113
  f"Attempted to write to '{filename}'"
69
114
  )
115
+
70
116
  full_path = (base_path / filename).resolve()
71
117
  else:
72
118
  # No agent mode specified, fall back to old validation
@@ -125,6 +125,34 @@ def main(
125
125
  help="Continue previous TUI conversation",
126
126
  ),
127
127
  ] = False,
128
+ web: Annotated[
129
+ bool,
130
+ typer.Option(
131
+ "--web",
132
+ help="Serve TUI as web application",
133
+ ),
134
+ ] = False,
135
+ port: Annotated[
136
+ int,
137
+ typer.Option(
138
+ "--port",
139
+ help="Port for web server (only used with --web)",
140
+ ),
141
+ ] = 8000,
142
+ host: Annotated[
143
+ str,
144
+ typer.Option(
145
+ "--host",
146
+ help="Host address for web server (only used with --web)",
147
+ ),
148
+ ] = "localhost",
149
+ public_url: Annotated[
150
+ str | None,
151
+ typer.Option(
152
+ "--public-url",
153
+ help="Public URL if behind proxy (only used with --web)",
154
+ ),
155
+ ] = None,
128
156
  ) -> None:
129
157
  """Shotgun - AI-powered CLI tool."""
130
158
  logger.debug("Starting shotgun CLI application")
@@ -134,16 +162,32 @@ def main(
134
162
  perform_auto_update_async(no_update_check=no_update_check)
135
163
 
136
164
  if ctx.invoked_subcommand is None and not ctx.resilient_parsing:
137
- logger.debug("Launching shotgun TUI application")
138
- try:
139
- tui_app.run(
140
- no_update_check=no_update_check, continue_session=continue_session
141
- )
142
- finally:
143
- # Ensure PostHog is shut down cleanly even if TUI exits unexpectedly
144
- from shotgun.posthog_telemetry import shutdown
145
-
146
- shutdown()
165
+ if web:
166
+ logger.debug("Launching shotgun TUI as web application")
167
+ try:
168
+ tui_app.serve(
169
+ host=host,
170
+ port=port,
171
+ public_url=public_url,
172
+ no_update_check=no_update_check,
173
+ continue_session=continue_session,
174
+ )
175
+ finally:
176
+ # Ensure PostHog is shut down cleanly even if server exits unexpectedly
177
+ from shotgun.posthog_telemetry import shutdown
178
+
179
+ shutdown()
180
+ else:
181
+ logger.debug("Launching shotgun TUI application")
182
+ try:
183
+ tui_app.run(
184
+ no_update_check=no_update_check, continue_session=continue_session
185
+ )
186
+ finally:
187
+ # Ensure PostHog is shut down cleanly even if TUI exits unexpectedly
188
+ from shotgun.posthog_telemetry import shutdown
189
+
190
+ shutdown()
147
191
  raise typer.Exit()
148
192
 
149
193
  # For CLI commands, register PostHog shutdown handler