shotgun-sh 0.4.0.dev1__py3-none-any.whl → 0.6.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. shotgun/agents/agent_manager.py +307 -8
  2. shotgun/agents/cancellation.py +103 -0
  3. shotgun/agents/common.py +12 -0
  4. shotgun/agents/config/README.md +0 -1
  5. shotgun/agents/config/manager.py +10 -7
  6. shotgun/agents/config/models.py +5 -27
  7. shotgun/agents/config/provider.py +44 -27
  8. shotgun/agents/conversation/history/token_counting/base.py +51 -9
  9. shotgun/agents/file_read.py +176 -0
  10. shotgun/agents/messages.py +15 -3
  11. shotgun/agents/models.py +24 -1
  12. shotgun/agents/router/models.py +8 -0
  13. shotgun/agents/router/tools/delegation_tools.py +55 -1
  14. shotgun/agents/router/tools/plan_tools.py +88 -7
  15. shotgun/agents/runner.py +17 -2
  16. shotgun/agents/tools/__init__.py +8 -0
  17. shotgun/agents/tools/codebase/directory_lister.py +27 -39
  18. shotgun/agents/tools/codebase/file_read.py +26 -35
  19. shotgun/agents/tools/codebase/query_graph.py +9 -0
  20. shotgun/agents/tools/codebase/retrieve_code.py +9 -0
  21. shotgun/agents/tools/file_management.py +32 -2
  22. shotgun/agents/tools/file_read_tools/__init__.py +7 -0
  23. shotgun/agents/tools/file_read_tools/multimodal_file_read.py +167 -0
  24. shotgun/agents/tools/markdown_tools/__init__.py +62 -0
  25. shotgun/agents/tools/markdown_tools/insert_section.py +148 -0
  26. shotgun/agents/tools/markdown_tools/models.py +86 -0
  27. shotgun/agents/tools/markdown_tools/remove_section.py +114 -0
  28. shotgun/agents/tools/markdown_tools/replace_section.py +119 -0
  29. shotgun/agents/tools/markdown_tools/utils.py +453 -0
  30. shotgun/agents/tools/registry.py +44 -6
  31. shotgun/agents/tools/web_search/openai.py +42 -23
  32. shotgun/attachments/__init__.py +41 -0
  33. shotgun/attachments/errors.py +60 -0
  34. shotgun/attachments/models.py +107 -0
  35. shotgun/attachments/parser.py +257 -0
  36. shotgun/attachments/processor.py +193 -0
  37. shotgun/build_constants.py +4 -7
  38. shotgun/cli/clear.py +2 -2
  39. shotgun/cli/codebase/commands.py +181 -65
  40. shotgun/cli/compact.py +2 -2
  41. shotgun/cli/context.py +2 -2
  42. shotgun/cli/error_handler.py +2 -2
  43. shotgun/cli/run.py +90 -0
  44. shotgun/cli/spec/backup.py +2 -1
  45. shotgun/codebase/__init__.py +2 -0
  46. shotgun/codebase/benchmarks/__init__.py +35 -0
  47. shotgun/codebase/benchmarks/benchmark_runner.py +309 -0
  48. shotgun/codebase/benchmarks/exporters.py +119 -0
  49. shotgun/codebase/benchmarks/formatters/__init__.py +49 -0
  50. shotgun/codebase/benchmarks/formatters/base.py +34 -0
  51. shotgun/codebase/benchmarks/formatters/json_formatter.py +106 -0
  52. shotgun/codebase/benchmarks/formatters/markdown.py +136 -0
  53. shotgun/codebase/benchmarks/models.py +129 -0
  54. shotgun/codebase/core/__init__.py +4 -0
  55. shotgun/codebase/core/call_resolution.py +91 -0
  56. shotgun/codebase/core/change_detector.py +11 -6
  57. shotgun/codebase/core/errors.py +159 -0
  58. shotgun/codebase/core/extractors/__init__.py +23 -0
  59. shotgun/codebase/core/extractors/base.py +138 -0
  60. shotgun/codebase/core/extractors/factory.py +63 -0
  61. shotgun/codebase/core/extractors/go/__init__.py +7 -0
  62. shotgun/codebase/core/extractors/go/extractor.py +122 -0
  63. shotgun/codebase/core/extractors/javascript/__init__.py +7 -0
  64. shotgun/codebase/core/extractors/javascript/extractor.py +132 -0
  65. shotgun/codebase/core/extractors/protocol.py +109 -0
  66. shotgun/codebase/core/extractors/python/__init__.py +7 -0
  67. shotgun/codebase/core/extractors/python/extractor.py +141 -0
  68. shotgun/codebase/core/extractors/rust/__init__.py +7 -0
  69. shotgun/codebase/core/extractors/rust/extractor.py +139 -0
  70. shotgun/codebase/core/extractors/types.py +15 -0
  71. shotgun/codebase/core/extractors/typescript/__init__.py +7 -0
  72. shotgun/codebase/core/extractors/typescript/extractor.py +92 -0
  73. shotgun/codebase/core/gitignore.py +252 -0
  74. shotgun/codebase/core/ingestor.py +644 -354
  75. shotgun/codebase/core/kuzu_compat.py +119 -0
  76. shotgun/codebase/core/language_config.py +239 -0
  77. shotgun/codebase/core/manager.py +256 -46
  78. shotgun/codebase/core/metrics_collector.py +310 -0
  79. shotgun/codebase/core/metrics_types.py +347 -0
  80. shotgun/codebase/core/parallel_executor.py +424 -0
  81. shotgun/codebase/core/work_distributor.py +254 -0
  82. shotgun/codebase/core/worker.py +768 -0
  83. shotgun/codebase/indexing_state.py +86 -0
  84. shotgun/codebase/models.py +94 -0
  85. shotgun/codebase/service.py +13 -0
  86. shotgun/exceptions.py +9 -9
  87. shotgun/main.py +3 -16
  88. shotgun/posthog_telemetry.py +165 -24
  89. shotgun/prompts/agents/file_read.j2 +48 -0
  90. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +19 -47
  91. shotgun/prompts/agents/partials/content_formatting.j2 +12 -33
  92. shotgun/prompts/agents/partials/interactive_mode.j2 +9 -32
  93. shotgun/prompts/agents/partials/router_delegation_mode.j2 +21 -22
  94. shotgun/prompts/agents/plan.j2 +14 -0
  95. shotgun/prompts/agents/router.j2 +531 -258
  96. shotgun/prompts/agents/specify.j2 +14 -0
  97. shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +14 -1
  98. shotgun/prompts/agents/state/system_state.j2 +13 -11
  99. shotgun/prompts/agents/tasks.j2 +14 -0
  100. shotgun/settings.py +49 -10
  101. shotgun/tui/app.py +149 -18
  102. shotgun/tui/commands/__init__.py +9 -1
  103. shotgun/tui/components/attachment_bar.py +87 -0
  104. shotgun/tui/components/prompt_input.py +25 -28
  105. shotgun/tui/components/status_bar.py +14 -7
  106. shotgun/tui/dependencies.py +3 -8
  107. shotgun/tui/protocols.py +18 -0
  108. shotgun/tui/screens/chat/chat.tcss +15 -0
  109. shotgun/tui/screens/chat/chat_screen.py +766 -235
  110. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +8 -4
  111. shotgun/tui/screens/chat_screen/attachment_hint.py +40 -0
  112. shotgun/tui/screens/chat_screen/command_providers.py +0 -10
  113. shotgun/tui/screens/chat_screen/history/chat_history.py +54 -14
  114. shotgun/tui/screens/chat_screen/history/formatters.py +22 -0
  115. shotgun/tui/screens/chat_screen/history/user_question.py +25 -3
  116. shotgun/tui/screens/database_locked_dialog.py +219 -0
  117. shotgun/tui/screens/database_timeout_dialog.py +158 -0
  118. shotgun/tui/screens/kuzu_error_dialog.py +135 -0
  119. shotgun/tui/screens/model_picker.py +1 -3
  120. shotgun/tui/screens/models.py +11 -0
  121. shotgun/tui/state/processing_state.py +19 -0
  122. shotgun/tui/widgets/widget_coordinator.py +18 -0
  123. shotgun/utils/file_system_utils.py +4 -1
  124. {shotgun_sh-0.4.0.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/METADATA +87 -34
  125. {shotgun_sh-0.4.0.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/RECORD +128 -79
  126. shotgun/cli/export.py +0 -81
  127. shotgun/cli/plan.py +0 -73
  128. shotgun/cli/research.py +0 -93
  129. shotgun/cli/specify.py +0 -70
  130. shotgun/cli/tasks.py +0 -78
  131. shotgun/sentry_telemetry.py +0 -232
  132. shotgun/tui/screens/onboarding.py +0 -584
  133. {shotgun_sh-0.4.0.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/WHEEL +0 -0
  134. {shotgun_sh-0.4.0.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/entry_points.txt +0 -0
  135. {shotgun_sh-0.4.0.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/licenses/LICENSE +0 -0
@@ -38,6 +38,20 @@ Before I expand it, I have a few questions:
38
38
 
39
39
  {% include 'agents/partials/router_delegation_mode.j2' %}
40
40
 
41
+ ## CRITICAL: YOUR OUTPUT IS THE FILE
42
+
43
+ Your deliverable is specification.md - content must be saved to the file, not just output to chat.
44
+
45
+ For updates, prefer markdown tools (faster, cheaper, less error-prone):
46
+ - replace_markdown_section - update a specific section
47
+ - insert_markdown_section - add a new section
48
+ - remove_markdown_section - remove a section
49
+
50
+ Only use write_file when creating the file from scratch or doing major restructuring.
51
+
52
+ FAILURE: Rewriting the entire file when user asked to update one section
53
+ SUCCESS: Using markdown tools for targeted updates
54
+
41
55
  ## YOUR SCOPE
42
56
 
43
57
  You are the **Specification agent**. Your files are `specification.md` and `.shotgun/contracts/*` - these are the ONLY files you can write to.
@@ -5,12 +5,25 @@
5
5
  You have access to the following codebase graphs:
6
6
 
7
7
  {% for graph in codebase_understanding_graphs -%}
8
+ {% if indexing_graph_ids and graph.graph_id in indexing_graph_ids -%}
9
+ - {{ graph.name }} ID: {{ graph.graph_id }} Path: {{ graph.repo_path }} **[INDEXING - NOT AVAILABLE]**
10
+ {% else -%}
8
11
  - {{ graph.name }} ID: {{ graph.graph_id }} Path: {{ graph.repo_path }}
12
+ {% endif -%}
9
13
  {% endfor -%}
10
14
 
15
+ {% if indexing_graph_ids -%}
16
+
17
+ Note: Graphs marked [INDEXING - NOT AVAILABLE] are currently being built. Do not attempt to query these graphs until indexing is complete.
18
+ {% endif -%}
19
+
11
20
  {% else -%}
12
21
 
13
- {% if is_tui_context -%}
22
+ {% if indexing_graph_ids -%}
23
+ A codebase is currently being indexed. This process can take a few minutes for large codebases. Once indexing completes, you will be able to query the code structure and answer questions about it.
24
+
25
+ Please ask the user to wait for indexing to finish before asking questions about the codebase.
26
+ {% elif is_tui_context -%}
14
27
  No codebase has been indexed yet. To enable code analysis, please tell the user to restart the TUI and follow the prompt to 'Index this codebase?' when it appears.
15
28
  {% else -%}
16
29
  No codebase has been indexed yet. If the user needs code analysis, ask them to index a codebase first.
@@ -1,24 +1,25 @@
1
- ## System Status
2
-
1
+ <SYSTEM_STATUS>
3
2
  Your training data may be old. The current date and time is: {{ current_datetime }} in {{ timezone_name }} (UTC{{ utc_offset }})
3
+ </SYSTEM_STATUS>
4
4
 
5
5
  {% include 'agents/state/codebase/codebase_graphs_available.j2' %}
6
6
 
7
7
  {% if execution_plan %}
8
- ## Current Execution Plan
9
-
8
+ <EXECUTION_PLAN>
10
9
  {{ execution_plan }}
10
+ </EXECUTION_PLAN>
11
11
 
12
12
  {% if pending_approval %}
13
- **⚠️ PLAN AWAITING USER APPROVAL**
14
-
13
+ <PLAN_RULES>
14
+ The current plan is pending approval for the user.
15
15
  The plan above requires user approval before execution can begin.
16
16
  You MUST call `final_result` now to present this plan to the user.
17
17
  Do NOT attempt to delegate to any sub-agents until the user approves.
18
+ </PLAN_RULES>
18
19
  {% endif %}
19
20
 
20
21
  {% endif %}
21
- ## Available Files
22
+ <AVAILABLE_FILES>
22
23
 
23
24
  {% if existing_files %}
24
25
  The following files already exist.
@@ -32,11 +33,12 @@ No research or planning documents exist yet. Refer to your agent-specific instru
32
33
  {% endif %}
33
34
 
34
35
  {% if markdown_toc %}
35
- ## Document Table of Contents - READ THE ENTIRE FILE TO UNDERSTAND MORE
36
-
36
+ <TABLE_OF_CONTENTS note="READ THE ENTIRE FILE TO UNDERSTAND MORE">
37
37
  {{ markdown_toc }}
38
+ </TABLE_OF_CONTENTS>
38
39
 
39
- **IMPORTANT**: The above shows ONLY the Table of Contents from prior stages in the pipeline. Review this context before asking questions or creating new content.
40
+ It is imporant that TABLE_OF_CONTENTS shows ONLY the Table of Contents from prior stages in the pipeline. You must review this context before asking questions or creating new content.
40
41
  {% else %}
41
42
  Review the existing documents above before adding new content to avoid duplication.
42
- {% endif %}
43
+ {% endif %}
44
+ </AVAILABLE_FILES>
@@ -28,6 +28,20 @@ Instead:
28
28
 
29
29
  {% include 'agents/partials/router_delegation_mode.j2' %}
30
30
 
31
+ ## CRITICAL: YOUR OUTPUT IS THE FILE
32
+
33
+ Your deliverable is tasks.md - content must be saved to the file, not just output to chat.
34
+
35
+ For updates, prefer markdown tools (faster, cheaper, less error-prone):
36
+ - replace_markdown_section - update a specific stage's tasks
37
+ - insert_markdown_section - add a new stage of tasks
38
+ - remove_markdown_section - remove a stage
39
+
40
+ Only use write_file when creating the file from scratch or doing major restructuring.
41
+
42
+ FAILURE: Rewriting the entire file when user asked to update one stage
43
+ SUCCESS: Using markdown tools for targeted updates
44
+
31
45
  ## YOUR SCOPE
32
46
 
33
47
  You are the **Tasks agent**. Your file is `tasks.md` - this is the ONLY file you can write to.
shotgun/settings.py CHANGED
@@ -10,8 +10,8 @@ Example usage:
10
10
  from shotgun.settings import settings
11
11
 
12
12
  # Access telemetry settings
13
- if settings.telemetry.sentry_dsn:
14
- sentry_sdk.init(dsn=settings.telemetry.sentry_dsn)
13
+ if settings.telemetry.posthog_api_key:
14
+ posthog.init(api_key=settings.telemetry.posthog_api_key)
15
15
 
16
16
  # Access logging settings
17
17
  logger.setLevel(settings.logging.log_level)
@@ -30,7 +30,7 @@ def _get_build_constant(name: str, default: Any = None) -> Any:
30
30
  """Get a value from build_constants.py, falling back to default.
31
31
 
32
32
  Args:
33
- name: The constant name to retrieve (e.g., "SENTRY_DSN")
33
+ name: The constant name to retrieve (e.g., "POSTHOG_API_KEY")
34
34
  default: Default value if constant not found
35
35
 
36
36
  Returns:
@@ -47,14 +47,10 @@ def _get_build_constant(name: str, default: Any = None) -> Any:
47
47
  class TelemetrySettings(BaseSettings):
48
48
  """Telemetry and observability settings.
49
49
 
50
- These settings control error tracking (Sentry), analytics (PostHog),
51
- and observability (Logfire) integrations.
50
+ These settings control analytics (PostHog) and observability (Logfire)
51
+ integrations. PostHog handles both analytics and exception tracking.
52
52
  """
53
53
 
54
- sentry_dsn: str = Field(
55
- default_factory=lambda: _get_build_constant("SENTRY_DSN", ""),
56
- description="Sentry DSN for error tracking",
57
- )
58
54
  posthog_api_key: str = Field(
59
55
  default_factory=lambda: _get_build_constant("POSTHOG_API_KEY", ""),
60
56
  description="PostHog API key for analytics",
@@ -198,6 +194,45 @@ class DevelopmentSettings(BaseSettings):
198
194
  return bool(v)
199
195
 
200
196
 
197
+ class IndexingSettings(BaseSettings):
198
+ """Codebase indexing settings.
199
+
200
+ Controls parallel processing behavior for code indexing.
201
+ """
202
+
203
+ index_parallel: bool = Field(
204
+ default=True,
205
+ description="Enable parallel indexing (requires 4+ CPU cores)",
206
+ )
207
+ index_workers: int | None = Field(
208
+ default=None,
209
+ description="Number of worker processes for parallel indexing (default: CPU count - 1)",
210
+ ge=1,
211
+ )
212
+ index_batch_size: int | None = Field(
213
+ default=None,
214
+ description="Files per batch for parallel indexing (default: auto-calculated)",
215
+ ge=1,
216
+ )
217
+
218
+ model_config = SettingsConfigDict(
219
+ env_prefix="SHOTGUN_",
220
+ env_file=".env",
221
+ env_file_encoding="utf-8",
222
+ extra="ignore",
223
+ )
224
+
225
+ @field_validator("index_parallel", mode="before")
226
+ @classmethod
227
+ def parse_bool(cls, v: Any) -> bool:
228
+ """Parse boolean values from strings."""
229
+ if isinstance(v, bool):
230
+ return v
231
+ if isinstance(v, str):
232
+ return v.lower() in ("true", "1", "yes")
233
+ return bool(v)
234
+
235
+
201
236
  class Settings(BaseSettings):
202
237
  """Main application settings with SHOTGUN_ prefix.
203
238
 
@@ -208,7 +243,6 @@ class Settings(BaseSettings):
208
243
  from shotgun.settings import settings
209
244
 
210
245
  # Telemetry settings
211
- settings.telemetry.sentry_dsn
212
246
  settings.telemetry.posthog_api_key
213
247
  settings.telemetry.logfire_enabled
214
248
 
@@ -223,12 +257,17 @@ class Settings(BaseSettings):
223
257
  # Development settings
224
258
  settings.dev.home
225
259
  settings.dev.pipx_simulate
260
+
261
+ # Indexing settings
262
+ settings.indexing.index_parallel
263
+ settings.indexing.index_workers
226
264
  """
227
265
 
228
266
  telemetry: TelemetrySettings = Field(default_factory=TelemetrySettings)
229
267
  logging: LoggingSettings = Field(default_factory=LoggingSettings)
230
268
  api: ApiSettings = Field(default_factory=ApiSettings)
231
269
  dev: DevelopmentSettings = Field(default_factory=DevelopmentSettings)
270
+ indexing: IndexingSettings = Field(default_factory=IndexingSettings)
232
271
 
233
272
  model_config = SettingsConfigDict(
234
273
  env_prefix="SHOTGUN_",
shotgun/tui/app.py CHANGED
@@ -1,5 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import platform
5
+ import sys
1
6
  from collections.abc import Iterable
2
- from typing import Any
7
+ from typing import TYPE_CHECKING, Any
8
+
9
+ if TYPE_CHECKING:
10
+ from shotgun.codebase.core.errors import DatabaseIssue
3
11
 
4
12
  from textual.app import App, SystemCommand
5
13
  from textual.binding import Binding
@@ -45,7 +53,10 @@ class ShotgunApp(App[None]):
45
53
  "github_issue": GitHubIssueScreen,
46
54
  }
47
55
  BINDINGS = [
48
- Binding("ctrl+c", "quit", "Quit the app"),
56
+ # Use smart_quit to support ctrl+c for copying selected text
57
+ Binding("ctrl+c", "smart_quit", "Quit/Copy", show=False),
58
+ # Cancel quit confirmation with ESC
59
+ Binding("escape", "cancel_quit", "Cancel Quit", show=False),
49
60
  ]
50
61
 
51
62
  CSS_PATH = "styles.tcss"
@@ -57,6 +68,7 @@ class ShotgunApp(App[None]):
57
68
  force_reindex: bool = False,
58
69
  show_pull_hint: bool = False,
59
70
  pull_version_id: str | None = None,
71
+ pending_db_issues: list[DatabaseIssue] | None = None,
60
72
  ) -> None:
61
73
  super().__init__()
62
74
  self.config_manager: ConfigManager = get_config_manager()
@@ -65,6 +77,12 @@ class ShotgunApp(App[None]):
65
77
  self.force_reindex = force_reindex
66
78
  self.show_pull_hint = show_pull_hint
67
79
  self.pull_version_id = pull_version_id
80
+ # Database issues detected at startup (locked, corrupted, timeout)
81
+ # These will be shown to the user via dialogs when ChatScreen mounts
82
+ self.pending_db_issues = pending_db_issues or []
83
+
84
+ # Quit confirmation state for double Ctrl+C to quit
85
+ self._quit_pending = False
68
86
 
69
87
  # Initialize dependency injection container
70
88
  self.container = TUIContainer()
@@ -227,6 +245,63 @@ class ShotgunApp(App[None]):
227
245
  # Continue to ChatScreen
228
246
  self.refresh_startup_screen()
229
247
 
248
+ @property
249
+ def quit_pending(self) -> bool:
250
+ """Whether a quit confirmation is pending.
251
+
252
+ Returns True if user pressed Ctrl+C and needs to press again or ESC to cancel.
253
+ """
254
+ return self._quit_pending
255
+
256
+ def _reset_quit_pending(self) -> None:
257
+ """Reset the quit confirmation state and refresh the status bar."""
258
+ self._quit_pending = False
259
+ self._refresh_status_bar()
260
+
261
+ def _refresh_status_bar(self) -> None:
262
+ """Refresh the StatusBar widget to reflect current state."""
263
+ from textual.css.query import NoMatches
264
+
265
+ from shotgun.tui.components.status_bar import StatusBar
266
+
267
+ try:
268
+ status_bar = self.screen.query_one(StatusBar)
269
+ status_bar.refresh()
270
+ except NoMatches:
271
+ # StatusBar might not exist on all screens
272
+ pass
273
+
274
+ def action_cancel_quit(self) -> None:
275
+ """Cancel the quit confirmation when ESC is pressed."""
276
+ if self._quit_pending:
277
+ self._reset_quit_pending()
278
+
279
+ async def action_smart_quit(self) -> None:
280
+ """Handle ctrl+c: copy selected text if any, otherwise quit.
281
+
282
+ This allows users to select text in the TUI and copy it with ctrl+c,
283
+ while still supporting ctrl+c to quit when no text is selected.
284
+ Requires pressing Ctrl+C twice to quit, or ESC to cancel.
285
+ """
286
+ # Check if there's selected text on the current screen
287
+ selected_text = self.screen.get_selected_text()
288
+ if selected_text:
289
+ # Copy selected text to clipboard
290
+ self.copy_to_clipboard(selected_text)
291
+ # Clear the selection after copying
292
+ self.screen.clear_selection()
293
+ self.notify("Copied to clipboard", timeout=2)
294
+ return
295
+
296
+ # No selection - check if quit is already pending
297
+ if self._quit_pending:
298
+ await self.action_quit()
299
+ return
300
+
301
+ # Start quit confirmation
302
+ self._quit_pending = True
303
+ self._refresh_status_bar()
304
+
230
305
  async def action_quit(self) -> None:
231
306
  """Quit the application."""
232
307
  # Shut down PostHog client to prevent threading errors
@@ -249,6 +324,22 @@ class ShotgunApp(App[None]):
249
324
  self.push_screen(GitHubIssueScreen())
250
325
 
251
326
 
327
+ def _log_startup_info() -> None:
328
+ """Log startup information for debugging purposes."""
329
+ # Import here to avoid circular import (shotgun.__init__ imports from submodules)
330
+ from shotgun import __version__
331
+
332
+ logger.info("=" * 60)
333
+ logger.info("Shotgun TUI Starting")
334
+ logger.info("=" * 60)
335
+ logger.info(f" Version: {__version__}")
336
+ logger.info(f" Python: {sys.version.split()[0]}")
337
+ logger.info(f" Platform: {platform.system()} {platform.release()}")
338
+ logger.info(f" Architecture: {platform.machine()}")
339
+ logger.info(f" Working Directory: {os.getcwd()}")
340
+ logger.info("=" * 60)
341
+
342
+
252
343
  def run(
253
344
  no_update_check: bool = False,
254
345
  continue_session: bool = False,
@@ -265,24 +356,54 @@ def run(
265
356
  show_pull_hint: If True, show hint about recently pulled spec.
266
357
  pull_version_id: If provided, pull this spec version before showing ChatScreen.
267
358
  """
268
- # Clean up any corrupted databases BEFORE starting the TUI
269
- # This prevents crashes from corrupted databases during initialization
359
+ # Log startup information
360
+ _log_startup_info()
361
+
362
+ # Detect database issues BEFORE starting the TUI (but don't auto-delete)
363
+ # Issues will be presented to the user via dialogs once the TUI is running
270
364
  import asyncio
271
365
 
366
+ from shotgun.codebase.core.errors import KuzuErrorType
272
367
  from shotgun.codebase.core.manager import CodebaseGraphManager
273
368
  from shotgun.utils import get_shotgun_home
274
369
 
275
370
  storage_dir = get_shotgun_home() / "codebases"
276
371
  manager = CodebaseGraphManager(storage_dir)
277
372
 
373
+ pending_db_issues: list[DatabaseIssue] = []
278
374
  try:
279
- removed = asyncio.run(manager.cleanup_corrupted_databases())
280
- if removed:
281
- logger.info(
282
- f"Cleaned up {len(removed)} corrupted database(s) before TUI startup"
283
- )
375
+ # First pass: 10-second timeout
376
+ issues = asyncio.run(manager.detect_database_issues(timeout_seconds=10.0))
377
+ if issues:
378
+ # Categorize issues for logging
379
+ for issue in issues:
380
+ logger.info(
381
+ f"Detected database issue: {issue.graph_id} - "
382
+ f"{issue.error_type.value}: {issue.message}"
383
+ )
384
+
385
+ # Only pass issues that require user interaction to the TUI
386
+ # Schema issues (incomplete builds) can be auto-cleaned silently
387
+ user_facing_issues = [
388
+ i
389
+ for i in issues
390
+ if i.error_type
391
+ in (
392
+ KuzuErrorType.LOCKED,
393
+ KuzuErrorType.CORRUPTION,
394
+ KuzuErrorType.TIMEOUT,
395
+ )
396
+ ]
397
+
398
+ # Auto-delete schema issues (incomplete builds) - safe to remove
399
+ schema_issues = [i for i in issues if i.error_type == KuzuErrorType.SCHEMA]
400
+ for issue in schema_issues:
401
+ asyncio.run(manager.delete_database(issue.graph_id))
402
+ logger.info(f"Auto-removed incomplete database: {issue.graph_id}")
403
+
404
+ pending_db_issues = user_facing_issues
284
405
  except Exception as e:
285
- logger.error(f"Failed to cleanup corrupted databases: {e}")
406
+ logger.error(f"Failed to detect database issues: {e}")
286
407
  # Continue anyway - the TUI can still function
287
408
 
288
409
  app = ShotgunApp(
@@ -291,6 +412,7 @@ def run(
291
412
  force_reindex=force_reindex,
292
413
  show_pull_hint=show_pull_hint,
293
414
  pull_version_id=pull_version_id,
415
+ pending_db_issues=pending_db_issues,
294
416
  )
295
417
  app.run(inline_no_clear=True)
296
418
 
@@ -313,12 +435,14 @@ def serve(
313
435
  continue_session: If True, continue from previous conversation.
314
436
  force_reindex: If True, force re-indexing of codebase (ignores existing index).
315
437
  """
316
- # Clean up any corrupted databases BEFORE starting the TUI
317
- # This prevents crashes from corrupted databases during initialization
438
+ # Detect database issues BEFORE starting the TUI
439
+ # Note: In serve mode, issues are logged but user interaction happens in
440
+ # the spawned process via run()
318
441
  import asyncio
319
442
 
320
443
  from textual_serve.server import Server
321
444
 
445
+ from shotgun.codebase.core.errors import KuzuErrorType
322
446
  from shotgun.codebase.core.manager import CodebaseGraphManager
323
447
  from shotgun.utils import get_shotgun_home
324
448
 
@@ -326,13 +450,20 @@ def serve(
326
450
  manager = CodebaseGraphManager(storage_dir)
327
451
 
328
452
  try:
329
- removed = asyncio.run(manager.cleanup_corrupted_databases())
330
- if removed:
331
- logger.info(
332
- f"Cleaned up {len(removed)} corrupted database(s) before TUI startup"
333
- )
453
+ issues = asyncio.run(manager.detect_database_issues(timeout_seconds=10.0))
454
+ if issues:
455
+ for issue in issues:
456
+ logger.info(
457
+ f"Detected database issue: {issue.graph_id} - "
458
+ f"{issue.error_type.value}: {issue.message}"
459
+ )
460
+ # Auto-delete only schema issues (incomplete builds)
461
+ schema_issues = [i for i in issues if i.error_type == KuzuErrorType.SCHEMA]
462
+ for issue in schema_issues:
463
+ asyncio.run(manager.delete_database(issue.graph_id))
464
+ logger.info(f"Auto-removed incomplete database: {issue.graph_id}")
334
465
  except Exception as e:
335
- logger.error(f"Failed to cleanup corrupted databases: {e}")
466
+ logger.error(f"Failed to detect database issues: {e}")
336
467
  # Continue anyway - the TUI can still function
337
468
 
338
469
  # Create a new event loop after asyncio.run() closes the previous one
@@ -54,10 +54,18 @@ class CommandHandler:
54
54
  **Commands:**
55
55
  • `/help` - Show this help message
56
56
 
57
+ **Shell Commands:**
58
+ • `!<command>` - Execute shell commands directly (e.g., `!ls`, `!git status`)
59
+ - Commands run in your current working directory
60
+ - Output is displayed in the chat (not sent to AI)
61
+ - Commands are NOT added to conversation history
62
+ - Leading whitespace is allowed: ` !echo hi` works
63
+ - Note: `!!` is treated as `!` (no history expansion in this version)
64
+
57
65
  **Keyboard Shortcuts:**
58
66
 
59
67
  * `Enter` - Send message
60
- * `Ctrl+P` - Open command palette (for usage, context, and other commands)
68
+ * `/` - Open command palette (for usage, context, and other commands)
61
69
  * `Shift+Tab` - Cycle agent modes
62
70
  * `Ctrl+C` - Quit application
63
71
 
@@ -0,0 +1,87 @@
1
+ """Attachment bar widget for showing pending file attachment."""
2
+
3
+ from textual.app import ComposeResult
4
+ from textual.css.query import NoMatches
5
+ from textual.reactive import reactive
6
+ from textual.widget import Widget
7
+ from textual.widgets import Static
8
+
9
+ from shotgun.attachments import (
10
+ AttachmentBarState,
11
+ FileAttachment,
12
+ format_file_size,
13
+ get_attachment_icon,
14
+ )
15
+
16
+
17
+ class AttachmentBar(Widget):
18
+ """Widget showing pending attachment above input.
19
+
20
+ Displays format: [icon filename.ext (size)]
21
+ Hidden when no attachment is pending.
22
+
23
+ Styles defined in chat.tcss.
24
+ """
25
+
26
+ state: reactive[AttachmentBarState] = reactive(AttachmentBarState, init=False)
27
+
28
+ def __init__(
29
+ self,
30
+ *,
31
+ name: str | None = None,
32
+ id: str | None = None,
33
+ classes: str | None = None,
34
+ ) -> None:
35
+ """Initialize the attachment bar.
36
+
37
+ Args:
38
+ name: Optional widget name.
39
+ id: Optional widget ID.
40
+ classes: Optional CSS classes.
41
+ """
42
+ super().__init__(name=name, id=id, classes=classes)
43
+ self.state = AttachmentBarState(attachment=None)
44
+ self.add_class("hidden")
45
+
46
+ def compose(self) -> ComposeResult:
47
+ """Compose the attachment bar widget."""
48
+ yield Static("", id="attachment-display")
49
+
50
+ def update_attachment(self, attachment: FileAttachment | None) -> None:
51
+ """Update the displayed attachment.
52
+
53
+ Args:
54
+ attachment: FileAttachment to display, or None to hide bar.
55
+ """
56
+ self.state = AttachmentBarState(attachment=attachment)
57
+
58
+ if attachment is None:
59
+ self.add_class("hidden")
60
+ else:
61
+ self.remove_class("hidden")
62
+ self._refresh_display()
63
+
64
+ def _refresh_display(self) -> None:
65
+ """Refresh the attachment display text."""
66
+ attachment = self.state.attachment
67
+ if attachment is None:
68
+ return
69
+
70
+ icon = get_attachment_icon(attachment.file_type)
71
+ size_str = format_file_size(attachment.file_size_bytes)
72
+ display_text = f"[{icon} {attachment.file_name} ({size_str})]"
73
+
74
+ try:
75
+ display_widget = self.query_one("#attachment-display", Static)
76
+ display_widget.update(display_text)
77
+ except NoMatches:
78
+ pass # Widget not mounted yet
79
+
80
+ def watch_state(self, new_state: AttachmentBarState) -> None:
81
+ """React to state changes.
82
+
83
+ Args:
84
+ new_state: The new attachment bar state.
85
+ """
86
+ if new_state.attachment is not None:
87
+ self._refresh_display()
@@ -27,43 +27,40 @@ class PromptInput(TextArea):
27
27
  super().__init__()
28
28
  self.text = text
29
29
 
30
+ class OpenCommandPalette(Message):
31
+ """Request to open the command palette."""
32
+
30
33
  def action_submit(self) -> None:
31
34
  """An action to submit the text."""
32
35
  self.post_message(self.Submitted(self.text))
33
36
 
34
- async def _on_key(self, event: events.Key) -> None:
35
- """Handle key presses which correspond to document inserts."""
36
-
37
- # Don't handle Enter key here - let the binding handle it
37
+ def on_key(self, event: events.Key) -> None:
38
+ """Handle key presses for special actions."""
39
+ # Submit on Enter
38
40
  if event.key == "enter":
41
+ event.stop()
42
+ event.prevent_default()
39
43
  self.action_submit()
40
-
41
- self._restart_blink()
42
-
43
- if self.read_only:
44
44
  return
45
45
 
46
- key = event.key
47
- insert_values = {
48
- "ctrl+j": "\n",
49
- }
50
- if self.tab_behavior == "indent":
51
- if key == "escape":
52
- event.stop()
53
- event.prevent_default()
54
- self.screen.focus_next()
55
- return
56
- if self.indent_type == "tabs":
57
- insert_values["tab"] = "\t"
58
- else:
59
- insert_values["tab"] = " " * self._find_columns_to_next_tab_stop()
46
+ # Detect "/" as first character to trigger command palette
47
+ if event.character == "/" and not self.text.strip():
48
+ event.stop()
49
+ event.prevent_default()
50
+ self.post_message(self.OpenCommandPalette())
51
+ return
60
52
 
61
- if event.is_printable or key in insert_values:
53
+ # Handle ctrl+j or shift+enter for newline (since enter is for submit)
54
+ # Note: shift+enter only works if terminal is configured to send escape sequence
55
+ # Common terminals: iTerm2, VS Code, WezTerm can be configured for this
56
+ if event.key in ("ctrl+j", "shift+enter"):
62
57
  event.stop()
63
58
  event.prevent_default()
64
- insert = insert_values.get(key, event.character)
65
- # `insert` is not None because event.character cannot be
66
- # None because we've checked that it's printable.
67
- assert insert is not None # noqa: S101
68
59
  start, end = self.selection
69
- self._replace_via_keyboard(insert, start, end)
60
+ self.replace(
61
+ "\n",
62
+ start,
63
+ end,
64
+ maintain_selection_offset=False,
65
+ )
66
+ return