shotgun-sh 0.1.0__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.
Potentially problematic release.
This version of shotgun-sh might be problematic. Click here for more details.
- shotgun/__init__.py +5 -0
- shotgun/agents/__init__.py +1 -0
- shotgun/agents/agent_manager.py +651 -0
- shotgun/agents/common.py +549 -0
- shotgun/agents/config/__init__.py +13 -0
- shotgun/agents/config/constants.py +17 -0
- shotgun/agents/config/manager.py +294 -0
- shotgun/agents/config/models.py +185 -0
- shotgun/agents/config/provider.py +206 -0
- shotgun/agents/conversation_history.py +106 -0
- shotgun/agents/conversation_manager.py +105 -0
- shotgun/agents/export.py +96 -0
- shotgun/agents/history/__init__.py +5 -0
- shotgun/agents/history/compaction.py +85 -0
- shotgun/agents/history/constants.py +19 -0
- shotgun/agents/history/context_extraction.py +108 -0
- shotgun/agents/history/history_building.py +104 -0
- shotgun/agents/history/history_processors.py +426 -0
- shotgun/agents/history/message_utils.py +84 -0
- shotgun/agents/history/token_counting.py +429 -0
- shotgun/agents/history/token_estimation.py +138 -0
- shotgun/agents/messages.py +35 -0
- shotgun/agents/models.py +275 -0
- shotgun/agents/plan.py +98 -0
- shotgun/agents/research.py +108 -0
- shotgun/agents/specify.py +98 -0
- shotgun/agents/tasks.py +96 -0
- shotgun/agents/tools/__init__.py +34 -0
- shotgun/agents/tools/codebase/__init__.py +28 -0
- shotgun/agents/tools/codebase/codebase_shell.py +256 -0
- shotgun/agents/tools/codebase/directory_lister.py +141 -0
- shotgun/agents/tools/codebase/file_read.py +144 -0
- shotgun/agents/tools/codebase/models.py +252 -0
- shotgun/agents/tools/codebase/query_graph.py +67 -0
- shotgun/agents/tools/codebase/retrieve_code.py +81 -0
- shotgun/agents/tools/file_management.py +218 -0
- shotgun/agents/tools/user_interaction.py +37 -0
- shotgun/agents/tools/web_search/__init__.py +60 -0
- shotgun/agents/tools/web_search/anthropic.py +144 -0
- shotgun/agents/tools/web_search/gemini.py +85 -0
- shotgun/agents/tools/web_search/openai.py +98 -0
- shotgun/agents/tools/web_search/utils.py +20 -0
- shotgun/build_constants.py +20 -0
- shotgun/cli/__init__.py +1 -0
- shotgun/cli/codebase/__init__.py +5 -0
- shotgun/cli/codebase/commands.py +202 -0
- shotgun/cli/codebase/models.py +21 -0
- shotgun/cli/config.py +275 -0
- shotgun/cli/export.py +81 -0
- shotgun/cli/models.py +10 -0
- shotgun/cli/plan.py +73 -0
- shotgun/cli/research.py +85 -0
- shotgun/cli/specify.py +69 -0
- shotgun/cli/tasks.py +78 -0
- shotgun/cli/update.py +152 -0
- shotgun/cli/utils.py +25 -0
- shotgun/codebase/__init__.py +12 -0
- shotgun/codebase/core/__init__.py +46 -0
- shotgun/codebase/core/change_detector.py +358 -0
- shotgun/codebase/core/code_retrieval.py +243 -0
- shotgun/codebase/core/ingestor.py +1497 -0
- shotgun/codebase/core/language_config.py +297 -0
- shotgun/codebase/core/manager.py +1662 -0
- shotgun/codebase/core/nl_query.py +331 -0
- shotgun/codebase/core/parser_loader.py +128 -0
- shotgun/codebase/models.py +111 -0
- shotgun/codebase/service.py +206 -0
- shotgun/logging_config.py +227 -0
- shotgun/main.py +167 -0
- shotgun/posthog_telemetry.py +158 -0
- shotgun/prompts/__init__.py +5 -0
- shotgun/prompts/agents/__init__.py +1 -0
- shotgun/prompts/agents/export.j2 +350 -0
- shotgun/prompts/agents/partials/codebase_understanding.j2 +87 -0
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +37 -0
- shotgun/prompts/agents/partials/content_formatting.j2 +65 -0
- shotgun/prompts/agents/partials/interactive_mode.j2 +26 -0
- shotgun/prompts/agents/plan.j2 +144 -0
- shotgun/prompts/agents/research.j2 +69 -0
- shotgun/prompts/agents/specify.j2 +51 -0
- shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +19 -0
- shotgun/prompts/agents/state/system_state.j2 +31 -0
- shotgun/prompts/agents/tasks.j2 +143 -0
- shotgun/prompts/codebase/__init__.py +1 -0
- shotgun/prompts/codebase/cypher_query_patterns.j2 +223 -0
- shotgun/prompts/codebase/cypher_system.j2 +28 -0
- shotgun/prompts/codebase/enhanced_query_context.j2 +10 -0
- shotgun/prompts/codebase/partials/cypher_rules.j2 +24 -0
- shotgun/prompts/codebase/partials/graph_schema.j2 +30 -0
- shotgun/prompts/codebase/partials/temporal_context.j2 +21 -0
- shotgun/prompts/history/__init__.py +1 -0
- shotgun/prompts/history/incremental_summarization.j2 +53 -0
- shotgun/prompts/history/summarization.j2 +46 -0
- shotgun/prompts/loader.py +140 -0
- shotgun/py.typed +0 -0
- shotgun/sdk/__init__.py +13 -0
- shotgun/sdk/codebase.py +219 -0
- shotgun/sdk/exceptions.py +17 -0
- shotgun/sdk/models.py +189 -0
- shotgun/sdk/services.py +23 -0
- shotgun/sentry_telemetry.py +87 -0
- shotgun/telemetry.py +93 -0
- shotgun/tui/__init__.py +0 -0
- shotgun/tui/app.py +116 -0
- shotgun/tui/commands/__init__.py +76 -0
- shotgun/tui/components/prompt_input.py +69 -0
- shotgun/tui/components/spinner.py +86 -0
- shotgun/tui/components/splash.py +25 -0
- shotgun/tui/components/vertical_tail.py +13 -0
- shotgun/tui/screens/chat.py +782 -0
- shotgun/tui/screens/chat.tcss +43 -0
- shotgun/tui/screens/chat_screen/__init__.py +0 -0
- shotgun/tui/screens/chat_screen/command_providers.py +219 -0
- shotgun/tui/screens/chat_screen/hint_message.py +40 -0
- shotgun/tui/screens/chat_screen/history.py +221 -0
- shotgun/tui/screens/directory_setup.py +113 -0
- shotgun/tui/screens/provider_config.py +221 -0
- shotgun/tui/screens/splash.py +31 -0
- shotgun/tui/styles.tcss +10 -0
- shotgun/tui/utils/__init__.py +5 -0
- shotgun/tui/utils/mode_progress.py +257 -0
- shotgun/utils/__init__.py +5 -0
- shotgun/utils/env_utils.py +35 -0
- shotgun/utils/file_system_utils.py +36 -0
- shotgun/utils/update_checker.py +375 -0
- shotgun_sh-0.1.0.dist-info/METADATA +466 -0
- shotgun_sh-0.1.0.dist-info/RECORD +130 -0
- shotgun_sh-0.1.0.dist-info/WHEEL +4 -0
- shotgun_sh-0.1.0.dist-info/entry_points.txt +2 -0
- shotgun_sh-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Screen for configuring provider API keys before entering chat."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, cast
|
|
6
|
+
|
|
7
|
+
from textual import on
|
|
8
|
+
from textual.app import ComposeResult
|
|
9
|
+
from textual.containers import Horizontal, Vertical
|
|
10
|
+
from textual.reactive import reactive
|
|
11
|
+
from textual.screen import Screen
|
|
12
|
+
from textual.widgets import Button, Input, Label, ListItem, ListView, Static
|
|
13
|
+
|
|
14
|
+
from shotgun.agents.config import ConfigManager, ProviderType
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from ..app import ShotgunApp
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ProviderConfigScreen(Screen[None]):
|
|
21
|
+
"""Collect API keys for available providers."""
|
|
22
|
+
|
|
23
|
+
CSS = """
|
|
24
|
+
ProviderConfig {
|
|
25
|
+
layout: vertical;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
ProviderConfig > * {
|
|
29
|
+
height: auto;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
#titlebox {
|
|
33
|
+
height: auto;
|
|
34
|
+
margin: 2 0;
|
|
35
|
+
padding: 1;
|
|
36
|
+
border: hkey $border;
|
|
37
|
+
content-align: center middle;
|
|
38
|
+
|
|
39
|
+
& > * {
|
|
40
|
+
text-align: center;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#provider-config-title {
|
|
45
|
+
padding: 1 0;
|
|
46
|
+
text-style: bold;
|
|
47
|
+
color: $text-accent;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#provider-list {
|
|
51
|
+
margin: 2 0;
|
|
52
|
+
height: auto;
|
|
53
|
+
& > * {
|
|
54
|
+
padding: 1 0;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
#provider-actions {
|
|
58
|
+
padding: 1;
|
|
59
|
+
}
|
|
60
|
+
#provider-actions > * {
|
|
61
|
+
margin-right: 2;
|
|
62
|
+
}
|
|
63
|
+
#provider-list {
|
|
64
|
+
padding: 1;
|
|
65
|
+
}
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
BINDINGS = [
|
|
69
|
+
("escape", "done", "Back"),
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
selected_provider: reactive[ProviderType] = reactive(ProviderType.OPENAI)
|
|
73
|
+
|
|
74
|
+
def compose(self) -> ComposeResult:
|
|
75
|
+
with Vertical(id="titlebox"):
|
|
76
|
+
yield Static("Provider setup", id="provider-config-title")
|
|
77
|
+
yield Static(
|
|
78
|
+
"Select a provider and enter the API key needed to activate it.",
|
|
79
|
+
id="provider-config-summary",
|
|
80
|
+
)
|
|
81
|
+
yield ListView(*self._build_provider_items(), id="provider-list")
|
|
82
|
+
yield Input(
|
|
83
|
+
placeholder=self._input_placeholder(self.selected_provider),
|
|
84
|
+
password=True,
|
|
85
|
+
id="api-key",
|
|
86
|
+
)
|
|
87
|
+
with Horizontal(id="provider-actions"):
|
|
88
|
+
yield Button("Save key \\[ENTER]", variant="primary", id="save")
|
|
89
|
+
yield Button("Clear key", id="clear", variant="warning")
|
|
90
|
+
yield Button("Done \\[ESC]", id="done")
|
|
91
|
+
|
|
92
|
+
def on_mount(self) -> None:
|
|
93
|
+
self.refresh_provider_status()
|
|
94
|
+
list_view = self.query_one(ListView)
|
|
95
|
+
if list_view.children:
|
|
96
|
+
list_view.index = 0
|
|
97
|
+
self.selected_provider = ProviderType.OPENAI
|
|
98
|
+
self.set_focus(self.query_one("#api-key", Input))
|
|
99
|
+
|
|
100
|
+
def action_done(self) -> None:
|
|
101
|
+
self.dismiss()
|
|
102
|
+
|
|
103
|
+
@on(ListView.Highlighted)
|
|
104
|
+
def _on_provider_highlighted(self, event: ListView.Highlighted) -> None:
|
|
105
|
+
provider = self._provider_from_item(event.item)
|
|
106
|
+
if provider:
|
|
107
|
+
self.selected_provider = provider
|
|
108
|
+
|
|
109
|
+
@on(ListView.Selected)
|
|
110
|
+
def _on_provider_selected(self, event: ListView.Selected) -> None:
|
|
111
|
+
provider = self._provider_from_item(event.item)
|
|
112
|
+
if provider:
|
|
113
|
+
self.selected_provider = provider
|
|
114
|
+
self.set_focus(self.query_one("#api-key", Input))
|
|
115
|
+
|
|
116
|
+
@on(Button.Pressed, "#save")
|
|
117
|
+
def _on_save_pressed(self) -> None:
|
|
118
|
+
self._save_api_key()
|
|
119
|
+
|
|
120
|
+
@on(Button.Pressed, "#clear")
|
|
121
|
+
def _on_clear_pressed(self) -> None:
|
|
122
|
+
self._clear_api_key()
|
|
123
|
+
|
|
124
|
+
@on(Button.Pressed, "#done")
|
|
125
|
+
def _on_done_pressed(self) -> None:
|
|
126
|
+
self.action_done()
|
|
127
|
+
|
|
128
|
+
@on(Input.Submitted, "#api-key")
|
|
129
|
+
def _on_input_submitted(self, event: Input.Submitted) -> None:
|
|
130
|
+
del event # unused
|
|
131
|
+
self._save_api_key()
|
|
132
|
+
|
|
133
|
+
def watch_selected_provider(self, provider: ProviderType) -> None:
|
|
134
|
+
if not self.is_mounted:
|
|
135
|
+
return
|
|
136
|
+
input_widget = self.query_one("#api-key", Input)
|
|
137
|
+
input_widget.placeholder = self._input_placeholder(provider)
|
|
138
|
+
input_widget.value = ""
|
|
139
|
+
|
|
140
|
+
@property
|
|
141
|
+
def config_manager(self) -> ConfigManager:
|
|
142
|
+
app = cast("ShotgunApp", self.app)
|
|
143
|
+
return app.config_manager
|
|
144
|
+
|
|
145
|
+
def refresh_provider_status(self) -> None:
|
|
146
|
+
"""Update the list view entries to reflect configured providers."""
|
|
147
|
+
for provider in ProviderType:
|
|
148
|
+
label = self.query_one(f"#label-{provider.value}", Label)
|
|
149
|
+
label.update(self._provider_label(provider))
|
|
150
|
+
|
|
151
|
+
def _build_provider_items(self) -> list[ListItem]:
|
|
152
|
+
items: list[ListItem] = []
|
|
153
|
+
for provider in ProviderType:
|
|
154
|
+
label = Label(self._provider_label(provider), id=f"label-{provider.value}")
|
|
155
|
+
items.append(ListItem(label, id=f"provider-{provider.value}"))
|
|
156
|
+
return items
|
|
157
|
+
|
|
158
|
+
def _provider_from_item(self, item: ListItem | None) -> ProviderType | None:
|
|
159
|
+
if item is None or item.id is None:
|
|
160
|
+
return None
|
|
161
|
+
provider_id = item.id.removeprefix("provider-")
|
|
162
|
+
try:
|
|
163
|
+
return ProviderType(provider_id)
|
|
164
|
+
except ValueError:
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
def _provider_label(self, provider: ProviderType) -> str:
|
|
168
|
+
display = self._provider_display_name(provider)
|
|
169
|
+
status = (
|
|
170
|
+
"Configured"
|
|
171
|
+
if self.config_manager.has_provider_key(provider)
|
|
172
|
+
else "Not configured"
|
|
173
|
+
)
|
|
174
|
+
return f"{display} · {status}"
|
|
175
|
+
|
|
176
|
+
def _provider_display_name(self, provider: ProviderType) -> str:
|
|
177
|
+
names = {
|
|
178
|
+
ProviderType.OPENAI: "OpenAI",
|
|
179
|
+
ProviderType.ANTHROPIC: "Anthropic",
|
|
180
|
+
ProviderType.GOOGLE: "Google Gemini",
|
|
181
|
+
}
|
|
182
|
+
return names.get(provider, provider.value.title())
|
|
183
|
+
|
|
184
|
+
def _input_placeholder(self, provider: ProviderType) -> str:
|
|
185
|
+
return f"{self._provider_display_name(provider)} API key"
|
|
186
|
+
|
|
187
|
+
def _save_api_key(self) -> None:
|
|
188
|
+
input_widget = self.query_one("#api-key", Input)
|
|
189
|
+
api_key = input_widget.value.strip()
|
|
190
|
+
|
|
191
|
+
if not api_key:
|
|
192
|
+
self.notify("Enter an API key before saving.", severity="error")
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
try:
|
|
196
|
+
self.config_manager.update_provider(
|
|
197
|
+
self.selected_provider,
|
|
198
|
+
api_key=api_key,
|
|
199
|
+
)
|
|
200
|
+
except Exception as exc: # pragma: no cover - defensive; textual path
|
|
201
|
+
self.notify(f"Failed to save key: {exc}", severity="error")
|
|
202
|
+
return
|
|
203
|
+
|
|
204
|
+
input_widget.value = ""
|
|
205
|
+
self.refresh_provider_status()
|
|
206
|
+
self.notify(
|
|
207
|
+
f"Saved API key for {self._provider_display_name(self.selected_provider)}."
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
def _clear_api_key(self) -> None:
|
|
211
|
+
try:
|
|
212
|
+
self.config_manager.clear_provider_key(self.selected_provider)
|
|
213
|
+
except Exception as exc: # pragma: no cover - defensive; textual path
|
|
214
|
+
self.notify(f"Failed to clear key: {exc}", severity="error")
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
self.refresh_provider_status()
|
|
218
|
+
self.query_one("#api-key", Input).value = ""
|
|
219
|
+
self.notify(
|
|
220
|
+
f"Cleared API key for {self._provider_display_name(self.selected_provider)}."
|
|
221
|
+
)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from textual.app import ComposeResult
|
|
2
|
+
from textual.containers import Container
|
|
3
|
+
from textual.screen import Screen
|
|
4
|
+
|
|
5
|
+
from ..components.splash import SplashWidget
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SplashScreen(Screen[None]):
|
|
9
|
+
CSS = """
|
|
10
|
+
#splash-container {
|
|
11
|
+
align: center middle;
|
|
12
|
+
width: 100%;
|
|
13
|
+
height: 100%;
|
|
14
|
+
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
SplashWidget {
|
|
18
|
+
color: $text-accent;
|
|
19
|
+
}
|
|
20
|
+
"""
|
|
21
|
+
"""Splash screen for the app."""
|
|
22
|
+
|
|
23
|
+
def on_mount(self) -> None:
|
|
24
|
+
self.set_timer(2, self.on_timer_tick)
|
|
25
|
+
|
|
26
|
+
def on_timer_tick(self) -> None:
|
|
27
|
+
self.dismiss()
|
|
28
|
+
|
|
29
|
+
def compose(self) -> ComposeResult:
|
|
30
|
+
with Container(id="splash-container"):
|
|
31
|
+
yield SplashWidget()
|
shotgun/tui/styles.tcss
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""Utility module for checking mode progress in .shotgun directories."""
|
|
2
|
+
|
|
3
|
+
import random
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from shotgun.agents.models import AgentType
|
|
7
|
+
from shotgun.utils.file_system_utils import get_shotgun_base_path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ModeProgressChecker:
|
|
11
|
+
"""Checks progress across different agent modes based on file contents."""
|
|
12
|
+
|
|
13
|
+
# Minimum file size in characters to consider a mode as "started"
|
|
14
|
+
MIN_CONTENT_SIZE = 20
|
|
15
|
+
|
|
16
|
+
# Map agent types to their corresponding files (in workflow order)
|
|
17
|
+
MODE_FILES = {
|
|
18
|
+
AgentType.RESEARCH: "research.md",
|
|
19
|
+
AgentType.SPECIFY: "specification.md",
|
|
20
|
+
AgentType.PLAN: "plan.md",
|
|
21
|
+
AgentType.TASKS: "tasks.md",
|
|
22
|
+
AgentType.EXPORT: "exports/", # Export mode creates files in exports folder
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
def __init__(self, base_path: Path | None = None):
|
|
26
|
+
"""Initialize the progress checker.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
base_path: Base path for .shotgun directory. Defaults to current directory.
|
|
30
|
+
"""
|
|
31
|
+
self.base_path = base_path or get_shotgun_base_path()
|
|
32
|
+
|
|
33
|
+
def has_mode_content(self, mode: AgentType) -> bool:
|
|
34
|
+
"""Check if a mode has meaningful content.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
mode: The agent mode to check.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
True if the mode has a file with >20 characters.
|
|
41
|
+
"""
|
|
42
|
+
if mode not in self.MODE_FILES:
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
file_or_dir = self.MODE_FILES[mode]
|
|
46
|
+
|
|
47
|
+
# Special handling for export mode (checks directory)
|
|
48
|
+
if mode == AgentType.EXPORT:
|
|
49
|
+
export_path = self.base_path / file_or_dir
|
|
50
|
+
if export_path.exists() and export_path.is_dir():
|
|
51
|
+
# Check if any files exist in exports directory
|
|
52
|
+
for item in export_path.glob("*"):
|
|
53
|
+
if item.is_file() and not item.name.startswith("."):
|
|
54
|
+
try:
|
|
55
|
+
content = item.read_text(encoding="utf-8")
|
|
56
|
+
if len(content.strip()) > self.MIN_CONTENT_SIZE:
|
|
57
|
+
return True
|
|
58
|
+
except (OSError, UnicodeDecodeError):
|
|
59
|
+
continue
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
# Check single file for other modes
|
|
63
|
+
file_path = self.base_path / file_or_dir
|
|
64
|
+
if not file_path.exists() or not file_path.is_file():
|
|
65
|
+
return False
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
content = file_path.read_text(encoding="utf-8")
|
|
69
|
+
# Check if file has meaningful content
|
|
70
|
+
return len(content.strip()) > self.MIN_CONTENT_SIZE
|
|
71
|
+
except (OSError, UnicodeDecodeError):
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
def get_next_suggested_mode(self, current_mode: AgentType) -> AgentType | None:
|
|
75
|
+
"""Get the next suggested mode based on current progress.
|
|
76
|
+
|
|
77
|
+
Args:
|
|
78
|
+
current_mode: The current agent mode.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
The next suggested mode, or None if no suggestion.
|
|
82
|
+
"""
|
|
83
|
+
mode_order = [
|
|
84
|
+
AgentType.RESEARCH,
|
|
85
|
+
AgentType.SPECIFY,
|
|
86
|
+
AgentType.TASKS,
|
|
87
|
+
AgentType.EXPORT,
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
current_index = mode_order.index(current_mode)
|
|
92
|
+
except ValueError:
|
|
93
|
+
# Mode not in standard order (e.g., PLAN mode)
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
# Check if current mode has content
|
|
97
|
+
if not self.has_mode_content(current_mode):
|
|
98
|
+
# Current mode is empty, no suggestion for next mode
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
# Get next mode in sequence
|
|
102
|
+
if current_index < len(mode_order) - 1:
|
|
103
|
+
return mode_order[current_index + 1]
|
|
104
|
+
|
|
105
|
+
# Export mode cycles back to Research
|
|
106
|
+
return mode_order[0]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class PlaceholderHints:
|
|
110
|
+
"""Manages dynamic placeholder hints for each mode based on progress."""
|
|
111
|
+
|
|
112
|
+
# Placeholder variations for each mode and state
|
|
113
|
+
HINTS = {
|
|
114
|
+
# Research mode
|
|
115
|
+
AgentType.RESEARCH: {
|
|
116
|
+
False: [
|
|
117
|
+
"Research a product or idea (SHIFT+TAB to cycle modes)",
|
|
118
|
+
"What would you like to explore? Start your research journey here (SHIFT+TAB to switch modes)",
|
|
119
|
+
"Dive into discovery mode - research anything that sparks curiosity (SHIFT+TAB for mode menu)",
|
|
120
|
+
"Ready to investigate? Feed me your burning questions (SHIFT+TAB to explore other modes)",
|
|
121
|
+
" 🔍 The research rabbit hole awaits! What shall we uncover? (SHIFT+TAB for mode carousel)",
|
|
122
|
+
],
|
|
123
|
+
True: [
|
|
124
|
+
"Research complete! SHIFT+TAB to move to Specify mode",
|
|
125
|
+
"Great research! Time to specify (SHIFT+TAB to Specify mode)",
|
|
126
|
+
"Research done! Ready to create specifications (SHIFT+TAB to Specify)",
|
|
127
|
+
"Findings gathered! Move to specifications (SHIFT+TAB for Specify mode)",
|
|
128
|
+
" 🎯 Research complete! Advance to Specify mode (SHIFT+TAB)",
|
|
129
|
+
],
|
|
130
|
+
},
|
|
131
|
+
# Specify mode
|
|
132
|
+
AgentType.SPECIFY: {
|
|
133
|
+
False: [
|
|
134
|
+
"Create detailed specifications and requirements (SHIFT+TAB to switch modes)",
|
|
135
|
+
"Define your project specifications here (SHIFT+TAB to navigate modes)",
|
|
136
|
+
"Time to get specific - write comprehensive specs (SHIFT+TAB for mode options)",
|
|
137
|
+
"Specification station: Document requirements and designs (SHIFT+TAB to change modes)",
|
|
138
|
+
" 📋 Spec-tacular time! Let's architect your ideas (SHIFT+TAB for mode magic)",
|
|
139
|
+
],
|
|
140
|
+
True: [
|
|
141
|
+
"Specifications complete! SHIFT+TAB to create a Plan",
|
|
142
|
+
"Specs ready! Time to plan (SHIFT+TAB to Plan mode)",
|
|
143
|
+
"Requirements defined! Move to planning (SHIFT+TAB to Plan)",
|
|
144
|
+
"Specifications done! Create your roadmap (SHIFT+TAB for Plan mode)",
|
|
145
|
+
" 🚀 Specs complete! Advance to Plan mode (SHIFT+TAB)",
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
# Tasks mode
|
|
149
|
+
AgentType.TASKS: {
|
|
150
|
+
False: [
|
|
151
|
+
"Break down your project into actionable tasks (SHIFT+TAB for modes)",
|
|
152
|
+
"Task creation time! Define your implementation steps (SHIFT+TAB to switch)",
|
|
153
|
+
"Ready to get tactical? Create your task list (SHIFT+TAB for mode options)",
|
|
154
|
+
"Task command center: Organize your work items (SHIFT+TAB to navigate)",
|
|
155
|
+
" ✅ Task mode activated! Break it down into bite-sized pieces (SHIFT+TAB)",
|
|
156
|
+
],
|
|
157
|
+
True: [
|
|
158
|
+
"Tasks defined! Ready to export or cycle back (SHIFT+TAB)",
|
|
159
|
+
"Task list complete! Export your work (SHIFT+TAB to Export)",
|
|
160
|
+
"All tasks created! Time to export (SHIFT+TAB for Export mode)",
|
|
161
|
+
"Implementation plan ready! Export everything (SHIFT+TAB to Export)",
|
|
162
|
+
" 🎊 Tasks complete! Export your masterpiece (SHIFT+TAB)",
|
|
163
|
+
],
|
|
164
|
+
},
|
|
165
|
+
# Export mode
|
|
166
|
+
AgentType.EXPORT: {
|
|
167
|
+
False: [
|
|
168
|
+
"Export your complete project documentation (SHIFT+TAB for modes)",
|
|
169
|
+
"Ready to package everything? Export time! (SHIFT+TAB to switch)",
|
|
170
|
+
"Export station: Generate deliverables (SHIFT+TAB for mode menu)",
|
|
171
|
+
"Time to share your work! Export documents (SHIFT+TAB to navigate)",
|
|
172
|
+
" 📦 Export mode! Package and share your creation (SHIFT+TAB)",
|
|
173
|
+
],
|
|
174
|
+
True: [
|
|
175
|
+
"Exported! Start new research or continue refining (SHIFT+TAB)",
|
|
176
|
+
"Export complete! New cycle begins (SHIFT+TAB to Research)",
|
|
177
|
+
"All exported! Ready for another round (SHIFT+TAB for Research)",
|
|
178
|
+
"Documents exported! Start fresh (SHIFT+TAB to Research mode)",
|
|
179
|
+
" 🎉 Export complete! Begin a new adventure (SHIFT+TAB)",
|
|
180
|
+
],
|
|
181
|
+
},
|
|
182
|
+
# Plan mode
|
|
183
|
+
AgentType.PLAN: {
|
|
184
|
+
False: [
|
|
185
|
+
"Create a strategic plan for your project (SHIFT+TAB for modes)",
|
|
186
|
+
"Planning phase: Map out your roadmap (SHIFT+TAB to switch)",
|
|
187
|
+
"Time to strategize! Create your project plan (SHIFT+TAB for options)",
|
|
188
|
+
"Plan your approach and milestones (SHIFT+TAB to navigate)",
|
|
189
|
+
" 🗺️ Plan mode! Chart your course to success (SHIFT+TAB)",
|
|
190
|
+
],
|
|
191
|
+
True: [
|
|
192
|
+
"Plan complete! Move to Tasks mode (SHIFT+TAB)",
|
|
193
|
+
"Strategy ready! Time for tasks (SHIFT+TAB to Tasks mode)",
|
|
194
|
+
"Roadmap done! Create task list (SHIFT+TAB for Tasks)",
|
|
195
|
+
"Planning complete! Break into tasks (SHIFT+TAB to Tasks)",
|
|
196
|
+
" ⚡ Plan ready! Advance to Tasks mode (SHIFT+TAB)",
|
|
197
|
+
],
|
|
198
|
+
},
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
def __init__(self, base_path: Path | None = None):
|
|
202
|
+
"""Initialize placeholder hints with progress checker.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
base_path: Base path for checking progress. Defaults to current directory.
|
|
206
|
+
"""
|
|
207
|
+
self.progress_checker = ModeProgressChecker(base_path)
|
|
208
|
+
self._cached_hints: dict[tuple[AgentType, bool], str] = {}
|
|
209
|
+
self._hint_indices: dict[tuple[AgentType, bool], int] = {}
|
|
210
|
+
|
|
211
|
+
def get_hint(self, current_mode: AgentType, force_refresh: bool = False) -> str:
|
|
212
|
+
"""Get a dynamic hint based on current mode and progress.
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
current_mode: The current agent mode.
|
|
216
|
+
force_refresh: Force recalculation of progress state.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
A contextual hint string for the placeholder.
|
|
220
|
+
"""
|
|
221
|
+
# Default hint if mode not configured
|
|
222
|
+
if current_mode not in self.HINTS:
|
|
223
|
+
return f"Enter your {current_mode.value} mode prompt (SHIFT+TAB to switch modes)"
|
|
224
|
+
|
|
225
|
+
# Determine if mode has content
|
|
226
|
+
has_content = self.progress_checker.has_mode_content(current_mode)
|
|
227
|
+
|
|
228
|
+
# Get hint variations for this mode and state
|
|
229
|
+
hints_list = self.HINTS[current_mode][has_content]
|
|
230
|
+
|
|
231
|
+
# Cache key for this mode and state
|
|
232
|
+
cache_key = (current_mode, has_content)
|
|
233
|
+
|
|
234
|
+
# Force refresh or first time
|
|
235
|
+
if force_refresh or cache_key not in self._cached_hints:
|
|
236
|
+
# Initialize index for this cache key if not exists
|
|
237
|
+
if cache_key not in self._hint_indices:
|
|
238
|
+
self._hint_indices[cache_key] = random.randint(0, len(hints_list) - 1) # noqa: S311
|
|
239
|
+
|
|
240
|
+
# Get hint at current index
|
|
241
|
+
hint_index = self._hint_indices[cache_key]
|
|
242
|
+
self._cached_hints[cache_key] = hints_list[hint_index]
|
|
243
|
+
|
|
244
|
+
return self._cached_hints[cache_key]
|
|
245
|
+
|
|
246
|
+
def get_placeholder_for_mode(self, current_mode: AgentType) -> str:
|
|
247
|
+
"""Get placeholder text for a given mode.
|
|
248
|
+
|
|
249
|
+
This is an alias for get_hint() to maintain compatibility.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
current_mode: The current agent mode.
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
A contextual hint string for the placeholder.
|
|
256
|
+
"""
|
|
257
|
+
return self.get_hint(current_mode)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Utilities for working with environment variables."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def is_truthy(value: str | None) -> bool:
|
|
5
|
+
"""Check if a string value represents true.
|
|
6
|
+
|
|
7
|
+
Args:
|
|
8
|
+
value: String value to check (e.g., from environment variable)
|
|
9
|
+
|
|
10
|
+
Returns:
|
|
11
|
+
True if value is "true", "1", or "yes" (case-insensitive)
|
|
12
|
+
False otherwise (including None, empty string, or any other value)
|
|
13
|
+
"""
|
|
14
|
+
if not value:
|
|
15
|
+
return False
|
|
16
|
+
return value.lower() in ("true", "1", "yes")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def is_falsy(value: str | None) -> bool:
|
|
20
|
+
"""Check if a string value explicitly represents false.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
value: String value to check (e.g., from environment variable)
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
True if value is "false", "0", or "no" (case-insensitive)
|
|
27
|
+
False otherwise (including None, empty string, or any other value)
|
|
28
|
+
|
|
29
|
+
Note:
|
|
30
|
+
This is NOT the opposite of is_truthy(). A value can be neither
|
|
31
|
+
truthy nor falsy (e.g., None, "", "maybe", etc.)
|
|
32
|
+
"""
|
|
33
|
+
if not value:
|
|
34
|
+
return False
|
|
35
|
+
return value.lower() in ("false", "0", "no")
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""File system utility functions."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_shotgun_base_path() -> Path:
|
|
8
|
+
"""Get the absolute path to the .shotgun directory."""
|
|
9
|
+
return Path.cwd() / ".shotgun"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_shotgun_home() -> Path:
|
|
13
|
+
"""Get the Shotgun home directory path.
|
|
14
|
+
|
|
15
|
+
Can be overridden with SHOTGUN_HOME environment variable for testing.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Path to shotgun home directory (default: ~/.shotgun-sh/)
|
|
19
|
+
"""
|
|
20
|
+
# Allow override via environment variable (useful for testing)
|
|
21
|
+
if custom_home := os.environ.get("SHOTGUN_HOME"):
|
|
22
|
+
return Path(custom_home)
|
|
23
|
+
|
|
24
|
+
return Path.home() / ".shotgun-sh"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def ensure_shotgun_directory_exists() -> Path:
|
|
28
|
+
"""Ensure the .shotgun directory exists and return its path.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Path: The path to the .shotgun directory.
|
|
32
|
+
"""
|
|
33
|
+
shotgun_dir = get_shotgun_base_path()
|
|
34
|
+
shotgun_dir.mkdir(exist_ok=True)
|
|
35
|
+
# Note: Removed logger to avoid circular dependency with logging_config
|
|
36
|
+
return shotgun_dir
|