shotgun-sh 0.1.0.dev12__py3-none-any.whl → 0.1.0.dev13__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/common.py +94 -79
- shotgun/agents/config/constants.py +18 -0
- shotgun/agents/config/manager.py +68 -16
- shotgun/agents/config/provider.py +11 -6
- shotgun/agents/models.py +6 -0
- shotgun/agents/plan.py +15 -37
- shotgun/agents/research.py +10 -45
- shotgun/agents/specify.py +97 -0
- shotgun/agents/tasks.py +7 -36
- shotgun/agents/tools/artifact_management.py +450 -0
- shotgun/agents/tools/file_management.py +2 -2
- shotgun/artifacts/__init__.py +17 -0
- shotgun/artifacts/exceptions.py +89 -0
- shotgun/artifacts/manager.py +529 -0
- shotgun/artifacts/models.py +332 -0
- shotgun/artifacts/service.py +463 -0
- shotgun/artifacts/templates/__init__.py +10 -0
- shotgun/artifacts/templates/loader.py +252 -0
- shotgun/artifacts/templates/models.py +136 -0
- shotgun/artifacts/templates/plan/delivery_and_release_plan.yaml +66 -0
- shotgun/artifacts/templates/research/market_research.yaml +585 -0
- shotgun/artifacts/templates/research/sdk_comparison.yaml +257 -0
- shotgun/artifacts/templates/specify/prd.yaml +331 -0
- shotgun/artifacts/templates/specify/product_spec.yaml +301 -0
- shotgun/artifacts/utils.py +76 -0
- shotgun/cli/plan.py +1 -4
- shotgun/cli/specify.py +69 -0
- shotgun/cli/tasks.py +0 -4
- shotgun/logging_config.py +23 -7
- shotgun/main.py +7 -6
- shotgun/prompts/agents/partials/artifact_system.j2 +32 -0
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +28 -2
- shotgun/prompts/agents/partials/content_formatting.j2 +65 -0
- shotgun/prompts/agents/partials/interactive_mode.j2 +10 -2
- shotgun/prompts/agents/plan.j2 +31 -32
- shotgun/prompts/agents/research.j2 +37 -29
- shotgun/prompts/agents/specify.j2 +31 -0
- shotgun/prompts/agents/tasks.j2 +27 -12
- shotgun/sdk/artifact_models.py +186 -0
- shotgun/sdk/artifacts.py +448 -0
- shotgun/tui/app.py +26 -7
- shotgun/tui/screens/chat.py +28 -3
- shotgun/tui/screens/directory_setup.py +113 -0
- {shotgun_sh-0.1.0.dev12.dist-info → shotgun_sh-0.1.0.dev13.dist-info}/METADATA +2 -2
- {shotgun_sh-0.1.0.dev12.dist-info → shotgun_sh-0.1.0.dev13.dist-info}/RECORD +48 -25
- shotgun/prompts/user/research.j2 +0 -5
- {shotgun_sh-0.1.0.dev12.dist-info → shotgun_sh-0.1.0.dev13.dist-info}/WHEEL +0 -0
- {shotgun_sh-0.1.0.dev12.dist-info → shotgun_sh-0.1.0.dev13.dist-info}/entry_points.txt +0 -0
- {shotgun_sh-0.1.0.dev12.dist-info → shotgun_sh-0.1.0.dev13.dist-info}/licenses/LICENSE +0 -0
shotgun/sdk/artifacts.py
ADDED
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
"""Artifact SDK for framework-agnostic business logic."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from shotgun.artifacts.exceptions import (
|
|
8
|
+
ArtifactAlreadyExistsError,
|
|
9
|
+
ArtifactError,
|
|
10
|
+
ArtifactNotFoundError,
|
|
11
|
+
SectionAlreadyExistsError,
|
|
12
|
+
SectionNotFoundError,
|
|
13
|
+
)
|
|
14
|
+
from shotgun.artifacts.models import AgentMode, ArtifactSection
|
|
15
|
+
from shotgun.artifacts.service import ArtifactService
|
|
16
|
+
from shotgun.artifacts.utils import generate_artifact_name
|
|
17
|
+
|
|
18
|
+
from .artifact_models import (
|
|
19
|
+
ArtifactCreateResult,
|
|
20
|
+
ArtifactDeleteResult,
|
|
21
|
+
ArtifactErrorResult,
|
|
22
|
+
ArtifactInfoResult,
|
|
23
|
+
ArtifactListResult,
|
|
24
|
+
SectionContentResult,
|
|
25
|
+
SectionCreateResult,
|
|
26
|
+
SectionDeleteResult,
|
|
27
|
+
SectionUpdateResult,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ArtifactSDK:
|
|
32
|
+
"""Framework-agnostic SDK for artifact operations.
|
|
33
|
+
|
|
34
|
+
This SDK provides business logic for artifact management that can be
|
|
35
|
+
used by both CLI and TUI implementations without framework dependencies.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, base_path: Path | None = None):
|
|
39
|
+
"""Initialize SDK with optional base path.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
base_path: Optional custom base path for artifacts.
|
|
43
|
+
Defaults to .shotgun in current directory.
|
|
44
|
+
"""
|
|
45
|
+
self.service = ArtifactService(base_path)
|
|
46
|
+
|
|
47
|
+
# Artifact operations
|
|
48
|
+
|
|
49
|
+
def list_artifacts(
|
|
50
|
+
self, agent_mode: AgentMode | None = None
|
|
51
|
+
) -> ArtifactListResult | ArtifactErrorResult:
|
|
52
|
+
"""List all artifacts, optionally filtered by agent mode.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
agent_mode: Optional agent mode filter
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
ArtifactListResult containing list of artifacts or ArtifactErrorResult
|
|
59
|
+
"""
|
|
60
|
+
try:
|
|
61
|
+
artifacts = self.service.list_artifacts(agent_mode)
|
|
62
|
+
return ArtifactListResult(artifacts=artifacts, agent_mode=agent_mode)
|
|
63
|
+
except ArtifactError as e:
|
|
64
|
+
return ArtifactErrorResult(error_message=str(e), agent_mode=agent_mode)
|
|
65
|
+
|
|
66
|
+
def create_artifact(
|
|
67
|
+
self,
|
|
68
|
+
artifact_id: str,
|
|
69
|
+
agent_mode: AgentMode,
|
|
70
|
+
name: str,
|
|
71
|
+
template_id: str | None = None,
|
|
72
|
+
) -> ArtifactCreateResult | ArtifactErrorResult:
|
|
73
|
+
"""Create a new artifact.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
artifact_id: Unique identifier for the artifact
|
|
77
|
+
agent_mode: Agent mode this artifact belongs to
|
|
78
|
+
name: Human-readable name for the artifact
|
|
79
|
+
template_id: Optional template ID to use for creating the artifact
|
|
80
|
+
|
|
81
|
+
Returns:
|
|
82
|
+
ArtifactCreateResult or ArtifactErrorResult
|
|
83
|
+
"""
|
|
84
|
+
try:
|
|
85
|
+
self.service.create_artifact(artifact_id, agent_mode, name, template_id)
|
|
86
|
+
return ArtifactCreateResult(
|
|
87
|
+
artifact_id=artifact_id,
|
|
88
|
+
agent_mode=agent_mode,
|
|
89
|
+
name=name,
|
|
90
|
+
)
|
|
91
|
+
except ArtifactAlreadyExistsError as e:
|
|
92
|
+
return ArtifactErrorResult(
|
|
93
|
+
error_message="Artifact already exists",
|
|
94
|
+
artifact_id=artifact_id,
|
|
95
|
+
agent_mode=agent_mode,
|
|
96
|
+
details=str(e),
|
|
97
|
+
)
|
|
98
|
+
except ArtifactError as e:
|
|
99
|
+
return ArtifactErrorResult(
|
|
100
|
+
error_message=str(e),
|
|
101
|
+
artifact_id=artifact_id,
|
|
102
|
+
agent_mode=agent_mode,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def delete_artifact(
|
|
106
|
+
self,
|
|
107
|
+
artifact_id: str,
|
|
108
|
+
agent_mode: AgentMode,
|
|
109
|
+
confirm_callback: Callable[[str, AgentMode], bool] | None = None,
|
|
110
|
+
) -> ArtifactDeleteResult | ArtifactErrorResult:
|
|
111
|
+
"""Delete an artifact with optional confirmation.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
artifact_id: ID of the artifact to delete
|
|
115
|
+
agent_mode: Agent mode
|
|
116
|
+
confirm_callback: Optional callback for confirmation that receives
|
|
117
|
+
artifact_id and agent_mode and returns boolean.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
ArtifactDeleteResult or ArtifactErrorResult
|
|
121
|
+
"""
|
|
122
|
+
try:
|
|
123
|
+
# Handle confirmation callback if provided
|
|
124
|
+
if confirm_callback and not confirm_callback(artifact_id, agent_mode):
|
|
125
|
+
return ArtifactDeleteResult(
|
|
126
|
+
artifact_id=artifact_id,
|
|
127
|
+
agent_mode=agent_mode,
|
|
128
|
+
deleted=False,
|
|
129
|
+
cancelled=True,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
self.service.delete_artifact(artifact_id, agent_mode)
|
|
133
|
+
return ArtifactDeleteResult(
|
|
134
|
+
artifact_id=artifact_id,
|
|
135
|
+
agent_mode=agent_mode,
|
|
136
|
+
deleted=True,
|
|
137
|
+
)
|
|
138
|
+
except ArtifactNotFoundError as e:
|
|
139
|
+
return ArtifactErrorResult(
|
|
140
|
+
error_message="Artifact not found",
|
|
141
|
+
artifact_id=artifact_id,
|
|
142
|
+
agent_mode=agent_mode,
|
|
143
|
+
details=str(e),
|
|
144
|
+
)
|
|
145
|
+
except ArtifactError as e:
|
|
146
|
+
return ArtifactErrorResult(
|
|
147
|
+
error_message=str(e),
|
|
148
|
+
artifact_id=artifact_id,
|
|
149
|
+
agent_mode=agent_mode,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def get_artifact_info(
|
|
153
|
+
self, artifact_id: str, agent_mode: AgentMode
|
|
154
|
+
) -> ArtifactInfoResult | ArtifactErrorResult:
|
|
155
|
+
"""Get detailed information about an artifact.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
artifact_id: ID of the artifact to get info for
|
|
159
|
+
agent_mode: Agent mode
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
ArtifactInfoResult or ArtifactErrorResult
|
|
163
|
+
"""
|
|
164
|
+
try:
|
|
165
|
+
artifact = self.service.get_artifact(artifact_id, agent_mode, "")
|
|
166
|
+
return ArtifactInfoResult(artifact=artifact)
|
|
167
|
+
except ArtifactNotFoundError as e:
|
|
168
|
+
return ArtifactErrorResult(
|
|
169
|
+
error_message="Artifact not found",
|
|
170
|
+
artifact_id=artifact_id,
|
|
171
|
+
agent_mode=agent_mode,
|
|
172
|
+
details=str(e),
|
|
173
|
+
)
|
|
174
|
+
except ArtifactError as e:
|
|
175
|
+
return ArtifactErrorResult(
|
|
176
|
+
error_message=str(e),
|
|
177
|
+
artifact_id=artifact_id,
|
|
178
|
+
agent_mode=agent_mode,
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
# Section operations
|
|
182
|
+
|
|
183
|
+
def create_section(
|
|
184
|
+
self,
|
|
185
|
+
artifact_id: str,
|
|
186
|
+
agent_mode: AgentMode,
|
|
187
|
+
section_number: int,
|
|
188
|
+
section_slug: str,
|
|
189
|
+
section_title: str,
|
|
190
|
+
content: str = "",
|
|
191
|
+
) -> SectionCreateResult | ArtifactErrorResult:
|
|
192
|
+
"""Create a new section in an artifact.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
artifact_id: Artifact identifier
|
|
196
|
+
agent_mode: Agent mode
|
|
197
|
+
section_number: Section number
|
|
198
|
+
section_slug: Section slug
|
|
199
|
+
section_title: Section title
|
|
200
|
+
content: Section content
|
|
201
|
+
|
|
202
|
+
Returns:
|
|
203
|
+
SectionCreateResult or ArtifactErrorResult
|
|
204
|
+
"""
|
|
205
|
+
try:
|
|
206
|
+
section = ArtifactSection(
|
|
207
|
+
number=section_number,
|
|
208
|
+
slug=section_slug,
|
|
209
|
+
title=section_title,
|
|
210
|
+
content=content,
|
|
211
|
+
)
|
|
212
|
+
self.service.add_section(artifact_id, agent_mode, section)
|
|
213
|
+
return SectionCreateResult(
|
|
214
|
+
artifact_id=artifact_id,
|
|
215
|
+
agent_mode=agent_mode,
|
|
216
|
+
section_number=section_number,
|
|
217
|
+
section_title=section_title,
|
|
218
|
+
)
|
|
219
|
+
except (SectionAlreadyExistsError, ArtifactNotFoundError) as e:
|
|
220
|
+
return ArtifactErrorResult(
|
|
221
|
+
error_message=str(e),
|
|
222
|
+
artifact_id=artifact_id,
|
|
223
|
+
agent_mode=agent_mode,
|
|
224
|
+
section_number=section_number,
|
|
225
|
+
)
|
|
226
|
+
except ArtifactError as e:
|
|
227
|
+
return ArtifactErrorResult(
|
|
228
|
+
error_message=str(e),
|
|
229
|
+
artifact_id=artifact_id,
|
|
230
|
+
agent_mode=agent_mode,
|
|
231
|
+
section_number=section_number,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
def update_section(
|
|
235
|
+
self,
|
|
236
|
+
artifact_id: str,
|
|
237
|
+
agent_mode: AgentMode,
|
|
238
|
+
section_number: int,
|
|
239
|
+
**kwargs: Any,
|
|
240
|
+
) -> SectionUpdateResult | ArtifactErrorResult:
|
|
241
|
+
"""Update a section in an artifact.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
artifact_id: Artifact identifier
|
|
245
|
+
agent_mode: Agent mode
|
|
246
|
+
section_number: Section number to update
|
|
247
|
+
**kwargs: Fields to update
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
SectionUpdateResult or ArtifactErrorResult
|
|
251
|
+
"""
|
|
252
|
+
try:
|
|
253
|
+
self.service.update_section(
|
|
254
|
+
artifact_id, agent_mode, section_number, **kwargs
|
|
255
|
+
)
|
|
256
|
+
return SectionUpdateResult(
|
|
257
|
+
artifact_id=artifact_id,
|
|
258
|
+
agent_mode=agent_mode,
|
|
259
|
+
section_number=section_number,
|
|
260
|
+
updated_fields=list(kwargs.keys()),
|
|
261
|
+
)
|
|
262
|
+
except (SectionNotFoundError, ArtifactNotFoundError) as e:
|
|
263
|
+
return ArtifactErrorResult(
|
|
264
|
+
error_message=str(e),
|
|
265
|
+
artifact_id=artifact_id,
|
|
266
|
+
agent_mode=agent_mode,
|
|
267
|
+
section_number=section_number,
|
|
268
|
+
)
|
|
269
|
+
except ArtifactError as e:
|
|
270
|
+
return ArtifactErrorResult(
|
|
271
|
+
error_message=str(e),
|
|
272
|
+
artifact_id=artifact_id,
|
|
273
|
+
agent_mode=agent_mode,
|
|
274
|
+
section_number=section_number,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
def delete_section(
|
|
278
|
+
self,
|
|
279
|
+
artifact_id: str,
|
|
280
|
+
agent_mode: AgentMode,
|
|
281
|
+
section_number: int,
|
|
282
|
+
) -> SectionDeleteResult | ArtifactErrorResult:
|
|
283
|
+
"""Delete a section from an artifact.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
artifact_id: Artifact identifier
|
|
287
|
+
agent_mode: Agent mode
|
|
288
|
+
section_number: Section number to delete
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
SectionDeleteResult or ArtifactErrorResult
|
|
292
|
+
"""
|
|
293
|
+
try:
|
|
294
|
+
self.service.delete_section(artifact_id, agent_mode, section_number)
|
|
295
|
+
return SectionDeleteResult(
|
|
296
|
+
artifact_id=artifact_id,
|
|
297
|
+
agent_mode=agent_mode,
|
|
298
|
+
section_number=section_number,
|
|
299
|
+
)
|
|
300
|
+
except (SectionNotFoundError, ArtifactNotFoundError) as e:
|
|
301
|
+
return ArtifactErrorResult(
|
|
302
|
+
error_message=str(e),
|
|
303
|
+
artifact_id=artifact_id,
|
|
304
|
+
agent_mode=agent_mode,
|
|
305
|
+
section_number=section_number,
|
|
306
|
+
)
|
|
307
|
+
except ArtifactError as e:
|
|
308
|
+
return ArtifactErrorResult(
|
|
309
|
+
error_message=str(e),
|
|
310
|
+
artifact_id=artifact_id,
|
|
311
|
+
agent_mode=agent_mode,
|
|
312
|
+
section_number=section_number,
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
def get_section_content(
|
|
316
|
+
self,
|
|
317
|
+
artifact_id: str,
|
|
318
|
+
agent_mode: AgentMode,
|
|
319
|
+
section_number: int,
|
|
320
|
+
) -> SectionContentResult | ArtifactErrorResult:
|
|
321
|
+
"""Get the content of a section.
|
|
322
|
+
|
|
323
|
+
Args:
|
|
324
|
+
artifact_id: Artifact identifier
|
|
325
|
+
agent_mode: Agent mode
|
|
326
|
+
section_number: Section number
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
SectionContentResult or ArtifactErrorResult
|
|
330
|
+
"""
|
|
331
|
+
try:
|
|
332
|
+
content = self.service.get_section_content(
|
|
333
|
+
artifact_id, agent_mode, section_number
|
|
334
|
+
)
|
|
335
|
+
return SectionContentResult(
|
|
336
|
+
artifact_id=artifact_id,
|
|
337
|
+
agent_mode=agent_mode,
|
|
338
|
+
section_number=section_number,
|
|
339
|
+
content=content,
|
|
340
|
+
)
|
|
341
|
+
except (SectionNotFoundError, ArtifactNotFoundError) as e:
|
|
342
|
+
return ArtifactErrorResult(
|
|
343
|
+
error_message=str(e),
|
|
344
|
+
artifact_id=artifact_id,
|
|
345
|
+
agent_mode=agent_mode,
|
|
346
|
+
section_number=section_number,
|
|
347
|
+
)
|
|
348
|
+
except ArtifactError as e:
|
|
349
|
+
return ArtifactErrorResult(
|
|
350
|
+
error_message=str(e),
|
|
351
|
+
artifact_id=artifact_id,
|
|
352
|
+
agent_mode=agent_mode,
|
|
353
|
+
section_number=section_number,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
# Template operations
|
|
357
|
+
|
|
358
|
+
def list_templates(
|
|
359
|
+
self, agent_mode: AgentMode | None = None
|
|
360
|
+
) -> list[Any] | ArtifactErrorResult:
|
|
361
|
+
"""List available artifact templates.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
agent_mode: Optional agent mode filter
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
List of template summaries or ArtifactErrorResult
|
|
368
|
+
"""
|
|
369
|
+
try:
|
|
370
|
+
return self.service.list_templates(agent_mode)
|
|
371
|
+
except Exception as e:
|
|
372
|
+
return ArtifactErrorResult(
|
|
373
|
+
error_message=f"Failed to list templates: {str(e)}",
|
|
374
|
+
agent_mode=agent_mode,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
# Convenience methods
|
|
378
|
+
|
|
379
|
+
def ensure_artifact_exists(
|
|
380
|
+
self,
|
|
381
|
+
artifact_id: str,
|
|
382
|
+
agent_mode: AgentMode,
|
|
383
|
+
name: str | None = None,
|
|
384
|
+
) -> ArtifactCreateResult | ArtifactInfoResult | ArtifactErrorResult:
|
|
385
|
+
"""Ensure an artifact exists, creating it if necessary.
|
|
386
|
+
|
|
387
|
+
Args:
|
|
388
|
+
artifact_id: Artifact identifier
|
|
389
|
+
agent_mode: Agent mode
|
|
390
|
+
name: Optional name (defaults to formatted artifact_id)
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
ArtifactCreateResult if created, ArtifactInfoResult if already existed, ArtifactErrorResult on error
|
|
394
|
+
"""
|
|
395
|
+
if name is None:
|
|
396
|
+
name = generate_artifact_name(artifact_id)
|
|
397
|
+
|
|
398
|
+
# Try to get existing artifact
|
|
399
|
+
info_result = self.get_artifact_info(artifact_id, agent_mode)
|
|
400
|
+
if isinstance(info_result, ArtifactInfoResult):
|
|
401
|
+
return info_result
|
|
402
|
+
|
|
403
|
+
# Create new artifact
|
|
404
|
+
create_result = self.create_artifact(artifact_id, agent_mode, name)
|
|
405
|
+
return create_result
|
|
406
|
+
|
|
407
|
+
def ensure_section_exists(
|
|
408
|
+
self,
|
|
409
|
+
artifact_id: str,
|
|
410
|
+
agent_mode: AgentMode,
|
|
411
|
+
section_number: int,
|
|
412
|
+
section_slug: str,
|
|
413
|
+
section_title: str,
|
|
414
|
+
initial_content: str = "",
|
|
415
|
+
) -> SectionCreateResult | SectionContentResult | ArtifactErrorResult:
|
|
416
|
+
"""Ensure a section exists, creating it if necessary.
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
artifact_id: Artifact identifier
|
|
420
|
+
agent_mode: Agent mode
|
|
421
|
+
section_number: Section number
|
|
422
|
+
section_slug: Section slug
|
|
423
|
+
section_title: Section title
|
|
424
|
+
initial_content: Initial content for new sections
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
SectionCreateResult if created, SectionContentResult if already existed, ArtifactErrorResult on error
|
|
428
|
+
"""
|
|
429
|
+
# Try to get existing section
|
|
430
|
+
content_result = self.get_section_content(
|
|
431
|
+
artifact_id, agent_mode, section_number
|
|
432
|
+
)
|
|
433
|
+
if isinstance(content_result, SectionContentResult):
|
|
434
|
+
return content_result
|
|
435
|
+
|
|
436
|
+
# Ensure artifact exists first
|
|
437
|
+
self.ensure_artifact_exists(artifact_id, agent_mode)
|
|
438
|
+
|
|
439
|
+
# Create new section
|
|
440
|
+
create_result = self.create_section(
|
|
441
|
+
artifact_id,
|
|
442
|
+
agent_mode,
|
|
443
|
+
section_number,
|
|
444
|
+
section_slug,
|
|
445
|
+
section_title,
|
|
446
|
+
initial_content,
|
|
447
|
+
)
|
|
448
|
+
return create_result
|
shotgun/tui/app.py
CHANGED
|
@@ -2,18 +2,24 @@ from textual.app import App
|
|
|
2
2
|
from textual.binding import Binding
|
|
3
3
|
|
|
4
4
|
from shotgun.agents.config import ConfigManager, get_config_manager
|
|
5
|
+
from shotgun.agents.tools.file_management import get_shotgun_base_path
|
|
5
6
|
from shotgun.logging_config import get_logger
|
|
6
7
|
from shotgun.tui.screens.splash import SplashScreen
|
|
7
8
|
from shotgun.utils.update_checker import check_for_updates_async
|
|
8
9
|
|
|
9
10
|
from .screens.chat import ChatScreen
|
|
11
|
+
from .screens.directory_setup import DirectorySetupScreen
|
|
10
12
|
from .screens.provider_config import ProviderConfigScreen
|
|
11
13
|
|
|
12
14
|
logger = get_logger(__name__)
|
|
13
15
|
|
|
14
16
|
|
|
15
17
|
class ShotgunApp(App[None]):
|
|
16
|
-
SCREENS = {
|
|
18
|
+
SCREENS = {
|
|
19
|
+
"chat": ChatScreen,
|
|
20
|
+
"provider_config": ProviderConfigScreen,
|
|
21
|
+
"directory_setup": DirectorySetupScreen,
|
|
22
|
+
}
|
|
17
23
|
BINDINGS = [
|
|
18
24
|
Binding("ctrl+c", "quit", "Quit the app"),
|
|
19
25
|
]
|
|
@@ -43,21 +49,34 @@ class ShotgunApp(App[None]):
|
|
|
43
49
|
self.push_screen(
|
|
44
50
|
SplashScreen(), callback=lambda _arg: self.refresh_startup_screen()
|
|
45
51
|
)
|
|
46
|
-
# self.refresh_startup_screen()
|
|
47
52
|
|
|
48
53
|
def refresh_startup_screen(self) -> None:
|
|
49
54
|
"""Push the appropriate screen based on configured providers."""
|
|
50
|
-
if self.config_manager.has_any_provider_key():
|
|
51
|
-
if isinstance(self.screen, ChatScreen):
|
|
52
|
-
return
|
|
53
|
-
self.push_screen("chat")
|
|
54
|
-
else:
|
|
55
|
+
if not self.config_manager.has_any_provider_key():
|
|
55
56
|
if isinstance(self.screen, ProviderConfigScreen):
|
|
56
57
|
return
|
|
57
58
|
|
|
58
59
|
self.push_screen(
|
|
59
60
|
"provider_config", callback=lambda _arg: self.refresh_startup_screen()
|
|
60
61
|
)
|
|
62
|
+
return
|
|
63
|
+
|
|
64
|
+
if not self.check_local_shotgun_directory_exists():
|
|
65
|
+
if isinstance(self.screen, DirectorySetupScreen):
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
self.push_screen(
|
|
69
|
+
"directory_setup", callback=lambda _arg: self.refresh_startup_screen()
|
|
70
|
+
)
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
if isinstance(self.screen, ChatScreen):
|
|
74
|
+
return
|
|
75
|
+
self.push_screen("chat")
|
|
76
|
+
|
|
77
|
+
def check_local_shotgun_directory_exists(self) -> bool:
|
|
78
|
+
shotgun_dir = get_shotgun_base_path()
|
|
79
|
+
return shotgun_dir.exists() and shotgun_dir.is_dir()
|
|
61
80
|
|
|
62
81
|
async def action_quit(self) -> None:
|
|
63
82
|
"""Override quit action to show update notification."""
|
shotgun/tui/screens/chat.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from collections.abc import AsyncGenerator
|
|
2
2
|
from typing import cast
|
|
3
3
|
|
|
4
|
-
from pydantic_ai import DeferredToolResults
|
|
4
|
+
from pydantic_ai import DeferredToolResults, RunContext
|
|
5
5
|
from pydantic_ai.messages import (
|
|
6
6
|
BuiltinToolCallPart,
|
|
7
7
|
BuiltinToolReturnPart,
|
|
@@ -31,6 +31,11 @@ from ..components.spinner import Spinner
|
|
|
31
31
|
from ..components.vertical_tail import VerticalTail
|
|
32
32
|
|
|
33
33
|
|
|
34
|
+
def _dummy_system_prompt_fn(ctx: RunContext[AgentDeps]) -> str:
|
|
35
|
+
"""Dummy system prompt function for TUI chat interface."""
|
|
36
|
+
return "You are a helpful AI assistant."
|
|
37
|
+
|
|
38
|
+
|
|
34
39
|
class PromptHistory:
|
|
35
40
|
def __init__(self) -> None:
|
|
36
41
|
self.prompts: list[str] = ["Hello there!"]
|
|
@@ -289,6 +294,18 @@ class ChatScreen(Screen[None]):
|
|
|
289
294
|
|
|
290
295
|
COMMANDS = {AgentModeProvider, ProviderSetupProvider}
|
|
291
296
|
|
|
297
|
+
_PLACEHOLDER_BY_MODE: dict[AgentType, str] = {
|
|
298
|
+
AgentType.RESEARCH: (
|
|
299
|
+
"Ask for investigations, e.g. research strengths and weaknesses of PydanticAI vs its rivals"
|
|
300
|
+
),
|
|
301
|
+
AgentType.PLAN: (
|
|
302
|
+
"Describe a goal to plan, e.g. draft a rollout plan for launching our Slack automation"
|
|
303
|
+
),
|
|
304
|
+
AgentType.TASKS: (
|
|
305
|
+
"Request actionable work, e.g. break down tasks to wire OpenTelemetry into the API"
|
|
306
|
+
),
|
|
307
|
+
}
|
|
308
|
+
|
|
292
309
|
value = reactive("")
|
|
293
310
|
mode = reactive(AgentType.RESEARCH)
|
|
294
311
|
history: PromptHistory = PromptHistory()
|
|
@@ -305,6 +322,7 @@ class ChatScreen(Screen[None]):
|
|
|
305
322
|
interactive_mode=True,
|
|
306
323
|
llm_model=model_config,
|
|
307
324
|
codebase_service=codebase_service,
|
|
325
|
+
system_prompt_fn=_dummy_system_prompt_fn,
|
|
308
326
|
)
|
|
309
327
|
self.agent_manager = AgentManager(deps=self.deps, initial_type=self.mode)
|
|
310
328
|
|
|
@@ -323,6 +341,10 @@ class ChatScreen(Screen[None]):
|
|
|
323
341
|
mode_indicator.mode = new_mode
|
|
324
342
|
mode_indicator.refresh()
|
|
325
343
|
|
|
344
|
+
prompt_input = self.query_one(PromptInput)
|
|
345
|
+
prompt_input.placeholder = self._placeholder_for_mode(new_mode)
|
|
346
|
+
prompt_input.refresh()
|
|
347
|
+
|
|
326
348
|
def watch_working(self, is_working: bool) -> None:
|
|
327
349
|
"""Show or hide the spinner based on working state."""
|
|
328
350
|
if self.is_mounted:
|
|
@@ -379,7 +401,7 @@ class ChatScreen(Screen[None]):
|
|
|
379
401
|
text=self.value,
|
|
380
402
|
highlight_cursor_line=False,
|
|
381
403
|
id="prompt-input",
|
|
382
|
-
placeholder=
|
|
404
|
+
placeholder=self._placeholder_for_mode(self.mode),
|
|
383
405
|
)
|
|
384
406
|
yield ModeIndicator(mode=self.mode)
|
|
385
407
|
|
|
@@ -398,7 +420,10 @@ class ChatScreen(Screen[None]):
|
|
|
398
420
|
|
|
399
421
|
prompt_input = self.query_one(PromptInput)
|
|
400
422
|
prompt_input.clear()
|
|
401
|
-
|
|
423
|
+
|
|
424
|
+
def _placeholder_for_mode(self, mode: AgentType) -> str:
|
|
425
|
+
"""Return the placeholder text appropriate for the current mode."""
|
|
426
|
+
return self._PLACEHOLDER_BY_MODE.get(mode, "Type your message")
|
|
402
427
|
|
|
403
428
|
@work
|
|
404
429
|
async def run_agent(self, message: str) -> None:
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Screen for setting up the local .shotgun directory."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from textual import on
|
|
8
|
+
from textual.app import ComposeResult
|
|
9
|
+
from textual.containers import Horizontal, Vertical
|
|
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
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DirectorySetupScreen(Screen[None]):
|
|
17
|
+
"""Prompt the user to initialize the .shotgun directory."""
|
|
18
|
+
|
|
19
|
+
CSS = """
|
|
20
|
+
DirectorySetupScreen {
|
|
21
|
+
layout: vertical;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
DirectorySetupScreen > * {
|
|
25
|
+
height: auto;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
#titlebox {
|
|
29
|
+
height: auto;
|
|
30
|
+
margin: 2 0;
|
|
31
|
+
padding: 1;
|
|
32
|
+
border: hkey $border;
|
|
33
|
+
content-align: center middle;
|
|
34
|
+
|
|
35
|
+
& > * {
|
|
36
|
+
text-align: center;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#directory-setup-title {
|
|
41
|
+
padding: 1 0;
|
|
42
|
+
text-style: bold;
|
|
43
|
+
color: $text-accent;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
#directory-setup-summary {
|
|
47
|
+
padding: 0 1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
#directory-actions {
|
|
51
|
+
padding: 1;
|
|
52
|
+
content-align: center middle;
|
|
53
|
+
align: center middle;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
#directory-actions > * {
|
|
57
|
+
margin-right: 2;
|
|
58
|
+
}
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
BINDINGS = [
|
|
62
|
+
("enter", "confirm", "Initialize"),
|
|
63
|
+
("escape", "cancel", "Exit"),
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
def compose(self) -> ComposeResult:
|
|
67
|
+
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"
|
|
75
|
+
)
|
|
76
|
+
yield Button("Exit without setup \\[ESC]", variant="default", id="exit")
|
|
77
|
+
|
|
78
|
+
def on_mount(self) -> None:
|
|
79
|
+
self.set_focus(self.query_one("#initialize", Button))
|
|
80
|
+
|
|
81
|
+
def action_confirm(self) -> None:
|
|
82
|
+
self._initialize_directory()
|
|
83
|
+
|
|
84
|
+
def action_cancel(self) -> None:
|
|
85
|
+
self._exit_application()
|
|
86
|
+
|
|
87
|
+
@on(Button.Pressed, "#initialize")
|
|
88
|
+
def _on_initialize_pressed(self) -> None:
|
|
89
|
+
self._initialize_directory()
|
|
90
|
+
|
|
91
|
+
@on(Button.Pressed, "#exit")
|
|
92
|
+
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:
|
|
113
|
+
self.app.exit()
|