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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. shotgun/agents/agent_manager.py +219 -37
  2. shotgun/agents/common.py +79 -78
  3. shotgun/agents/config/README.md +89 -0
  4. shotgun/agents/config/__init__.py +10 -1
  5. shotgun/agents/config/manager.py +364 -53
  6. shotgun/agents/config/models.py +101 -21
  7. shotgun/agents/config/provider.py +51 -13
  8. shotgun/agents/config/streaming_test.py +119 -0
  9. shotgun/agents/context_analyzer/analyzer.py +6 -2
  10. shotgun/agents/conversation/__init__.py +18 -0
  11. shotgun/agents/conversation/filters.py +164 -0
  12. shotgun/agents/conversation/history/chunking.py +278 -0
  13. shotgun/agents/{history → conversation/history}/compaction.py +27 -1
  14. shotgun/agents/{history → conversation/history}/constants.py +5 -0
  15. shotgun/agents/conversation/history/file_content_deduplication.py +239 -0
  16. shotgun/agents/{history → conversation/history}/history_processors.py +267 -3
  17. shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +8 -0
  18. shotgun/agents/{conversation_manager.py → conversation/manager.py} +1 -1
  19. shotgun/agents/{conversation_history.py → conversation/models.py} +8 -94
  20. shotgun/agents/error/__init__.py +11 -0
  21. shotgun/agents/error/models.py +19 -0
  22. shotgun/agents/export.py +12 -13
  23. shotgun/agents/models.py +66 -1
  24. shotgun/agents/plan.py +12 -13
  25. shotgun/agents/research.py +13 -10
  26. shotgun/agents/router/__init__.py +47 -0
  27. shotgun/agents/router/models.py +376 -0
  28. shotgun/agents/router/router.py +185 -0
  29. shotgun/agents/router/tools/__init__.py +18 -0
  30. shotgun/agents/router/tools/delegation_tools.py +503 -0
  31. shotgun/agents/router/tools/plan_tools.py +322 -0
  32. shotgun/agents/runner.py +230 -0
  33. shotgun/agents/specify.py +12 -13
  34. shotgun/agents/tasks.py +12 -13
  35. shotgun/agents/tools/file_management.py +49 -1
  36. shotgun/agents/tools/registry.py +2 -0
  37. shotgun/agents/tools/web_search/__init__.py +1 -2
  38. shotgun/agents/tools/web_search/gemini.py +1 -3
  39. shotgun/agents/tools/web_search/openai.py +1 -1
  40. shotgun/build_constants.py +2 -2
  41. shotgun/cli/clear.py +1 -1
  42. shotgun/cli/compact.py +5 -3
  43. shotgun/cli/context.py +44 -1
  44. shotgun/cli/error_handler.py +24 -0
  45. shotgun/cli/export.py +34 -34
  46. shotgun/cli/plan.py +34 -34
  47. shotgun/cli/research.py +17 -9
  48. shotgun/cli/spec/__init__.py +5 -0
  49. shotgun/cli/spec/backup.py +81 -0
  50. shotgun/cli/spec/commands.py +132 -0
  51. shotgun/cli/spec/models.py +48 -0
  52. shotgun/cli/spec/pull_service.py +219 -0
  53. shotgun/cli/specify.py +20 -19
  54. shotgun/cli/tasks.py +34 -34
  55. shotgun/codebase/core/change_detector.py +1 -1
  56. shotgun/codebase/core/ingestor.py +154 -8
  57. shotgun/codebase/core/manager.py +1 -1
  58. shotgun/codebase/models.py +2 -0
  59. shotgun/exceptions.py +325 -0
  60. shotgun/llm_proxy/__init__.py +17 -0
  61. shotgun/llm_proxy/client.py +215 -0
  62. shotgun/llm_proxy/models.py +137 -0
  63. shotgun/logging_config.py +42 -0
  64. shotgun/main.py +4 -0
  65. shotgun/posthog_telemetry.py +1 -1
  66. shotgun/prompts/agents/export.j2 +2 -0
  67. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +23 -3
  68. shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
  69. shotgun/prompts/agents/partials/router_delegation_mode.j2 +36 -0
  70. shotgun/prompts/agents/plan.j2 +29 -1
  71. shotgun/prompts/agents/research.j2 +75 -23
  72. shotgun/prompts/agents/router.j2 +440 -0
  73. shotgun/prompts/agents/specify.j2 +80 -4
  74. shotgun/prompts/agents/state/system_state.j2 +15 -8
  75. shotgun/prompts/agents/tasks.j2 +63 -23
  76. shotgun/prompts/history/chunk_summarization.j2 +34 -0
  77. shotgun/prompts/history/combine_summaries.j2 +53 -0
  78. shotgun/sdk/codebase.py +14 -3
  79. shotgun/settings.py +5 -0
  80. shotgun/shotgun_web/__init__.py +67 -1
  81. shotgun/shotgun_web/client.py +42 -1
  82. shotgun/shotgun_web/constants.py +46 -0
  83. shotgun/shotgun_web/exceptions.py +29 -0
  84. shotgun/shotgun_web/models.py +390 -0
  85. shotgun/shotgun_web/shared_specs/__init__.py +32 -0
  86. shotgun/shotgun_web/shared_specs/file_scanner.py +175 -0
  87. shotgun/shotgun_web/shared_specs/hasher.py +83 -0
  88. shotgun/shotgun_web/shared_specs/models.py +71 -0
  89. shotgun/shotgun_web/shared_specs/upload_pipeline.py +329 -0
  90. shotgun/shotgun_web/shared_specs/utils.py +34 -0
  91. shotgun/shotgun_web/specs_client.py +703 -0
  92. shotgun/shotgun_web/supabase_client.py +31 -0
  93. shotgun/tui/app.py +78 -15
  94. shotgun/tui/components/mode_indicator.py +120 -25
  95. shotgun/tui/components/status_bar.py +2 -2
  96. shotgun/tui/containers.py +1 -1
  97. shotgun/tui/dependencies.py +64 -9
  98. shotgun/tui/layout.py +5 -0
  99. shotgun/tui/protocols.py +37 -0
  100. shotgun/tui/screens/chat/chat.tcss +9 -1
  101. shotgun/tui/screens/chat/chat_screen.py +1015 -106
  102. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +196 -17
  103. shotgun/tui/screens/chat_screen/command_providers.py +13 -89
  104. shotgun/tui/screens/chat_screen/hint_message.py +76 -1
  105. shotgun/tui/screens/chat_screen/history/agent_response.py +7 -3
  106. shotgun/tui/screens/chat_screen/history/chat_history.py +12 -0
  107. shotgun/tui/screens/chat_screen/history/formatters.py +53 -15
  108. shotgun/tui/screens/chat_screen/history/partial_response.py +11 -1
  109. shotgun/tui/screens/chat_screen/messages.py +219 -0
  110. shotgun/tui/screens/confirmation_dialog.py +40 -0
  111. shotgun/tui/screens/directory_setup.py +45 -41
  112. shotgun/tui/screens/feedback.py +10 -3
  113. shotgun/tui/screens/github_issue.py +11 -2
  114. shotgun/tui/screens/model_picker.py +28 -8
  115. shotgun/tui/screens/onboarding.py +179 -26
  116. shotgun/tui/screens/pipx_migration.py +58 -6
  117. shotgun/tui/screens/provider_config.py +66 -8
  118. shotgun/tui/screens/shared_specs/__init__.py +21 -0
  119. shotgun/tui/screens/shared_specs/create_spec_dialog.py +273 -0
  120. shotgun/tui/screens/shared_specs/models.py +56 -0
  121. shotgun/tui/screens/shared_specs/share_specs_dialog.py +390 -0
  122. shotgun/tui/screens/shared_specs/upload_progress_screen.py +452 -0
  123. shotgun/tui/screens/shotgun_auth.py +110 -16
  124. shotgun/tui/screens/spec_pull.py +288 -0
  125. shotgun/tui/screens/welcome.py +123 -0
  126. shotgun/tui/services/conversation_service.py +5 -2
  127. shotgun/tui/utils/mode_progress.py +20 -86
  128. shotgun/tui/widgets/__init__.py +2 -1
  129. shotgun/tui/widgets/approval_widget.py +152 -0
  130. shotgun/tui/widgets/cascade_confirmation_widget.py +203 -0
  131. shotgun/tui/widgets/plan_panel.py +129 -0
  132. shotgun/tui/widgets/step_checkpoint_widget.py +180 -0
  133. shotgun/tui/widgets/widget_coordinator.py +1 -1
  134. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/METADATA +11 -4
  135. shotgun_sh-0.4.0.dev1.dist-info/RECORD +242 -0
  136. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/WHEEL +1 -1
  137. shotgun_sh-0.2.17.dist-info/RECORD +0 -194
  138. /shotgun/agents/{history → conversation/history}/__init__.py +0 -0
  139. /shotgun/agents/{history → conversation/history}/context_extraction.py +0 -0
  140. /shotgun/agents/{history → conversation/history}/history_building.py +0 -0
  141. /shotgun/agents/{history → conversation/history}/message_utils.py +0 -0
  142. /shotgun/agents/{history → conversation/history}/token_counting/__init__.py +0 -0
  143. /shotgun/agents/{history → conversation/history}/token_counting/base.py +0 -0
  144. /shotgun/agents/{history → conversation/history}/token_counting/openai.py +0 -0
  145. /shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +0 -0
  146. /shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +0 -0
  147. /shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -0
  148. /shotgun/agents/{history → conversation/history}/token_estimation.py +0 -0
  149. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/entry_points.txt +0 -0
  150. {shotgun_sh-0.2.17.dist-info → shotgun_sh-0.4.0.dev1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,219 @@
1
+ """Message types for ChatScreen communication.
2
+
3
+ This module defines Textual message types used for communication
4
+ between widgets and the ChatScreen, particularly for step checkpoints
5
+ and cascade confirmation in the Router's Planning mode.
6
+ """
7
+
8
+ from textual.message import Message
9
+
10
+ from shotgun.agents.models import AgentType
11
+ from shotgun.agents.router.models import CascadeScope, ExecutionPlan, ExecutionStep
12
+
13
+ __all__ = [
14
+ # Step checkpoint messages (Stage 4)
15
+ "StepCompleted",
16
+ "CheckpointContinue",
17
+ "CheckpointModify",
18
+ "CheckpointStop",
19
+ # Cascade confirmation messages (Stage 5)
20
+ "CascadeConfirmationRequired",
21
+ "CascadeConfirmed",
22
+ "CascadeDeclined",
23
+ # Plan approval messages (Stage 7)
24
+ "PlanApprovalRequired",
25
+ "PlanApproved",
26
+ "PlanRejected",
27
+ # Sub-agent lifecycle messages (Stage 8)
28
+ "SubAgentStarted",
29
+ "SubAgentCompleted",
30
+ # Plan panel messages (Stage 11)
31
+ "PlanUpdated",
32
+ "PlanPanelClosed",
33
+ ]
34
+
35
+
36
+ class StepCompleted(Message):
37
+ """Posted when a plan step completes in Planning mode.
38
+
39
+ This message triggers the checkpoint UI to appear, allowing the user
40
+ to choose whether to continue, modify the plan, or stop execution.
41
+
42
+ Attributes:
43
+ step: The step that was just completed.
44
+ next_step: The next step to execute, or None if this was the last step.
45
+ """
46
+
47
+ def __init__(self, step: ExecutionStep, next_step: ExecutionStep | None) -> None:
48
+ super().__init__()
49
+ self.step = step
50
+ self.next_step = next_step
51
+
52
+
53
+ class CheckpointContinue(Message):
54
+ """Posted when user chooses to continue to next step.
55
+
56
+ This message indicates the user wants to proceed with the next
57
+ step in the execution plan.
58
+ """
59
+
60
+
61
+ class CheckpointModify(Message):
62
+ """Posted when user wants to modify the plan.
63
+
64
+ This message indicates the user wants to return to the prompt input
65
+ to make adjustments to the plan before continuing.
66
+ """
67
+
68
+
69
+ class CheckpointStop(Message):
70
+ """Posted when user wants to stop execution.
71
+
72
+ This message indicates the user wants to halt execution while
73
+ keeping the remaining steps in the plan as pending.
74
+ """
75
+
76
+
77
+ # =============================================================================
78
+ # Cascade Confirmation Messages (Stage 5)
79
+ # =============================================================================
80
+
81
+
82
+ class CascadeConfirmationRequired(Message):
83
+ """Posted when a file with dependents was modified and needs cascade confirmation.
84
+
85
+ In Planning mode, after modifying a file like specification.md that has
86
+ dependent files (plan.md, tasks.md), this message triggers the cascade
87
+ confirmation UI to appear.
88
+
89
+ Attributes:
90
+ updated_file: The file that was just updated (e.g., "specification.md").
91
+ dependent_files: List of files that depend on the updated file.
92
+ """
93
+
94
+ def __init__(self, updated_file: str, dependent_files: list[str]) -> None:
95
+ super().__init__()
96
+ self.updated_file = updated_file
97
+ self.dependent_files = dependent_files
98
+
99
+
100
+ class CascadeConfirmed(Message):
101
+ """Posted when user confirms cascade update.
102
+
103
+ This message indicates the user wants to proceed with updating
104
+ dependent files based on the selected scope.
105
+
106
+ Attributes:
107
+ scope: The scope of files to update (ALL, PLAN_ONLY, TASKS_ONLY, NONE).
108
+ """
109
+
110
+ def __init__(self, scope: CascadeScope) -> None:
111
+ super().__init__()
112
+ self.scope = scope
113
+
114
+
115
+ class CascadeDeclined(Message):
116
+ """Posted when user declines cascade update.
117
+
118
+ This message indicates the user does not want to update dependent
119
+ files and will handle them manually.
120
+ """
121
+
122
+
123
+ # =============================================================================
124
+ # Plan Approval Messages (Stage 7)
125
+ # =============================================================================
126
+
127
+
128
+ class PlanApprovalRequired(Message):
129
+ """Posted when a multi-step plan is created and needs user approval.
130
+
131
+ In Planning mode, after the router creates a plan with multiple steps,
132
+ this message triggers the approval UI to appear.
133
+
134
+ Attributes:
135
+ plan: The execution plan that needs user approval.
136
+ """
137
+
138
+ def __init__(self, plan: ExecutionPlan) -> None:
139
+ super().__init__()
140
+ self.plan = plan
141
+
142
+
143
+ class PlanApproved(Message):
144
+ """Posted when user approves the plan.
145
+
146
+ This message indicates the user wants to proceed with executing
147
+ the plan ("Go Ahead").
148
+ """
149
+
150
+
151
+ class PlanRejected(Message):
152
+ """Posted when user rejects the plan to clarify/modify.
153
+
154
+ This message indicates the user wants to return to the prompt input
155
+ to modify or clarify the request ("No, Let Me Clarify").
156
+ """
157
+
158
+
159
+ # =============================================================================
160
+ # Sub-Agent Lifecycle Messages (Stage 8)
161
+ # =============================================================================
162
+
163
+
164
+ class SubAgentStarted(Message):
165
+ """Posted when router starts delegating to a sub-agent.
166
+
167
+ This message triggers the mode indicator to show the active sub-agent
168
+ in the format "📋 Planning → Research".
169
+
170
+ Attributes:
171
+ agent_type: The type of sub-agent that started executing.
172
+ """
173
+
174
+ def __init__(self, agent_type: AgentType) -> None:
175
+ super().__init__()
176
+ self.agent_type = agent_type
177
+
178
+
179
+ class SubAgentCompleted(Message):
180
+ """Posted when sub-agent delegation completes.
181
+
182
+ This message triggers the mode indicator to clear the sub-agent
183
+ display and return to showing just the mode.
184
+
185
+ Attributes:
186
+ agent_type: The type of sub-agent that completed.
187
+ """
188
+
189
+ def __init__(self, agent_type: AgentType) -> None:
190
+ super().__init__()
191
+ self.agent_type = agent_type
192
+
193
+
194
+ # =============================================================================
195
+ # Plan Panel Messages (Stage 11)
196
+ # =============================================================================
197
+
198
+
199
+ class PlanUpdated(Message):
200
+ """Posted when the current plan changes.
201
+
202
+ This message triggers the plan panel to auto-show/hide based on
203
+ whether a plan exists.
204
+
205
+ Attributes:
206
+ plan: The updated execution plan, or None if plan was cleared.
207
+ """
208
+
209
+ def __init__(self, plan: ExecutionPlan | None) -> None:
210
+ super().__init__()
211
+ self.plan = plan
212
+
213
+
214
+ class PlanPanelClosed(Message):
215
+ """Posted when user closes the plan panel with × button.
216
+
217
+ This message indicates the user wants to dismiss the plan panel
218
+ temporarily. The panel will reopen when the plan changes.
219
+ """
@@ -5,9 +5,12 @@ from typing import Literal
5
5
  from textual import on
6
6
  from textual.app import ComposeResult
7
7
  from textual.containers import Container
8
+ from textual.events import Resize
8
9
  from textual.screen import ModalScreen
9
10
  from textual.widgets import Button, Label, Static
10
11
 
12
+ from shotgun.tui.layout import COMPACT_HEIGHT_THRESHOLD
13
+
11
14
  ButtonVariant = Literal["default", "primary", "success", "warning", "error"]
12
15
 
13
16
 
@@ -87,6 +90,20 @@ class ConfirmationDialog(ModalScreen[bool]):
87
90
  #dialog-buttons Button {
88
91
  margin-left: 1;
89
92
  }
93
+
94
+ /* Compact styles for short terminals */
95
+ #dialog-container.compact {
96
+ padding: 0 2;
97
+ max-height: 98%;
98
+ }
99
+
100
+ #dialog-title.compact {
101
+ padding-bottom: 0;
102
+ }
103
+
104
+ #dialog-message.compact {
105
+ padding-bottom: 0;
106
+ }
90
107
  """
91
108
 
92
109
  def __init__(
@@ -138,6 +155,29 @@ class ConfirmationDialog(ModalScreen[bool]):
138
155
  # Focus cancel button by default for safety
139
156
  self.query_one("#cancel", Button).focus()
140
157
 
158
+ # Apply compact layout if starting in a short terminal
159
+ self._apply_compact_layout(self.app.size.height < COMPACT_HEIGHT_THRESHOLD)
160
+
161
+ @on(Resize)
162
+ def handle_resize(self, event: Resize) -> None:
163
+ """Adjust layout based on terminal height."""
164
+ self._apply_compact_layout(event.size.height < COMPACT_HEIGHT_THRESHOLD)
165
+
166
+ def _apply_compact_layout(self, compact: bool) -> None:
167
+ """Apply or remove compact layout classes for short terminals."""
168
+ container = self.query_one("#dialog-container")
169
+ title = self.query_one("#dialog-title")
170
+ message = self.query_one("#dialog-message")
171
+
172
+ if compact:
173
+ container.add_class("compact")
174
+ title.add_class("compact")
175
+ message.add_class("compact")
176
+ else:
177
+ container.remove_class("compact")
178
+ title.remove_class("compact")
179
+ message.remove_class("compact")
180
+
141
181
  @on(Button.Pressed, "#cancel")
142
182
  def handle_cancel(self, event: Button.Pressed) -> None:
143
183
  """Handle cancel button press."""
@@ -1,4 +1,4 @@
1
- """Screen for setting up the local .shotgun directory."""
1
+ """Screen for displaying .shotgun directory creation errors."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -8,13 +8,20 @@ from textual import on
8
8
  from textual.app import ComposeResult
9
9
  from textual.containers import Horizontal, Vertical
10
10
  from textual.screen import Screen
11
- from textual.widgets import Button, Static
12
-
13
- from shotgun.utils.file_system_utils import ensure_shotgun_directory_exists
11
+ from textual.widgets import Button, Label, Static
14
12
 
15
13
 
16
14
  class DirectorySetupScreen(Screen[None]):
17
- """Prompt the user to initialize the .shotgun directory."""
15
+ """Display an error when .shotgun directory creation fails."""
16
+
17
+ def __init__(self, error_message: str) -> None:
18
+ """Initialize the error screen.
19
+
20
+ Args:
21
+ error_message: The error message to display to the user.
22
+ """
23
+ super().__init__()
24
+ self.error_message = error_message
18
25
 
19
26
  CSS = """
20
27
  DirectorySetupScreen {
@@ -56,58 +63,55 @@ class DirectorySetupScreen(Screen[None]):
56
63
  #directory-actions > * {
57
64
  margin-right: 2;
58
65
  }
66
+
67
+ #directory-status {
68
+ height: auto;
69
+ padding: 0 1;
70
+ min-height: 1;
71
+ color: $error;
72
+ text-align: center;
73
+ }
59
74
  """
60
75
 
61
76
  BINDINGS = [
62
- ("enter", "confirm", "Initialize"),
77
+ ("enter", "retry", "Retry"),
63
78
  ("escape", "cancel", "Exit"),
64
79
  ]
65
80
 
66
81
  def compose(self) -> ComposeResult:
67
82
  with Vertical(id="titlebox"):
68
- yield Static("Directory setup", id="directory-setup-title")
69
- yield Static("Shotgun keeps workspace data in a .shotgun directory.\n")
70
- yield Static("Initialize it in the current directory?\n")
71
- yield Static(f"[$foreground-muted]({Path.cwd().resolve()})[/]")
72
- with Horizontal(id="directory-actions"):
73
- yield Button(
74
- "Initialize and proceed \\[ENTER]", variant="primary", id="initialize"
83
+ yield Static(
84
+ "Failed to create .shotgun directory", id="directory-setup-title"
85
+ )
86
+ yield Static("Shotgun was unable to create the .shotgun directory in:\n")
87
+ yield Static(f"[$foreground-muted]({Path.cwd().resolve()})[/]\n")
88
+ yield Static(f"[bold red]Error:[/] {self.error_message}\n")
89
+ yield Static(
90
+ "This directory is required for storing workspace data. "
91
+ "Please check permissions and try again."
75
92
  )
76
- yield Button("Exit without setup \\[ESC]", variant="default", id="exit")
93
+ yield Label("", id="directory-status")
94
+ with Horizontal(id="directory-actions"):
95
+ yield Button("Retry \\[ENTER]", variant="primary", id="retry")
96
+ yield Button("Exit \\[ESC]", variant="default", id="exit")
77
97
 
78
98
  def on_mount(self) -> None:
79
- self.set_focus(self.query_one("#initialize", Button))
99
+ self.set_focus(self.query_one("#retry", Button))
80
100
 
81
- def action_confirm(self) -> None:
82
- self._initialize_directory()
101
+ def action_retry(self) -> None:
102
+ """Retry by dismissing the screen, which will trigger refresh_startup_screen."""
103
+ self.dismiss()
83
104
 
84
105
  def action_cancel(self) -> None:
85
- self._exit_application()
106
+ """Exit the application."""
107
+ self.app.exit()
86
108
 
87
- @on(Button.Pressed, "#initialize")
88
- def _on_initialize_pressed(self) -> None:
89
- self._initialize_directory()
109
+ @on(Button.Pressed, "#retry")
110
+ def _on_retry_pressed(self) -> None:
111
+ """Retry by dismissing the screen."""
112
+ self.dismiss()
90
113
 
91
114
  @on(Button.Pressed, "#exit")
92
115
  def _on_exit_pressed(self) -> None:
93
- self._exit_application()
94
-
95
- def _initialize_directory(self) -> None:
96
- try:
97
- path = ensure_shotgun_directory_exists()
98
- except Exception as exc: # pragma: no cover - defensive; textual path
99
- self.notify(f"Failed to initialize directory: {exc}", severity="error")
100
- return
101
-
102
- # Double-check a directory now exists; guard against unexpected filesystem state.
103
- if not path.is_dir():
104
- self.notify(
105
- "Unable to initialize .shotgun directory due to filesystem conflict.",
106
- severity="error",
107
- )
108
- return
109
-
110
- self.dismiss()
111
-
112
- def _exit_application(self) -> None:
116
+ """Exit the application."""
113
117
  self.app.exit()
@@ -76,6 +76,13 @@ class FeedbackScreen(Screen[Feedback | None]):
76
76
  #feedback-type-list {
77
77
  padding: 1;
78
78
  }
79
+
80
+ #feedback-status {
81
+ height: auto;
82
+ padding: 0 1;
83
+ min-height: 1;
84
+ color: $error;
85
+ }
79
86
  """
80
87
 
81
88
  BINDINGS = [
@@ -96,6 +103,7 @@ class FeedbackScreen(Screen[Feedback | None]):
96
103
  "",
97
104
  id="feedback-description",
98
105
  )
106
+ yield Label("", id="feedback-status")
99
107
  with Horizontal(id="feedback-actions"):
100
108
  yield Button("Submit", variant="primary", id="submit")
101
109
  yield Button("Cancel \\[ESC]", id="cancel")
@@ -176,9 +184,8 @@ class FeedbackScreen(Screen[Feedback | None]):
176
184
  description = text_area.text.strip()
177
185
 
178
186
  if not description:
179
- self.notify(
180
- "Please enter a description before submitting.", severity="error"
181
- )
187
+ status_label = self.query_one("#feedback-status", Label)
188
+ status_label.update("Please enter a description before submitting.")
182
189
  return
183
190
 
184
191
  app = cast("ShotgunApp", self.app)
@@ -6,7 +6,7 @@ from textual import on
6
6
  from textual.app import ComposeResult
7
7
  from textual.containers import Container, Vertical
8
8
  from textual.screen import ModalScreen
9
- from textual.widgets import Button, Markdown, Static
9
+ from textual.widgets import Button, Label, Markdown, Static
10
10
 
11
11
 
12
12
  class GitHubIssueScreen(ModalScreen[None]):
@@ -47,6 +47,13 @@ class GitHubIssueScreen(ModalScreen[None]):
47
47
  margin: 1 1;
48
48
  min-width: 20;
49
49
  }
50
+
51
+ #issue-status {
52
+ height: auto;
53
+ padding: 1;
54
+ min-height: 1;
55
+ text-align: center;
56
+ }
50
57
  """
51
58
 
52
59
  BINDINGS = [
@@ -85,6 +92,7 @@ We review all issues and will respond as soon as possible!
85
92
  id="issue-markdown",
86
93
  )
87
94
  with Vertical(id="issue-buttons"):
95
+ yield Label("", id="issue-status")
88
96
  yield Button(
89
97
  "🐙 Open GitHub Issues", id="github-button", variant="primary"
90
98
  )
@@ -94,7 +102,8 @@ We review all issues and will respond as soon as possible!
94
102
  def handle_github(self) -> None:
95
103
  """Open GitHub issues page in browser."""
96
104
  webbrowser.open("https://github.com/shotgun-sh/shotgun/issues")
97
- self.notify("Opening GitHub Issues in your browser...")
105
+ status_label = self.query_one("#issue-status", Label)
106
+ status_label.update("✓ Opening GitHub Issues in your browser...")
98
107
 
99
108
  @on(Button.Pressed, "#close-button")
100
109
  def handle_close(self) -> None:
@@ -72,6 +72,11 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
72
72
  padding: 1 0;
73
73
  }
74
74
  }
75
+ #model-picker-status {
76
+ height: auto;
77
+ padding: 0 1;
78
+ color: $error;
79
+ }
75
80
  #model-actions {
76
81
  padding: 1;
77
82
  }
@@ -84,7 +89,7 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
84
89
  ("escape", "done", "Back"),
85
90
  ]
86
91
 
87
- selected_model: reactive[ModelName] = reactive(ModelName.GPT_5)
92
+ selected_model: reactive[ModelName] = reactive(ModelName.GPT_5_1)
88
93
 
89
94
  def compose(self) -> ComposeResult:
90
95
  with Vertical(id="titlebox"):
@@ -94,6 +99,7 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
94
99
  id="model-picker-summary",
95
100
  )
96
101
  yield ListView(id="model-list")
102
+ yield Label("", id="model-picker-status")
97
103
  with Horizontal(id="model-actions"):
98
104
  yield Button("Select \\[ENTER]", variant="primary", id="select")
99
105
  yield Button("Done \\[ESC]", id="done")
@@ -287,6 +293,12 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
287
293
  )
288
294
  return has_key
289
295
 
296
+ def _format_tokens(self, tokens: int) -> str:
297
+ """Format token count for display (K for thousands, M for millions)."""
298
+ if tokens >= 1_000_000:
299
+ return f"{tokens / 1_000_000:.1f}M"
300
+ return f"{tokens // 1000}K"
301
+
290
302
  def _model_label(self, model_name: ModelName, is_current: bool) -> str:
291
303
  """Generate label for model with specs and current indicator."""
292
304
  if model_name not in MODEL_SPECS:
@@ -296,13 +308,13 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
296
308
  display_name = self._model_display_name(model_name)
297
309
 
298
310
  # Format context/output tokens in readable format
299
- input_k = spec.max_input_tokens // 1000
300
- output_k = spec.max_output_tokens // 1000
311
+ input_fmt = self._format_tokens(spec.max_input_tokens)
312
+ output_fmt = self._format_tokens(spec.max_output_tokens)
301
313
 
302
- label = f"{display_name} · {input_k}K context · {output_k}K output"
314
+ label = f"{display_name} · {input_fmt} context · {output_fmt} output"
303
315
 
304
316
  # Add cost indicator for expensive models
305
- if model_name == ModelName.CLAUDE_OPUS_4_1:
317
+ if model_name == ModelName.CLAUDE_OPUS_4_5:
306
318
  label += " · Expensive"
307
319
 
308
320
  if is_current:
@@ -313,10 +325,17 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
313
325
  def _model_display_name(self, model_name: ModelName) -> str:
314
326
  """Get human-readable model name."""
315
327
  names = {
316
- ModelName.GPT_5: "GPT-5 (OpenAI)",
317
- ModelName.CLAUDE_OPUS_4_1: "Claude Opus 4.1 (Anthropic)",
328
+ ModelName.GPT_5_1: "GPT-5.1 (OpenAI)",
329
+ ModelName.GPT_5_1_CODEX: "GPT-5.1 Codex (OpenAI)",
330
+ ModelName.GPT_5_1_CODEX_MINI: "GPT-5.1 Codex Mini (OpenAI)",
331
+ ModelName.CLAUDE_OPUS_4_5: "Claude Opus 4.5 (Anthropic)",
332
+ ModelName.CLAUDE_SONNET_4: "Claude Sonnet 4 (Anthropic)",
318
333
  ModelName.CLAUDE_SONNET_4_5: "Claude Sonnet 4.5 (Anthropic)",
334
+ ModelName.CLAUDE_HAIKU_4_5: "Claude Haiku 4.5 (Anthropic)",
319
335
  ModelName.GEMINI_2_5_PRO: "Gemini 2.5 Pro (Google)",
336
+ ModelName.GEMINI_2_5_FLASH: "Gemini 2.5 Flash (Google)",
337
+ ModelName.GEMINI_2_5_FLASH_LITE: "Gemini 2.5 Flash Lite (Google)",
338
+ ModelName.GEMINI_3_PRO_PREVIEW: "Gemini 3 Pro Preview (Google)",
320
339
  }
321
340
  return names.get(model_name, model_name.value)
322
341
 
@@ -349,4 +368,5 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
349
368
  )
350
369
  )
351
370
  except Exception as exc: # pragma: no cover - defensive; textual path
352
- self.notify(f"Failed to select model: {exc}", severity="error")
371
+ status_label = self.query_one("#model-picker-status", Label)
372
+ status_label.update(f"❌ Failed to select model: {exc}")