shotgun-sh 0.1.15.dev2__tar.gz → 0.1.16.dev2__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 (137) hide show
  1. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/PKG-INFO +2 -1
  2. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/pyproject.toml +2 -1
  3. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/agent_manager.py +11 -3
  4. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/models.py +7 -0
  5. shotgun_sh-0.1.16.dev2/src/shotgun/agents/usage_manager.py +159 -0
  6. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/tui/screens/chat.py +39 -26
  7. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/tui/screens/chat_screen/command_providers.py +32 -0
  8. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/tui/screens/provider_config.py +9 -1
  9. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/.gitignore +0 -0
  10. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/LICENSE +0 -0
  11. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/README.md +0 -0
  12. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/hatch_build.py +0 -0
  13. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/__init__.py +0 -0
  14. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/__init__.py +0 -0
  15. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/common.py +0 -0
  16. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/config/__init__.py +0 -0
  17. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/config/constants.py +0 -0
  18. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/config/manager.py +0 -0
  19. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/config/models.py +0 -0
  20. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/config/provider.py +0 -0
  21. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/conversation_history.py +0 -0
  22. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/conversation_manager.py +0 -0
  23. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/export.py +0 -0
  24. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/history/__init__.py +0 -0
  25. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/history/compaction.py +0 -0
  26. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/history/constants.py +0 -0
  27. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/history/context_extraction.py +0 -0
  28. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/history/history_building.py +0 -0
  29. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/history/history_processors.py +0 -0
  30. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/history/message_utils.py +0 -0
  31. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/history/token_counting.py +0 -0
  32. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/history/token_estimation.py +0 -0
  33. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/messages.py +0 -0
  34. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/plan.py +0 -0
  35. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/research.py +0 -0
  36. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/specify.py +0 -0
  37. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/tasks.py +0 -0
  38. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/tools/__init__.py +0 -0
  39. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/tools/codebase/__init__.py +0 -0
  40. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/tools/codebase/codebase_shell.py +0 -0
  41. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/tools/codebase/directory_lister.py +0 -0
  42. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/tools/codebase/file_read.py +0 -0
  43. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/tools/codebase/models.py +0 -0
  44. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/tools/codebase/query_graph.py +0 -0
  45. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/tools/codebase/retrieve_code.py +0 -0
  46. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/tools/file_management.py +0 -0
  47. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/tools/user_interaction.py +0 -0
  48. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/tools/web_search/__init__.py +0 -0
  49. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/tools/web_search/anthropic.py +0 -0
  50. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/tools/web_search/gemini.py +0 -0
  51. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/tools/web_search/openai.py +0 -0
  52. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/agents/tools/web_search/utils.py +0 -0
  53. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/build_constants.py +0 -0
  54. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/cli/__init__.py +0 -0
  55. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/cli/codebase/__init__.py +0 -0
  56. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/cli/codebase/commands.py +0 -0
  57. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/cli/codebase/models.py +0 -0
  58. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/cli/config.py +0 -0
  59. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/cli/export.py +0 -0
  60. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/cli/feedback.py +0 -0
  61. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/cli/models.py +0 -0
  62. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/cli/plan.py +0 -0
  63. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/cli/research.py +0 -0
  64. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/cli/specify.py +0 -0
  65. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/cli/tasks.py +0 -0
  66. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/cli/update.py +0 -0
  67. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/cli/utils.py +0 -0
  68. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/codebase/__init__.py +0 -0
  69. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/codebase/core/__init__.py +0 -0
  70. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/codebase/core/change_detector.py +0 -0
  71. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/codebase/core/code_retrieval.py +0 -0
  72. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/codebase/core/cypher_models.py +0 -0
  73. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/codebase/core/ingestor.py +0 -0
  74. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/codebase/core/language_config.py +0 -0
  75. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/codebase/core/manager.py +0 -0
  76. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/codebase/core/nl_query.py +0 -0
  77. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/codebase/core/parser_loader.py +0 -0
  78. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/codebase/models.py +0 -0
  79. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/codebase/service.py +0 -0
  80. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/logging_config.py +0 -0
  81. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/main.py +0 -0
  82. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/posthog_telemetry.py +0 -0
  83. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/prompts/__init__.py +0 -0
  84. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/prompts/agents/__init__.py +0 -0
  85. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/prompts/agents/export.j2 +0 -0
  86. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/prompts/agents/partials/codebase_understanding.j2 +0 -0
  87. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +0 -0
  88. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/prompts/agents/partials/content_formatting.j2 +0 -0
  89. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/prompts/agents/partials/interactive_mode.j2 +0 -0
  90. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/prompts/agents/plan.j2 +0 -0
  91. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/prompts/agents/research.j2 +0 -0
  92. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/prompts/agents/specify.j2 +0 -0
  93. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +0 -0
  94. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/prompts/agents/state/system_state.j2 +0 -0
  95. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/prompts/agents/tasks.j2 +0 -0
  96. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/prompts/codebase/__init__.py +0 -0
  97. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/prompts/codebase/cypher_query_patterns.j2 +0 -0
  98. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/prompts/codebase/cypher_system.j2 +0 -0
  99. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/prompts/codebase/enhanced_query_context.j2 +0 -0
  100. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/prompts/codebase/partials/cypher_rules.j2 +0 -0
  101. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/prompts/codebase/partials/graph_schema.j2 +0 -0
  102. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/prompts/codebase/partials/temporal_context.j2 +0 -0
  103. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/prompts/history/__init__.py +0 -0
  104. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/prompts/history/incremental_summarization.j2 +0 -0
  105. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/prompts/history/summarization.j2 +0 -0
  106. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/prompts/loader.py +0 -0
  107. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/py.typed +0 -0
  108. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/sdk/__init__.py +0 -0
  109. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/sdk/codebase.py +0 -0
  110. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/sdk/exceptions.py +0 -0
  111. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/sdk/models.py +0 -0
  112. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/sdk/services.py +0 -0
  113. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/sentry_telemetry.py +0 -0
  114. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/telemetry.py +0 -0
  115. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/tui/__init__.py +0 -0
  116. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/tui/app.py +0 -0
  117. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/tui/commands/__init__.py +0 -0
  118. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/tui/components/prompt_input.py +0 -0
  119. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/tui/components/spinner.py +0 -0
  120. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/tui/components/splash.py +0 -0
  121. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/tui/components/vertical_tail.py +0 -0
  122. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/tui/filtered_codebase_service.py +0 -0
  123. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/tui/screens/chat.tcss +0 -0
  124. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/tui/screens/chat_screen/__init__.py +0 -0
  125. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/tui/screens/chat_screen/hint_message.py +0 -0
  126. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/tui/screens/chat_screen/history.py +0 -0
  127. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/tui/screens/directory_setup.py +0 -0
  128. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/tui/screens/feedback.py +0 -0
  129. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/tui/screens/splash.py +0 -0
  130. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/tui/styles.tcss +0 -0
  131. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/tui/utils/__init__.py +0 -0
  132. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/tui/utils/mode_progress.py +0 -0
  133. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/utils/__init__.py +0 -0
  134. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/utils/env_utils.py +0 -0
  135. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/utils/file_system_utils.py +0 -0
  136. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/src/shotgun/utils/source_detection.py +0 -0
  137. {shotgun_sh-0.1.15.dev2 → shotgun_sh-0.1.16.dev2}/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.1.15.dev2
3
+ Version: 0.1.16.dev2
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
@@ -22,6 +22,7 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
22
22
  Classifier: Topic :: Utilities
23
23
  Requires-Python: >=3.11
24
24
  Requires-Dist: anthropic>=0.39.0
25
+ Requires-Dist: genai-prices>=0.0.27
25
26
  Requires-Dist: google-generativeai>=0.8.5
26
27
  Requires-Dist: httpx>=0.27.0
27
28
  Requires-Dist: jinja2>=3.1.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "shotgun-sh"
3
- version = "0.1.15.dev2"
3
+ version = "0.1.16.dev2"
4
4
  description = "AI-powered research, planning, and task management CLI tool"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -45,6 +45,7 @@ dependencies = [
45
45
  "google-generativeai>=0.8.5",
46
46
  "tiktoken>=0.7.0",
47
47
  "packaging>=23.0",
48
+ "genai-prices>=0.0.27",
48
49
  ]
49
50
 
50
51
  [project.urls]
@@ -115,12 +115,12 @@ class AgentManager(Widget):
115
115
  super().__init__()
116
116
  self.display = False
117
117
 
118
+ if deps is None:
119
+ raise ValueError("AgentDeps must be provided to AgentManager")
120
+
118
121
  # Use provided deps or create default with interactive mode
119
122
  self.deps = deps
120
123
 
121
- if self.deps is None:
122
- raise ValueError("AgentDeps must be provided to AgentManager")
123
-
124
124
  # Create AgentRuntimeOptions from deps for agent creation
125
125
  agent_runtime_options = AgentRuntimeOptions(
126
126
  interactive_mode=self.deps.interactive_mode,
@@ -269,6 +269,7 @@ class AgentManager(Widget):
269
269
  Returns:
270
270
  The agent run result.
271
271
  """
272
+ logger.info(f"Running agent {self._current_agent_type.value}")
272
273
  # Use merged deps (shared state + agent-specific system prompt) if not provided
273
274
  if deps is None:
274
275
  deps = self._create_merged_deps(self._current_agent_type)
@@ -395,6 +396,10 @@ class AgentManager(Widget):
395
396
  # Apply compaction to persistent message history to prevent cascading growth
396
397
  all_messages = result.all_messages()
397
398
  self.message_history = await apply_persistent_compaction(all_messages, deps)
399
+ usage = result.usage()
400
+ deps.usage_manager.add_usage(
401
+ usage, model_name=deps.llm_model.name, provider=deps.llm_model.provider
402
+ )
398
403
 
399
404
  # Log file operations summary if any files were modified
400
405
  file_operations = deps.file_tracker.operations.copy()
@@ -641,6 +646,9 @@ class AgentManager(Widget):
641
646
  filtered_messages.append(msg)
642
647
  return filtered_messages
643
648
 
649
+ def get_usage_hint(self) -> str | None:
650
+ return self.deps.usage_manager.build_usage_hint()
651
+
644
652
  def get_conversation_state(self) -> "ConversationState":
645
653
  """Get the current conversation state.
646
654
 
@@ -11,6 +11,8 @@ from typing import TYPE_CHECKING
11
11
  from pydantic import BaseModel, ConfigDict, Field
12
12
  from pydantic_ai import RunContext
13
13
 
14
+ from shotgun.agents.usage_manager import SessionUsageManager, get_session_usage_manager
15
+
14
16
  from .config.models import ModelConfig
15
17
 
16
18
  if TYPE_CHECKING:
@@ -108,6 +110,11 @@ class AgentRuntimeOptions(BaseModel):
108
110
  description="Tasks for storing deferred tool results",
109
111
  )
110
112
 
113
+ usage_manager: SessionUsageManager = Field(
114
+ default_factory=get_session_usage_manager,
115
+ description="Usage manager for tracking usage",
116
+ )
117
+
111
118
 
112
119
  class FileOperationType(str, Enum):
113
120
  """Types of file operations that can be tracked."""
@@ -0,0 +1,159 @@
1
+ import json
2
+ from collections import defaultdict
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ from logging import getLogger
6
+ from pathlib import Path
7
+ from typing import TypeAlias
8
+
9
+ from genai_prices import calc_price
10
+ from pydantic import BaseModel, Field
11
+ from pydantic_ai import RunUsage
12
+
13
+ from shotgun.agents.config.models import ProviderType
14
+ from shotgun.utils import get_shotgun_home
15
+
16
+ logger = getLogger(__name__)
17
+ ModelName: TypeAlias = str
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class UsageSummaryEntry:
22
+ model_name: ModelName
23
+ provider: ProviderType
24
+ usage: RunUsage
25
+
26
+
27
+ class UsageLogEntry(BaseModel):
28
+ timestamp: datetime = Field(default_factory=datetime.now)
29
+ model_name: ModelName
30
+ usage: RunUsage
31
+ provider: ProviderType
32
+
33
+
34
+ class SessionUsage(BaseModel):
35
+ usage: RunUsage
36
+ log: list[UsageLogEntry]
37
+
38
+
39
+ class UsageState(BaseModel):
40
+ usage: dict[ModelName, RunUsage] = Field(default_factory=dict)
41
+ model_providers: dict[ModelName, ProviderType] = Field(default_factory=dict)
42
+ usage_log: list[UsageLogEntry] = Field(default_factory=list)
43
+
44
+
45
+ class SessionUsageManager:
46
+ def __init__(self) -> None:
47
+ self.usage: defaultdict[ModelName, RunUsage] = defaultdict(RunUsage)
48
+ self._model_providers: dict[ModelName, ProviderType] = {}
49
+ self._usage_log: list[UsageLogEntry] = []
50
+ self._usage_path: Path = get_shotgun_home() / "usage.json"
51
+ self.restore_usage_state()
52
+
53
+ def add_usage(
54
+ self, usage: RunUsage, *, model_name: ModelName, provider: ProviderType
55
+ ) -> None:
56
+ self.usage[model_name] += usage
57
+ self._model_providers[model_name] = provider
58
+ self._usage_log.append(
59
+ UsageLogEntry(model_name=model_name, usage=usage, provider=provider)
60
+ )
61
+ self.persist_usage_state()
62
+
63
+ def get_usage_report(self) -> dict[ModelName, RunUsage]:
64
+ return self.usage.copy()
65
+
66
+ def get_usage_breakdown(self) -> list[UsageSummaryEntry]:
67
+ breakdown: list[UsageSummaryEntry] = []
68
+ for model_name, usage in self.usage.items():
69
+ provider = self._model_providers.get(model_name)
70
+ if provider is None:
71
+ continue
72
+ breakdown.append(
73
+ UsageSummaryEntry(model_name=model_name, provider=provider, usage=usage)
74
+ )
75
+ breakdown.sort(key=lambda entry: entry.model_name.lower())
76
+ return breakdown
77
+
78
+ def build_usage_hint(self) -> str | None:
79
+ return format_usage_hint(self.get_usage_breakdown())
80
+
81
+ def persist_usage_state(self) -> None:
82
+ state = UsageState(
83
+ usage=dict(self.usage.items()),
84
+ model_providers=self._model_providers.copy(),
85
+ usage_log=self._usage_log.copy(),
86
+ )
87
+
88
+ try:
89
+ self._usage_path.parent.mkdir(parents=True, exist_ok=True)
90
+ with self._usage_path.open("w", encoding="utf-8") as f:
91
+ json.dump(state.model_dump(mode="json"), f, indent=2)
92
+ logger.debug("Usage state persisted to %s", self._usage_path)
93
+ except Exception as exc:
94
+ logger.error(
95
+ "Failed to persist usage state to %s: %s", self._usage_path, exc
96
+ )
97
+
98
+ def restore_usage_state(self) -> None:
99
+ if not self._usage_path.exists():
100
+ logger.debug("No usage state file found at %s", self._usage_path)
101
+ return
102
+
103
+ try:
104
+ with self._usage_path.open(encoding="utf-8") as f:
105
+ data = json.load(f)
106
+
107
+ state = UsageState.model_validate(data)
108
+ except Exception as exc:
109
+ logger.error(
110
+ "Failed to restore usage state from %s: %s", self._usage_path, exc
111
+ )
112
+ return
113
+
114
+ self.usage = defaultdict(RunUsage)
115
+ for model_name, usage in state.usage.items():
116
+ self.usage[model_name] = usage
117
+
118
+ self._model_providers = state.model_providers.copy()
119
+ self._usage_log = state.usage_log.copy()
120
+
121
+
122
+ def format_usage_hint(breakdown: list[UsageSummaryEntry]) -> str | None:
123
+ if not breakdown:
124
+ return None
125
+
126
+ lines = ["# Token usage by model"]
127
+
128
+ for entry in breakdown:
129
+ usage = entry.usage
130
+ input_tokens = usage.input_tokens
131
+ output_tokens = usage.output_tokens
132
+ cached_tokens = usage.cache_read_tokens
133
+
134
+ cost = calc_price(usage=usage, model_ref=entry.model_name)
135
+ input_line = f"* Input: {input_tokens:,}"
136
+ if cached_tokens > 0:
137
+ input_line += f" (+ {cached_tokens:,} cached)"
138
+ input_line += " tokens"
139
+ section = f"""
140
+ ### {entry.model_name}
141
+
142
+ {input_line}
143
+ * Output: {output_tokens:,} tokens
144
+ * Total: {input_tokens + output_tokens:,} tokens
145
+ * Cost: ${cost.total_price:,.2f}
146
+ """.strip()
147
+ lines.append(section)
148
+
149
+ return "\n\n".join(lines)
150
+
151
+
152
+ _usage_manager = None
153
+
154
+
155
+ def get_session_usage_manager() -> SessionUsageManager:
156
+ global _usage_manager
157
+ if _usage_manager is None:
158
+ _usage_manager = SessionUsageManager()
159
+ return _usage_manager
@@ -58,6 +58,7 @@ from .chat_screen.command_providers import (
58
58
  CodebaseCommandProvider,
59
59
  DeleteCodebasePaletteProvider,
60
60
  ProviderSetupProvider,
61
+ UsageProvider,
61
62
  )
62
63
 
63
64
  logger = logging.getLogger(__name__)
@@ -228,9 +229,15 @@ class ChatScreen(Screen[None]):
228
229
  BINDINGS = [
229
230
  ("ctrl+p", "command_palette", "Command Palette"),
230
231
  ("shift+tab", "toggle_mode", "Toggle mode"),
232
+ ("ctrl+u", "show_usage", "Show usage"),
231
233
  ]
232
234
 
233
- COMMANDS = {AgentModeProvider, ProviderSetupProvider, CodebaseCommandProvider}
235
+ COMMANDS = {
236
+ AgentModeProvider,
237
+ ProviderSetupProvider,
238
+ CodebaseCommandProvider,
239
+ UsageProvider,
240
+ }
234
241
 
235
242
  value = reactive("")
236
243
  mode = reactive(AgentType.RESEARCH)
@@ -401,6 +408,14 @@ class ChatScreen(Screen[None]):
401
408
  # whoops it actually changes focus. Let's be brutal for now
402
409
  self.call_later(lambda: self.query_one(PromptInput).focus())
403
410
 
411
+ def action_show_usage(self) -> None:
412
+ usage_hint = self.agent_manager.get_usage_hint()
413
+ logger.info(f"Usage hint: {usage_hint}")
414
+ if usage_hint:
415
+ self.mount_hint(usage_hint)
416
+ else:
417
+ self.notify("No usage hint available", severity="error")
418
+
404
419
  @work
405
420
  async def add_question_listener(self) -> None:
406
421
  while True:
@@ -589,7 +604,7 @@ class ChatScreen(Screen[None]):
589
604
  async def index_codebase(self, selection: CodebaseIndexSelection) -> None:
590
605
  label = self.query_one("#indexing-job-display", Static)
591
606
  label.update(
592
- f"[$foreground-muted]Indexing [bold $text-accent]{selection.name}[/]...[/]"
607
+ f"[$foreground-muted]Indexing codebase: [bold $text-accent]{selection.name}[/][/]"
593
608
  )
594
609
  label.refresh()
595
610
 
@@ -599,33 +614,33 @@ class ChatScreen(Screen[None]):
599
614
  empty = width - filled
600
615
  return "▓" * filled + "░" * empty
601
616
 
602
- # Spinner animation state (shared between timer and progress callback)
617
+ # Spinner animation frames
603
618
  spinner_frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
604
- spinner_state: dict[str, int | str | float] = {
619
+
620
+ # Progress state (shared between timer and progress callback)
621
+ progress_state: dict[str, int | float] = {
605
622
  "frame_index": 0,
606
- "phase_name": "Starting...",
607
623
  "percentage": 0.0,
608
624
  }
609
625
 
610
- def update_spinner_display() -> None:
611
- """Update spinner frame on timer - runs every 100ms."""
626
+ def update_progress_display() -> None:
627
+ """Update progress bar on timer - runs every 100ms."""
612
628
  # Advance spinner frame
613
- frame_idx = int(spinner_state["frame_index"])
614
- spinner_state["frame_index"] = (frame_idx + 1) % len(spinner_frames)
629
+ frame_idx = int(progress_state["frame_index"])
630
+ progress_state["frame_index"] = (frame_idx + 1) % len(spinner_frames)
615
631
  spinner = spinner_frames[frame_idx]
616
632
 
617
633
  # Get current state
618
- phase = str(spinner_state["phase_name"])
619
- pct = float(spinner_state["percentage"])
634
+ pct = float(progress_state["percentage"])
620
635
  bar = create_progress_bar(pct)
621
636
 
622
637
  # Update label
623
638
  label.update(
624
- f"[$foreground-muted]Indexing codebase: {spinner} {phase}... {bar} {pct:.0f}%[/]"
639
+ f"[$foreground-muted]Indexing codebase: {spinner} {bar} {pct:.0f}%[/]"
625
640
  )
626
641
 
627
642
  def progress_callback(progress_info: IndexProgress) -> None:
628
- """Update progress state (spinner animates independently on timer)."""
643
+ """Update progress state (timer renders it independently)."""
629
644
  # Calculate overall percentage (0-95%, reserve 95-100% for finalization)
630
645
  if progress_info.phase == ProgressPhase.STRUCTURE:
631
646
  # Phase 1: 0-10%, always show 5% while running, 10% when complete
@@ -648,11 +663,10 @@ class ChatScreen(Screen[None]):
648
663
  overall_pct = 0.0
649
664
 
650
665
  # Update shared state (timer will render it)
651
- spinner_state["phase_name"] = progress_info.phase_name
652
- spinner_state["percentage"] = overall_pct
666
+ progress_state["percentage"] = overall_pct
653
667
 
654
- # Start spinner animation timer (10 fps = 100ms interval)
655
- spinner_timer = self.set_interval(0.1, update_spinner_display)
668
+ # Start progress animation timer (10 fps = 100ms interval)
669
+ progress_timer = self.set_interval(0.1, update_progress_display)
656
670
 
657
671
  try:
658
672
  # Pass the current working directory as the indexed_from_cwd
@@ -667,14 +681,12 @@ class ChatScreen(Screen[None]):
667
681
  progress_callback=progress_callback,
668
682
  )
669
683
 
670
- # Stop spinner animation
671
- spinner_timer.stop()
684
+ # Stop progress animation
685
+ progress_timer.stop()
672
686
 
673
687
  # Show 100% completion after indexing finishes
674
688
  final_bar = create_progress_bar(100.0)
675
- label.update(
676
- f"[$foreground-muted]Indexing codebase: ✓ Complete {final_bar} 100%[/]"
677
- )
689
+ label.update(f"[$foreground-muted]Indexing codebase: {final_bar} 100%[/]")
678
690
  label.refresh()
679
691
 
680
692
  logger.info(
@@ -687,12 +699,12 @@ class ChatScreen(Screen[None]):
687
699
  )
688
700
 
689
701
  except CodebaseAlreadyIndexedError as exc:
690
- spinner_timer.stop()
702
+ progress_timer.stop()
691
703
  logger.warning(f"Codebase already indexed: {exc}")
692
704
  self.notify(str(exc), severity="warning")
693
705
  return
694
706
  except InvalidPathError as exc:
695
- spinner_timer.stop()
707
+ progress_timer.stop()
696
708
  logger.error(f"Invalid path error: {exc}")
697
709
  self.notify(str(exc), severity="error")
698
710
 
@@ -704,8 +716,8 @@ class ChatScreen(Screen[None]):
704
716
  )
705
717
  self.notify(f"Failed to index codebase: {exc}", severity="error")
706
718
  finally:
707
- # Always stop the spinner timer
708
- spinner_timer.stop()
719
+ # Always stop the progress timer
720
+ progress_timer.stop()
709
721
  label.update("")
710
722
  label.refresh()
711
723
 
@@ -774,6 +786,7 @@ class ChatScreen(Screen[None]):
774
786
 
775
787
  # Update the current mode
776
788
  self.mode = AgentType(conversation.last_agent_model)
789
+ self.deps.usage_manager.restore_usage_state()
777
790
 
778
791
 
779
792
  def help_text_with_codebase(already_indexed: bool = False) -> str:
@@ -96,6 +96,38 @@ class AgentModeProvider(Provider):
96
96
  yield Hit(score, matcher.highlight(title), callback, help=help_text)
97
97
 
98
98
 
99
+ class UsageProvider(Provider):
100
+ """Command provider for agent mode switching."""
101
+
102
+ @property
103
+ def chat_screen(self) -> "ChatScreen":
104
+ from shotgun.tui.screens.chat import ChatScreen
105
+
106
+ return cast(ChatScreen, self.screen)
107
+
108
+ async def discover(self) -> AsyncGenerator[DiscoveryHit, None]:
109
+ """Provide default mode switching commands when palette opens."""
110
+ yield DiscoveryHit(
111
+ "Show usage",
112
+ self.chat_screen.action_show_usage,
113
+ help="Display usage information for the current session",
114
+ )
115
+
116
+ async def search(self, query: str) -> AsyncGenerator[Hit, None]:
117
+ """Search for mode commands."""
118
+ matcher = self.matcher(query)
119
+
120
+ async for discovery_hit in self.discover():
121
+ score = matcher.match(discovery_hit.text or "")
122
+ if score > 0:
123
+ yield Hit(
124
+ score,
125
+ matcher.highlight(discovery_hit.text or ""),
126
+ discovery_hit.command,
127
+ help=discovery_hit.help,
128
+ )
129
+
130
+
99
131
  class ProviderSetupProvider(Provider):
100
132
  """Command palette entries for provider configuration."""
101
133
 
@@ -9,7 +9,7 @@ from textual.app import ComposeResult
9
9
  from textual.containers import Horizontal, Vertical
10
10
  from textual.reactive import reactive
11
11
  from textual.screen import Screen
12
- from textual.widgets import Button, Input, Label, ListItem, ListView, Static
12
+ from textual.widgets import Button, Input, Label, ListItem, ListView, Markdown, Static
13
13
 
14
14
  from shotgun.agents.config import ConfigManager, ProviderType
15
15
 
@@ -47,6 +47,10 @@ class ProviderConfigScreen(Screen[None]):
47
47
  color: $text-accent;
48
48
  }
49
49
 
50
+ #provider-links {
51
+ padding: 1 0;
52
+ }
53
+
50
54
  #provider-list {
51
55
  margin: 2 0;
52
56
  height: auto;
@@ -78,6 +82,10 @@ class ProviderConfigScreen(Screen[None]):
78
82
  "Select a provider and enter the API key needed to activate it.",
79
83
  id="provider-config-summary",
80
84
  )
85
+ yield Markdown(
86
+ "Don't have an API Key? Use these links to get one: [OpenAI](https://platform.openai.com/api-keys) | [Anthropic](https://console.anthropic.com) | [Google Gemini](https://aistudio.google.com)",
87
+ id="provider-links",
88
+ )
81
89
  yield ListView(*self._build_provider_items(), id="provider-list")
82
90
  yield Input(
83
91
  placeholder=self._input_placeholder(self.selected_provider),