srcodex 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. srcodex/__init__.py +0 -0
  2. srcodex/backend/__init__.py +0 -0
  3. srcodex/backend/chat.py +79 -0
  4. srcodex/backend/main.py +98 -0
  5. srcodex/backend/services/__init__.py +0 -0
  6. srcodex/backend/services/claude_service.py +754 -0
  7. srcodex/backend/services/config_loader.py +113 -0
  8. srcodex/backend/services/file_access_tools.py +279 -0
  9. srcodex/backend/services/file_tree.py +480 -0
  10. srcodex/backend/services/graph_tools.py +874 -0
  11. srcodex/backend/services/logger_setup.py +91 -0
  12. srcodex/backend/services/session_manager.py +81 -0
  13. srcodex/backend/services/status_tracker.py +91 -0
  14. srcodex/cli.py +255 -0
  15. srcodex/core/__init__.py +0 -0
  16. srcodex/core/config.py +113 -0
  17. srcodex/core/logger.py +23 -0
  18. srcodex/indexer/__init__.py +0 -0
  19. srcodex/indexer/cscope_client.py +183 -0
  20. srcodex/indexer/ctags_compat.py +223 -0
  21. srcodex/indexer/ctags_parser.py +456 -0
  22. srcodex/indexer/explorer.py +135 -0
  23. srcodex/indexer/field_access_analyzer.py +436 -0
  24. srcodex/indexer/indexer.py +664 -0
  25. srcodex/indexer/reference_ingestor.py +293 -0
  26. srcodex/indexer/reference_resolver.py +544 -0
  27. srcodex/tui/__init__.py +0 -0
  28. srcodex/tui/app.py +103 -0
  29. srcodex/tui/app.tcss +24 -0
  30. srcodex/tui/components/__init__.py +0 -0
  31. srcodex/tui/components/bars/__init__.py +0 -0
  32. srcodex/tui/components/bars/chat_header.py +48 -0
  33. srcodex/tui/components/bars/code_tab_bar.py +157 -0
  34. srcodex/tui/components/bars/footer_bar.py +128 -0
  35. srcodex/tui/components/bars/left_tab.py +54 -0
  36. srcodex/tui/components/logger.py +57 -0
  37. srcodex/tui/components/panels/__init__.py +0 -0
  38. srcodex/tui/components/panels/chat_panel.py +523 -0
  39. srcodex/tui/components/panels/code_panel.py +229 -0
  40. srcodex/tui/components/panels/side_panel.py +128 -0
  41. srcodex/tui/components/views/__init__.py +0 -0
  42. srcodex/tui/components/views/explorer_view.py +20 -0
  43. srcodex/tui/components/views/search_view.py +148 -0
  44. srcodex/tui/components/widgets/__init__.py +0 -0
  45. srcodex/tui/components/widgets/file_browser.py +16 -0
  46. srcodex/tui/components/widgets/find_box.py +85 -0
  47. srcodex-0.2.0.dist-info/METADATA +170 -0
  48. srcodex-0.2.0.dist-info/RECORD +52 -0
  49. srcodex-0.2.0.dist-info/WHEEL +5 -0
  50. srcodex-0.2.0.dist-info/entry_points.txt +2 -0
  51. srcodex-0.2.0.dist-info/licenses/LICENSE +21 -0
  52. srcodex-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,48 @@
1
+ from textual.widgets import Static
2
+ from textual.containers import Horizontal
3
+ from textual.message import Message
4
+
5
+
6
+ class ChatHeaderButton(Static):
7
+ def __init__(self, label: str, button_id: str, **kwargs):
8
+ super().__init__(label, **kwargs)
9
+ self.button_id = button_id
10
+
11
+
12
+ class ChatHeader(Horizontal):
13
+ class SettingsClicked(Message):
14
+ pass
15
+
16
+ DEFAULT_CSS = """
17
+ ChatHeader {
18
+ height: 1;
19
+ width: 100%;
20
+ align: right middle;
21
+ dock: top;
22
+ }
23
+
24
+ ChatHeaderButton {
25
+ width: auto;
26
+ height: 1;
27
+ background: transparent;
28
+ padding: 0 1;
29
+ }
30
+
31
+ ChatHeaderButton:hover {
32
+ background: transparent;
33
+ }
34
+
35
+ ChatHeaderButton#header-settings {
36
+ margin-right: 4;
37
+ }
38
+ """
39
+
40
+ def compose(self):
41
+ yield ChatHeaderButton("✳", "header-chat", id="header-chat")
42
+ yield ChatHeaderButton("⚙", "header-settings", id="header-settings")
43
+
44
+ def on_click(self, event) -> None:
45
+ clicked = event.widget
46
+ if isinstance(clicked, ChatHeaderButton):
47
+ if clicked.button_id == "header-settings":
48
+ self.post_message(self.SettingsClicked())
@@ -0,0 +1,157 @@
1
+ from pathlib import Path
2
+ from textual.widgets import Static
3
+ from textual.containers import Horizontal
4
+ from textual.message import Message
5
+
6
+ class CloseButton(Static):
7
+ """Close button for file tab"""
8
+ def __init__(self, **kwargs):
9
+ super().__init__("✕", **kwargs)
10
+
11
+ class FileTab(Horizontal):
12
+ """File tab with filename and close button"""
13
+ def __init__(self, file_path: str, active: bool = False, **kwargs):
14
+ super().__init__(**kwargs)
15
+ self.file_path = file_path
16
+ self.filename = Path(file_path).name
17
+ if active:
18
+ self.add_class("active")
19
+
20
+ def compose(self):
21
+ yield Static(self.filename, classes="tab-label")
22
+ yield CloseButton(classes="tab-close")
23
+
24
+ class CodeTabBar(Horizontal):
25
+ """Horizonatal tab bar for open files"""
26
+ class TabClicked(Message):
27
+ """Posted when file tab is clicked"""
28
+ def __init__(self, file_path: str):
29
+ super().__init__()
30
+ self.file_path = file_path
31
+
32
+ class TabClosed(Message):
33
+ """Posted when tab close button is clicked"""
34
+ def __init__(self, file_path: str):
35
+ super().__init__()
36
+ self.file_path = file_path
37
+
38
+ DEFAULT_CSS = """
39
+ CodeTabBar {
40
+ height: 1;
41
+ width: 100%;
42
+ align: left middle;
43
+ dock: top;
44
+ }
45
+
46
+ FileTab {
47
+ width: auto;
48
+ height: 1;
49
+ background: transparent;
50
+ padding: 0 1;
51
+ }
52
+
53
+ FileTab:hover {
54
+ background: transparent;
55
+ }
56
+
57
+ FileTab.active {
58
+ text-style: bold;
59
+ }
60
+
61
+ FileTab .tab-label {
62
+ width: auto;
63
+ height: 1;
64
+ }
65
+
66
+ FileTab .tab-close {
67
+ width: auto;
68
+ height: 1;
69
+ color: $text-muted;
70
+ padding: 0 1;
71
+ }
72
+
73
+ FileTab .tab-close:hover {
74
+ color: $error;
75
+ text-style: bold;
76
+ }
77
+ """
78
+
79
+ def __init__(self, **kwargs):
80
+ self.open_files = [] #list of open files
81
+ self.active_file = None
82
+ super().__init__(**kwargs)
83
+
84
+ def compose(self):
85
+ return []
86
+
87
+ def add_tab(self, file_path: str, set_active: bool = True):
88
+ """add new file tab"""
89
+ if file_path in self.open_files:
90
+ if set_active:
91
+ self._set_active_tab(file_path)
92
+ return
93
+
94
+ # add to open files list
95
+ self.open_files.append(file_path)
96
+ # create and mount the tab
97
+ is_active = set_active or len(self.open_files) == 1
98
+ tab = FileTab(file_path, active=is_active)
99
+ self.mount(tab)
100
+
101
+ if is_active:
102
+ self.active_file = file_path
103
+
104
+ def _set_active_tab(self, file_path: str):
105
+ """set which tab is active"""
106
+ self.active_file = file_path
107
+
108
+ for tab in self.query(FileTab):
109
+ if tab.file_path == file_path:
110
+ tab.add_class("active")
111
+ else:
112
+ tab.remove_class("active")
113
+
114
+ def close_tab(self, file_path: str):
115
+ """Close a tab"""
116
+ if file_path not in self.open_files:
117
+ return
118
+
119
+ self.open_files.remove(file_path)
120
+
121
+ # Remove the tab widget
122
+ for tab in self.query(FileTab):
123
+ if tab.file_path == file_path:
124
+ tab.remove()
125
+ break
126
+
127
+ # If this was the active tab, switch to another
128
+ if self.active_file == file_path:
129
+ if self.open_files:
130
+ new_active = self.open_files[-1]
131
+ self._set_active_tab(new_active)
132
+ self.post_message(self.TabClicked(new_active))
133
+ else:
134
+ self.active_file = None
135
+
136
+ def on_click(self, event):
137
+ """Handle tab clicks and close button"""
138
+ clicked = event.widget
139
+
140
+ # Check if close button was clicked
141
+ if isinstance(clicked, CloseButton):
142
+ # Find parent FileTab
143
+ parent = clicked.parent
144
+ if isinstance(parent, FileTab):
145
+ self.close_tab(parent.file_path)
146
+ self.post_message(self.TabClosed(parent.file_path))
147
+
148
+ # Check if tab label or tab itself was clicked
149
+ elif isinstance(clicked, FileTab):
150
+ self._set_active_tab(clicked.file_path)
151
+ self.post_message(self.TabClicked(clicked.file_path))
152
+
153
+ # Check if tab label (Static) inside FileTab was clicked
154
+ elif isinstance(clicked, Static) and isinstance(clicked.parent, FileTab):
155
+ parent = clicked.parent
156
+ self._set_active_tab(parent.file_path)
157
+ self.post_message(self.TabClicked(parent.file_path))
@@ -0,0 +1,128 @@
1
+ from textual.widgets import Static
2
+ from textual.containers import Horizontal
3
+ from pathlib import Path
4
+ import json
5
+ import importlib.metadata
6
+
7
+
8
+ class FooterItem(Static):
9
+ """Individual item in the footer bar"""
10
+ def __init__(self, label: str, item_id: str, **kwargs):
11
+ super().__init__(label, **kwargs)
12
+ self.item_id = item_id
13
+
14
+
15
+ class FooterBar(Horizontal):
16
+ """Footer bar spanning full width at bottom of screen
17
+
18
+ Left: version, project name, database name
19
+ Right: persistent token usage (lifetime stats, never resets)
20
+ """
21
+
22
+ DEFAULT_CSS = """
23
+ FooterBar {
24
+ height: 1;
25
+ width: 100%;
26
+ align: left middle;
27
+ dock: bottom;
28
+ }
29
+
30
+ FooterItem {
31
+ width: auto;
32
+ height: 1;
33
+ background: transparent;
34
+ padding: 0 1;
35
+ }
36
+
37
+ FooterItem:hover {
38
+ background: transparent;
39
+ }
40
+
41
+ #footer-spacer {
42
+ width: 1fr;
43
+ }
44
+ """
45
+
46
+ def __init__(self, project_root: str = None, **kwargs):
47
+ super().__init__(**kwargs)
48
+ self.project_root = Path(project_root) if project_root else Path.cwd()
49
+ self.query_count = 0
50
+ self.total_user_input = 0
51
+ self.total_output = 0
52
+ self.savings_percentage = 0.0
53
+
54
+ def compose(self):
55
+ # Get version from package metadata
56
+ try:
57
+ version = importlib.metadata.version("srcodex")
58
+ except Exception:
59
+ version = "0.3.0"
60
+
61
+ # Get project name and database name from metadata.json
62
+ metadata_path = self.project_root / ".srcodex" / "metadata.json"
63
+ project_name = self.project_root.name
64
+ db_name = "unknown.db"
65
+
66
+ try:
67
+ with open(metadata_path, 'r') as f:
68
+ metadata = json.load(f)
69
+ project_name = metadata.get("project", {}).get("name", project_name)
70
+ db_path = metadata.get("paths", {}).get("database", "")
71
+ if db_path:
72
+ db_name = Path(db_path).name
73
+ except Exception:
74
+ pass
75
+
76
+ # Load token stats from session
77
+ self.load_stats()
78
+
79
+ # Left side: version, project, database
80
+ yield FooterItem(f"v{version}", "footer-version", id="footer-version")
81
+ yield FooterItem(f"🖿 {project_name}", "footer-project", id="footer-project")
82
+ yield FooterItem(f"☰ {db_name}", "footer-database", id="footer-database")
83
+
84
+ # Spacer (pushes token stats to the right)
85
+ yield FooterItem("", "footer-spacer", id="footer-spacer")
86
+
87
+ # Right side: token stats (persistent)
88
+ yield FooterItem(self._format_stats(), "footer-tokens", id="footer-tokens")
89
+
90
+ def load_stats(self):
91
+ """Load persistent stats from conversation metadata"""
92
+ metadata_path = self.project_root / ".srcodex" / "conversations" / "latest.json"
93
+ try:
94
+ with open(metadata_path, 'r') as f:
95
+ session_data = json.load(f)
96
+ metadata = session_data.get("metadata", {})
97
+ self.query_count = metadata.get("query_count", 0)
98
+ self.total_user_input = metadata.get("total_user_input", 0)
99
+ self.total_output = metadata.get("total_output", 0)
100
+ self.savings_percentage = metadata.get("total_savings_percentage", 0.0)
101
+ except Exception:
102
+ pass
103
+
104
+ def update_stats(self, query_count, total_user_input, total_output, savings_percentage):
105
+ """Update footer token stats"""
106
+ self.query_count = query_count
107
+ self.total_user_input = total_user_input
108
+ self.total_output = total_output
109
+ self.savings_percentage = savings_percentage
110
+
111
+ # Update footer display (right side only)
112
+ footer_item = self.query_one("#footer-tokens", FooterItem)
113
+ footer_item.update(self._format_stats())
114
+
115
+ def _format_stats(self):
116
+ """Format stats string"""
117
+ # Format tokens (12500 → 12K, 125000 → 125K)
118
+ def format_tokens(tokens):
119
+ if tokens >= 1000:
120
+ return f"{tokens // 1000}K"
121
+ return str(tokens)
122
+
123
+ return (
124
+ f"{self.query_count} queries 🪙"
125
+ f"{format_tokens(self.total_user_input)} input 🪙"
126
+ f"{format_tokens(self.total_output)} output "
127
+ f"({self.savings_percentage:.0f}% savings)"
128
+ )
@@ -0,0 +1,54 @@
1
+ from textual.widgets import Static
2
+ from textual.containers import Horizontal
3
+ from textual.message import Message
4
+
5
+
6
+ class TabButton(Static):
7
+ def __init__(self, label: str, tab_id: str, active: bool = False, **kwargs):
8
+ super().__init__(label, **kwargs)
9
+ self.tab_id = tab_id
10
+ if active:
11
+ self.add_class("active")
12
+
13
+
14
+ class LeftTab(Horizontal):
15
+ class TabClicked(Message):
16
+ def __init__(self, tab_id: str):
17
+ super().__init__()
18
+ self.tab_id = tab_id
19
+
20
+ DEFAULT_CSS = """
21
+ LeftTab {
22
+ height: 1;
23
+ width: 100%;
24
+ align: left middle;
25
+ dock: top;
26
+ }
27
+
28
+ TabButton {
29
+ width: auto;
30
+ height: 1;
31
+ background: transparent;
32
+ padding: 0 1;
33
+ }
34
+
35
+ TabButton:hover {
36
+ background: transparent;
37
+ }
38
+
39
+ TabButton.active {
40
+ text-style: bold;
41
+ }
42
+ """
43
+
44
+ def compose(self):
45
+ yield TabButton("📁", "tab-explorer", active=True, id="tab-explorer")
46
+ yield TabButton("🔍", "tab-search", id="tab-search")
47
+
48
+ def on_click(self, event) -> None:
49
+ clicked = event.widget
50
+ if isinstance(clicked, TabButton):
51
+ for tab in self.query(TabButton):
52
+ tab.remove_class("active")
53
+ clicked.add_class("active")
54
+ self.post_message(self.TabClicked(clicked.tab_id))
@@ -0,0 +1,57 @@
1
+ """
2
+ TUI Logging Setup
3
+ Logs TUI events to .srcodex/.debug/tui.log
4
+ """
5
+ import logging
6
+ from pathlib import Path
7
+ import sys
8
+
9
+ # Add backend to path for config loader
10
+ backend_path = Path(__file__).parent.parent.parent / "backend"
11
+ sys.path.insert(0, str(backend_path))
12
+
13
+ from services.config_loader import get_config
14
+
15
+
16
+ def setup_tui_logging():
17
+ """
18
+ Configure TUI logging to write to .srcodex/.debug/tui.log
19
+ Returns the logger instance
20
+ """
21
+ try:
22
+ config = get_config()
23
+ debug_dir = config.project_root / ".srcodex" / ".debug"
24
+ debug_dir.mkdir(parents=True, exist_ok=True)
25
+
26
+ log_file = debug_dir / "tui.log"
27
+
28
+ # Create logger
29
+ logger = logging.getLogger('srcodex.tui')
30
+ logger.setLevel(logging.INFO)
31
+
32
+ # File handler (append mode)
33
+ file_handler = logging.FileHandler(log_file, mode='a')
34
+ file_handler.setLevel(logging.INFO)
35
+ file_handler.setFormatter(
36
+ logging.Formatter('%(asctime)s [%(levelname)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
37
+ )
38
+
39
+ logger.addHandler(file_handler)
40
+ logger.info("=" * 80)
41
+ logger.info("TUI session started")
42
+
43
+ return logger
44
+
45
+ except Exception as e:
46
+ # Fallback logger
47
+ logger = logging.getLogger('srcodex.tui')
48
+ logger.setLevel(logging.INFO)
49
+ handler = logging.StreamHandler()
50
+ handler.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] %(message)s'))
51
+ logger.addHandler(handler)
52
+ logger.warning(f"Could not set up TUI file logging: {e}")
53
+ return logger
54
+
55
+
56
+ # Initialize logger module-level
57
+ logger = setup_tui_logging()
File without changes