shotgun-sh 0.1.11.dev1__tar.gz → 0.1.12__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 (134) hide show
  1. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/PKG-INFO +1 -1
  2. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/pyproject.toml +1 -1
  3. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/build_constants.py +2 -2
  4. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/codebase/core/manager.py +13 -1
  5. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/codebase/service.py +4 -0
  6. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/main.py +9 -1
  7. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/tui/app.py +4 -0
  8. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/tui/components/vertical_tail.py +6 -0
  9. shotgun_sh-0.1.12/src/shotgun/tui/filtered_codebase_service.py +46 -0
  10. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/tui/screens/chat.py +35 -126
  11. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/tui/screens/chat_screen/history.py +110 -21
  12. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/.gitignore +0 -0
  13. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/LICENSE +0 -0
  14. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/README.md +0 -0
  15. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/hatch_build.py +0 -0
  16. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/__init__.py +0 -0
  17. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/__init__.py +0 -0
  18. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/agent_manager.py +0 -0
  19. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/common.py +0 -0
  20. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/config/__init__.py +0 -0
  21. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/config/constants.py +0 -0
  22. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/config/manager.py +0 -0
  23. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/config/models.py +0 -0
  24. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/config/provider.py +0 -0
  25. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/conversation_history.py +0 -0
  26. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/conversation_manager.py +0 -0
  27. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/export.py +0 -0
  28. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/history/__init__.py +0 -0
  29. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/history/compaction.py +0 -0
  30. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/history/constants.py +0 -0
  31. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/history/context_extraction.py +0 -0
  32. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/history/history_building.py +0 -0
  33. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/history/history_processors.py +0 -0
  34. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/history/message_utils.py +0 -0
  35. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/history/token_counting.py +0 -0
  36. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/history/token_estimation.py +0 -0
  37. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/messages.py +0 -0
  38. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/models.py +0 -0
  39. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/plan.py +0 -0
  40. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/research.py +0 -0
  41. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/specify.py +0 -0
  42. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/tasks.py +0 -0
  43. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/tools/__init__.py +0 -0
  44. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/tools/codebase/__init__.py +0 -0
  45. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/tools/codebase/codebase_shell.py +0 -0
  46. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/tools/codebase/directory_lister.py +0 -0
  47. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/tools/codebase/file_read.py +0 -0
  48. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/tools/codebase/models.py +0 -0
  49. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/tools/codebase/query_graph.py +0 -0
  50. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/tools/codebase/retrieve_code.py +0 -0
  51. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/tools/file_management.py +0 -0
  52. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/tools/user_interaction.py +0 -0
  53. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/tools/web_search/__init__.py +0 -0
  54. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/tools/web_search/anthropic.py +0 -0
  55. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/tools/web_search/gemini.py +0 -0
  56. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/tools/web_search/openai.py +0 -0
  57. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/agents/tools/web_search/utils.py +0 -0
  58. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/cli/__init__.py +0 -0
  59. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/cli/codebase/__init__.py +0 -0
  60. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/cli/codebase/commands.py +0 -0
  61. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/cli/codebase/models.py +0 -0
  62. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/cli/config.py +0 -0
  63. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/cli/export.py +0 -0
  64. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/cli/models.py +0 -0
  65. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/cli/plan.py +0 -0
  66. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/cli/research.py +0 -0
  67. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/cli/specify.py +0 -0
  68. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/cli/tasks.py +0 -0
  69. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/cli/update.py +0 -0
  70. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/cli/utils.py +0 -0
  71. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/codebase/__init__.py +0 -0
  72. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/codebase/core/__init__.py +0 -0
  73. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/codebase/core/change_detector.py +0 -0
  74. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/codebase/core/code_retrieval.py +0 -0
  75. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/codebase/core/cypher_models.py +0 -0
  76. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/codebase/core/ingestor.py +0 -0
  77. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/codebase/core/language_config.py +0 -0
  78. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/codebase/core/nl_query.py +0 -0
  79. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/codebase/core/parser_loader.py +0 -0
  80. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/codebase/models.py +0 -0
  81. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/logging_config.py +0 -0
  82. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/posthog_telemetry.py +0 -0
  83. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/prompts/__init__.py +0 -0
  84. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/prompts/agents/__init__.py +0 -0
  85. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/prompts/agents/export.j2 +0 -0
  86. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/prompts/agents/partials/codebase_understanding.j2 +0 -0
  87. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +0 -0
  88. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/prompts/agents/partials/content_formatting.j2 +0 -0
  89. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/prompts/agents/partials/interactive_mode.j2 +0 -0
  90. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/prompts/agents/plan.j2 +0 -0
  91. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/prompts/agents/research.j2 +0 -0
  92. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/prompts/agents/specify.j2 +0 -0
  93. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +0 -0
  94. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/prompts/agents/state/system_state.j2 +0 -0
  95. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/prompts/agents/tasks.j2 +0 -0
  96. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/prompts/codebase/__init__.py +0 -0
  97. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/prompts/codebase/cypher_query_patterns.j2 +0 -0
  98. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/prompts/codebase/cypher_system.j2 +0 -0
  99. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/prompts/codebase/enhanced_query_context.j2 +0 -0
  100. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/prompts/codebase/partials/cypher_rules.j2 +0 -0
  101. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/prompts/codebase/partials/graph_schema.j2 +0 -0
  102. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/prompts/codebase/partials/temporal_context.j2 +0 -0
  103. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/prompts/history/__init__.py +0 -0
  104. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/prompts/history/incremental_summarization.j2 +0 -0
  105. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/prompts/history/summarization.j2 +0 -0
  106. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/prompts/loader.py +0 -0
  107. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/py.typed +0 -0
  108. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/sdk/__init__.py +0 -0
  109. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/sdk/codebase.py +0 -0
  110. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/sdk/exceptions.py +0 -0
  111. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/sdk/models.py +0 -0
  112. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/sdk/services.py +0 -0
  113. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/sentry_telemetry.py +0 -0
  114. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/telemetry.py +0 -0
  115. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/tui/__init__.py +0 -0
  116. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/tui/commands/__init__.py +0 -0
  117. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/tui/components/prompt_input.py +0 -0
  118. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/tui/components/spinner.py +0 -0
  119. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/tui/components/splash.py +0 -0
  120. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/tui/screens/chat.tcss +0 -0
  121. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/tui/screens/chat_screen/__init__.py +0 -0
  122. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/tui/screens/chat_screen/command_providers.py +0 -0
  123. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/tui/screens/chat_screen/hint_message.py +0 -0
  124. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/tui/screens/directory_setup.py +0 -0
  125. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/tui/screens/provider_config.py +0 -0
  126. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/tui/screens/splash.py +0 -0
  127. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/tui/styles.tcss +0 -0
  128. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/tui/utils/__init__.py +0 -0
  129. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/tui/utils/mode_progress.py +0 -0
  130. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/utils/__init__.py +0 -0
  131. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/utils/env_utils.py +0 -0
  132. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/utils/file_system_utils.py +0 -0
  133. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/src/shotgun/utils/source_detection.py +0 -0
  134. {shotgun_sh-0.1.11.dev1 → shotgun_sh-0.1.12}/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.11.dev1
3
+ Version: 0.1.12
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "shotgun-sh"
3
- version = "0.1.11.dev1"
3
+ version = "0.1.12"
4
4
  description = "AI-powered research, planning, and task management CLI tool"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -12,8 +12,8 @@ POSTHOG_API_KEY = ''
12
12
  POSTHOG_PROJECT_ID = '191396'
13
13
 
14
14
  # Logfire configuration embedded at build time (only for dev builds)
15
- LOGFIRE_ENABLED = 'true'
16
- LOGFIRE_TOKEN = 'pylf_v1_us_KZ5NM1pP3NwgJkbBJt6Ftdzk8mMhmrXcGJHQQgDJ1LfK'
15
+ LOGFIRE_ENABLED = ''
16
+ LOGFIRE_TOKEN = ''
17
17
 
18
18
  # Build metadata
19
19
  BUILD_TIME_ENV = "production" if SENTRY_DSN else "development"
@@ -353,7 +353,19 @@ class CodebaseGraphManager:
353
353
 
354
354
  # Check if graph already exists
355
355
  if graph_path.exists():
356
- raise CodebaseAlreadyIndexedError(repo_path)
356
+ # Verify it's not corrupted by checking if we can load the Project node
357
+ existing_graph = await self.get_graph(graph_id)
358
+ if existing_graph:
359
+ # Valid existing graph
360
+ raise CodebaseAlreadyIndexedError(repo_path)
361
+ else:
362
+ # Corrupted database - remove and re-index
363
+ logger.warning(
364
+ f"Found corrupted database at {graph_path}, removing for re-indexing..."
365
+ )
366
+ import shutil
367
+
368
+ shutil.rmtree(graph_path)
357
369
 
358
370
  # Import the builder from local core module
359
371
  from shotgun.codebase.core import CodebaseIngestor
@@ -69,6 +69,10 @@ class CodebaseService:
69
69
  # Otherwise, check if current directory is in the allowed list
70
70
  elif target_path in graph.indexed_from_cwds:
71
71
  filtered_graphs.append(graph)
72
+ # Also allow access if current directory IS the repository itself
73
+ # Use Path.resolve() for robust comparison (handles symlinks, etc.)
74
+ elif Path(target_path).resolve() == Path(graph.repo_path).resolve():
75
+ filtered_graphs.append(graph)
72
76
 
73
77
  return filtered_graphs
74
78
 
@@ -118,7 +118,15 @@ def main(
118
118
 
119
119
  if ctx.invoked_subcommand is None and not ctx.resilient_parsing:
120
120
  logger.debug("Launching shotgun TUI application")
121
- tui_app.run(no_update_check=no_update_check, continue_session=continue_session)
121
+ try:
122
+ tui_app.run(
123
+ no_update_check=no_update_check, continue_session=continue_session
124
+ )
125
+ finally:
126
+ # Ensure PostHog is shut down cleanly even if TUI exits unexpectedly
127
+ from shotgun.posthog_telemetry import shutdown
128
+
129
+ shutdown()
122
130
  raise typer.Exit()
123
131
 
124
132
  # For CLI commands, register PostHog shutdown handler
@@ -83,6 +83,10 @@ class ShotgunApp(App[None]):
83
83
 
84
84
  async def action_quit(self) -> None:
85
85
  """Quit the application."""
86
+ # Shut down PostHog client to prevent threading errors
87
+ from shotgun.posthog_telemetry import shutdown
88
+
89
+ shutdown()
86
90
  self.exit()
87
91
 
88
92
  def get_system_commands(self, screen: Screen[Any]) -> Iterable[SystemCommand]:
@@ -1,4 +1,5 @@
1
1
  from textual.containers import VerticalScroll
2
+ from textual.geometry import Size
2
3
  from textual.reactive import reactive
3
4
 
4
5
 
@@ -11,3 +12,8 @@ class VerticalTail(VerticalScroll):
11
12
  """Handle auto_scroll property changes."""
12
13
  if value:
13
14
  self.scroll_end(animate=False)
15
+
16
+ def watch_virtual_size(self, value: Size) -> None:
17
+ """Handle virtual_size property changes."""
18
+
19
+ self.call_later(self.scroll_end, animate=False)
@@ -0,0 +1,46 @@
1
+ """Filtered codebase service that restricts access to current directory's codebase only."""
2
+
3
+ from pathlib import Path
4
+
5
+ from shotgun.codebase.models import CodebaseGraph
6
+ from shotgun.codebase.service import CodebaseService
7
+
8
+
9
+ class FilteredCodebaseService(CodebaseService):
10
+ """CodebaseService subclass that filters graphs to only those accessible from CWD.
11
+
12
+ This ensures TUI agents can only see and access the codebase indexed from the
13
+ current working directory, providing isolation between different project directories.
14
+ """
15
+
16
+ def __init__(self, storage_dir: Path | str):
17
+ """Initialize the filtered service.
18
+
19
+ Args:
20
+ storage_dir: Directory to store graph databases
21
+ """
22
+ super().__init__(storage_dir)
23
+ self._cwd = str(Path.cwd().resolve())
24
+
25
+ async def list_graphs(self) -> list[CodebaseGraph]:
26
+ """List only graphs accessible from the current working directory.
27
+
28
+ Returns:
29
+ Filtered list of CodebaseGraph objects accessible from CWD
30
+ """
31
+ # Use the existing filtering logic from list_graphs_for_directory
32
+ return await super().list_graphs_for_directory(self._cwd)
33
+
34
+ async def list_graphs_for_directory(
35
+ self, directory: Path | str | None = None
36
+ ) -> list[CodebaseGraph]:
37
+ """List graphs for directory - always filters to CWD for TUI context.
38
+
39
+ Args:
40
+ directory: Ignored in TUI context, always uses CWD
41
+
42
+ Returns:
43
+ Filtered list of CodebaseGraph objects accessible from CWD
44
+ """
45
+ # Always use CWD regardless of what directory is passed
46
+ return await super().list_graphs_for_directory(self._cwd)
@@ -1,6 +1,5 @@
1
1
  import asyncio
2
2
  import logging
3
- from collections.abc import Iterable
4
3
  from dataclasses import dataclass
5
4
  from pathlib import Path
6
5
  from typing import cast
@@ -17,10 +16,11 @@ from textual import events, on, work
17
16
  from textual.app import ComposeResult
18
17
  from textual.command import CommandPalette
19
18
  from textual.containers import Container, Grid
19
+ from textual.keys import Keys
20
20
  from textual.reactive import reactive
21
21
  from textual.screen import ModalScreen, Screen
22
22
  from textual.widget import Widget
23
- from textual.widgets import Button, DirectoryTree, Input, Label, Markdown, Static
23
+ from textual.widgets import Button, Label, Markdown, Static
24
24
 
25
25
  from shotgun.agents.agent_manager import (
26
26
  AgentManager,
@@ -44,10 +44,11 @@ from shotgun.codebase.core.manager import CodebaseAlreadyIndexedError
44
44
  from shotgun.posthog_telemetry import track_event
45
45
  from shotgun.sdk.codebase import CodebaseSDK
46
46
  from shotgun.sdk.exceptions import CodebaseNotFoundError, InvalidPathError
47
- from shotgun.sdk.services import get_codebase_service
48
47
  from shotgun.tui.commands import CommandHandler
48
+ from shotgun.tui.filtered_codebase_service import FilteredCodebaseService
49
49
  from shotgun.tui.screens.chat_screen.hint_message import HintMessage
50
50
  from shotgun.tui.screens.chat_screen.history import ChatHistory
51
+ from shotgun.utils import get_shotgun_home
51
52
 
52
53
  from ..components.prompt_input import PromptInput
53
54
  from ..components.spinner import Spinner
@@ -167,11 +168,6 @@ class ModeIndicator(Widget):
167
168
  return f"[bold $text-accent]{mode_title}{status_icon} mode[/][$foreground-muted] ({description})[/]"
168
169
 
169
170
 
170
- class FilteredDirectoryTree(DirectoryTree):
171
- def filter_paths(self, paths: Iterable[Path]) -> Iterable[Path]:
172
- return [path for path in paths if path.is_dir()]
173
-
174
-
175
171
  class CodebaseIndexPromptScreen(ModalScreen[bool]):
176
172
  """Modal dialog asking whether to index the detected codebase."""
177
173
 
@@ -226,105 +222,6 @@ class CodebaseIndexPromptScreen(ModalScreen[bool]):
226
222
  self.dismiss(True)
227
223
 
228
224
 
229
- class CodebaseIndexScreen(ModalScreen[CodebaseIndexSelection | None]):
230
- """Modal dialog for choosing a repository and name to index."""
231
-
232
- DEFAULT_CSS = """
233
- CodebaseIndexScreen {
234
- align: center middle;
235
- background: rgba(0, 0, 0, 0.0);
236
- }
237
- CodebaseIndexScreen > #index-dialog {
238
- width: 80%;
239
- max-width: 80;
240
- height: 80%;
241
- max-height: 40;
242
- border: wide $primary;
243
- padding: 1;
244
- layout: vertical;
245
- background: $surface;
246
- }
247
-
248
- #index-dialog DirectoryTree {
249
- height: 1fr;
250
- border: solid $accent;
251
- overflow: auto;
252
- }
253
-
254
- #index-dialog-controls {
255
- layout: horizontal;
256
- align-horizontal: right;
257
- padding-top: 1;
258
- }
259
- """
260
-
261
- def __init__(self, start_path: Path | None = None) -> None:
262
- super().__init__()
263
- self.start_path = Path(start_path or Path.cwd())
264
- self.selected_path: Path | None = self.start_path
265
-
266
- def compose(self) -> ComposeResult:
267
- with Container(id="index-dialog"):
268
- yield Label("Index a codebase", id="index-dialog-title")
269
- yield FilteredDirectoryTree(self.start_path, id="index-directory-tree")
270
- yield Input(
271
- placeholder="Enter a name for the codebase",
272
- id="index-codebase-name",
273
- )
274
- with Container(id="index-dialog-controls"):
275
- yield Button("Cancel", id="index-cancel")
276
- yield Button(
277
- "Index",
278
- id="index-confirm",
279
- variant="primary",
280
- disabled=True,
281
- )
282
-
283
- def on_mount(self) -> None:
284
- name_input = self.query_one("#index-codebase-name", Input)
285
- if not name_input.value and self.selected_path:
286
- name_input.value = self.selected_path.name
287
- self._update_confirm()
288
-
289
- def _update_confirm(self) -> None:
290
- confirm = self.query_one("#index-confirm", Button)
291
- name_input = self.query_one("#index-codebase-name", Input)
292
- confirm.disabled = not (self.selected_path and name_input.value.strip())
293
-
294
- @on(DirectoryTree.DirectorySelected, "#index-directory-tree")
295
- def handle_directory_selected(self, event: DirectoryTree.DirectorySelected) -> None:
296
- event.stop()
297
- selected = event.path if event.path.is_dir() else event.path.parent
298
- self.selected_path = selected
299
- name_input = self.query_one("#index-codebase-name", Input)
300
- if not name_input.value:
301
- name_input.value = selected.name
302
- self._update_confirm()
303
-
304
- @on(Input.Changed, "#index-codebase-name")
305
- def handle_name_changed(self, event: Input.Changed) -> None:
306
- event.stop()
307
- self._update_confirm()
308
-
309
- @on(Button.Pressed, "#index-cancel")
310
- def handle_cancel(self, event: Button.Pressed) -> None:
311
- event.stop()
312
- self.dismiss(None)
313
-
314
- @on(Button.Pressed, "#index-confirm")
315
- def handle_confirm(self, event: Button.Pressed) -> None:
316
- event.stop()
317
- name_input = self.query_one("#index-codebase-name", Input)
318
- if not self.selected_path:
319
- self.dismiss(None)
320
- return
321
- selection = CodebaseIndexSelection(
322
- repo_path=self.selected_path,
323
- name=name_input.value.strip(),
324
- )
325
- self.dismiss(selection)
326
-
327
-
328
225
  class ChatScreen(Screen[None]):
329
226
  CSS_PATH = "chat.tcss"
330
227
 
@@ -349,7 +246,9 @@ class ChatScreen(Screen[None]):
349
246
  super().__init__()
350
247
  # Get the model configuration and services
351
248
  model_config = get_provider_model()
352
- codebase_service = get_codebase_service()
249
+ # Use filtered service in TUI to restrict access to CWD codebase only
250
+ storage_dir = get_shotgun_home() / "codebases"
251
+ codebase_service = FilteredCodebaseService(storage_dir)
353
252
  self.codebase_sdk = CodebaseSDK()
354
253
 
355
254
  # Create shared deps without system_prompt_fn (agents provide their own)
@@ -387,13 +286,18 @@ class ChatScreen(Screen[None]):
387
286
 
388
287
  async def on_key(self, event: events.Key) -> None:
389
288
  """Handle key presses for cancellation."""
390
- # If escape is pressed while agent is working, cancel the operation
391
- if event.key == "escape" and self.working and self._current_worker:
392
- # Track ESC cancellation event
289
+ # If escape or ctrl+c is pressed while agent is working, cancel the operation
290
+ if (
291
+ event.key in (Keys.Escape, Keys.ControlC)
292
+ and self.working
293
+ and self._current_worker
294
+ ):
295
+ # Track cancellation event
393
296
  track_event(
394
- "agent_cancelled_escape",
297
+ "agent_cancelled",
395
298
  {
396
299
  "agent_mode": self.mode.value,
300
+ "cancel_key": event.key,
397
301
  },
398
302
  )
399
303
 
@@ -404,6 +308,8 @@ class ChatScreen(Screen[None]):
404
308
  # Re-enable the input
405
309
  prompt_input = self.query_one(PromptInput)
406
310
  prompt_input.focus()
311
+ # Prevent the event from propagating (don't quit the app)
312
+ event.stop()
407
313
 
408
314
  @work
409
315
  async def check_if_codebase_is_indexed(self) -> None:
@@ -423,6 +329,7 @@ class ChatScreen(Screen[None]):
423
329
  self.mount_hint(help_text_with_codebase(already_indexed=True))
424
330
  return
425
331
 
332
+ # Ask user if they want to index the current directory
426
333
  should_index = await self.app.push_screen_wait(CodebaseIndexPromptScreen())
427
334
  if not should_index:
428
335
  self.mount_hint(help_text_empty_dir())
@@ -430,7 +337,10 @@ class ChatScreen(Screen[None]):
430
337
 
431
338
  self.mount_hint(help_text_with_codebase(already_indexed=False))
432
339
 
433
- self.index_codebase_command()
340
+ # Auto-index the current directory with its name
341
+ cwd_name = cur_dir.name
342
+ selection = CodebaseIndexSelection(repo_path=cur_dir, name=cwd_name)
343
+ self.call_later(lambda: self.index_codebase(selection))
434
344
 
435
345
  def watch_mode(self, new_mode: AgentType) -> None:
436
346
  """React to mode changes by updating the agent manager."""
@@ -529,12 +439,16 @@ class ChatScreen(Screen[None]):
529
439
  @on(PartialResponseMessage)
530
440
  def handle_partial_response(self, event: PartialResponseMessage) -> None:
531
441
  self.partial_message = event.message
532
-
533
442
  history = self.query_one(ChatHistory)
534
- history.update_messages(
535
- self.messages + cast(list[ModelMessage | HintMessage], event.messages)
443
+
444
+ # Only update messages if the message list changed
445
+ new_message_list = self.messages + cast(
446
+ list[ModelMessage | HintMessage], event.messages
536
447
  )
448
+ if len(new_message_list) != len(history.items):
449
+ history.update_messages(new_message_list)
537
450
 
451
+ # Always update the partial response (reactive property handles the update)
538
452
  history.partial_response = self.partial_message
539
453
 
540
454
  def _clear_partial_response(self) -> None:
@@ -640,16 +554,11 @@ class ChatScreen(Screen[None]):
640
554
  return self.placeholder_hints.get_placeholder_for_mode(mode)
641
555
 
642
556
  def index_codebase_command(self) -> None:
643
- start_path = Path.cwd()
644
-
645
- def handle_result(result: CodebaseIndexSelection | None) -> None:
646
- if result:
647
- self.call_later(lambda: self.index_codebase(result))
648
-
649
- self.app.push_screen(
650
- CodebaseIndexScreen(start_path=start_path),
651
- handle_result,
652
- )
557
+ # Simplified: always index current working directory with its name
558
+ cur_dir = Path.cwd().resolve()
559
+ cwd_name = cur_dir.name
560
+ selection = CodebaseIndexSelection(repo_path=cur_dir, name=cwd_name)
561
+ self.call_later(lambda: self.index_codebase(selection))
653
562
 
654
563
  def delete_codebase_command(self) -> None:
655
564
  self.app.push_screen(
@@ -41,7 +41,6 @@ class PartialResponseWidget(Widget): # TODO: doesn't work lol
41
41
  self.item = item
42
42
 
43
43
  def compose(self) -> ComposeResult:
44
- yield Markdown(markdown="**partial response**")
45
44
  if self.item is None:
46
45
  pass
47
46
  elif self.item.kind == "response":
@@ -82,12 +81,14 @@ class ChatHistory(Widget):
82
81
  self.items: Sequence[ModelMessage | HintMessage] = []
83
82
  self.vertical_tail: VerticalTail | None = None
84
83
  self.partial_response = None
84
+ self._rendered_count = 0 # Track how many messages have been mounted
85
85
 
86
86
  def compose(self) -> ComposeResult:
87
87
  self.vertical_tail = VerticalTail()
88
88
 
89
+ filtered = list(self.filtered_items())
89
90
  with self.vertical_tail:
90
- for item in self.filtered_items():
91
+ for item in filtered:
91
92
  if isinstance(item, ModelRequest):
92
93
  yield UserQuestionWidget(item)
93
94
  elif isinstance(item, HintMessage):
@@ -97,7 +98,9 @@ class ChatHistory(Widget):
97
98
  yield PartialResponseWidget(self.partial_response).data_bind(
98
99
  item=ChatHistory.partial_response
99
100
  )
100
- self.call_later(self.autoscroll)
101
+
102
+ # Track how many messages were rendered during initial compose
103
+ self._rendered_count = len(filtered)
101
104
 
102
105
  def filtered_items(self) -> Generator[ModelMessage | HintMessage, None, None]:
103
106
  for idx, next_item in enumerate(self.items):
@@ -138,17 +141,31 @@ class ChatHistory(Widget):
138
141
  yield next_item
139
142
 
140
143
  def update_messages(self, messages: list[ModelMessage | HintMessage]) -> None:
141
- """Update the displayed messages without recomposing."""
144
+ """Update the displayed messages using incremental mounting."""
142
145
  if not self.vertical_tail:
143
146
  return
144
147
 
145
148
  self.items = messages
146
- self.refresh(recompose=True)
147
- self.call_later(self.autoscroll)
149
+ filtered = list(self.filtered_items())
150
+
151
+ # Only mount new messages that haven't been rendered yet
152
+ if len(filtered) > self._rendered_count:
153
+ new_messages = filtered[self._rendered_count :]
154
+ for item in new_messages:
155
+ widget: Widget
156
+ if isinstance(item, ModelRequest):
157
+ widget = UserQuestionWidget(item)
158
+ elif isinstance(item, HintMessage):
159
+ widget = HintMessageWidget(item)
160
+ elif isinstance(item, ModelResponse):
161
+ widget = AgentResponseWidget(item)
162
+ else:
163
+ continue
164
+
165
+ # Mount before the PartialResponseWidget
166
+ self.vertical_tail.mount(widget, before=self.vertical_tail.children[-1])
148
167
 
149
- def autoscroll(self) -> None:
150
- if self.vertical_tail:
151
- self.vertical_tail.scroll_end(animate=False, immediate=False, force=True)
168
+ self._rendered_count = len(filtered)
152
169
 
153
170
 
154
171
  class UserQuestionWidget(Widget):
@@ -221,25 +238,97 @@ class AgentResponseWidget(Widget):
221
238
  continue
222
239
  return acc.strip()
223
240
 
241
+ def _truncate(self, text: str, max_length: int = 100) -> str:
242
+ """Truncate text to max_length characters, adding ellipsis if needed."""
243
+ if len(text) <= max_length:
244
+ return text
245
+ return text[: max_length - 3] + "..."
246
+
247
+ def _parse_args(self, args: dict[str, object] | str | None) -> dict[str, object]:
248
+ """Parse tool call arguments, handling both dict and JSON string formats."""
249
+ if args is None:
250
+ return {}
251
+ if isinstance(args, str):
252
+ try:
253
+ return json.loads(args) if args.strip() else {}
254
+ except json.JSONDecodeError:
255
+ return {}
256
+ return args if isinstance(args, dict) else {}
257
+
224
258
  def _format_tool_call_part(self, part: ToolCallPart) -> str:
225
259
  if part.tool_name == "ask_user":
226
260
  return self._format_ask_user_part(part)
261
+
262
+ # Parse args once (handles both JSON string and dict)
263
+ args = self._parse_args(part.args)
264
+
265
+ # Codebase tools - show friendly names
266
+ if part.tool_name == "query_graph":
267
+ if "query" in args:
268
+ query = self._truncate(str(args["query"]))
269
+ return f'Querying code: "{query}"'
270
+ return "Querying code"
271
+
272
+ if part.tool_name == "retrieve_code":
273
+ if "qualified_name" in args:
274
+ return f'Retrieving code: "{args["qualified_name"]}"'
275
+ return "Retrieving code"
276
+
277
+ if part.tool_name == "file_read":
278
+ if "file_path" in args:
279
+ return f'Reading file: "{args["file_path"]}"'
280
+ return "Reading file"
281
+
282
+ if part.tool_name == "directory_lister":
283
+ if "directory" in args:
284
+ return f'Listing directory: "{args["directory"]}"'
285
+ return "Listing directory"
286
+
287
+ if part.tool_name == "codebase_shell":
288
+ command = args.get("command", "")
289
+ cmd_args = args.get("args", [])
290
+ # Handle cmd_args as list of strings
291
+ if isinstance(cmd_args, list):
292
+ args_str = " ".join(str(arg) for arg in cmd_args)
293
+ else:
294
+ args_str = ""
295
+ full_cmd = f"{command} {args_str}".strip()
296
+ if full_cmd:
297
+ return f'Running shell: "{self._truncate(full_cmd)}"'
298
+ return "Running shell"
299
+
300
+ # File management tools
301
+ if part.tool_name == "read_file":
302
+ if "filename" in args:
303
+ return f'Reading file: "{args["filename"]}"'
304
+ return "Reading file"
305
+
306
+ # Web search tools
307
+ if part.tool_name in [
308
+ "openai_web_search_tool",
309
+ "anthropic_web_search_tool",
310
+ "gemini_web_search_tool",
311
+ ]:
312
+ if "query" in args:
313
+ query = self._truncate(str(args["query"]))
314
+ return f'Searching web: "{query}"'
315
+ return "Searching web"
316
+
227
317
  # write_file
228
318
  if part.tool_name == "write_file" or part.tool_name == "append_file":
229
- if isinstance(part.args, dict) and "filename" in part.args:
230
- return f"{part.tool_name}({part.args['filename']})"
231
- else:
232
- return f"{part.tool_name}()"
319
+ if "filename" in args:
320
+ return f"{part.tool_name}({args['filename']})"
321
+ return f"{part.tool_name}()"
322
+
233
323
  if part.tool_name == "write_artifact_section":
234
- if isinstance(part.args, dict) and "section_title" in part.args:
235
- return f"{part.tool_name}({part.args['section_title']})"
236
- else:
237
- return f"{part.tool_name}()"
324
+ if "section_title" in args:
325
+ return f"{part.tool_name}({args['section_title']})"
326
+ return f"{part.tool_name}()"
327
+
238
328
  if part.tool_name == "create_artifact":
239
- if isinstance(part.args, dict) and "name" in part.args:
240
- return f"{part.tool_name}({part.args['name']})"
241
- else:
242
- return f"▪ {part.tool_name}()"
329
+ if "name" in args:
330
+ return f"{part.tool_name}({args['name']})"
331
+ return f"▪ {part.tool_name}()"
243
332
 
244
333
  return f"{part.tool_name}({part.args})"
245
334
 
File without changes
File without changes
File without changes