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.
- shotgun/agents/agent_manager.py +307 -8
- shotgun/agents/cancellation.py +103 -0
- shotgun/agents/common.py +12 -0
- shotgun/agents/config/README.md +0 -1
- shotgun/agents/config/manager.py +10 -7
- shotgun/agents/config/models.py +5 -27
- shotgun/agents/config/provider.py +44 -27
- shotgun/agents/conversation/history/token_counting/base.py +51 -9
- shotgun/agents/file_read.py +176 -0
- shotgun/agents/messages.py +15 -3
- shotgun/agents/models.py +24 -1
- shotgun/agents/router/models.py +8 -0
- shotgun/agents/router/tools/delegation_tools.py +55 -1
- shotgun/agents/router/tools/plan_tools.py +88 -7
- shotgun/agents/runner.py +17 -2
- shotgun/agents/tools/__init__.py +8 -0
- shotgun/agents/tools/codebase/directory_lister.py +27 -39
- shotgun/agents/tools/codebase/file_read.py +26 -35
- shotgun/agents/tools/codebase/query_graph.py +9 -0
- shotgun/agents/tools/codebase/retrieve_code.py +9 -0
- shotgun/agents/tools/file_management.py +32 -2
- shotgun/agents/tools/file_read_tools/__init__.py +7 -0
- shotgun/agents/tools/file_read_tools/multimodal_file_read.py +167 -0
- shotgun/agents/tools/markdown_tools/__init__.py +62 -0
- shotgun/agents/tools/markdown_tools/insert_section.py +148 -0
- shotgun/agents/tools/markdown_tools/models.py +86 -0
- shotgun/agents/tools/markdown_tools/remove_section.py +114 -0
- shotgun/agents/tools/markdown_tools/replace_section.py +119 -0
- shotgun/agents/tools/markdown_tools/utils.py +453 -0
- shotgun/agents/tools/registry.py +44 -6
- shotgun/agents/tools/web_search/openai.py +42 -23
- shotgun/attachments/__init__.py +41 -0
- shotgun/attachments/errors.py +60 -0
- shotgun/attachments/models.py +107 -0
- shotgun/attachments/parser.py +257 -0
- shotgun/attachments/processor.py +193 -0
- shotgun/build_constants.py +4 -7
- shotgun/cli/clear.py +2 -2
- shotgun/cli/codebase/commands.py +181 -65
- shotgun/cli/compact.py +2 -2
- shotgun/cli/context.py +2 -2
- shotgun/cli/error_handler.py +2 -2
- shotgun/cli/run.py +90 -0
- shotgun/cli/spec/backup.py +2 -1
- shotgun/codebase/__init__.py +2 -0
- shotgun/codebase/benchmarks/__init__.py +35 -0
- shotgun/codebase/benchmarks/benchmark_runner.py +309 -0
- shotgun/codebase/benchmarks/exporters.py +119 -0
- shotgun/codebase/benchmarks/formatters/__init__.py +49 -0
- shotgun/codebase/benchmarks/formatters/base.py +34 -0
- shotgun/codebase/benchmarks/formatters/json_formatter.py +106 -0
- shotgun/codebase/benchmarks/formatters/markdown.py +136 -0
- shotgun/codebase/benchmarks/models.py +129 -0
- shotgun/codebase/core/__init__.py +4 -0
- shotgun/codebase/core/call_resolution.py +91 -0
- shotgun/codebase/core/change_detector.py +11 -6
- shotgun/codebase/core/errors.py +159 -0
- shotgun/codebase/core/extractors/__init__.py +23 -0
- shotgun/codebase/core/extractors/base.py +138 -0
- shotgun/codebase/core/extractors/factory.py +63 -0
- shotgun/codebase/core/extractors/go/__init__.py +7 -0
- shotgun/codebase/core/extractors/go/extractor.py +122 -0
- shotgun/codebase/core/extractors/javascript/__init__.py +7 -0
- shotgun/codebase/core/extractors/javascript/extractor.py +132 -0
- shotgun/codebase/core/extractors/protocol.py +109 -0
- shotgun/codebase/core/extractors/python/__init__.py +7 -0
- shotgun/codebase/core/extractors/python/extractor.py +141 -0
- shotgun/codebase/core/extractors/rust/__init__.py +7 -0
- shotgun/codebase/core/extractors/rust/extractor.py +139 -0
- shotgun/codebase/core/extractors/types.py +15 -0
- shotgun/codebase/core/extractors/typescript/__init__.py +7 -0
- shotgun/codebase/core/extractors/typescript/extractor.py +92 -0
- shotgun/codebase/core/gitignore.py +252 -0
- shotgun/codebase/core/ingestor.py +644 -354
- shotgun/codebase/core/kuzu_compat.py +119 -0
- shotgun/codebase/core/language_config.py +239 -0
- shotgun/codebase/core/manager.py +256 -46
- shotgun/codebase/core/metrics_collector.py +310 -0
- shotgun/codebase/core/metrics_types.py +347 -0
- shotgun/codebase/core/parallel_executor.py +424 -0
- shotgun/codebase/core/work_distributor.py +254 -0
- shotgun/codebase/core/worker.py +768 -0
- shotgun/codebase/indexing_state.py +86 -0
- shotgun/codebase/models.py +94 -0
- shotgun/codebase/service.py +13 -0
- shotgun/exceptions.py +9 -9
- shotgun/main.py +3 -16
- shotgun/posthog_telemetry.py +165 -24
- shotgun/prompts/agents/file_read.j2 +48 -0
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +19 -47
- shotgun/prompts/agents/partials/content_formatting.j2 +12 -33
- shotgun/prompts/agents/partials/interactive_mode.j2 +9 -32
- shotgun/prompts/agents/partials/router_delegation_mode.j2 +21 -22
- shotgun/prompts/agents/plan.j2 +14 -0
- shotgun/prompts/agents/router.j2 +531 -258
- shotgun/prompts/agents/specify.j2 +14 -0
- shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +14 -1
- shotgun/prompts/agents/state/system_state.j2 +13 -11
- shotgun/prompts/agents/tasks.j2 +14 -0
- shotgun/settings.py +49 -10
- shotgun/tui/app.py +149 -18
- shotgun/tui/commands/__init__.py +9 -1
- shotgun/tui/components/attachment_bar.py +87 -0
- shotgun/tui/components/prompt_input.py +25 -28
- shotgun/tui/components/status_bar.py +14 -7
- shotgun/tui/dependencies.py +3 -8
- shotgun/tui/protocols.py +18 -0
- shotgun/tui/screens/chat/chat.tcss +15 -0
- shotgun/tui/screens/chat/chat_screen.py +766 -235
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +8 -4
- shotgun/tui/screens/chat_screen/attachment_hint.py +40 -0
- shotgun/tui/screens/chat_screen/command_providers.py +0 -10
- shotgun/tui/screens/chat_screen/history/chat_history.py +54 -14
- shotgun/tui/screens/chat_screen/history/formatters.py +22 -0
- shotgun/tui/screens/chat_screen/history/user_question.py +25 -3
- shotgun/tui/screens/database_locked_dialog.py +219 -0
- shotgun/tui/screens/database_timeout_dialog.py +158 -0
- shotgun/tui/screens/kuzu_error_dialog.py +135 -0
- shotgun/tui/screens/model_picker.py +1 -3
- shotgun/tui/screens/models.py +11 -0
- shotgun/tui/state/processing_state.py +19 -0
- shotgun/tui/widgets/widget_coordinator.py +18 -0
- shotgun/utils/file_system_utils.py +4 -1
- {shotgun_sh-0.4.0.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/METADATA +87 -34
- {shotgun_sh-0.4.0.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/RECORD +128 -79
- shotgun/cli/export.py +0 -81
- shotgun/cli/plan.py +0 -73
- shotgun/cli/research.py +0 -93
- shotgun/cli/specify.py +0 -70
- shotgun/cli/tasks.py +0 -78
- shotgun/sentry_telemetry.py +0 -232
- shotgun/tui/screens/onboarding.py +0 -584
- {shotgun_sh-0.4.0.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.4.0.dev1.dist-info → shotgun_sh-0.6.2.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
-
|
|
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
|
-
|
|
9
|
-
|
|
8
|
+
<EXECUTION_PLAN>
|
|
10
9
|
{{ execution_plan }}
|
|
10
|
+
</EXECUTION_PLAN>
|
|
11
11
|
|
|
12
12
|
{% if pending_approval %}
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
36
|
+
<TABLE_OF_CONTENTS note="READ THE ENTIRE FILE TO UNDERSTAND MORE">
|
|
37
37
|
{{ markdown_toc }}
|
|
38
|
+
</TABLE_OF_CONTENTS>
|
|
38
39
|
|
|
39
|
-
|
|
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>
|
shotgun/prompts/agents/tasks.j2
CHANGED
|
@@ -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.
|
|
14
|
-
|
|
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., "
|
|
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
|
|
51
|
-
and
|
|
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
|
-
|
|
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
|
-
#
|
|
269
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
|
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
|
-
#
|
|
317
|
-
#
|
|
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
|
-
|
|
330
|
-
if
|
|
331
|
-
|
|
332
|
-
|
|
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
|
|
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
|
shotgun/tui/commands/__init__.py
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
35
|
-
"""Handle key presses
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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.
|
|
60
|
+
self.replace(
|
|
61
|
+
"\n",
|
|
62
|
+
start,
|
|
63
|
+
end,
|
|
64
|
+
maintain_selection_offset=False,
|
|
65
|
+
)
|
|
66
|
+
return
|