shotgun-sh 0.2.29.dev2__py3-none-any.whl β†’ 0.6.1.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.

Potentially problematic release.


This version of shotgun-sh might be problematic. Click here for more details.

Files changed (161) hide show
  1. shotgun/agents/agent_manager.py +497 -30
  2. shotgun/agents/cancellation.py +103 -0
  3. shotgun/agents/common.py +90 -77
  4. shotgun/agents/config/README.md +0 -1
  5. shotgun/agents/config/manager.py +52 -8
  6. shotgun/agents/config/models.py +48 -45
  7. shotgun/agents/config/provider.py +44 -29
  8. shotgun/agents/conversation/history/file_content_deduplication.py +66 -43
  9. shotgun/agents/conversation/history/token_counting/base.py +51 -9
  10. shotgun/agents/export.py +12 -13
  11. shotgun/agents/file_read.py +176 -0
  12. shotgun/agents/messages.py +15 -3
  13. shotgun/agents/models.py +90 -2
  14. shotgun/agents/plan.py +12 -13
  15. shotgun/agents/research.py +13 -10
  16. shotgun/agents/router/__init__.py +47 -0
  17. shotgun/agents/router/models.py +384 -0
  18. shotgun/agents/router/router.py +185 -0
  19. shotgun/agents/router/tools/__init__.py +18 -0
  20. shotgun/agents/router/tools/delegation_tools.py +557 -0
  21. shotgun/agents/router/tools/plan_tools.py +403 -0
  22. shotgun/agents/runner.py +17 -2
  23. shotgun/agents/specify.py +12 -13
  24. shotgun/agents/tasks.py +12 -13
  25. shotgun/agents/tools/__init__.py +8 -0
  26. shotgun/agents/tools/codebase/directory_lister.py +27 -39
  27. shotgun/agents/tools/codebase/file_read.py +26 -35
  28. shotgun/agents/tools/codebase/query_graph.py +9 -0
  29. shotgun/agents/tools/codebase/retrieve_code.py +9 -0
  30. shotgun/agents/tools/file_management.py +81 -3
  31. shotgun/agents/tools/file_read_tools/__init__.py +7 -0
  32. shotgun/agents/tools/file_read_tools/multimodal_file_read.py +167 -0
  33. shotgun/agents/tools/markdown_tools/__init__.py +62 -0
  34. shotgun/agents/tools/markdown_tools/insert_section.py +148 -0
  35. shotgun/agents/tools/markdown_tools/models.py +86 -0
  36. shotgun/agents/tools/markdown_tools/remove_section.py +114 -0
  37. shotgun/agents/tools/markdown_tools/replace_section.py +119 -0
  38. shotgun/agents/tools/markdown_tools/utils.py +453 -0
  39. shotgun/agents/tools/registry.py +41 -0
  40. shotgun/agents/tools/web_search/__init__.py +1 -2
  41. shotgun/agents/tools/web_search/gemini.py +1 -3
  42. shotgun/agents/tools/web_search/openai.py +42 -23
  43. shotgun/attachments/__init__.py +41 -0
  44. shotgun/attachments/errors.py +60 -0
  45. shotgun/attachments/models.py +107 -0
  46. shotgun/attachments/parser.py +257 -0
  47. shotgun/attachments/processor.py +193 -0
  48. shotgun/cli/clear.py +2 -2
  49. shotgun/cli/codebase/commands.py +181 -65
  50. shotgun/cli/compact.py +2 -2
  51. shotgun/cli/context.py +2 -2
  52. shotgun/cli/run.py +90 -0
  53. shotgun/cli/spec/backup.py +2 -1
  54. shotgun/cli/spec/commands.py +2 -0
  55. shotgun/cli/spec/models.py +18 -0
  56. shotgun/cli/spec/pull_service.py +122 -68
  57. shotgun/codebase/__init__.py +2 -0
  58. shotgun/codebase/benchmarks/__init__.py +35 -0
  59. shotgun/codebase/benchmarks/benchmark_runner.py +309 -0
  60. shotgun/codebase/benchmarks/exporters.py +119 -0
  61. shotgun/codebase/benchmarks/formatters/__init__.py +49 -0
  62. shotgun/codebase/benchmarks/formatters/base.py +34 -0
  63. shotgun/codebase/benchmarks/formatters/json_formatter.py +106 -0
  64. shotgun/codebase/benchmarks/formatters/markdown.py +136 -0
  65. shotgun/codebase/benchmarks/models.py +129 -0
  66. shotgun/codebase/core/__init__.py +4 -0
  67. shotgun/codebase/core/call_resolution.py +91 -0
  68. shotgun/codebase/core/change_detector.py +11 -6
  69. shotgun/codebase/core/errors.py +159 -0
  70. shotgun/codebase/core/extractors/__init__.py +23 -0
  71. shotgun/codebase/core/extractors/base.py +138 -0
  72. shotgun/codebase/core/extractors/factory.py +63 -0
  73. shotgun/codebase/core/extractors/go/__init__.py +7 -0
  74. shotgun/codebase/core/extractors/go/extractor.py +122 -0
  75. shotgun/codebase/core/extractors/javascript/__init__.py +7 -0
  76. shotgun/codebase/core/extractors/javascript/extractor.py +132 -0
  77. shotgun/codebase/core/extractors/protocol.py +109 -0
  78. shotgun/codebase/core/extractors/python/__init__.py +7 -0
  79. shotgun/codebase/core/extractors/python/extractor.py +141 -0
  80. shotgun/codebase/core/extractors/rust/__init__.py +7 -0
  81. shotgun/codebase/core/extractors/rust/extractor.py +139 -0
  82. shotgun/codebase/core/extractors/types.py +15 -0
  83. shotgun/codebase/core/extractors/typescript/__init__.py +7 -0
  84. shotgun/codebase/core/extractors/typescript/extractor.py +92 -0
  85. shotgun/codebase/core/gitignore.py +252 -0
  86. shotgun/codebase/core/ingestor.py +644 -354
  87. shotgun/codebase/core/kuzu_compat.py +119 -0
  88. shotgun/codebase/core/language_config.py +239 -0
  89. shotgun/codebase/core/manager.py +256 -46
  90. shotgun/codebase/core/metrics_collector.py +310 -0
  91. shotgun/codebase/core/metrics_types.py +347 -0
  92. shotgun/codebase/core/parallel_executor.py +424 -0
  93. shotgun/codebase/core/work_distributor.py +254 -0
  94. shotgun/codebase/core/worker.py +768 -0
  95. shotgun/codebase/indexing_state.py +86 -0
  96. shotgun/codebase/models.py +94 -0
  97. shotgun/codebase/service.py +13 -0
  98. shotgun/exceptions.py +1 -1
  99. shotgun/main.py +2 -10
  100. shotgun/prompts/agents/export.j2 +2 -0
  101. shotgun/prompts/agents/file_read.j2 +48 -0
  102. shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +20 -28
  103. shotgun/prompts/agents/partials/content_formatting.j2 +12 -33
  104. shotgun/prompts/agents/partials/interactive_mode.j2 +9 -32
  105. shotgun/prompts/agents/partials/router_delegation_mode.j2 +35 -0
  106. shotgun/prompts/agents/plan.j2 +43 -1
  107. shotgun/prompts/agents/research.j2 +75 -20
  108. shotgun/prompts/agents/router.j2 +713 -0
  109. shotgun/prompts/agents/specify.j2 +94 -4
  110. shotgun/prompts/agents/state/codebase/codebase_graphs_available.j2 +14 -1
  111. shotgun/prompts/agents/state/system_state.j2 +24 -15
  112. shotgun/prompts/agents/tasks.j2 +77 -23
  113. shotgun/settings.py +44 -0
  114. shotgun/shotgun_web/shared_specs/upload_pipeline.py +38 -0
  115. shotgun/tui/app.py +90 -23
  116. shotgun/tui/commands/__init__.py +9 -1
  117. shotgun/tui/components/attachment_bar.py +87 -0
  118. shotgun/tui/components/mode_indicator.py +120 -25
  119. shotgun/tui/components/prompt_input.py +23 -28
  120. shotgun/tui/components/status_bar.py +5 -4
  121. shotgun/tui/dependencies.py +58 -8
  122. shotgun/tui/protocols.py +37 -0
  123. shotgun/tui/screens/chat/chat.tcss +24 -1
  124. shotgun/tui/screens/chat/chat_screen.py +1374 -211
  125. shotgun/tui/screens/chat/codebase_index_prompt_screen.py +8 -4
  126. shotgun/tui/screens/chat_screen/attachment_hint.py +40 -0
  127. shotgun/tui/screens/chat_screen/command_providers.py +0 -97
  128. shotgun/tui/screens/chat_screen/history/agent_response.py +7 -3
  129. shotgun/tui/screens/chat_screen/history/chat_history.py +49 -6
  130. shotgun/tui/screens/chat_screen/history/formatters.py +75 -15
  131. shotgun/tui/screens/chat_screen/history/partial_response.py +11 -1
  132. shotgun/tui/screens/chat_screen/history/user_question.py +25 -3
  133. shotgun/tui/screens/chat_screen/messages.py +219 -0
  134. shotgun/tui/screens/database_locked_dialog.py +219 -0
  135. shotgun/tui/screens/database_timeout_dialog.py +158 -0
  136. shotgun/tui/screens/kuzu_error_dialog.py +135 -0
  137. shotgun/tui/screens/model_picker.py +14 -9
  138. shotgun/tui/screens/models.py +11 -0
  139. shotgun/tui/screens/shotgun_auth.py +50 -0
  140. shotgun/tui/screens/spec_pull.py +2 -0
  141. shotgun/tui/state/processing_state.py +19 -0
  142. shotgun/tui/utils/mode_progress.py +20 -86
  143. shotgun/tui/widgets/__init__.py +2 -1
  144. shotgun/tui/widgets/approval_widget.py +152 -0
  145. shotgun/tui/widgets/cascade_confirmation_widget.py +203 -0
  146. shotgun/tui/widgets/plan_panel.py +129 -0
  147. shotgun/tui/widgets/step_checkpoint_widget.py +180 -0
  148. shotgun/tui/widgets/widget_coordinator.py +18 -0
  149. shotgun/utils/file_system_utils.py +4 -1
  150. {shotgun_sh-0.2.29.dev2.dist-info β†’ shotgun_sh-0.6.1.dev1.dist-info}/METADATA +88 -34
  151. shotgun_sh-0.6.1.dev1.dist-info/RECORD +292 -0
  152. shotgun/cli/export.py +0 -81
  153. shotgun/cli/plan.py +0 -73
  154. shotgun/cli/research.py +0 -93
  155. shotgun/cli/specify.py +0 -70
  156. shotgun/cli/tasks.py +0 -78
  157. shotgun/tui/screens/onboarding.py +0 -580
  158. shotgun_sh-0.2.29.dev2.dist-info/RECORD +0 -229
  159. {shotgun_sh-0.2.29.dev2.dist-info β†’ shotgun_sh-0.6.1.dev1.dist-info}/WHEEL +0 -0
  160. {shotgun_sh-0.2.29.dev2.dist-info β†’ shotgun_sh-0.6.1.dev1.dist-info}/entry_points.txt +0 -0
  161. {shotgun_sh-0.2.29.dev2.dist-info β†’ shotgun_sh-0.6.1.dev1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,135 @@
1
+ """Error dialog for Windows kuzu/graph database import failures."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from textual.app import ComposeResult
6
+ from textual.containers import Container, Horizontal
7
+ from textual.screen import ModalScreen
8
+ from textual.widgets import Button, Label, Static
9
+
10
+ from shotgun.codebase.core.kuzu_compat import (
11
+ _VC_INSTALL_SCRIPT,
12
+ _VC_REDIST_URL,
13
+ copy_vcpp_script_to_clipboard,
14
+ open_vcpp_download_page,
15
+ )
16
+ from shotgun.logging_config import get_logger
17
+
18
+ logger = get_logger(__name__)
19
+
20
+
21
+ class KuzuErrorDialog(ModalScreen[bool]):
22
+ """Error dialog for Windows kuzu import failures with copy/open buttons."""
23
+
24
+ DEFAULT_CSS = """
25
+ KuzuErrorDialog {
26
+ align: center middle;
27
+ background: rgba(0, 0, 0, 0.5);
28
+ }
29
+
30
+ KuzuErrorDialog > #dialog-container {
31
+ width: 90%;
32
+ max-width: 100;
33
+ height: auto;
34
+ max-height: 90%;
35
+ border: wide $error;
36
+ padding: 1 2;
37
+ layout: vertical;
38
+ background: $surface;
39
+ }
40
+
41
+ #error-title {
42
+ text-style: bold;
43
+ color: $error;
44
+ padding-bottom: 1;
45
+ }
46
+
47
+ #error-message {
48
+ padding: 1 0;
49
+ }
50
+
51
+ #script-display {
52
+ padding: 1;
53
+ margin: 1 0;
54
+ background: $surface-darken-1;
55
+ border: round $primary;
56
+ overflow-x: auto;
57
+ }
58
+
59
+ #status-label {
60
+ color: $success;
61
+ padding: 1 0;
62
+ min-height: 1;
63
+ }
64
+
65
+ #dialog-buttons {
66
+ layout: horizontal;
67
+ align-horizontal: center;
68
+ height: auto;
69
+ padding-top: 1;
70
+ }
71
+
72
+ #dialog-buttons Button {
73
+ margin: 0 1;
74
+ }
75
+ """
76
+
77
+ BINDINGS = [
78
+ ("escape", "close", "Close"),
79
+ ]
80
+
81
+ def compose(self) -> ComposeResult:
82
+ with Container(id="dialog-container"):
83
+ yield Label("Code Indexing Requires Visual C++", id="error-title")
84
+ yield Static(
85
+ "The graph database library requires the Visual C++ Redistributable "
86
+ "to be installed on Windows.\n\n"
87
+ "Run this PowerShell script as Administrator:",
88
+ id="error-message",
89
+ )
90
+ yield Static(_VC_INSTALL_SCRIPT, id="script-display")
91
+ yield Static("", id="status-label")
92
+ with Horizontal(id="dialog-buttons"):
93
+ yield Button(
94
+ "Copy Script to Clipboard", id="copy-btn", variant="primary"
95
+ )
96
+ yield Button("Open Download Page", id="open-btn", variant="default")
97
+ yield Button("Close", id="close-btn", variant="default")
98
+
99
+ def on_button_pressed(self, event: Button.Pressed) -> None:
100
+ """Handle button presses."""
101
+ event.stop()
102
+
103
+ if event.button.id == "copy-btn":
104
+ self._copy_script()
105
+ elif event.button.id == "open-btn":
106
+ self._open_download()
107
+ elif event.button.id == "close-btn":
108
+ self.dismiss(True)
109
+
110
+ def _copy_script(self) -> None:
111
+ """Copy the PowerShell installation script to clipboard."""
112
+ status_label = self.query_one("#status-label", Static)
113
+
114
+ if copy_vcpp_script_to_clipboard():
115
+ status_label.update("Copied script to clipboard!")
116
+ self.query_one("#copy-btn", Button).label = "Copied!"
117
+ logger.debug("Copied VC++ installation script to clipboard")
118
+ else:
119
+ status_label.update(f"Could not copy. Download from: {_VC_REDIST_URL}")
120
+ logger.warning("Failed to copy VC++ script to clipboard")
121
+
122
+ def _open_download(self) -> None:
123
+ """Open the download page in the default browser."""
124
+ status_label = self.query_one("#status-label", Static)
125
+
126
+ if open_vcpp_download_page():
127
+ status_label.update("Opened download page in browser")
128
+ logger.debug(f"Opened VC++ download page: {_VC_REDIST_URL}")
129
+ else:
130
+ status_label.update(f"Could not open browser. URL: {_VC_REDIST_URL}")
131
+ logger.warning("Failed to open VC++ download page")
132
+
133
+ def action_close(self) -> None:
134
+ """Close the dialog."""
135
+ self.dismiss(True)
@@ -293,6 +293,12 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
293
293
  )
294
294
  return has_key
295
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
+
296
302
  def _model_label(self, model_name: ModelName, is_current: bool) -> str:
297
303
  """Generate label for model with specs and current indicator."""
298
304
  if model_name not in MODEL_SPECS:
@@ -302,13 +308,13 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
302
308
  display_name = self._model_display_name(model_name)
303
309
 
304
310
  # Format context/output tokens in readable format
305
- input_k = spec.max_input_tokens // 1000
306
- 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)
307
313
 
308
- label = f"{display_name} Β· {input_k}K context Β· {output_k}K output"
314
+ label = f"{display_name} Β· {input_fmt} context Β· {output_fmt} output"
309
315
 
310
316
  # Add cost indicator for expensive models
311
- if model_name == ModelName.CLAUDE_OPUS_4_1:
317
+ if model_name == ModelName.CLAUDE_OPUS_4_5:
312
318
  label += " Β· Expensive"
313
319
 
314
320
  if is_current:
@@ -319,16 +325,15 @@ class ModelPickerScreen(Screen[ModelConfigUpdated | None]):
319
325
  def _model_display_name(self, model_name: ModelName) -> str:
320
326
  """Get human-readable model name."""
321
327
  names = {
322
- ModelName.GPT_5: "GPT-5 (OpenAI)",
323
- ModelName.GPT_5_MINI: "GPT-5 Mini (OpenAI)",
324
328
  ModelName.GPT_5_1: "GPT-5.1 (OpenAI)",
325
- ModelName.GPT_5_1_CODEX: "GPT-5.1 Codex (OpenAI)",
326
- ModelName.GPT_5_1_CODEX_MINI: "GPT-5.1 Codex Mini (OpenAI)",
327
- ModelName.CLAUDE_OPUS_4_1: "Claude Opus 4.1 (Anthropic)",
329
+ ModelName.GPT_5_2: "GPT-5.2 (OpenAI)",
330
+ ModelName.CLAUDE_OPUS_4_5: "Claude Opus 4.5 (Anthropic)",
328
331
  ModelName.CLAUDE_SONNET_4_5: "Claude Sonnet 4.5 (Anthropic)",
329
332
  ModelName.CLAUDE_HAIKU_4_5: "Claude Haiku 4.5 (Anthropic)",
330
333
  ModelName.GEMINI_2_5_PRO: "Gemini 2.5 Pro (Google)",
331
334
  ModelName.GEMINI_2_5_FLASH: "Gemini 2.5 Flash (Google)",
335
+ ModelName.GEMINI_2_5_FLASH_LITE: "Gemini 2.5 Flash Lite (Google)",
336
+ ModelName.GEMINI_3_PRO_PREVIEW: "Gemini 3 Pro Preview (Google)",
332
337
  }
333
338
  return names.get(model_name, model_name.value)
334
339
 
@@ -0,0 +1,11 @@
1
+ """Models and enums for TUI screens."""
2
+
3
+ from enum import StrEnum
4
+
5
+
6
+ class LockedDialogAction(StrEnum):
7
+ """Actions available in the database locked dialog."""
8
+
9
+ RETRY = "retry"
10
+ DELETE = "delete"
11
+ QUIT = "quit"
@@ -1,6 +1,7 @@
1
1
  """Shotgun Account authentication screen."""
2
2
 
3
3
  import asyncio
4
+ import time
4
5
  import webbrowser
5
6
  from typing import TYPE_CHECKING, cast
6
7
 
@@ -15,6 +16,7 @@ from textual.worker import Worker, WorkerState
15
16
 
16
17
  from shotgun.agents.config import ConfigManager
17
18
  from shotgun.logging_config import get_logger
19
+ from shotgun.posthog_telemetry import track_event
18
20
  from shotgun.shotgun_web import (
19
21
  ShotgunWebClient,
20
22
  TokenStatus,
@@ -118,6 +120,7 @@ class ShotgunAuthScreen(Screen[bool]):
118
120
  self.token: str | None = None
119
121
  self.auth_url: str | None = None
120
122
  self.poll_worker: Worker[None] | None = None
123
+ self._auth_start_time: float | None = None
121
124
 
122
125
  def compose(self) -> ComposeResult:
123
126
  with Vertical(id="titlebox"):
@@ -158,6 +161,7 @@ class ShotgunAuthScreen(Screen[bool]):
158
161
 
159
162
  def action_cancel(self) -> None:
160
163
  """Cancel authentication and close screen."""
164
+ track_event("auth_cancelled")
161
165
  if self.poll_worker and self.poll_worker.state == WorkerState.RUNNING:
162
166
  self.poll_worker.cancel()
163
167
  self.dismiss(False)
@@ -174,6 +178,9 @@ class ShotgunAuthScreen(Screen[bool]):
174
178
 
175
179
  async def _start_auth_flow(self) -> None:
176
180
  """Start the authentication flow."""
181
+ self._auth_start_time = time.time()
182
+ track_event("auth_started")
183
+
177
184
  try:
178
185
  # Get shotgun instance ID from config
179
186
  shotgun_instance_id = await self.config_manager.get_shotgun_instance_id()
@@ -220,12 +227,20 @@ class ShotgunAuthScreen(Screen[bool]):
220
227
 
221
228
  except httpx.HTTPError as e:
222
229
  logger.error("Failed to create auth token: %s", e)
230
+ track_event(
231
+ "auth_failed",
232
+ {"phase": "token_creation", "error_type": type(e).__name__},
233
+ )
223
234
  self.query_one("#status", Label).update(
224
235
  f"❌ Error: Failed to create authentication token\n{e}"
225
236
  )
226
237
 
227
238
  except Exception as e:
228
239
  logger.error("Unexpected error during auth flow: %s", e)
240
+ track_event(
241
+ "auth_failed",
242
+ {"phase": "token_creation", "error_type": type(e).__name__},
243
+ )
229
244
  self.query_one("#status", Label).update(f"❌ Unexpected error: {e}")
230
245
 
231
246
  async def _poll_token_status(self) -> None:
@@ -272,6 +287,17 @@ class ShotgunAuthScreen(Screen[bool]):
272
287
  workspace_id=workspace_id,
273
288
  )
274
289
 
290
+ # Track successful auth
291
+ duration = (
292
+ time.time() - self._auth_start_time
293
+ if self._auth_start_time
294
+ else 0
295
+ )
296
+ track_event(
297
+ "auth_completed",
298
+ {"duration_seconds": round(duration, 2)},
299
+ )
300
+
275
301
  self.query_one("#status", Label).update(
276
302
  "βœ… Authentication successful! Saving credentials..."
277
303
  )
@@ -279,6 +305,10 @@ class ShotgunAuthScreen(Screen[bool]):
279
305
  self.dismiss(True)
280
306
  else:
281
307
  logger.error("Completed but missing keys")
308
+ track_event(
309
+ "auth_failed",
310
+ {"phase": "polling", "error_type": "MissingKeys"},
311
+ )
282
312
  self.query_one("#status", Label).update(
283
313
  "❌ Error: Authentication completed but keys are missing"
284
314
  )
@@ -293,6 +323,10 @@ class ShotgunAuthScreen(Screen[bool]):
293
323
 
294
324
  elif status_response.status == TokenStatus.EXPIRED:
295
325
  logger.error("Token expired")
326
+ track_event(
327
+ "auth_failed",
328
+ {"phase": "token_expired", "error_type": "TokenExpired"},
329
+ )
296
330
  self.query_one("#status", Label).update(
297
331
  "❌ Authentication token expired (30 minutes)\n"
298
332
  "Please try again."
@@ -312,6 +346,10 @@ class ShotgunAuthScreen(Screen[bool]):
312
346
  if e.response.status_code == 410:
313
347
  # Token expired
314
348
  logger.error("Token expired (410)")
349
+ track_event(
350
+ "auth_failed",
351
+ {"phase": "token_expired", "error_type": "TokenExpired"},
352
+ )
315
353
  self.query_one("#status", Label).update(
316
354
  "❌ Authentication token expired"
317
355
  )
@@ -320,6 +358,10 @@ class ShotgunAuthScreen(Screen[bool]):
320
358
  return
321
359
  else:
322
360
  logger.error("HTTP error polling status: %s", e)
361
+ track_event(
362
+ "auth_failed",
363
+ {"phase": "polling_error", "error_type": type(e).__name__},
364
+ )
323
365
  self.query_one("#status", Label).update(
324
366
  f"❌ Error checking status: {e}"
325
367
  )
@@ -327,11 +369,19 @@ class ShotgunAuthScreen(Screen[bool]):
327
369
 
328
370
  except Exception as e:
329
371
  logger.error("Error polling token status: %s", e)
372
+ track_event(
373
+ "auth_failed",
374
+ {"phase": "polling_error", "error_type": type(e).__name__},
375
+ )
330
376
  self.query_one("#status", Label).update(f"⚠️ Error checking status: {e}")
331
377
  await asyncio.sleep(5) # Wait a bit longer on error
332
378
 
333
379
  # Timeout reached
334
380
  logger.error("Polling timeout reached")
381
+ track_event(
382
+ "auth_failed",
383
+ {"phase": "timeout", "error_type": "Timeout"},
384
+ )
335
385
  self.query_one("#status", Label).update(
336
386
  "❌ Authentication timeout (30 minutes)\nPlease try again."
337
387
  )
@@ -8,6 +8,7 @@ from textual.screen import ModalScreen
8
8
  from textual.widgets import Button, Label, ProgressBar, Static
9
9
  from textual.worker import Worker, get_current_worker
10
10
 
11
+ from shotgun.cli.spec.models import PullSource
11
12
  from shotgun.cli.spec.pull_service import (
12
13
  CancelledError,
13
14
  PullProgress,
@@ -187,6 +188,7 @@ class SpecPullScreen(ModalScreen[bool]):
187
188
  shotgun_dir=shotgun_dir,
188
189
  on_progress=on_progress,
189
190
  is_cancelled=lambda: worker.is_cancelled,
191
+ source=PullSource.TUI,
190
192
  )
191
193
 
192
194
  if result.success:
@@ -7,6 +7,7 @@ This module provides centralized management of processing state including:
7
7
  - Providing clean cancellation API
8
8
  """
9
9
 
10
+ import asyncio
10
11
  from typing import TYPE_CHECKING, Any
11
12
 
12
13
  from shotgun.logging_config import get_logger
@@ -61,6 +62,7 @@ class ProcessingStateManager:
61
62
  self._spinner_widget: Spinner | None = None
62
63
  self._default_spinner_text = "Processing..."
63
64
  self._telemetry_context = telemetry_context or {}
65
+ self._cancellation_event: asyncio.Event | None = None
64
66
 
65
67
  @property
66
68
  def is_working(self) -> bool:
@@ -71,6 +73,15 @@ class ProcessingStateManager:
71
73
  """
72
74
  return self._working
73
75
 
76
+ @property
77
+ def cancellation_event(self) -> asyncio.Event | None:
78
+ """Get the current cancellation event for agent deps.
79
+
80
+ Returns:
81
+ The cancellation event for the current operation, or None if not processing
82
+ """
83
+ return self._cancellation_event
84
+
74
85
  def bind_spinner(self, spinner: "Spinner") -> None:
75
86
  """Bind a spinner widget for state coordination.
76
87
 
@@ -95,6 +106,9 @@ class ProcessingStateManager:
95
106
  self._working = True
96
107
  text = spinner_text or self._default_spinner_text
97
108
 
109
+ # Create a new cancellation event for this operation
110
+ self._cancellation_event = asyncio.Event()
111
+
98
112
  # Update screen's reactive working state
99
113
  if hasattr(self.screen, "working"):
100
114
  self.screen.working = True
@@ -113,6 +127,7 @@ class ProcessingStateManager:
113
127
 
114
128
  self._working = False
115
129
  self._current_worker = None
130
+ self._cancellation_event = None
116
131
 
117
132
  # Update screen's reactive working state
118
133
  if hasattr(self.screen, "working"):
@@ -151,6 +166,10 @@ class ProcessingStateManager:
151
166
  return False
152
167
 
153
168
  try:
169
+ # Set the cancellation event first for immediate effect on streaming
170
+ if self._cancellation_event:
171
+ self._cancellation_event.set()
172
+
154
173
  self._current_worker.cancel()
155
174
  logger.info("Operation cancelled successfully")
156
175
 
@@ -113,93 +113,24 @@ class ModeProgressChecker:
113
113
 
114
114
 
115
115
  class PlaceholderHints:
116
- """Manages dynamic placeholder hints for each mode based on progress."""
116
+ """Manages dynamic placeholder hints for the Router agent."""
117
117
 
118
- # Placeholder variations for each mode and state
118
+ # Placeholder variations for Router mode
119
119
  HINTS = {
120
- # Research mode
121
- AgentType.RESEARCH: {
120
+ AgentType.ROUTER: {
122
121
  False: [
123
- "Research a product or idea (SHIFT+TAB to cycle modes)",
124
- "What would you like to explore? Start your research journey here (SHIFT+TAB to switch modes)",
125
- "Dive into discovery mode - research anything that sparks curiosity (SHIFT+TAB for mode menu)",
126
- "Ready to investigate? Feed me your burning questions (SHIFT+TAB to explore other modes)",
127
- " πŸ” The research rabbit hole awaits! What shall we uncover? (SHIFT+TAB for mode carousel)",
122
+ "What would you like to work on? (SHIFT+TAB to toggle Planning/Drafting)",
123
+ "Ask me to research, plan, or implement anything (SHIFT+TAB toggles mode)",
124
+ "Describe your goal and I'll help break it down (SHIFT+TAB for mode toggle)",
125
+ "Ready to help with research, specs, plans, or tasks (SHIFT+TAB toggles mode)",
126
+ "Tell me what you need - I'll coordinate the work (SHIFT+TAB for Planning/Drafting)",
128
127
  ],
129
128
  True: [
130
- "Research complete! SHIFT+TAB to move to Specify mode",
131
- "Great research! Time to specify (SHIFT+TAB to Specify mode)",
132
- "Research done! Ready to create specifications (SHIFT+TAB to Specify)",
133
- "Findings gathered! Move to specifications (SHIFT+TAB for Specify mode)",
134
- " 🎯 Research complete! Advance to Specify mode (SHIFT+TAB)",
135
- ],
136
- },
137
- # Specify mode
138
- AgentType.SPECIFY: {
139
- False: [
140
- "Create detailed specifications and requirements (SHIFT+TAB to switch modes)",
141
- "Define your project specifications here (SHIFT+TAB to navigate modes)",
142
- "Time to get specific - write comprehensive specs (SHIFT+TAB for mode options)",
143
- "Specification station: Document requirements and designs (SHIFT+TAB to change modes)",
144
- " πŸ“‹ Spec-tacular time! Let's architect your ideas (SHIFT+TAB for mode magic)",
145
- ],
146
- True: [
147
- "Specifications complete! SHIFT+TAB to create a Plan",
148
- "Specs ready! Time to plan (SHIFT+TAB to Plan mode)",
149
- "Requirements defined! Move to planning (SHIFT+TAB to Plan)",
150
- "Specifications done! Create your roadmap (SHIFT+TAB for Plan mode)",
151
- " πŸš€ Specs complete! Advance to Plan mode (SHIFT+TAB)",
152
- ],
153
- },
154
- # Tasks mode
155
- AgentType.TASKS: {
156
- False: [
157
- "Break down your project into actionable tasks (SHIFT+TAB for modes)",
158
- "Task creation time! Define your implementation steps (SHIFT+TAB to switch)",
159
- "Ready to get tactical? Create your task list (SHIFT+TAB for mode options)",
160
- "Task command center: Organize your work items (SHIFT+TAB to navigate)",
161
- " βœ… Task mode activated! Break it down into bite-sized pieces (SHIFT+TAB)",
162
- ],
163
- True: [
164
- "Tasks defined! Ready to export or cycle back (SHIFT+TAB)",
165
- "Task list complete! Export your work (SHIFT+TAB to Export)",
166
- "All tasks created! Time to export (SHIFT+TAB for Export mode)",
167
- "Implementation plan ready! Export everything (SHIFT+TAB to Export)",
168
- " 🎊 Tasks complete! Export your masterpiece (SHIFT+TAB)",
169
- ],
170
- },
171
- # Export mode
172
- AgentType.EXPORT: {
173
- False: [
174
- "Export your complete project documentation (SHIFT+TAB for modes)",
175
- "Ready to package everything? Export time! (SHIFT+TAB to switch)",
176
- "Export station: Generate deliverables (SHIFT+TAB for mode menu)",
177
- "Time to share your work! Export documents (SHIFT+TAB to navigate)",
178
- " πŸ“¦ Export mode! Package and share your creation (SHIFT+TAB)",
179
- ],
180
- True: [
181
- "Exported! Start new research or continue refining (SHIFT+TAB)",
182
- "Export complete! New cycle begins (SHIFT+TAB to Research)",
183
- "All exported! Ready for another round (SHIFT+TAB for Research)",
184
- "Documents exported! Start fresh (SHIFT+TAB to Research mode)",
185
- " πŸŽ‰ Export complete! Begin a new adventure (SHIFT+TAB)",
186
- ],
187
- },
188
- # Plan mode
189
- AgentType.PLAN: {
190
- False: [
191
- "Create a strategic plan for your project (SHIFT+TAB for modes)",
192
- "Planning phase: Map out your roadmap (SHIFT+TAB to switch)",
193
- "Time to strategize! Create your project plan (SHIFT+TAB for options)",
194
- "Plan your approach and milestones (SHIFT+TAB to navigate)",
195
- " πŸ—ΊοΈ Plan mode! Chart your course to success (SHIFT+TAB)",
196
- ],
197
- True: [
198
- "Plan complete! Move to Tasks mode (SHIFT+TAB)",
199
- "Strategy ready! Time for tasks (SHIFT+TAB to Tasks mode)",
200
- "Roadmap done! Create task list (SHIFT+TAB for Tasks)",
201
- "Planning complete! Break into tasks (SHIFT+TAB to Tasks)",
202
- " ⚑ Plan ready! Advance to Tasks mode (SHIFT+TAB)",
129
+ "Continue working or start something new (SHIFT+TAB toggles mode)",
130
+ "What's next? (SHIFT+TAB to toggle Planning/Drafting)",
131
+ "Ready for the next task (SHIFT+TAB toggles Planning/Drafting)",
132
+ "Let's keep going! (SHIFT+TAB to toggle mode)",
133
+ "What else can I help with? (SHIFT+TAB for mode toggle)",
203
134
  ],
204
135
  },
205
136
  }
@@ -224,19 +155,22 @@ class PlaceholderHints:
224
155
  Returns:
225
156
  A contextual hint string for the placeholder.
226
157
  """
158
+ # Always use Router hints since Router is the only user-facing agent
159
+ mode_key = AgentType.ROUTER
160
+
227
161
  # Default hint if mode not configured
228
- if current_mode not in self.HINTS:
229
- return f"Enter your {current_mode.value} mode prompt (SHIFT+TAB to switch modes)"
162
+ if mode_key not in self.HINTS:
163
+ return "Enter your prompt (SHIFT+TAB to toggle Planning/Drafting mode)"
230
164
 
231
165
  # For placeholder text, we default to "no content" state (initial hints)
232
166
  # This avoids async file system checks in the UI rendering path
233
167
  has_content = False
234
168
 
235
169
  # Get hint variations for this mode and state
236
- hints_list = self.HINTS[current_mode][has_content]
170
+ hints_list = self.HINTS[mode_key][has_content]
237
171
 
238
172
  # Cache key for this mode and state
239
- cache_key = (current_mode, has_content)
173
+ cache_key = (mode_key, has_content)
240
174
 
241
175
  # Force refresh or first time
242
176
  if force_refresh or cache_key not in self._cached_hints:
@@ -1,5 +1,6 @@
1
1
  """Widget utilities and coordinators for TUI."""
2
2
 
3
+ from shotgun.tui.widgets.plan_panel import PlanPanelWidget
3
4
  from shotgun.tui.widgets.widget_coordinator import WidgetCoordinator
4
5
 
5
- __all__ = ["WidgetCoordinator"]
6
+ __all__ = ["PlanPanelWidget", "WidgetCoordinator"]