shotgun-sh 0.2.11.dev2__py3-none-any.whl → 0.2.11.dev7__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/agents/agent_manager.py +194 -28
- shotgun/agents/common.py +14 -8
- shotgun/agents/config/manager.py +64 -33
- shotgun/agents/config/models.py +25 -1
- shotgun/agents/config/provider.py +2 -2
- shotgun/agents/context_analyzer/analyzer.py +2 -24
- shotgun/agents/conversation_manager.py +35 -19
- shotgun/agents/export.py +2 -2
- shotgun/agents/history/history_processors.py +99 -3
- shotgun/agents/history/token_counting/anthropic.py +17 -1
- shotgun/agents/history/token_counting/base.py +14 -3
- shotgun/agents/history/token_counting/openai.py +11 -1
- shotgun/agents/history/token_counting/sentencepiece_counter.py +8 -0
- shotgun/agents/history/token_counting/tokenizer_cache.py +3 -1
- shotgun/agents/history/token_counting/utils.py +0 -3
- shotgun/agents/plan.py +2 -2
- shotgun/agents/research.py +3 -3
- shotgun/agents/specify.py +2 -2
- shotgun/agents/tasks.py +2 -2
- shotgun/agents/tools/codebase/file_read.py +5 -2
- shotgun/agents/tools/file_management.py +11 -7
- shotgun/agents/tools/web_search/__init__.py +8 -8
- shotgun/agents/tools/web_search/anthropic.py +2 -2
- shotgun/agents/tools/web_search/gemini.py +1 -1
- shotgun/agents/tools/web_search/openai.py +1 -1
- shotgun/agents/tools/web_search/utils.py +2 -2
- shotgun/agents/usage_manager.py +16 -11
- shotgun/cli/clear.py +2 -1
- shotgun/cli/compact.py +3 -3
- shotgun/cli/config.py +8 -5
- shotgun/cli/context.py +2 -2
- shotgun/cli/export.py +1 -1
- shotgun/cli/feedback.py +4 -2
- shotgun/cli/plan.py +1 -1
- shotgun/cli/research.py +1 -1
- shotgun/cli/specify.py +1 -1
- shotgun/cli/tasks.py +1 -1
- shotgun/codebase/core/change_detector.py +5 -3
- shotgun/codebase/core/code_retrieval.py +4 -2
- shotgun/codebase/core/ingestor.py +10 -8
- shotgun/codebase/core/manager.py +3 -3
- shotgun/codebase/core/nl_query.py +1 -1
- shotgun/exceptions.py +32 -0
- shotgun/logging_config.py +10 -17
- shotgun/main.py +3 -1
- shotgun/posthog_telemetry.py +14 -4
- shotgun/sentry_telemetry.py +22 -2
- shotgun/telemetry.py +3 -1
- shotgun/tui/app.py +71 -65
- shotgun/tui/components/context_indicator.py +43 -0
- shotgun/tui/containers.py +15 -17
- shotgun/tui/dependencies.py +2 -2
- shotgun/tui/screens/chat/chat_screen.py +164 -40
- shotgun/tui/screens/chat/help_text.py +16 -15
- shotgun/tui/screens/chat_screen/command_providers.py +10 -0
- shotgun/tui/screens/feedback.py +4 -4
- shotgun/tui/screens/github_issue.py +102 -0
- shotgun/tui/screens/model_picker.py +21 -20
- shotgun/tui/screens/onboarding.py +431 -0
- shotgun/tui/screens/provider_config.py +50 -27
- shotgun/tui/screens/shotgun_auth.py +2 -2
- shotgun/tui/screens/welcome.py +14 -11
- shotgun/tui/services/conversation_service.py +16 -14
- shotgun/tui/utils/mode_progress.py +14 -7
- shotgun/tui/widgets/widget_coordinator.py +15 -0
- shotgun/utils/file_system_utils.py +19 -0
- shotgun/utils/marketing.py +110 -0
- {shotgun_sh-0.2.11.dev2.dist-info → shotgun_sh-0.2.11.dev7.dist-info}/METADATA +2 -1
- {shotgun_sh-0.2.11.dev2.dist-info → shotgun_sh-0.2.11.dev7.dist-info}/RECORD +72 -68
- {shotgun_sh-0.2.11.dev2.dist-info → shotgun_sh-0.2.11.dev7.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.2.11.dev2.dist-info → shotgun_sh-0.2.11.dev7.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.2.11.dev2.dist-info → shotgun_sh-0.2.11.dev7.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
|
+
from datetime import datetime, timezone
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import cast
|
|
7
8
|
|
|
@@ -31,6 +32,7 @@ from shotgun.agents.agent_manager import (
|
|
|
31
32
|
ModelConfigUpdated,
|
|
32
33
|
PartialResponseMessage,
|
|
33
34
|
)
|
|
35
|
+
from shotgun.agents.config import get_config_manager
|
|
34
36
|
from shotgun.agents.config.models import MODEL_SPECS
|
|
35
37
|
from shotgun.agents.conversation_manager import ConversationManager
|
|
36
38
|
from shotgun.agents.history.compaction import apply_persistent_compaction
|
|
@@ -45,6 +47,7 @@ from shotgun.codebase.core.manager import (
|
|
|
45
47
|
CodebaseGraphManager,
|
|
46
48
|
)
|
|
47
49
|
from shotgun.codebase.models import IndexProgress, ProgressPhase
|
|
50
|
+
from shotgun.exceptions import ContextSizeLimitExceeded
|
|
48
51
|
from shotgun.posthog_telemetry import track_event
|
|
49
52
|
from shotgun.sdk.codebase import CodebaseSDK
|
|
50
53
|
from shotgun.sdk.exceptions import CodebaseNotFoundError, InvalidPathError
|
|
@@ -70,11 +73,13 @@ from shotgun.tui.screens.chat_screen.command_providers import (
|
|
|
70
73
|
from shotgun.tui.screens.chat_screen.hint_message import HintMessage
|
|
71
74
|
from shotgun.tui.screens.chat_screen.history import ChatHistory
|
|
72
75
|
from shotgun.tui.screens.confirmation_dialog import ConfirmationDialog
|
|
76
|
+
from shotgun.tui.screens.onboarding import OnboardingModal
|
|
73
77
|
from shotgun.tui.services.conversation_service import ConversationService
|
|
74
78
|
from shotgun.tui.state.processing_state import ProcessingStateManager
|
|
75
79
|
from shotgun.tui.utils.mode_progress import PlaceholderHints
|
|
76
80
|
from shotgun.tui.widgets.widget_coordinator import WidgetCoordinator
|
|
77
81
|
from shotgun.utils import get_shotgun_home
|
|
82
|
+
from shotgun.utils.marketing import MarketingManager
|
|
78
83
|
|
|
79
84
|
logger = logging.getLogger(__name__)
|
|
80
85
|
|
|
@@ -165,13 +170,17 @@ class ChatScreen(Screen[None]):
|
|
|
165
170
|
self.processing_state.bind_spinner(self.query_one("#spinner", Spinner))
|
|
166
171
|
|
|
167
172
|
# Load conversation history if --continue flag was provided
|
|
168
|
-
|
|
169
|
-
|
|
173
|
+
# Use call_later to handle async exists() check
|
|
174
|
+
if self.continue_session:
|
|
175
|
+
self.call_later(self._check_and_load_conversation)
|
|
170
176
|
|
|
171
177
|
self.call_later(self.check_if_codebase_is_indexed)
|
|
172
178
|
# Initial update of context indicator
|
|
173
179
|
self.update_context_indicator()
|
|
174
180
|
|
|
181
|
+
# Show onboarding popup if not shown before
|
|
182
|
+
self.call_later(self._check_and_show_onboarding)
|
|
183
|
+
|
|
175
184
|
async def on_key(self, event: events.Key) -> None:
|
|
176
185
|
"""Handle key presses for cancellation."""
|
|
177
186
|
# If escape is pressed during Q&A mode, exit Q&A
|
|
@@ -304,6 +313,10 @@ class ChatScreen(Screen[None]):
|
|
|
304
313
|
else:
|
|
305
314
|
self.notify("No context analysis available", severity="error")
|
|
306
315
|
|
|
316
|
+
def action_view_onboarding(self) -> None:
|
|
317
|
+
"""Show the onboarding modal."""
|
|
318
|
+
self.app.push_screen(OnboardingModal())
|
|
319
|
+
|
|
307
320
|
@work
|
|
308
321
|
async def action_compact_conversation(self) -> None:
|
|
309
322
|
"""Compact the conversation history to reduce size."""
|
|
@@ -386,11 +399,11 @@ class ChatScreen(Screen[None]):
|
|
|
386
399
|
# Save to conversation file
|
|
387
400
|
conversation_file = get_shotgun_home() / "conversation.json"
|
|
388
401
|
manager = ConversationManager(conversation_file)
|
|
389
|
-
conversation = manager.load()
|
|
402
|
+
conversation = await manager.load()
|
|
390
403
|
|
|
391
404
|
if conversation:
|
|
392
405
|
conversation.set_agent_messages(compacted_messages)
|
|
393
|
-
manager.save(conversation)
|
|
406
|
+
await manager.save(conversation)
|
|
394
407
|
|
|
395
408
|
# Post compaction completed event
|
|
396
409
|
self.agent_manager.post_message(CompactionCompletedMessage())
|
|
@@ -455,7 +468,7 @@ class ChatScreen(Screen[None]):
|
|
|
455
468
|
self.agent_manager.ui_message_history = []
|
|
456
469
|
|
|
457
470
|
# Use conversation service to clear conversation
|
|
458
|
-
self.conversation_service.clear_conversation()
|
|
471
|
+
await self.conversation_service.clear_conversation()
|
|
459
472
|
|
|
460
473
|
# Post message history updated event to refresh UI
|
|
461
474
|
self.agent_manager.post_message(
|
|
@@ -502,6 +515,34 @@ class ChatScreen(Screen[None]):
|
|
|
502
515
|
f"[CONTEXT] Failed to update context indicator: {e}", exc_info=True
|
|
503
516
|
)
|
|
504
517
|
|
|
518
|
+
@work(exclusive=False)
|
|
519
|
+
async def update_context_indicator_with_messages(
|
|
520
|
+
self,
|
|
521
|
+
agent_messages: list[ModelMessage],
|
|
522
|
+
ui_messages: list[ModelMessage | HintMessage],
|
|
523
|
+
) -> None:
|
|
524
|
+
"""Update the context indicator with specific message sets (for streaming updates).
|
|
525
|
+
|
|
526
|
+
Args:
|
|
527
|
+
agent_messages: Agent message history including streaming messages (for token counting)
|
|
528
|
+
ui_messages: UI message history including hints and streaming messages
|
|
529
|
+
"""
|
|
530
|
+
try:
|
|
531
|
+
from shotgun.agents.context_analyzer.analyzer import ContextAnalyzer
|
|
532
|
+
|
|
533
|
+
analyzer = ContextAnalyzer(self.deps.llm_model)
|
|
534
|
+
# Analyze the combined message histories for accurate progressive token counts
|
|
535
|
+
analysis = await analyzer.analyze_conversation(agent_messages, ui_messages)
|
|
536
|
+
|
|
537
|
+
if analysis:
|
|
538
|
+
model_name = self.deps.llm_model.name
|
|
539
|
+
self.widget_coordinator.update_context_indicator(analysis, model_name)
|
|
540
|
+
except Exception as e:
|
|
541
|
+
logger.error(
|
|
542
|
+
f"Failed to update context indicator with streaming messages: {e}",
|
|
543
|
+
exc_info=True,
|
|
544
|
+
)
|
|
545
|
+
|
|
505
546
|
def compose(self) -> ComposeResult:
|
|
506
547
|
"""Create child widgets for the app."""
|
|
507
548
|
with Container(id="window"):
|
|
@@ -551,7 +592,7 @@ class ChatScreen(Screen[None]):
|
|
|
551
592
|
# Keep all ModelResponse and other message types
|
|
552
593
|
filtered_event_messages.append(msg)
|
|
553
594
|
|
|
554
|
-
# Build new message list
|
|
595
|
+
# Build new message list combining existing messages with new streaming content
|
|
555
596
|
new_message_list = self.messages + cast(
|
|
556
597
|
list[ModelMessage | HintMessage], filtered_event_messages
|
|
557
598
|
)
|
|
@@ -561,6 +602,13 @@ class ChatScreen(Screen[None]):
|
|
|
561
602
|
self.partial_message, new_message_list
|
|
562
603
|
)
|
|
563
604
|
|
|
605
|
+
# Update context indicator with full message history including streaming messages
|
|
606
|
+
# Combine existing agent history with new streaming messages for accurate token count
|
|
607
|
+
combined_agent_history = self.agent_manager.message_history + event.messages
|
|
608
|
+
self.update_context_indicator_with_messages(
|
|
609
|
+
combined_agent_history, new_message_list
|
|
610
|
+
)
|
|
611
|
+
|
|
564
612
|
def _clear_partial_response(self) -> None:
|
|
565
613
|
# Use widget coordinator to clear partial response
|
|
566
614
|
self.widget_coordinator.set_partial_response(None, self.messages)
|
|
@@ -602,7 +650,9 @@ class ChatScreen(Screen[None]):
|
|
|
602
650
|
self.qa_answers = []
|
|
603
651
|
|
|
604
652
|
@on(MessageHistoryUpdated)
|
|
605
|
-
def handle_message_history_updated(
|
|
653
|
+
async def handle_message_history_updated(
|
|
654
|
+
self, event: MessageHistoryUpdated
|
|
655
|
+
) -> None:
|
|
606
656
|
"""Handle message history updates from the agent manager."""
|
|
607
657
|
self._clear_partial_response()
|
|
608
658
|
self.messages = event.messages
|
|
@@ -617,32 +667,50 @@ class ChatScreen(Screen[None]):
|
|
|
617
667
|
self.update_context_indicator()
|
|
618
668
|
|
|
619
669
|
# If there are file operations, add a message showing the modified files
|
|
670
|
+
# Skip if hint was already added by agent_manager (e.g., in QA mode)
|
|
620
671
|
if event.file_operations:
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
672
|
+
# Check if file operation hint already exists in recent messages
|
|
673
|
+
file_hint_exists = any(
|
|
674
|
+
isinstance(msg, HintMessage)
|
|
675
|
+
and (
|
|
676
|
+
msg.message.startswith("📝 Modified:")
|
|
677
|
+
or msg.message.startswith("📁 Modified")
|
|
678
|
+
)
|
|
679
|
+
for msg in event.messages[-5:] # Check last 5 messages
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
if not file_hint_exists:
|
|
683
|
+
chat_history = self.query_one(ChatHistory)
|
|
684
|
+
if chat_history.vertical_tail:
|
|
685
|
+
tracker = FileOperationTracker(operations=event.file_operations)
|
|
686
|
+
display_path = tracker.get_display_path()
|
|
687
|
+
|
|
688
|
+
if display_path:
|
|
689
|
+
# Create a simple markdown message with the file path
|
|
690
|
+
# The terminal emulator will make this clickable automatically
|
|
691
|
+
path_obj = Path(display_path)
|
|
692
|
+
|
|
693
|
+
if len(event.file_operations) == 1:
|
|
694
|
+
message = f"📝 Modified: `{display_path}`"
|
|
639
695
|
else:
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
f"📁 Modified {num_files} files in: `{path_obj.parent}`"
|
|
696
|
+
num_files = len(
|
|
697
|
+
{op.file_path for op in event.file_operations}
|
|
643
698
|
)
|
|
699
|
+
if path_obj.is_dir():
|
|
700
|
+
message = f"📁 Modified {num_files} files in: `{display_path}`"
|
|
701
|
+
else:
|
|
702
|
+
# Common path is a file, show parent directory
|
|
703
|
+
message = f"📁 Modified {num_files} files in: `{path_obj.parent}`"
|
|
644
704
|
|
|
645
|
-
|
|
705
|
+
self.mount_hint(message)
|
|
706
|
+
|
|
707
|
+
# Check and display any marketing messages
|
|
708
|
+
from shotgun.tui.app import ShotgunApp
|
|
709
|
+
|
|
710
|
+
app = cast(ShotgunApp, self.app)
|
|
711
|
+
await MarketingManager.check_and_display_messages(
|
|
712
|
+
app.config_manager, event.file_operations, self.mount_hint
|
|
713
|
+
)
|
|
646
714
|
|
|
647
715
|
@on(CompactionStartedMessage)
|
|
648
716
|
def handle_compaction_started(self, event: CompactionStartedMessage) -> None:
|
|
@@ -1048,6 +1116,9 @@ class ChatScreen(Screen[None]):
|
|
|
1048
1116
|
self.processing_state.start_processing("Processing...")
|
|
1049
1117
|
self.processing_state.bind_worker(get_current_worker())
|
|
1050
1118
|
|
|
1119
|
+
# Start context indicator animation immediately
|
|
1120
|
+
self.widget_coordinator.set_context_streaming(True)
|
|
1121
|
+
|
|
1051
1122
|
prompt = message
|
|
1052
1123
|
|
|
1053
1124
|
try:
|
|
@@ -1057,6 +1128,27 @@ class ChatScreen(Screen[None]):
|
|
|
1057
1128
|
except asyncio.CancelledError:
|
|
1058
1129
|
# Handle cancellation gracefully - DO NOT re-raise
|
|
1059
1130
|
self.mount_hint("⚠️ Operation cancelled by user")
|
|
1131
|
+
except ContextSizeLimitExceeded as e:
|
|
1132
|
+
# User-friendly error with actionable options
|
|
1133
|
+
hint = (
|
|
1134
|
+
f"⚠️ **Context too large for {e.model_name}**\n\n"
|
|
1135
|
+
f"Your conversation history exceeds this model's limit ({e.max_tokens:,} tokens).\n\n"
|
|
1136
|
+
f"**Choose an action:**\n\n"
|
|
1137
|
+
f"1. Switch to a larger model (`Ctrl+P` → Change Model)\n"
|
|
1138
|
+
f"2. Switch to a larger model, compact (`/compact`), then switch back to {e.model_name}\n"
|
|
1139
|
+
f"3. Clear conversation (`/clear`)\n"
|
|
1140
|
+
)
|
|
1141
|
+
|
|
1142
|
+
self.mount_hint(hint)
|
|
1143
|
+
|
|
1144
|
+
# Log for debugging (won't send to Sentry due to ErrorNotPickedUpBySentry)
|
|
1145
|
+
logger.info(
|
|
1146
|
+
"Context size limit exceeded",
|
|
1147
|
+
extra={
|
|
1148
|
+
"max_tokens": e.max_tokens,
|
|
1149
|
+
"model_name": e.model_name,
|
|
1150
|
+
},
|
|
1151
|
+
)
|
|
1060
1152
|
except Exception as e:
|
|
1061
1153
|
# Log with full stack trace to shotgun.log
|
|
1062
1154
|
logger.exception(
|
|
@@ -1083,6 +1175,8 @@ class ChatScreen(Screen[None]):
|
|
|
1083
1175
|
self.mount_hint(hint)
|
|
1084
1176
|
finally:
|
|
1085
1177
|
self.processing_state.stop_processing()
|
|
1178
|
+
# Stop context indicator animation
|
|
1179
|
+
self.widget_coordinator.set_context_streaming(False)
|
|
1086
1180
|
|
|
1087
1181
|
# Save conversation after each interaction
|
|
1088
1182
|
self._save_conversation()
|
|
@@ -1091,20 +1185,50 @@ class ChatScreen(Screen[None]):
|
|
|
1091
1185
|
|
|
1092
1186
|
def _save_conversation(self) -> None:
|
|
1093
1187
|
"""Save the current conversation to persistent storage."""
|
|
1094
|
-
# Use conversation service for saving
|
|
1095
|
-
|
|
1188
|
+
# Use conversation service for saving (run async in background)
|
|
1189
|
+
# Use exclusive=True to prevent concurrent saves that can cause file contention
|
|
1190
|
+
self.run_worker(
|
|
1191
|
+
self.conversation_service.save_conversation(self.agent_manager),
|
|
1192
|
+
exclusive=True,
|
|
1193
|
+
)
|
|
1194
|
+
|
|
1195
|
+
async def _check_and_load_conversation(self) -> None:
|
|
1196
|
+
"""Check if conversation exists and load it if it does."""
|
|
1197
|
+
if await self.conversation_manager.exists():
|
|
1198
|
+
self._load_conversation()
|
|
1096
1199
|
|
|
1097
1200
|
def _load_conversation(self) -> None:
|
|
1098
1201
|
"""Load conversation from persistent storage."""
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1202
|
+
|
|
1203
|
+
# Use conversation service for restoration (run async)
|
|
1204
|
+
async def _do_load() -> None:
|
|
1205
|
+
(
|
|
1206
|
+
success,
|
|
1207
|
+
error_msg,
|
|
1208
|
+
restored_type,
|
|
1209
|
+
) = await self.conversation_service.restore_conversation(
|
|
1102
1210
|
self.agent_manager, self.deps.usage_manager
|
|
1103
1211
|
)
|
|
1104
|
-
)
|
|
1105
1212
|
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1213
|
+
if not success and error_msg:
|
|
1214
|
+
self.mount_hint(error_msg)
|
|
1215
|
+
elif success and restored_type:
|
|
1216
|
+
# Update the current mode to match restored conversation
|
|
1217
|
+
self.mode = restored_type
|
|
1218
|
+
|
|
1219
|
+
self.run_worker(_do_load(), exclusive=False)
|
|
1220
|
+
|
|
1221
|
+
@work
|
|
1222
|
+
async def _check_and_show_onboarding(self) -> None:
|
|
1223
|
+
"""Check if onboarding should be shown and display modal if needed."""
|
|
1224
|
+
config_manager = get_config_manager()
|
|
1225
|
+
config = await config_manager.load()
|
|
1226
|
+
|
|
1227
|
+
# Only show onboarding if it hasn't been shown before
|
|
1228
|
+
if config.shown_onboarding_popup is None:
|
|
1229
|
+
# Show the onboarding modal
|
|
1230
|
+
await self.app.push_screen_wait(OnboardingModal())
|
|
1231
|
+
|
|
1232
|
+
# Mark as shown in config with current timestamp
|
|
1233
|
+
config.shown_onboarding_popup = datetime.now(timezone.utc)
|
|
1234
|
+
await config_manager.save(config)
|
|
@@ -11,14 +11,14 @@ def help_text_with_codebase(already_indexed: bool = False) -> str:
|
|
|
11
11
|
Formatted help text string.
|
|
12
12
|
"""
|
|
13
13
|
return (
|
|
14
|
-
"Howdy! Welcome to Shotgun -
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
|
|
18
|
-
"-
|
|
19
|
-
"-
|
|
20
|
-
"-
|
|
21
|
-
"
|
|
14
|
+
"Howdy! Welcome to Shotgun - Spec Driven Development for Developers and AI Agents.\n\n"
|
|
15
|
+
"Shotgun writes codebase-aware specs for your AI coding agents so they don't derail.\n\n"
|
|
16
|
+
f"{'It' if already_indexed else 'Once your codebase is indexed, it'} can help you:\n"
|
|
17
|
+
"- Research your codebase and spec out new features\n"
|
|
18
|
+
"- Create implementation plans that fit your architecture\n"
|
|
19
|
+
"- Generate AGENTS.md files for AI coding agents\n"
|
|
20
|
+
"- Onboard to existing projects or plan refactors\n\n"
|
|
21
|
+
"Ready to build something? Let's go.\n"
|
|
22
22
|
)
|
|
23
23
|
|
|
24
24
|
|
|
@@ -29,11 +29,12 @@ def help_text_empty_dir() -> str:
|
|
|
29
29
|
Formatted help text string.
|
|
30
30
|
"""
|
|
31
31
|
return (
|
|
32
|
-
"Howdy! Welcome to Shotgun -
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"-
|
|
37
|
-
"-
|
|
38
|
-
"-
|
|
32
|
+
"Howdy! Welcome to Shotgun - Spec Driven Development for Developers and AI Agents.\n\n"
|
|
33
|
+
"Shotgun writes codebase-aware specs for your AI coding agents so they don't derail.\n\n"
|
|
34
|
+
"It can help you:\n"
|
|
35
|
+
"- Research your codebase and spec out new features\n"
|
|
36
|
+
"- Create implementation plans that fit your architecture\n"
|
|
37
|
+
"- Generate AGENTS.md files for AI coding agents\n"
|
|
38
|
+
"- Onboard to existing projects or plan refactors\n\n"
|
|
39
|
+
"Ready to build something? Let's go.\n"
|
|
39
40
|
)
|
|
@@ -369,6 +369,11 @@ class UnifiedCommandProvider(Provider):
|
|
|
369
369
|
self.chat_screen.action_show_usage,
|
|
370
370
|
help="Display usage information for the current session",
|
|
371
371
|
)
|
|
372
|
+
yield DiscoveryHit(
|
|
373
|
+
"View Onboarding",
|
|
374
|
+
self.chat_screen.action_view_onboarding,
|
|
375
|
+
help="View the onboarding tutorial and helpful resources",
|
|
376
|
+
)
|
|
372
377
|
|
|
373
378
|
async def search(self, query: str) -> AsyncGenerator[Hit, None]:
|
|
374
379
|
"""Search for commands in alphabetical order."""
|
|
@@ -416,6 +421,11 @@ class UnifiedCommandProvider(Provider):
|
|
|
416
421
|
self.chat_screen.action_show_usage,
|
|
417
422
|
"Display usage information for the current session",
|
|
418
423
|
),
|
|
424
|
+
(
|
|
425
|
+
"View Onboarding",
|
|
426
|
+
self.chat_screen.action_view_onboarding,
|
|
427
|
+
"View the onboarding tutorial and helpful resources",
|
|
428
|
+
),
|
|
419
429
|
]
|
|
420
430
|
|
|
421
431
|
for title, callback, help_text in commands:
|
shotgun/tui/screens/feedback.py
CHANGED
|
@@ -125,8 +125,8 @@ class FeedbackScreen(Screen[Feedback | None]):
|
|
|
125
125
|
self.set_focus(self.query_one("#feedback-description", TextArea))
|
|
126
126
|
|
|
127
127
|
@on(Button.Pressed, "#submit")
|
|
128
|
-
def _on_submit_pressed(self) -> None:
|
|
129
|
-
self._submit_feedback()
|
|
128
|
+
async def _on_submit_pressed(self) -> None:
|
|
129
|
+
await self._submit_feedback()
|
|
130
130
|
|
|
131
131
|
@on(Button.Pressed, "#cancel")
|
|
132
132
|
def _on_cancel_pressed(self) -> None:
|
|
@@ -171,7 +171,7 @@ class FeedbackScreen(Screen[Feedback | None]):
|
|
|
171
171
|
}
|
|
172
172
|
return placeholders.get(kind, "Enter your feedback...")
|
|
173
173
|
|
|
174
|
-
def _submit_feedback(self) -> None:
|
|
174
|
+
async def _submit_feedback(self) -> None:
|
|
175
175
|
text_area = self.query_one("#feedback-description", TextArea)
|
|
176
176
|
description = text_area.text.strip()
|
|
177
177
|
|
|
@@ -182,7 +182,7 @@ class FeedbackScreen(Screen[Feedback | None]):
|
|
|
182
182
|
return
|
|
183
183
|
|
|
184
184
|
app = cast("ShotgunApp", self.app)
|
|
185
|
-
shotgun_instance_id = app.config_manager.get_shotgun_instance_id()
|
|
185
|
+
shotgun_instance_id = await app.config_manager.get_shotgun_instance_id()
|
|
186
186
|
|
|
187
187
|
feedback = Feedback(
|
|
188
188
|
kind=self.selected_kind,
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Screen for guiding users to create GitHub issues."""
|
|
2
|
+
|
|
3
|
+
import webbrowser
|
|
4
|
+
|
|
5
|
+
from textual import on
|
|
6
|
+
from textual.app import ComposeResult
|
|
7
|
+
from textual.containers import Container, Vertical
|
|
8
|
+
from textual.screen import ModalScreen
|
|
9
|
+
from textual.widgets import Button, Markdown, Static
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class GitHubIssueScreen(ModalScreen[None]):
|
|
13
|
+
"""Guide users to create issues on GitHub."""
|
|
14
|
+
|
|
15
|
+
CSS = """
|
|
16
|
+
GitHubIssueScreen {
|
|
17
|
+
align: center middle;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#issue-container {
|
|
21
|
+
width: 70;
|
|
22
|
+
max-width: 100;
|
|
23
|
+
height: auto;
|
|
24
|
+
border: thick $primary;
|
|
25
|
+
background: $surface;
|
|
26
|
+
padding: 2;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
#issue-header {
|
|
30
|
+
text-style: bold;
|
|
31
|
+
color: $text-accent;
|
|
32
|
+
padding-bottom: 1;
|
|
33
|
+
text-align: center;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#issue-content {
|
|
37
|
+
padding: 1 0;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#issue-buttons {
|
|
41
|
+
height: auto;
|
|
42
|
+
padding: 2 0 0 0;
|
|
43
|
+
align: center middle;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#issue-buttons Button {
|
|
47
|
+
margin: 1 1;
|
|
48
|
+
min-width: 20;
|
|
49
|
+
}
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
BINDINGS = [
|
|
53
|
+
("escape", "dismiss", "Close"),
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
def compose(self) -> ComposeResult:
|
|
57
|
+
"""Compose the GitHub issue screen."""
|
|
58
|
+
with Container(id="issue-container"):
|
|
59
|
+
yield Static("Create a GitHub Issue", id="issue-header")
|
|
60
|
+
with Vertical(id="issue-content"):
|
|
61
|
+
yield Markdown(
|
|
62
|
+
"""
|
|
63
|
+
## Report Bugs or Request Features
|
|
64
|
+
|
|
65
|
+
We track all bugs, feature requests, and improvements on GitHub Issues.
|
|
66
|
+
|
|
67
|
+
### How to Create an Issue:
|
|
68
|
+
|
|
69
|
+
1. Click the button below to open our GitHub Issues page
|
|
70
|
+
2. Click **"New Issue"**
|
|
71
|
+
3. Choose a template:
|
|
72
|
+
- **Bug Report** - Report a bug or unexpected behavior
|
|
73
|
+
- **Feature Request** - Suggest new functionality
|
|
74
|
+
- **Documentation** - Report docs issues or improvements
|
|
75
|
+
4. Fill in the details and submit
|
|
76
|
+
|
|
77
|
+
We review all issues and will respond as soon as possible!
|
|
78
|
+
|
|
79
|
+
### Before Creating an Issue:
|
|
80
|
+
|
|
81
|
+
- Search existing issues to avoid duplicates
|
|
82
|
+
- Include steps to reproduce for bugs
|
|
83
|
+
- Be specific about what you'd like for feature requests
|
|
84
|
+
""",
|
|
85
|
+
id="issue-markdown",
|
|
86
|
+
)
|
|
87
|
+
with Vertical(id="issue-buttons"):
|
|
88
|
+
yield Button(
|
|
89
|
+
"🐙 Open GitHub Issues", id="github-button", variant="primary"
|
|
90
|
+
)
|
|
91
|
+
yield Button("Close", id="close-button")
|
|
92
|
+
|
|
93
|
+
@on(Button.Pressed, "#github-button")
|
|
94
|
+
def handle_github(self) -> None:
|
|
95
|
+
"""Open GitHub issues page in browser."""
|
|
96
|
+
webbrowser.open("https://github.com/shotgun-sh/shotgun/issues")
|
|
97
|
+
self.notify("Opening GitHub Issues in your browser...")
|
|
98
|
+
|
|
99
|
+
@on(Button.Pressed, "#close-button")
|
|
100
|
+
def handle_close(self) -> None:
|
|
101
|
+
"""Handle close button press."""
|
|
102
|
+
self.dismiss()
|
|
@@ -98,7 +98,7 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
|
|
|
98
98
|
yield Button("Select \\[ENTER]", variant="primary", id="select")
|
|
99
99
|
yield Button("Done \\[ESC]", id="done")
|
|
100
100
|
|
|
101
|
-
def _rebuild_model_list(self) -> None:
|
|
101
|
+
async def _rebuild_model_list(self) -> None:
|
|
102
102
|
"""Rebuild the model list from current config.
|
|
103
103
|
|
|
104
104
|
This method is called both on first show and when screen is resumed
|
|
@@ -108,7 +108,7 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
|
|
|
108
108
|
|
|
109
109
|
# Load current config with force_reload to get latest API keys
|
|
110
110
|
config_manager = self.config_manager
|
|
111
|
-
config = config_manager.load(force_reload=True)
|
|
111
|
+
config = await config_manager.load(force_reload=True)
|
|
112
112
|
|
|
113
113
|
# Log provider key status
|
|
114
114
|
logger.debug(
|
|
@@ -133,7 +133,7 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
|
|
|
133
133
|
logger.debug("Removed %d existing model items from list", old_count)
|
|
134
134
|
|
|
135
135
|
# Add new items (labels already have correct text including current indicator)
|
|
136
|
-
new_items = self._build_model_items(config)
|
|
136
|
+
new_items = await self._build_model_items(config)
|
|
137
137
|
for item in new_items:
|
|
138
138
|
list_view.append(item)
|
|
139
139
|
logger.debug("Added %d available model items to list", len(new_items))
|
|
@@ -153,7 +153,7 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
|
|
|
153
153
|
def on_show(self) -> None:
|
|
154
154
|
"""Rebuild model list when screen is first shown."""
|
|
155
155
|
logger.debug("ModelPickerScreen.on_show() called")
|
|
156
|
-
self._rebuild_model_list()
|
|
156
|
+
self.run_worker(self._rebuild_model_list(), exclusive=False)
|
|
157
157
|
|
|
158
158
|
def on_screenresume(self) -> None:
|
|
159
159
|
"""Rebuild model list when screen is resumed (subsequent visits).
|
|
@@ -162,7 +162,7 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
|
|
|
162
162
|
ensuring the model list reflects any config changes made while away.
|
|
163
163
|
"""
|
|
164
164
|
logger.debug("ModelPickerScreen.on_screenresume() called")
|
|
165
|
-
self._rebuild_model_list()
|
|
165
|
+
self.run_worker(self._rebuild_model_list(), exclusive=False)
|
|
166
166
|
|
|
167
167
|
def action_done(self) -> None:
|
|
168
168
|
self.dismiss()
|
|
@@ -193,14 +193,14 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
|
|
|
193
193
|
app = cast("ShotgunApp", self.app)
|
|
194
194
|
return app.config_manager
|
|
195
195
|
|
|
196
|
-
def refresh_model_labels(self) -> None:
|
|
196
|
+
async def refresh_model_labels(self) -> None:
|
|
197
197
|
"""Update the list view entries to reflect current selection.
|
|
198
198
|
|
|
199
199
|
Note: This method only updates labels for currently displayed models.
|
|
200
200
|
To rebuild the entire list after provider changes, on_show() should be used.
|
|
201
201
|
"""
|
|
202
202
|
# Load config once with force_reload
|
|
203
|
-
config = self.config_manager.load(force_reload=True)
|
|
203
|
+
config = await self.config_manager.load(force_reload=True)
|
|
204
204
|
current_model = config.selected_model or get_default_model_for_provider(config)
|
|
205
205
|
|
|
206
206
|
# Update labels for available models only
|
|
@@ -215,9 +215,11 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
|
|
|
215
215
|
self._model_label(model_name, is_current=model_name == current_model)
|
|
216
216
|
)
|
|
217
217
|
|
|
218
|
-
def _build_model_items(
|
|
218
|
+
async def _build_model_items(
|
|
219
|
+
self, config: ShotgunConfig | None = None
|
|
220
|
+
) -> list[ListItem]:
|
|
219
221
|
if config is None:
|
|
220
|
-
config = self.config_manager.load(force_reload=True)
|
|
222
|
+
config = await self.config_manager.load(force_reload=True)
|
|
221
223
|
|
|
222
224
|
items: list[ListItem] = []
|
|
223
225
|
current_model = self.selected_model
|
|
@@ -246,9 +248,7 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
|
|
|
246
248
|
return model_name
|
|
247
249
|
return None
|
|
248
250
|
|
|
249
|
-
def _is_model_available(
|
|
250
|
-
self, model_name: ModelName, config: ShotgunConfig | None = None
|
|
251
|
-
) -> bool:
|
|
251
|
+
def _is_model_available(self, model_name: ModelName, config: ShotgunConfig) -> bool:
|
|
252
252
|
"""Check if a model is available based on provider key configuration.
|
|
253
253
|
|
|
254
254
|
A model is available if:
|
|
@@ -257,14 +257,11 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
|
|
|
257
257
|
|
|
258
258
|
Args:
|
|
259
259
|
model_name: The model to check availability for
|
|
260
|
-
config:
|
|
260
|
+
config: Pre-loaded config (must be provided)
|
|
261
261
|
|
|
262
262
|
Returns:
|
|
263
263
|
True if the model can be used, False otherwise
|
|
264
264
|
"""
|
|
265
|
-
if config is None:
|
|
266
|
-
config = self.config_manager.load(force_reload=True)
|
|
267
|
-
|
|
268
265
|
# If Shotgun Account is configured, all models are available
|
|
269
266
|
if self.config_manager._provider_has_api_key(config.shotgun):
|
|
270
267
|
logger.debug("Model %s available (Shotgun Account configured)", model_name)
|
|
@@ -325,17 +322,21 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
|
|
|
325
322
|
|
|
326
323
|
def _select_model(self) -> None:
|
|
327
324
|
"""Save the selected model."""
|
|
325
|
+
self.run_worker(self._do_select_model(), exclusive=True)
|
|
326
|
+
|
|
327
|
+
async def _do_select_model(self) -> None:
|
|
328
|
+
"""Async implementation of model selection."""
|
|
328
329
|
try:
|
|
329
330
|
# Get old model before updating
|
|
330
|
-
config = self.config_manager.load()
|
|
331
|
+
config = await self.config_manager.load()
|
|
331
332
|
old_model = config.selected_model
|
|
332
333
|
|
|
333
334
|
# Update the selected model in config
|
|
334
|
-
self.config_manager.update_selected_model(self.selected_model)
|
|
335
|
-
self.refresh_model_labels()
|
|
335
|
+
await self.config_manager.update_selected_model(self.selected_model)
|
|
336
|
+
await self.refresh_model_labels()
|
|
336
337
|
|
|
337
338
|
# Get the full model config with provider information
|
|
338
|
-
model_config = get_provider_model(self.selected_model)
|
|
339
|
+
model_config = await get_provider_model(self.selected_model)
|
|
339
340
|
|
|
340
341
|
# Dismiss the screen and return the model config update to the caller
|
|
341
342
|
self.dismiss(
|