shotgun-sh 0.1.12.dev3__py3-none-any.whl → 0.1.13.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.

@@ -31,6 +31,7 @@ from pydantic_ai.messages import (
31
31
  SystemPromptPart,
32
32
  ToolCallPart,
33
33
  ToolCallPartDelta,
34
+ ToolReturnPart,
34
35
  )
35
36
  from textual.message import Message
36
37
  from textual.widget import Widget
@@ -44,7 +45,7 @@ from shotgun.utils.source_detection import detect_source
44
45
  from .export import create_export_agent
45
46
  from .history.compaction import apply_persistent_compaction
46
47
  from .messages import AgentSystemPrompt
47
- from .models import AgentDeps, AgentRuntimeOptions
48
+ from .models import AgentDeps, AgentRuntimeOptions, UserAnswer
48
49
  from .plan import create_plan_agent
49
50
  from .research import create_research_agent
50
51
  from .specify import create_specify_agent
@@ -272,6 +273,8 @@ class AgentManager(Widget):
272
273
  # Use merged deps (shared state + agent-specific system prompt) if not provided
273
274
  if deps is None:
274
275
  deps = self._create_merged_deps(self._current_agent_type)
276
+ if not deferred_tool_results:
277
+ self.ensure_agent_canecelled_safely()
275
278
 
276
279
  # Ensure deps is not None
277
280
  if deps is None:
@@ -674,6 +677,32 @@ class AgentManager(Widget):
674
677
  self.ui_message_history.append(message)
675
678
  self._post_messages_updated()
676
679
 
680
+ def ensure_agent_canecelled_safely(self) -> None:
681
+ if not self.message_history:
682
+ return
683
+ self.last_response = self.message_history[-1]
684
+ ## we're searching for unanswered ask_user parts
685
+ found_tool = None
686
+ for part in self.message_history[-1].parts:
687
+ if isinstance(part, ToolCallPart) and part.tool_name == "ask_user":
688
+ found_tool = part
689
+ break
690
+ if not found_tool:
691
+ return
692
+ tool_result = ModelRequest(
693
+ parts=[
694
+ ToolReturnPart(
695
+ tool_call_id=found_tool.tool_call_id,
696
+ tool_name=found_tool.tool_name,
697
+ content=UserAnswer(
698
+ answer="⚠️ Operation cancelled by user",
699
+ tool_call_id=found_tool.tool_call_id,
700
+ ),
701
+ )
702
+ ]
703
+ )
704
+ self.message_history.append(tool_result)
705
+
677
706
 
678
707
  # Re-export AgentType for backward compatibility
679
708
  __all__ = [
@@ -6,8 +6,21 @@ from pathlib import Path
6
6
  from typing import Annotated
7
7
 
8
8
  import typer
9
+ from rich.console import Console
10
+ from rich.progress import (
11
+ BarColumn,
12
+ Progress,
13
+ SpinnerColumn,
14
+ TaskProgressColumn,
15
+ TextColumn,
16
+ TimeElapsedColumn,
17
+ )
9
18
 
10
- from shotgun.codebase.models import CodebaseGraph, QueryType
19
+ from shotgun.codebase.models import (
20
+ CodebaseGraph,
21
+ IndexProgress,
22
+ QueryType,
23
+ )
11
24
  from shotgun.logging_config import get_logger
12
25
  from shotgun.sdk.codebase import CodebaseSDK
13
26
  from shotgun.sdk.exceptions import CodebaseNotFoundError, InvalidPathError
@@ -59,10 +72,66 @@ def index(
59
72
  ) -> None:
60
73
  """Index a new codebase."""
61
74
  sdk = CodebaseSDK()
75
+ console = Console()
76
+
77
+ # Create progress display
78
+ progress = Progress(
79
+ SpinnerColumn(),
80
+ TextColumn("[bold blue]{task.description}"),
81
+ BarColumn(),
82
+ TaskProgressColumn(),
83
+ TimeElapsedColumn(),
84
+ console=console,
85
+ )
86
+
87
+ # Track tasks by phase
88
+ tasks = {}
89
+
90
+ def progress_callback(progress_info: IndexProgress) -> None:
91
+ """Update progress display based on indexing phase."""
92
+ phase = progress_info.phase
93
+
94
+ # Create task if it doesn't exist
95
+ if phase not in tasks:
96
+ if progress_info.total is not None:
97
+ tasks[phase] = progress.add_task(
98
+ progress_info.phase_name, total=progress_info.total
99
+ )
100
+ else:
101
+ # Indeterminate progress (spinner only)
102
+ tasks[phase] = progress.add_task(progress_info.phase_name, total=None)
103
+
104
+ task_id = tasks[phase]
105
+
106
+ # Update task
107
+ if progress_info.total is not None:
108
+ progress.update(
109
+ task_id,
110
+ completed=progress_info.current,
111
+ total=progress_info.total,
112
+ description=f"[bold blue]{progress_info.phase_name}",
113
+ )
114
+ else:
115
+ # Just update description for indeterminate tasks
116
+ progress.update(
117
+ task_id,
118
+ description=f"[bold blue]{progress_info.phase_name} ({progress_info.current} items)",
119
+ )
120
+
121
+ # Mark as complete if phase is done
122
+ if progress_info.phase_complete:
123
+ if progress_info.total is not None:
124
+ progress.update(task_id, completed=progress_info.total)
62
125
 
63
126
  try:
64
127
  repo_path = Path(path)
65
- result = asyncio.run(sdk.index_codebase(repo_path, name))
128
+
129
+ # Run indexing with progress display
130
+ with progress:
131
+ result = asyncio.run(
132
+ sdk.index_codebase(repo_path, name, progress_callback=progress_callback)
133
+ )
134
+
66
135
  output_result(result, format_type)
67
136
  except InvalidPathError as e:
68
137
  error_result = ErrorResult(error_message=str(e))
@@ -535,6 +535,7 @@ class SimpleGraphBuilder:
535
535
  parsers: dict[str, Parser],
536
536
  queries: dict[str, Any],
537
537
  exclude_patterns: list[str] | None = None,
538
+ progress_callback: Any | None = None,
538
539
  ):
539
540
  self.ingestor = ingestor
540
541
  self.repo_path = repo_path
@@ -544,6 +545,7 @@ class SimpleGraphBuilder:
544
545
  self.ignore_dirs = IGNORE_PATTERNS
545
546
  if exclude_patterns:
546
547
  self.ignore_dirs = self.ignore_dirs.union(set(exclude_patterns))
548
+ self.progress_callback = progress_callback
547
549
 
548
550
  # Caches
549
551
  self.structural_elements: dict[Path, str | None] = {}
@@ -552,6 +554,34 @@ class SimpleGraphBuilder:
552
554
  self.simple_name_lookup: dict[str, set[str]] = defaultdict(set)
553
555
  self.class_inheritance: dict[str, list[str]] = {} # class_qn -> [parent_qns]
554
556
 
557
+ def _report_progress(
558
+ self,
559
+ phase: str,
560
+ phase_name: str,
561
+ current: int,
562
+ total: int | None = None,
563
+ phase_complete: bool = False,
564
+ ) -> None:
565
+ """Report progress via callback if available."""
566
+ if not self.progress_callback:
567
+ return
568
+
569
+ try:
570
+ # Import here to avoid circular dependency
571
+ from shotgun.codebase.models import IndexProgress, ProgressPhase
572
+
573
+ progress = IndexProgress(
574
+ phase=ProgressPhase(phase),
575
+ phase_name=phase_name,
576
+ current=current,
577
+ total=total,
578
+ phase_complete=phase_complete,
579
+ )
580
+ self.progress_callback(progress)
581
+ except Exception as e:
582
+ # Don't let progress callback errors crash the build
583
+ logger.debug(f"Progress callback error: {e}")
584
+
555
585
  def run(self) -> None:
556
586
  """Run the three-pass graph building process."""
557
587
  logger.info(f"Building graph for project: {self.project_name}")
@@ -575,6 +605,7 @@ class SimpleGraphBuilder:
575
605
 
576
606
  def _identify_structure(self) -> None:
577
607
  """First pass: Walk directory to find packages and folders."""
608
+ dir_count = 0
578
609
  for root_str, dirs, _ in os.walk(self.repo_path, topdown=True):
579
610
  dirs[:] = [d for d in dirs if d not in self.ignore_dirs]
580
611
  root = Path(root_str)
@@ -584,6 +615,13 @@ class SimpleGraphBuilder:
584
615
  if root == self.repo_path:
585
616
  continue
586
617
 
618
+ dir_count += 1
619
+ # Report progress every 10 directories
620
+ if dir_count % 10 == 0:
621
+ self._report_progress(
622
+ "structure", "Identifying packages and folders", dir_count
623
+ )
624
+
587
625
  parent_rel_path = relative_root.parent
588
626
  parent_container_qn = self.structural_elements.get(parent_rel_path)
589
627
 
@@ -686,8 +724,34 @@ class SimpleGraphBuilder:
686
724
 
687
725
  self.structural_elements[relative_root] = None
688
726
 
727
+ # Report phase completion
728
+ self._report_progress(
729
+ "structure",
730
+ "Identifying packages and folders",
731
+ dir_count,
732
+ phase_complete=True,
733
+ )
734
+
689
735
  def _process_files(self) -> None:
690
736
  """Second pass: Process files and extract definitions."""
737
+ # First pass: Count total files
738
+ total_files = 0
739
+ for root_str, _, files in os.walk(self.repo_path):
740
+ root = Path(root_str)
741
+
742
+ # Skip ignored directories
743
+ if any(part in self.ignore_dirs for part in root.parts):
744
+ continue
745
+
746
+ for filename in files:
747
+ filepath = root / filename
748
+ ext = filepath.suffix
749
+ lang_config = get_language_config(ext)
750
+
751
+ if lang_config and lang_config.name in self.parsers:
752
+ total_files += 1
753
+
754
+ # Second pass: Process files with progress reporting
691
755
  file_count = 0
692
756
  for root_str, _, files in os.walk(self.repo_path):
693
757
  root = Path(root_str)
@@ -707,10 +771,27 @@ class SimpleGraphBuilder:
707
771
  self._process_single_file(filepath, lang_config.name)
708
772
  file_count += 1
709
773
 
774
+ # Report progress after each file
775
+ self._report_progress(
776
+ "definitions",
777
+ "Processing files and extracting definitions",
778
+ file_count,
779
+ total_files,
780
+ )
781
+
710
782
  if file_count % 100 == 0:
711
- logger.info(f" Processed {file_count} files...")
783
+ logger.info(f" Processed {file_count}/{total_files} files...")
784
+
785
+ logger.info(f" Total files processed: {file_count}/{total_files}")
712
786
 
713
- logger.info(f" Total files processed: {file_count}")
787
+ # Report phase completion
788
+ self._report_progress(
789
+ "definitions",
790
+ "Processing files and extracting definitions",
791
+ file_count,
792
+ total_files,
793
+ phase_complete=True,
794
+ )
714
795
 
715
796
  def _process_single_file(self, filepath: Path, language: str) -> None:
716
797
  """Process a single file."""
@@ -1143,7 +1224,8 @@ class SimpleGraphBuilder:
1143
1224
  self._process_inheritance()
1144
1225
 
1145
1226
  # Then process function calls
1146
- logger.info(f"Processing function calls for {len(self.ast_cache)} files...")
1227
+ total_files = len(self.ast_cache)
1228
+ logger.info(f"Processing function calls for {total_files} files...")
1147
1229
  logger.info(f"Function registry has {len(self.function_registry)} entries")
1148
1230
  logger.info(
1149
1231
  f"Simple name lookup has {len(self.simple_name_lookup)} unique names"
@@ -1157,10 +1239,29 @@ class SimpleGraphBuilder:
1157
1239
  f" Example: '{name}' -> {list(self.simple_name_lookup[name])[:3]}"
1158
1240
  )
1159
1241
 
1242
+ file_count = 0
1160
1243
  for filepath, (root_node, language) in self.ast_cache.items():
1161
1244
  self._process_calls(filepath, root_node, language)
1162
1245
  # NOTE: Add import processing. wtf does this mean?
1163
1246
 
1247
+ file_count += 1
1248
+ # Report progress after each file
1249
+ self._report_progress(
1250
+ "relationships",
1251
+ "Processing relationships (calls, imports)",
1252
+ file_count,
1253
+ total_files,
1254
+ )
1255
+
1256
+ # Report phase completion
1257
+ self._report_progress(
1258
+ "relationships",
1259
+ "Processing relationships (calls, imports)",
1260
+ file_count,
1261
+ total_files,
1262
+ phase_complete=True,
1263
+ )
1264
+
1164
1265
  def _process_inheritance(self) -> None:
1165
1266
  """Process inheritance relationships between classes."""
1166
1267
  logger.info("Processing inheritance relationships...")
@@ -1444,6 +1545,7 @@ class CodebaseIngestor:
1444
1545
  db_path: str,
1445
1546
  project_name: str | None = None,
1446
1547
  exclude_patterns: list[str] | None = None,
1548
+ progress_callback: Any | None = None,
1447
1549
  ):
1448
1550
  """Initialize the ingestor.
1449
1551
 
@@ -1451,10 +1553,12 @@ class CodebaseIngestor:
1451
1553
  db_path: Path to Kuzu database
1452
1554
  project_name: Optional project name
1453
1555
  exclude_patterns: Patterns to exclude from processing
1556
+ progress_callback: Optional callback for progress reporting
1454
1557
  """
1455
1558
  self.db_path = Path(db_path)
1456
1559
  self.project_name = project_name
1457
1560
  self.exclude_patterns = exclude_patterns or []
1561
+ self.progress_callback = progress_callback
1458
1562
 
1459
1563
  def build_graph_from_directory(self, repo_path: str) -> None:
1460
1564
  """Build a code knowledge graph from a directory.
@@ -1484,7 +1588,12 @@ class CodebaseIngestor:
1484
1588
 
1485
1589
  # Build graph
1486
1590
  builder = SimpleGraphBuilder(
1487
- ingestor, repo_path_obj, parsers, queries, self.exclude_patterns
1591
+ ingestor,
1592
+ repo_path_obj,
1593
+ parsers,
1594
+ queries,
1595
+ self.exclude_patterns,
1596
+ self.progress_callback,
1488
1597
  )
1489
1598
  if self.project_name:
1490
1599
  builder.project_name = self.project_name
@@ -329,6 +329,7 @@ class CodebaseGraphManager:
329
329
  languages: list[str] | None = None,
330
330
  exclude_patterns: list[str] | None = None,
331
331
  indexed_from_cwd: str | None = None,
332
+ progress_callback: Any | None = None,
332
333
  ) -> CodebaseGraph:
333
334
  """Build a new code knowledge graph.
334
335
 
@@ -337,6 +338,7 @@ class CodebaseGraphManager:
337
338
  name: Optional human-readable name
338
339
  languages: Languages to parse (default: all supported)
339
340
  exclude_patterns: Patterns to exclude
341
+ progress_callback: Optional callback for progress reporting
340
342
 
341
343
  Returns:
342
344
  Created graph metadata
@@ -353,7 +355,19 @@ class CodebaseGraphManager:
353
355
 
354
356
  # Check if graph already exists
355
357
  if graph_path.exists():
356
- raise CodebaseAlreadyIndexedError(repo_path)
358
+ # Verify it's not corrupted by checking if we can load the Project node
359
+ existing_graph = await self.get_graph(graph_id)
360
+ if existing_graph:
361
+ # Valid existing graph
362
+ raise CodebaseAlreadyIndexedError(repo_path)
363
+ else:
364
+ # Corrupted database - remove and re-index
365
+ logger.warning(
366
+ f"Found corrupted database at {graph_path}, removing for re-indexing..."
367
+ )
368
+ import shutil
369
+
370
+ shutil.rmtree(graph_path)
357
371
 
358
372
  # Import the builder from local core module
359
373
  from shotgun.codebase.core import CodebaseIngestor
@@ -379,6 +393,7 @@ class CodebaseGraphManager:
379
393
  db_path=str(graph_path),
380
394
  project_name=name,
381
395
  exclude_patterns=exclude_patterns or [],
396
+ progress_callback=progress_callback,
382
397
  )
383
398
 
384
399
  # Run build in thread pool
@@ -1193,6 +1208,136 @@ class CodebaseGraphManager:
1193
1208
  )
1194
1209
  return None
1195
1210
 
1211
+ async def cleanup_corrupted_databases(self) -> list[str]:
1212
+ """Detect and remove corrupted Kuzu databases.
1213
+
1214
+ This method iterates through all .kuzu files in the storage directory,
1215
+ attempts to open them, and removes any that are corrupted or unreadable.
1216
+
1217
+ Returns:
1218
+ List of graph_ids that were removed due to corruption
1219
+ """
1220
+ import shutil
1221
+
1222
+ removed_graphs = []
1223
+
1224
+ # Find all .kuzu files (can be files or directories)
1225
+ for path in self.storage_dir.glob("*.kuzu"):
1226
+ graph_id = path.stem
1227
+
1228
+ # If it's a plain file (not a directory), it's corrupted
1229
+ # Valid Kuzu databases are always directories
1230
+ if path.is_file():
1231
+ logger.warning(
1232
+ f"Detected corrupted database file (should be directory) at {path}, removing it"
1233
+ )
1234
+ try:
1235
+ await anyio.to_thread.run_sync(path.unlink)
1236
+ removed_graphs.append(graph_id)
1237
+ logger.info(f"Removed corrupted database file: {graph_id}")
1238
+ except Exception as e:
1239
+ logger.error(
1240
+ f"Failed to remove corrupted database file {graph_id}: {e}"
1241
+ )
1242
+ continue
1243
+
1244
+ try:
1245
+ # Try to open the database with a timeout to prevent hanging
1246
+ async def try_open_database(
1247
+ gid: str = graph_id, db_path: Path = path
1248
+ ) -> bool:
1249
+ lock = await self._get_lock()
1250
+ async with lock:
1251
+ # Close existing connections if any
1252
+ if gid in self._connections:
1253
+ try:
1254
+ self._connections[gid].close()
1255
+ except Exception as e:
1256
+ logger.debug(
1257
+ f"Failed to close connection for {gid}: {e}"
1258
+ )
1259
+ del self._connections[gid]
1260
+ if gid in self._databases:
1261
+ try:
1262
+ self._databases[gid].close()
1263
+ except Exception as e:
1264
+ logger.debug(f"Failed to close database for {gid}: {e}")
1265
+ del self._databases[gid]
1266
+
1267
+ # Try to open the database
1268
+ def _open_and_query(g: str = gid, p: Path = db_path) -> bool:
1269
+ db = kuzu.Database(str(p))
1270
+ conn = kuzu.Connection(db)
1271
+ try:
1272
+ result = conn.execute(
1273
+ "MATCH (p:Project {graph_id: $graph_id}) RETURN p",
1274
+ {"graph_id": g},
1275
+ )
1276
+ has_results = (
1277
+ result.has_next()
1278
+ if hasattr(result, "has_next")
1279
+ else False
1280
+ )
1281
+ return has_results
1282
+ finally:
1283
+ conn.close()
1284
+ db.close()
1285
+
1286
+ return await anyio.to_thread.run_sync(_open_and_query)
1287
+
1288
+ # Try to open with 5 second timeout
1289
+ has_project = await asyncio.wait_for(try_open_database(), timeout=5.0)
1290
+
1291
+ if not has_project:
1292
+ # Database exists but has no Project node - consider it corrupted
1293
+ raise ValueError("No Project node found in database")
1294
+
1295
+ except (Exception, asyncio.TimeoutError) as e:
1296
+ # Database is corrupted or timed out - remove it
1297
+ error_type = (
1298
+ "timed out" if isinstance(e, asyncio.TimeoutError) else "corrupted"
1299
+ )
1300
+ logger.warning(
1301
+ f"Detected {error_type} database at {path}, removing it. "
1302
+ f"Error: {str(e) if not isinstance(e, asyncio.TimeoutError) else 'Operation timed out after 5 seconds'}"
1303
+ )
1304
+
1305
+ try:
1306
+ # Clean up any open connections
1307
+ lock = await self._get_lock()
1308
+ async with lock:
1309
+ if graph_id in self._connections:
1310
+ try:
1311
+ self._connections[graph_id].close()
1312
+ except Exception as e:
1313
+ logger.debug(
1314
+ f"Failed to close connection during cleanup for {graph_id}: {e}"
1315
+ )
1316
+ del self._connections[graph_id]
1317
+ if graph_id in self._databases:
1318
+ try:
1319
+ self._databases[graph_id].close()
1320
+ except Exception as e:
1321
+ logger.debug(
1322
+ f"Failed to close database during cleanup for {graph_id}: {e}"
1323
+ )
1324
+ del self._databases[graph_id]
1325
+
1326
+ # Remove the database (could be file or directory)
1327
+ if path.is_dir():
1328
+ await anyio.to_thread.run_sync(shutil.rmtree, path)
1329
+ else:
1330
+ await anyio.to_thread.run_sync(path.unlink)
1331
+ removed_graphs.append(graph_id)
1332
+ logger.info(f"Removed {error_type} database: {graph_id}")
1333
+
1334
+ except Exception as cleanup_error:
1335
+ logger.error(
1336
+ f"Failed to remove corrupted database {graph_id}: {cleanup_error}"
1337
+ )
1338
+
1339
+ return removed_graphs
1340
+
1196
1341
  async def list_graphs(self) -> list[CodebaseGraph]:
1197
1342
  """List all available graphs.
1198
1343
 
@@ -1464,6 +1609,7 @@ class CodebaseGraphManager:
1464
1609
  languages: list[str] | None,
1465
1610
  exclude_patterns: list[str] | None,
1466
1611
  indexed_from_cwd: str | None = None,
1612
+ progress_callback: Any | None = None,
1467
1613
  ) -> CodebaseGraph:
1468
1614
  """Internal implementation of graph building (runs in background)."""
1469
1615
  operation_id = str(uuid.uuid4())
@@ -1487,7 +1633,13 @@ class CodebaseGraphManager:
1487
1633
 
1488
1634
  # Do the actual build work
1489
1635
  graph = await self._do_build_graph(
1490
- graph_id, repo_path, name, languages, exclude_patterns, indexed_from_cwd
1636
+ graph_id,
1637
+ repo_path,
1638
+ name,
1639
+ languages,
1640
+ exclude_patterns,
1641
+ indexed_from_cwd,
1642
+ progress_callback,
1491
1643
  )
1492
1644
 
1493
1645
  # Update operation stats
@@ -1536,6 +1688,7 @@ class CodebaseGraphManager:
1536
1688
  languages: list[str] | None,
1537
1689
  exclude_patterns: list[str] | None,
1538
1690
  indexed_from_cwd: str | None = None,
1691
+ progress_callback: Any | None = None,
1539
1692
  ) -> CodebaseGraph:
1540
1693
  """Execute the actual graph building logic (extracted from original build_graph)."""
1541
1694
  # The database and Project node already exist from _initialize_graph_metadata
@@ -1591,6 +1744,7 @@ class CodebaseGraphManager:
1591
1744
  parsers=parsers,
1592
1745
  queries=queries,
1593
1746
  exclude_patterns=exclude_patterns,
1747
+ progress_callback=progress_callback,
1594
1748
  )
1595
1749
 
1596
1750
  # Build the graph
@@ -1616,6 +1770,7 @@ class CodebaseGraphManager:
1616
1770
  languages: list[str] | None = None,
1617
1771
  exclude_patterns: list[str] | None = None,
1618
1772
  indexed_from_cwd: str | None = None,
1773
+ progress_callback: Any | None = None,
1619
1774
  ) -> str:
1620
1775
  """Start building a new code knowledge graph asynchronously.
1621
1776
 
@@ -1654,7 +1809,13 @@ class CodebaseGraphManager:
1654
1809
  # Start the build operation in background
1655
1810
  task = asyncio.create_task(
1656
1811
  self._build_graph_impl(
1657
- graph_id, repo_path, name, languages, exclude_patterns, indexed_from_cwd
1812
+ graph_id,
1813
+ repo_path,
1814
+ name,
1815
+ languages,
1816
+ exclude_patterns,
1817
+ indexed_from_cwd,
1818
+ progress_callback,
1658
1819
  )
1659
1820
  )
1660
1821
  self._operations[graph_id] = task
@@ -1,5 +1,6 @@
1
1
  """Data models for codebase service."""
2
2
 
3
+ from collections.abc import Callable
3
4
  from enum import Enum
4
5
  from typing import Any
5
6
 
@@ -22,6 +23,30 @@ class QueryType(str, Enum):
22
23
  CYPHER = "cypher"
23
24
 
24
25
 
26
+ class ProgressPhase(str, Enum):
27
+ """Phase of codebase indexing progress."""
28
+
29
+ STRUCTURE = "structure" # Identifying packages and folders
30
+ DEFINITIONS = "definitions" # Processing files and extracting definitions
31
+ RELATIONSHIPS = "relationships" # Processing relationships (calls, imports)
32
+
33
+
34
+ class IndexProgress(BaseModel):
35
+ """Progress information for codebase indexing."""
36
+
37
+ phase: ProgressPhase = Field(..., description="Current indexing phase")
38
+ phase_name: str = Field(..., description="Human-readable phase name")
39
+ current: int = Field(..., description="Current item count")
40
+ total: int | None = Field(None, description="Total items (None if unknown)")
41
+ phase_complete: bool = Field(
42
+ default=False, description="Whether this phase is complete"
43
+ )
44
+
45
+
46
+ # Type alias for progress callback function
47
+ ProgressCallback = Callable[[IndexProgress], None]
48
+
49
+
25
50
  class OperationStats(BaseModel):
26
51
  """Statistics for a graph operation (build/update)."""
27
52
 
@@ -69,11 +69,19 @@ class CodebaseService:
69
69
  # Otherwise, check if current directory is in the allowed list
70
70
  elif target_path in graph.indexed_from_cwds:
71
71
  filtered_graphs.append(graph)
72
+ # Also allow access if current directory IS the repository itself
73
+ # Use Path.resolve() for robust comparison (handles symlinks, etc.)
74
+ elif Path(target_path).resolve() == Path(graph.repo_path).resolve():
75
+ filtered_graphs.append(graph)
72
76
 
73
77
  return filtered_graphs
74
78
 
75
79
  async def create_graph(
76
- self, repo_path: str | Path, name: str, indexed_from_cwd: str | None = None
80
+ self,
81
+ repo_path: str | Path,
82
+ name: str,
83
+ indexed_from_cwd: str | None = None,
84
+ progress_callback: Any | None = None,
77
85
  ) -> CodebaseGraph:
78
86
  """Create and index a new graph from a repository.
79
87
 
@@ -81,12 +89,16 @@ class CodebaseService:
81
89
  repo_path: Path to the repository to index
82
90
  name: Human-readable name for the graph
83
91
  indexed_from_cwd: Working directory from which indexing was initiated
92
+ progress_callback: Optional callback for progress reporting
84
93
 
85
94
  Returns:
86
95
  The created CodebaseGraph
87
96
  """
88
97
  return await self.manager.build_graph(
89
- str(repo_path), name, indexed_from_cwd=indexed_from_cwd
98
+ str(repo_path),
99
+ name,
100
+ indexed_from_cwd=indexed_from_cwd,
101
+ progress_callback=progress_callback,
90
102
  )
91
103
 
92
104
  async def get_graph(self, graph_id: str) -> CodebaseGraph | None:
@@ -21,4 +21,17 @@
21
21
  MATCH (f:Function) RETURN f.name AS name, f.qualified_name AS qname
22
22
  UNION ALL
23
23
  MATCH (m:Method) RETURN m.name AS name, m.qualified_name AS qname, m.path AS path // ERROR: Extra column!
24
+ ```
25
+ - **NULL Type Casting in UNION ALL**: When a property doesn't exist on a node type, you MUST use a typed placeholder instead of bare NULL. Bare NULL has type ANY which causes "qualified_name has data type ANY but STRING was expected" errors.
26
+ CORRECT usage (use empty string or explicit cast):
27
+ ```
28
+ MATCH (f:Function) RETURN f.name AS name, f.qualified_name AS qname, 'Function' AS type
29
+ UNION ALL
30
+ MATCH (fi:File) RETURN fi.name AS name, '' AS qname, 'File' AS type // Empty string for missing STRING property
31
+ ```
32
+ INCORRECT usage (bare NULL causes type mismatch):
33
+ ```
34
+ MATCH (f:Function) RETURN f.name AS name, f.qualified_name AS qname, 'Function' AS type
35
+ UNION ALL
36
+ MATCH (fi:File) RETURN fi.name AS name, NULL AS qname, 'File' AS type // ERROR: NULL is type ANY, not STRING!
24
37
  ```
shotgun/sdk/codebase.py CHANGED
@@ -3,6 +3,7 @@
3
3
  import asyncio
4
4
  from collections.abc import Awaitable, Callable
5
5
  from pathlib import Path
6
+ from typing import Any
6
7
 
7
8
  from shotgun.codebase.models import CodebaseGraph, QueryType
8
9
  from shotgun.logging_config import get_logger
@@ -63,7 +64,11 @@ class CodebaseSDK:
63
64
  return ListResult(graphs=graphs)
64
65
 
65
66
  async def index_codebase(
66
- self, path: Path, name: str, indexed_from_cwd: str | None = None
67
+ self,
68
+ path: Path,
69
+ name: str,
70
+ indexed_from_cwd: str | None = None,
71
+ progress_callback: Any | None = None,
67
72
  ) -> IndexResult:
68
73
  """Index a new codebase.
69
74
 
@@ -72,6 +77,7 @@ class CodebaseSDK:
72
77
  name: Human-readable name for the codebase
73
78
  indexed_from_cwd: Working directory from which indexing was initiated.
74
79
  If None, uses current working directory.
80
+ progress_callback: Optional callback for progress reporting
75
81
 
76
82
  Returns:
77
83
  IndexResult with indexing details
@@ -88,7 +94,10 @@ class CodebaseSDK:
88
94
  indexed_from_cwd = str(Path.cwd().resolve())
89
95
 
90
96
  graph = await self.service.create_graph(
91
- resolved_path, name, indexed_from_cwd=indexed_from_cwd
97
+ resolved_path,
98
+ name,
99
+ indexed_from_cwd=indexed_from_cwd,
100
+ progress_callback=progress_callback,
92
101
  )
93
102
  file_count = sum(graph.language_stats.values()) if graph.language_stats else 0
94
103
 
shotgun/tui/app.py CHANGED
@@ -100,6 +100,26 @@ def run(no_update_check: bool = False, continue_session: bool = False) -> None:
100
100
  no_update_check: If True, disable automatic update checks.
101
101
  continue_session: If True, continue from previous conversation.
102
102
  """
103
+ # Clean up any corrupted databases BEFORE starting the TUI
104
+ # This prevents crashes from corrupted databases during initialization
105
+ import asyncio
106
+
107
+ from shotgun.codebase.core.manager import CodebaseGraphManager
108
+ from shotgun.utils import get_shotgun_home
109
+
110
+ storage_dir = get_shotgun_home() / "codebases"
111
+ manager = CodebaseGraphManager(storage_dir)
112
+
113
+ try:
114
+ removed = asyncio.run(manager.cleanup_corrupted_databases())
115
+ if removed:
116
+ logger.info(
117
+ f"Cleaned up {len(removed)} corrupted database(s) before TUI startup"
118
+ )
119
+ except Exception as e:
120
+ logger.error(f"Failed to cleanup corrupted databases: {e}")
121
+ # Continue anyway - the TUI can still function
122
+
103
123
  app = ShotgunApp(no_update_check=no_update_check, continue_session=continue_session)
104
124
  app.run(inline_no_clear=True)
105
125
 
@@ -41,6 +41,7 @@ from shotgun.agents.models import (
41
41
  UserQuestion,
42
42
  )
43
43
  from shotgun.codebase.core.manager import CodebaseAlreadyIndexedError
44
+ from shotgun.codebase.models import IndexProgress, ProgressPhase
44
45
  from shotgun.posthog_telemetry import track_event
45
46
  from shotgun.sdk.codebase import CodebaseSDK
46
47
  from shotgun.sdk.exceptions import CodebaseNotFoundError, InvalidPathError
@@ -592,6 +593,68 @@ class ChatScreen(Screen[None]):
592
593
  f"[$foreground-muted]Indexing [bold $text-accent]{selection.name}[/]...[/]"
593
594
  )
594
595
  label.refresh()
596
+
597
+ def create_progress_bar(percentage: float, width: int = 20) -> str:
598
+ """Create a visual progress bar using Unicode block characters."""
599
+ filled = int((percentage / 100) * width)
600
+ empty = width - filled
601
+ return "▓" * filled + "░" * empty
602
+
603
+ # Spinner animation state (shared between timer and progress callback)
604
+ spinner_frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
605
+ spinner_state: dict[str, int | str | float] = {
606
+ "frame_index": 0,
607
+ "phase_name": "Starting...",
608
+ "percentage": 0.0,
609
+ }
610
+
611
+ def update_spinner_display() -> None:
612
+ """Update spinner frame on timer - runs every 100ms."""
613
+ # Advance spinner frame
614
+ frame_idx = int(spinner_state["frame_index"])
615
+ spinner_state["frame_index"] = (frame_idx + 1) % len(spinner_frames)
616
+ spinner = spinner_frames[frame_idx]
617
+
618
+ # Get current state
619
+ phase = str(spinner_state["phase_name"])
620
+ pct = float(spinner_state["percentage"])
621
+ bar = create_progress_bar(pct)
622
+
623
+ # Update label
624
+ label.update(
625
+ f"[$foreground-muted]Indexing codebase: {spinner} {phase}... {bar} {pct:.0f}%[/]"
626
+ )
627
+
628
+ def progress_callback(progress_info: IndexProgress) -> None:
629
+ """Update progress state (spinner animates independently on timer)."""
630
+ # Calculate overall percentage (0-95%, reserve 95-100% for finalization)
631
+ if progress_info.phase == ProgressPhase.STRUCTURE:
632
+ # Phase 1: 0-10%, always show 5% while running, 10% when complete
633
+ overall_pct = 10.0 if progress_info.phase_complete else 5.0
634
+ elif progress_info.phase == ProgressPhase.DEFINITIONS:
635
+ # Phase 2: 10-80% based on files processed
636
+ if progress_info.total and progress_info.total > 0:
637
+ phase_pct = (progress_info.current / progress_info.total) * 70.0
638
+ overall_pct = 10.0 + phase_pct
639
+ else:
640
+ overall_pct = 10.0
641
+ elif progress_info.phase == ProgressPhase.RELATIONSHIPS:
642
+ # Phase 3: 80-95% based on relationships processed (cap at 95%)
643
+ if progress_info.total and progress_info.total > 0:
644
+ phase_pct = (progress_info.current / progress_info.total) * 15.0
645
+ overall_pct = 80.0 + phase_pct
646
+ else:
647
+ overall_pct = 80.0
648
+ else:
649
+ overall_pct = 0.0
650
+
651
+ # Update shared state (timer will render it)
652
+ spinner_state["phase_name"] = progress_info.phase_name
653
+ spinner_state["percentage"] = overall_pct
654
+
655
+ # Start spinner animation timer (10 fps = 100ms interval)
656
+ spinner_timer = self.set_interval(0.1, update_spinner_display)
657
+
595
658
  try:
596
659
  # Pass the current working directory as the indexed_from_cwd
597
660
  logger.debug(
@@ -602,7 +665,19 @@ class ChatScreen(Screen[None]):
602
665
  selection.repo_path,
603
666
  selection.name,
604
667
  indexed_from_cwd=str(Path.cwd().resolve()),
668
+ progress_callback=progress_callback,
605
669
  )
670
+
671
+ # Stop spinner animation
672
+ spinner_timer.stop()
673
+
674
+ # Show 100% completion after indexing finishes
675
+ final_bar = create_progress_bar(100.0)
676
+ label.update(
677
+ f"[$foreground-muted]Indexing codebase: ✓ Complete {final_bar} 100%[/]"
678
+ )
679
+ label.refresh()
680
+
606
681
  logger.info(
607
682
  f"Successfully indexed codebase '{result.name}' (ID: {result.graph_id})"
608
683
  )
@@ -613,10 +688,12 @@ class ChatScreen(Screen[None]):
613
688
  )
614
689
 
615
690
  except CodebaseAlreadyIndexedError as exc:
691
+ spinner_timer.stop()
616
692
  logger.warning(f"Codebase already indexed: {exc}")
617
693
  self.notify(str(exc), severity="warning")
618
694
  return
619
695
  except InvalidPathError as exc:
696
+ spinner_timer.stop()
620
697
  logger.error(f"Invalid path error: {exc}")
621
698
  self.notify(str(exc), severity="error")
622
699
 
@@ -628,6 +705,8 @@ class ChatScreen(Screen[None]):
628
705
  )
629
706
  self.notify(f"Failed to index codebase: {exc}", severity="error")
630
707
  finally:
708
+ # Always stop the spinner timer
709
+ spinner_timer.stop()
631
710
  label.update("")
632
711
  label.refresh()
633
712
 
@@ -667,6 +746,7 @@ class ChatScreen(Screen[None]):
667
746
  except asyncio.CancelledError:
668
747
  # Handle cancellation gracefully - DO NOT re-raise
669
748
  self.mount_hint("⚠️ Operation cancelled by user")
749
+ self.agent_manager.ensure_agent_canecelled_safely()
670
750
  finally:
671
751
  self.working = False
672
752
  self._current_worker = None
@@ -238,25 +238,97 @@ class AgentResponseWidget(Widget):
238
238
  continue
239
239
  return acc.strip()
240
240
 
241
+ def _truncate(self, text: str, max_length: int = 100) -> str:
242
+ """Truncate text to max_length characters, adding ellipsis if needed."""
243
+ if len(text) <= max_length:
244
+ return text
245
+ return text[: max_length - 3] + "..."
246
+
247
+ def _parse_args(self, args: dict[str, object] | str | None) -> dict[str, object]:
248
+ """Parse tool call arguments, handling both dict and JSON string formats."""
249
+ if args is None:
250
+ return {}
251
+ if isinstance(args, str):
252
+ try:
253
+ return json.loads(args) if args.strip() else {}
254
+ except json.JSONDecodeError:
255
+ return {}
256
+ return args if isinstance(args, dict) else {}
257
+
241
258
  def _format_tool_call_part(self, part: ToolCallPart) -> str:
242
259
  if part.tool_name == "ask_user":
243
260
  return self._format_ask_user_part(part)
261
+
262
+ # Parse args once (handles both JSON string and dict)
263
+ args = self._parse_args(part.args)
264
+
265
+ # Codebase tools - show friendly names
266
+ if part.tool_name == "query_graph":
267
+ if "query" in args:
268
+ query = self._truncate(str(args["query"]))
269
+ return f'Querying code: "{query}"'
270
+ return "Querying code"
271
+
272
+ if part.tool_name == "retrieve_code":
273
+ if "qualified_name" in args:
274
+ return f'Retrieving code: "{args["qualified_name"]}"'
275
+ return "Retrieving code"
276
+
277
+ if part.tool_name == "file_read":
278
+ if "file_path" in args:
279
+ return f'Reading file: "{args["file_path"]}"'
280
+ return "Reading file"
281
+
282
+ if part.tool_name == "directory_lister":
283
+ if "directory" in args:
284
+ return f'Listing directory: "{args["directory"]}"'
285
+ return "Listing directory"
286
+
287
+ if part.tool_name == "codebase_shell":
288
+ command = args.get("command", "")
289
+ cmd_args = args.get("args", [])
290
+ # Handle cmd_args as list of strings
291
+ if isinstance(cmd_args, list):
292
+ args_str = " ".join(str(arg) for arg in cmd_args)
293
+ else:
294
+ args_str = ""
295
+ full_cmd = f"{command} {args_str}".strip()
296
+ if full_cmd:
297
+ return f'Running shell: "{self._truncate(full_cmd)}"'
298
+ return "Running shell"
299
+
300
+ # File management tools
301
+ if part.tool_name == "read_file":
302
+ if "filename" in args:
303
+ return f'Reading file: "{args["filename"]}"'
304
+ return "Reading file"
305
+
306
+ # Web search tools
307
+ if part.tool_name in [
308
+ "openai_web_search_tool",
309
+ "anthropic_web_search_tool",
310
+ "gemini_web_search_tool",
311
+ ]:
312
+ if "query" in args:
313
+ query = self._truncate(str(args["query"]))
314
+ return f'Searching web: "{query}"'
315
+ return "Searching web"
316
+
244
317
  # write_file
245
318
  if part.tool_name == "write_file" or part.tool_name == "append_file":
246
- if isinstance(part.args, dict) and "filename" in part.args:
247
- return f"{part.tool_name}({part.args['filename']})"
248
- else:
249
- return f"{part.tool_name}()"
319
+ if "filename" in args:
320
+ return f"{part.tool_name}({args['filename']})"
321
+ return f"{part.tool_name}()"
322
+
250
323
  if part.tool_name == "write_artifact_section":
251
- if isinstance(part.args, dict) and "section_title" in part.args:
252
- return f"{part.tool_name}({part.args['section_title']})"
253
- else:
254
- return f"{part.tool_name}()"
324
+ if "section_title" in args:
325
+ return f"{part.tool_name}({args['section_title']})"
326
+ return f"{part.tool_name}()"
327
+
255
328
  if part.tool_name == "create_artifact":
256
- if isinstance(part.args, dict) and "name" in part.args:
257
- return f"{part.tool_name}({part.args['name']})"
258
- else:
259
- return f"▪ {part.tool_name}()"
329
+ if "name" in args:
330
+ return f"{part.tool_name}({args['name']})"
331
+ return f"▪ {part.tool_name}()"
260
332
 
261
333
  return f"{part.tool_name}({part.args})"
262
334
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: shotgun-sh
3
- Version: 0.1.12.dev3
3
+ Version: 0.1.13.dev1
4
4
  Summary: AI-powered research, planning, and task management CLI tool
5
5
  Project-URL: Homepage, https://shotgun.sh/
6
6
  Project-URL: Repository, https://github.com/shotgun-sh/shotgun
@@ -7,7 +7,7 @@ shotgun/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  shotgun/sentry_telemetry.py,sha256=L7jFMNAnDIENWVeQYSLpyul2nmIm2w3wnOp2kDP_cic,2902
8
8
  shotgun/telemetry.py,sha256=Ves6Ih3hshpKVNVAUUmwRdtW8NkTjFPg8hEqvFKZ0t0,3208
9
9
  shotgun/agents/__init__.py,sha256=8Jzv1YsDuLyNPFJyckSr_qI4ehTVeDyIMDW4omsfPGc,25
10
- shotgun/agents/agent_manager.py,sha256=01MafRjUDFre00mZUWIyOImdBkylpEG75mLNuQ0cJv4,25308
10
+ shotgun/agents/agent_manager.py,sha256=hmJlcOX98vLn5hYuzjC_cDGpDc89DMjCpQupK-Gq2WU,26393
11
11
  shotgun/agents/common.py,sha256=vt7ECq1rT6GR5Rt63t0whH0R0cydrk7Mty2KyPL8mEg,19045
12
12
  shotgun/agents/conversation_history.py,sha256=5J8_1yxdZiiWTq22aDio88DkBDZ4_Lh_p5Iy5_ENszc,3898
13
13
  shotgun/agents/conversation_manager.py,sha256=fxAvXbEl3Cl2ugJ4N9aWXaqZtkrnfj3QzwjWC4LFXwI,3514
@@ -58,18 +58,18 @@ shotgun/cli/tasks.py,sha256=17qWoGCVYpNIxa2vaoIH1P-xz2RcGLaK8SF4JlPsOWI,2420
58
58
  shotgun/cli/update.py,sha256=Dn_No7jPmdZ-7qYlhzI0BtmlufetVdw1BN-xRi_UE5A,4718
59
59
  shotgun/cli/utils.py,sha256=umVWXDx8pelovMk-nT8B7m0c39AKY9hHsuAMnbw_Hcg,732
60
60
  shotgun/cli/codebase/__init__.py,sha256=rKdvx33p0i_BYbNkz5_4DCFgEMwzOOqLi9f5p7XTLKM,73
61
- shotgun/cli/codebase/commands.py,sha256=zvcM9gjHHO6styhXojb_1bnpq-Cozh2c77ZOIjw4B8s,6683
61
+ shotgun/cli/codebase/commands.py,sha256=1N2yOGmok0ZarqXPIpWGcsQrwm_ZJcyWiMxy6tm0j70,8711
62
62
  shotgun/cli/codebase/models.py,sha256=B9vs-d-Bq0aS6FZKebhHT-9tw90Y5f6k_t71VlZpL8k,374
63
63
  shotgun/codebase/__init__.py,sha256=QBgFE2Abd5Vl7_NdYOglF9S6d-vIjkb3C0cpIYoHZEU,309
64
- shotgun/codebase/models.py,sha256=hxjbfDUka8loTApXq9KTvkXKt272fzdjr5u2ImYrNtk,4367
65
- shotgun/codebase/service.py,sha256=CZR5f1vZyUS3gVXuDiZj0cIuuxiR7fbkHK65PTPAovI,7504
64
+ shotgun/codebase/models.py,sha256=1AAipm6KrGOHmYBBavugnyeOsVkzX-YXAD6dDsSVRWg,5299
65
+ shotgun/codebase/service.py,sha256=nyggapfHKdwkKXyuT9oA0tJ9qf4RNVsOxfY8lC5pHro,8006
66
66
  shotgun/codebase/core/__init__.py,sha256=GWWhJEqChiDXAF4omYCgzgoZmJjwsAf6P1aZ5Bl8OE0,1170
67
67
  shotgun/codebase/core/change_detector.py,sha256=kWCYLWzRzb3IGGOj71KBn7UOCOKMpINJbOBDf98aMxE,12409
68
68
  shotgun/codebase/core/code_retrieval.py,sha256=_JVyyQKHDFm3dxOOua1mw9eIIOHIVz3-I8aZtEsEj1E,7927
69
69
  shotgun/codebase/core/cypher_models.py,sha256=Yfysfa9lLguILftkmtuJCN3kLBFIo7WW7NigM-Zr-W4,1735
70
- shotgun/codebase/core/ingestor.py,sha256=H_kVCqdOKmnQpjcXvUdPFpep8OC2AbOhhE-9HKr_XZM,59836
70
+ shotgun/codebase/core/ingestor.py,sha256=yh6BEIuUUfXU3dVpP0Llk19SrxA-uo3pdGnfcQsDsSo,63368
71
71
  shotgun/codebase/core/language_config.py,sha256=vsqHyuFnumRPRBV1lMOxWKNOIiClO6FyfKQR0fGrtl4,8934
72
- shotgun/codebase/core/manager.py,sha256=6gyjfACbC5n1Hdy-JQIEDH2aNAlesUS9plQP_FHoJ94,59277
72
+ shotgun/codebase/core/manager.py,sha256=nwyDv6bnyZRIerra8LSC41fJasmfSs7mGpVYbbKGeYs,66378
73
73
  shotgun/codebase/core/nl_query.py,sha256=kPoSJXBlm5rLhzOofZhqPVMJ_Lj3rV2H6sld6BwtMdg,16115
74
74
  shotgun/codebase/core/parser_loader.py,sha256=LZRrDS8Sp518jIu3tQW-BxdwJ86lnsTteI478ER9Td8,4278
75
75
  shotgun/prompts/__init__.py,sha256=RswUm0HMdfm2m2YKUwUsEdRIwoczdbI7zlucoEvHYRo,132
@@ -90,19 +90,19 @@ shotgun/prompts/codebase/__init__.py,sha256=NYuPMtmYM2ptuwf3YxVuotNlJOUq0hnjmwlz
90
90
  shotgun/prompts/codebase/cypher_query_patterns.j2,sha256=ufTx_xT3VoS76KcVUbIgGQx-bJoJHx3bBE3dagAXv18,8913
91
91
  shotgun/prompts/codebase/cypher_system.j2,sha256=jo8d_AIoyAd0zKCvPXSmYGBxvtulMsCfeaOTdOfeC5g,2620
92
92
  shotgun/prompts/codebase/enhanced_query_context.j2,sha256=WzGnFaBLZO-mOdkZ_u_PewSu9niKy87DKNL4uzQq1Jg,724
93
- shotgun/prompts/codebase/partials/cypher_rules.j2,sha256=vtc5OqTp-z5Rq_ti-_RG31bVOIA_iNe80_x3CdxO6bs,2397
93
+ shotgun/prompts/codebase/partials/cypher_rules.j2,sha256=yhptyyZNzFNJWFGmOkxpF5PzZtUAXWDF4xl9ADu7yDw,3200
94
94
  shotgun/prompts/codebase/partials/graph_schema.j2,sha256=fUsD1ZgU1pIWUzrs97jHq3TatKeGSvZgG8XP5gCQUJc,1939
95
95
  shotgun/prompts/codebase/partials/temporal_context.j2,sha256=yYHQHBQ4EeSs6TtKPm9fflGW3y6H0-yAANcTdsApkk4,1388
96
96
  shotgun/prompts/history/__init__.py,sha256=wbMLQ8yWmYz1sfXXigEAUlNkFcM50KdQv0kp4VU_P58,43
97
97
  shotgun/prompts/history/incremental_summarization.j2,sha256=GmnNh0pWTjaEaI1sPwKNsGCys5fK8xrzWqalAs_LhJw,2447
98
98
  shotgun/prompts/history/summarization.j2,sha256=OYNVHg65zbuWB6_pXzTOs2T2k5qFD2gyfbmr6NP01rs,2268
99
99
  shotgun/sdk/__init__.py,sha256=ESV0WM9MigjXG30g9qVjcCMI40GQv-P-MSMGVuOisK4,380
100
- shotgun/sdk/codebase.py,sha256=3FzSI9eCg7idps_cD-cnRwMIsJVAVhZQWBR0bU4hdA8,8602
100
+ shotgun/sdk/codebase.py,sha256=7doUvwwl27RDJZIbP56LQsAx26GANtAKEBptTUhLT6w,8842
101
101
  shotgun/sdk/exceptions.py,sha256=qBcQv0v7ZTwP7CMcxZST4GqCsfOWtOUjSzGBo0-heqo,412
102
102
  shotgun/sdk/models.py,sha256=X9nOTUHH0cdkQW1NfnMEDu-QgK9oUsEISh1Jtwr5Am4,5496
103
103
  shotgun/sdk/services.py,sha256=J4PJFSxCQ6--u7rb3Ta-9eYtlYcxcbnzrMP6ThyCnw4,705
104
104
  shotgun/tui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
105
- shotgun/tui/app.py,sha256=3_g11ZyhJ0avcnX0vundJ9dF2EkkcZnHBBSYzg-P_54,3583
105
+ shotgun/tui/app.py,sha256=xosEcdzWzRv0Aa0ZLJ04DYlYPpJ6dk2BITEPNLyFxms,4330
106
106
  shotgun/tui/filtered_codebase_service.py,sha256=lJ8gTMhIveTatmvmGLP299msWWTkVYKwvY_2FhuL2s4,1687
107
107
  shotgun/tui/styles.tcss,sha256=ETyyw1bpMBOqTi5RLcAJUScdPWTvAWEqE9YcT0kVs_E,121
108
108
  shotgun/tui/commands/__init__.py,sha256=8D5lvtpqMW5-fF7Bg3oJtUzU75cKOv6aUaHYYszydU8,2518
@@ -110,7 +110,7 @@ shotgun/tui/components/prompt_input.py,sha256=Ss-htqraHZAPaehGE4x86ij0veMjc4Ugad
110
110
  shotgun/tui/components/spinner.py,sha256=ovTDeaJ6FD6chZx_Aepia6R3UkPOVJ77EKHfRmn39MY,2427
111
111
  shotgun/tui/components/splash.py,sha256=vppy9vEIEvywuUKRXn2y11HwXSRkQZHLYoVjhDVdJeU,1267
112
112
  shotgun/tui/components/vertical_tail.py,sha256=kROwTaRjUwVB7H35dtmNcUVPQqNYvvfq7K2tXBKEb6c,638
113
- shotgun/tui/screens/chat.py,sha256=HYHiHpvek465tOX2YBs_O1en29O524qaDlB3R_8Z9s4,27304
113
+ shotgun/tui/screens/chat.py,sha256=1XpiST6yIiFFGDN8VEQis9m4dWPvPdl7Bk_7avSsGUg,30928
114
114
  shotgun/tui/screens/chat.tcss,sha256=2Yq3E23jxsySYsgZf4G1AYrYVcpX0UDW6kNNI0tDmtM,437
115
115
  shotgun/tui/screens/directory_setup.py,sha256=lIZ1J4A6g5Q2ZBX8epW7BhR96Dmdcg22CyiM5S-I5WU,3237
116
116
  shotgun/tui/screens/provider_config.py,sha256=A_tvDHF5KLP5PV60LjMJ_aoOdT3TjI6_g04UIUqGPqM,7126
@@ -118,7 +118,7 @@ shotgun/tui/screens/splash.py,sha256=E2MsJihi3c9NY1L28o_MstDxGwrCnnV7zdq00MrGAsw
118
118
  shotgun/tui/screens/chat_screen/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
119
119
  shotgun/tui/screens/chat_screen/command_providers.py,sha256=55JIH9T8QnyHRsMoXhOi87FiVM-d6o7OKpCe82uDP9I,7840
120
120
  shotgun/tui/screens/chat_screen/hint_message.py,sha256=WOpbk8q7qt7eOHTyyHvh_IQIaublVDeJGaLpsxEk9FA,933
121
- shotgun/tui/screens/chat_screen/history.py,sha256=BncDlwXks5wKosPlU4_TioYVMWeep8gDxtpAQsgiB9k,9717
121
+ shotgun/tui/screens/chat_screen/history.py,sha256=NVLA3_tERTyB4vkH71w8ef_M5CszfkwbQOuMb100Fzc,12272
122
122
  shotgun/tui/utils/__init__.py,sha256=cFjDfoXTRBq29wgP7TGRWUu1eFfiIG-LLOzjIGfadgI,150
123
123
  shotgun/tui/utils/mode_progress.py,sha256=lseRRo7kMWLkBzI3cU5vqJmS2ZcCjyRYf9Zwtvc-v58,10931
124
124
  shotgun/utils/__init__.py,sha256=WinIEp9oL2iMrWaDkXz2QX4nYVPAm8C9aBSKTeEwLtE,198
@@ -126,8 +126,8 @@ shotgun/utils/env_utils.py,sha256=8QK5aw_f_V2AVTleQQlcL0RnD4sPJWXlDG46fsHu0d8,10
126
126
  shotgun/utils/file_system_utils.py,sha256=l-0p1bEHF34OU19MahnRFdClHufThfGAjQ431teAIp0,1004
127
127
  shotgun/utils/source_detection.py,sha256=Co6Q03R3fT771TF3RzB-70stfjNP2S4F_ArZKibwzm8,454
128
128
  shotgun/utils/update_checker.py,sha256=IgzPHRhS1ETH7PnJR_dIx6lxgr1qHpCkMTgzUxvGjhI,7586
129
- shotgun_sh-0.1.12.dev3.dist-info/METADATA,sha256=DVBz9JPoN3t6ypYy6oit9oEExatPN8WDvVlcvA1bFp4,11197
130
- shotgun_sh-0.1.12.dev3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
131
- shotgun_sh-0.1.12.dev3.dist-info/entry_points.txt,sha256=asZxLU4QILneq0MWW10saVCZc4VWhZfb0wFZvERnzfA,45
132
- shotgun_sh-0.1.12.dev3.dist-info/licenses/LICENSE,sha256=YebsZl590zCHrF_acCU5pmNt0pnAfD2DmAnevJPB1tY,1065
133
- shotgun_sh-0.1.12.dev3.dist-info/RECORD,,
129
+ shotgun_sh-0.1.13.dev1.dist-info/METADATA,sha256=ghoaqJH6WWZee1-qvat2Ac1RV9Ky6zvQkxJffBLvf_A,11197
130
+ shotgun_sh-0.1.13.dev1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
131
+ shotgun_sh-0.1.13.dev1.dist-info/entry_points.txt,sha256=asZxLU4QILneq0MWW10saVCZc4VWhZfb0wFZvERnzfA,45
132
+ shotgun_sh-0.1.13.dev1.dist-info/licenses/LICENSE,sha256=YebsZl590zCHrF_acCU5pmNt0pnAfD2DmAnevJPB1tY,1065
133
+ shotgun_sh-0.1.13.dev1.dist-info/RECORD,,