superqode 0.1.5__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 (288) hide show
  1. superqode/__init__.py +33 -0
  2. superqode/acp/__init__.py +23 -0
  3. superqode/acp/client.py +913 -0
  4. superqode/acp/permission_screen.py +457 -0
  5. superqode/acp/types.py +480 -0
  6. superqode/acp_discovery.py +856 -0
  7. superqode/agent/__init__.py +22 -0
  8. superqode/agent/edit_strategies.py +334 -0
  9. superqode/agent/loop.py +892 -0
  10. superqode/agent/qe_report_templates.py +39 -0
  11. superqode/agent/system_prompts.py +353 -0
  12. superqode/agent_output.py +721 -0
  13. superqode/agent_stream.py +953 -0
  14. superqode/agents/__init__.py +59 -0
  15. superqode/agents/acp_registry.py +305 -0
  16. superqode/agents/client.py +249 -0
  17. superqode/agents/data/augmentcode.com.toml +51 -0
  18. superqode/agents/data/cagent.dev.toml +51 -0
  19. superqode/agents/data/claude.com.toml +60 -0
  20. superqode/agents/data/codeassistant.dev.toml +51 -0
  21. superqode/agents/data/codex.openai.com.toml +57 -0
  22. superqode/agents/data/fastagent.ai.toml +66 -0
  23. superqode/agents/data/geminicli.com.toml +77 -0
  24. superqode/agents/data/goose.block.xyz.toml +54 -0
  25. superqode/agents/data/junie.jetbrains.com.toml +56 -0
  26. superqode/agents/data/kimi.moonshot.cn.toml +57 -0
  27. superqode/agents/data/llmlingagent.dev.toml +51 -0
  28. superqode/agents/data/molt.bot.toml +49 -0
  29. superqode/agents/data/opencode.ai.toml +60 -0
  30. superqode/agents/data/stakpak.dev.toml +51 -0
  31. superqode/agents/data/vtcode.dev.toml +51 -0
  32. superqode/agents/discovery.py +266 -0
  33. superqode/agents/messaging.py +160 -0
  34. superqode/agents/persona.py +166 -0
  35. superqode/agents/registry.py +421 -0
  36. superqode/agents/schema.py +72 -0
  37. superqode/agents/unified.py +367 -0
  38. superqode/app/__init__.py +111 -0
  39. superqode/app/constants.py +314 -0
  40. superqode/app/css.py +366 -0
  41. superqode/app/models.py +118 -0
  42. superqode/app/suggester.py +125 -0
  43. superqode/app/widgets.py +1591 -0
  44. superqode/app_enhanced.py +399 -0
  45. superqode/app_main.py +17187 -0
  46. superqode/approval.py +312 -0
  47. superqode/atomic.py +296 -0
  48. superqode/commands/__init__.py +1 -0
  49. superqode/commands/acp.py +965 -0
  50. superqode/commands/agents.py +180 -0
  51. superqode/commands/auth.py +278 -0
  52. superqode/commands/config.py +374 -0
  53. superqode/commands/init.py +826 -0
  54. superqode/commands/providers.py +819 -0
  55. superqode/commands/qe.py +1145 -0
  56. superqode/commands/roles.py +380 -0
  57. superqode/commands/serve.py +172 -0
  58. superqode/commands/suggestions.py +127 -0
  59. superqode/commands/superqe.py +460 -0
  60. superqode/config/__init__.py +51 -0
  61. superqode/config/loader.py +812 -0
  62. superqode/config/schema.py +498 -0
  63. superqode/core/__init__.py +111 -0
  64. superqode/core/roles.py +281 -0
  65. superqode/danger.py +386 -0
  66. superqode/data/superqode-template.yaml +1522 -0
  67. superqode/design_system.py +1080 -0
  68. superqode/dialogs/__init__.py +6 -0
  69. superqode/dialogs/base.py +39 -0
  70. superqode/dialogs/model.py +130 -0
  71. superqode/dialogs/provider.py +870 -0
  72. superqode/diff_view.py +919 -0
  73. superqode/enterprise.py +21 -0
  74. superqode/evaluation/__init__.py +25 -0
  75. superqode/evaluation/adapters.py +93 -0
  76. superqode/evaluation/behaviors.py +89 -0
  77. superqode/evaluation/engine.py +209 -0
  78. superqode/evaluation/scenarios.py +96 -0
  79. superqode/execution/__init__.py +36 -0
  80. superqode/execution/linter.py +538 -0
  81. superqode/execution/modes.py +347 -0
  82. superqode/execution/resolver.py +283 -0
  83. superqode/execution/runner.py +642 -0
  84. superqode/file_explorer.py +811 -0
  85. superqode/file_viewer.py +471 -0
  86. superqode/flash.py +183 -0
  87. superqode/guidance/__init__.py +58 -0
  88. superqode/guidance/config.py +203 -0
  89. superqode/guidance/prompts.py +71 -0
  90. superqode/harness/__init__.py +54 -0
  91. superqode/harness/accelerator.py +291 -0
  92. superqode/harness/config.py +319 -0
  93. superqode/harness/validator.py +147 -0
  94. superqode/history.py +279 -0
  95. superqode/integrations/superopt_runner.py +124 -0
  96. superqode/logging/__init__.py +49 -0
  97. superqode/logging/adapters.py +219 -0
  98. superqode/logging/formatter.py +923 -0
  99. superqode/logging/integration.py +341 -0
  100. superqode/logging/sinks.py +170 -0
  101. superqode/logging/unified_log.py +417 -0
  102. superqode/lsp/__init__.py +26 -0
  103. superqode/lsp/client.py +544 -0
  104. superqode/main.py +1069 -0
  105. superqode/mcp/__init__.py +89 -0
  106. superqode/mcp/auth_storage.py +380 -0
  107. superqode/mcp/client.py +1236 -0
  108. superqode/mcp/config.py +319 -0
  109. superqode/mcp/integration.py +337 -0
  110. superqode/mcp/oauth.py +436 -0
  111. superqode/mcp/oauth_callback.py +385 -0
  112. superqode/mcp/types.py +290 -0
  113. superqode/memory/__init__.py +31 -0
  114. superqode/memory/feedback.py +342 -0
  115. superqode/memory/store.py +522 -0
  116. superqode/notifications.py +369 -0
  117. superqode/optimization/__init__.py +5 -0
  118. superqode/optimization/config.py +33 -0
  119. superqode/permissions/__init__.py +25 -0
  120. superqode/permissions/rules.py +488 -0
  121. superqode/plan.py +323 -0
  122. superqode/providers/__init__.py +33 -0
  123. superqode/providers/gateway/__init__.py +165 -0
  124. superqode/providers/gateway/base.py +228 -0
  125. superqode/providers/gateway/litellm_gateway.py +1170 -0
  126. superqode/providers/gateway/openresponses_gateway.py +436 -0
  127. superqode/providers/health.py +297 -0
  128. superqode/providers/huggingface/__init__.py +74 -0
  129. superqode/providers/huggingface/downloader.py +472 -0
  130. superqode/providers/huggingface/endpoints.py +442 -0
  131. superqode/providers/huggingface/hub.py +531 -0
  132. superqode/providers/huggingface/inference.py +394 -0
  133. superqode/providers/huggingface/transformers_runner.py +516 -0
  134. superqode/providers/local/__init__.py +100 -0
  135. superqode/providers/local/base.py +438 -0
  136. superqode/providers/local/discovery.py +418 -0
  137. superqode/providers/local/lmstudio.py +256 -0
  138. superqode/providers/local/mlx.py +457 -0
  139. superqode/providers/local/ollama.py +486 -0
  140. superqode/providers/local/sglang.py +268 -0
  141. superqode/providers/local/tgi.py +260 -0
  142. superqode/providers/local/tool_support.py +477 -0
  143. superqode/providers/local/vllm.py +258 -0
  144. superqode/providers/manager.py +1338 -0
  145. superqode/providers/models.py +1016 -0
  146. superqode/providers/models_dev.py +578 -0
  147. superqode/providers/openresponses/__init__.py +87 -0
  148. superqode/providers/openresponses/converters/__init__.py +17 -0
  149. superqode/providers/openresponses/converters/messages.py +343 -0
  150. superqode/providers/openresponses/converters/tools.py +268 -0
  151. superqode/providers/openresponses/schema/__init__.py +56 -0
  152. superqode/providers/openresponses/schema/models.py +585 -0
  153. superqode/providers/openresponses/streaming/__init__.py +5 -0
  154. superqode/providers/openresponses/streaming/parser.py +338 -0
  155. superqode/providers/openresponses/tools/__init__.py +21 -0
  156. superqode/providers/openresponses/tools/apply_patch.py +352 -0
  157. superqode/providers/openresponses/tools/code_interpreter.py +290 -0
  158. superqode/providers/openresponses/tools/file_search.py +333 -0
  159. superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
  160. superqode/providers/registry.py +716 -0
  161. superqode/providers/usage.py +332 -0
  162. superqode/pure_mode.py +384 -0
  163. superqode/qr/__init__.py +23 -0
  164. superqode/qr/dashboard.py +781 -0
  165. superqode/qr/generator.py +1018 -0
  166. superqode/qr/templates.py +135 -0
  167. superqode/safety/__init__.py +41 -0
  168. superqode/safety/sandbox.py +413 -0
  169. superqode/safety/warnings.py +256 -0
  170. superqode/server/__init__.py +33 -0
  171. superqode/server/lsp_server.py +775 -0
  172. superqode/server/web.py +250 -0
  173. superqode/session/__init__.py +25 -0
  174. superqode/session/persistence.py +580 -0
  175. superqode/session/sharing.py +477 -0
  176. superqode/session.py +475 -0
  177. superqode/sidebar.py +2991 -0
  178. superqode/stream_view.py +648 -0
  179. superqode/styles/__init__.py +3 -0
  180. superqode/superqe/__init__.py +184 -0
  181. superqode/superqe/acp_runner.py +1064 -0
  182. superqode/superqe/constitution/__init__.py +62 -0
  183. superqode/superqe/constitution/evaluator.py +308 -0
  184. superqode/superqe/constitution/loader.py +432 -0
  185. superqode/superqe/constitution/schema.py +250 -0
  186. superqode/superqe/events.py +591 -0
  187. superqode/superqe/frameworks/__init__.py +65 -0
  188. superqode/superqe/frameworks/base.py +234 -0
  189. superqode/superqe/frameworks/e2e.py +263 -0
  190. superqode/superqe/frameworks/executor.py +237 -0
  191. superqode/superqe/frameworks/javascript.py +409 -0
  192. superqode/superqe/frameworks/python.py +373 -0
  193. superqode/superqe/frameworks/registry.py +92 -0
  194. superqode/superqe/mcp_tools/__init__.py +47 -0
  195. superqode/superqe/mcp_tools/core_tools.py +418 -0
  196. superqode/superqe/mcp_tools/registry.py +230 -0
  197. superqode/superqe/mcp_tools/testing_tools.py +167 -0
  198. superqode/superqe/noise.py +89 -0
  199. superqode/superqe/orchestrator.py +778 -0
  200. superqode/superqe/roles.py +609 -0
  201. superqode/superqe/session.py +713 -0
  202. superqode/superqe/skills/__init__.py +57 -0
  203. superqode/superqe/skills/base.py +106 -0
  204. superqode/superqe/skills/core_skills.py +899 -0
  205. superqode/superqe/skills/registry.py +90 -0
  206. superqode/superqe/verifier.py +101 -0
  207. superqode/superqe_cli.py +76 -0
  208. superqode/tool_call.py +358 -0
  209. superqode/tools/__init__.py +93 -0
  210. superqode/tools/agent_tools.py +496 -0
  211. superqode/tools/base.py +324 -0
  212. superqode/tools/batch_tool.py +133 -0
  213. superqode/tools/diagnostics.py +311 -0
  214. superqode/tools/edit_tools.py +653 -0
  215. superqode/tools/enhanced_base.py +515 -0
  216. superqode/tools/file_tools.py +269 -0
  217. superqode/tools/file_tracking.py +45 -0
  218. superqode/tools/lsp_tools.py +610 -0
  219. superqode/tools/network_tools.py +350 -0
  220. superqode/tools/permissions.py +400 -0
  221. superqode/tools/question_tool.py +324 -0
  222. superqode/tools/search_tools.py +598 -0
  223. superqode/tools/shell_tools.py +259 -0
  224. superqode/tools/todo_tools.py +121 -0
  225. superqode/tools/validation.py +80 -0
  226. superqode/tools/web_tools.py +639 -0
  227. superqode/tui.py +1152 -0
  228. superqode/tui_integration.py +875 -0
  229. superqode/tui_widgets/__init__.py +27 -0
  230. superqode/tui_widgets/widgets/__init__.py +18 -0
  231. superqode/tui_widgets/widgets/progress.py +185 -0
  232. superqode/tui_widgets/widgets/tool_display.py +188 -0
  233. superqode/undo_manager.py +574 -0
  234. superqode/utils/__init__.py +5 -0
  235. superqode/utils/error_handling.py +323 -0
  236. superqode/utils/fuzzy.py +257 -0
  237. superqode/widgets/__init__.py +477 -0
  238. superqode/widgets/agent_collab.py +390 -0
  239. superqode/widgets/agent_store.py +936 -0
  240. superqode/widgets/agent_switcher.py +395 -0
  241. superqode/widgets/animation_manager.py +284 -0
  242. superqode/widgets/code_context.py +356 -0
  243. superqode/widgets/command_palette.py +412 -0
  244. superqode/widgets/connection_status.py +537 -0
  245. superqode/widgets/conversation_history.py +470 -0
  246. superqode/widgets/diff_indicator.py +155 -0
  247. superqode/widgets/enhanced_status_bar.py +385 -0
  248. superqode/widgets/enhanced_toast.py +476 -0
  249. superqode/widgets/file_browser.py +809 -0
  250. superqode/widgets/file_reference.py +585 -0
  251. superqode/widgets/issue_timeline.py +340 -0
  252. superqode/widgets/leader_key.py +264 -0
  253. superqode/widgets/mode_switcher.py +445 -0
  254. superqode/widgets/model_picker.py +234 -0
  255. superqode/widgets/permission_preview.py +1205 -0
  256. superqode/widgets/prompt.py +358 -0
  257. superqode/widgets/provider_connect.py +725 -0
  258. superqode/widgets/pty_shell.py +587 -0
  259. superqode/widgets/qe_dashboard.py +321 -0
  260. superqode/widgets/resizable_sidebar.py +377 -0
  261. superqode/widgets/response_changes.py +218 -0
  262. superqode/widgets/response_display.py +528 -0
  263. superqode/widgets/rich_tool_display.py +613 -0
  264. superqode/widgets/sidebar_panels.py +1180 -0
  265. superqode/widgets/slash_complete.py +356 -0
  266. superqode/widgets/split_view.py +612 -0
  267. superqode/widgets/status_bar.py +273 -0
  268. superqode/widgets/superqode_display.py +786 -0
  269. superqode/widgets/thinking_display.py +815 -0
  270. superqode/widgets/throbber.py +87 -0
  271. superqode/widgets/toast.py +206 -0
  272. superqode/widgets/unified_output.py +1073 -0
  273. superqode/workspace/__init__.py +75 -0
  274. superqode/workspace/artifacts.py +472 -0
  275. superqode/workspace/coordinator.py +353 -0
  276. superqode/workspace/diff_tracker.py +429 -0
  277. superqode/workspace/git_guard.py +373 -0
  278. superqode/workspace/git_snapshot.py +526 -0
  279. superqode/workspace/manager.py +750 -0
  280. superqode/workspace/snapshot.py +357 -0
  281. superqode/workspace/watcher.py +535 -0
  282. superqode/workspace/worktree.py +440 -0
  283. superqode-0.1.5.dist-info/METADATA +204 -0
  284. superqode-0.1.5.dist-info/RECORD +288 -0
  285. superqode-0.1.5.dist-info/WHEEL +5 -0
  286. superqode-0.1.5.dist-info/entry_points.txt +3 -0
  287. superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
  288. superqode-0.1.5.dist-info/top_level.txt +1 -0
@@ -0,0 +1,811 @@
1
+ """File explorer and fuzzy search functionality for SuperQode."""
2
+
3
+ import os
4
+ import re
5
+ from pathlib import Path
6
+ from typing import Iterable, List, Optional, Tuple
7
+ import fnmatch
8
+ from functools import lru_cache
9
+
10
+ try:
11
+ from rich.console import Console
12
+ from rich.panel import Panel
13
+ from rich.table import Table
14
+ from rich.text import Text
15
+ from rich.prompt import Prompt, Confirm
16
+ except ImportError:
17
+ Console = None
18
+
19
+
20
+ class PathFilter:
21
+ """Filter paths based on gitignore patterns and common ignore rules."""
22
+
23
+ def __init__(self, patterns: List[str]):
24
+ self.patterns = patterns
25
+
26
+ @classmethod
27
+ def from_git_root(cls, path: Path) -> "PathFilter":
28
+ """Create filter from .gitignore and common ignore patterns."""
29
+ patterns = []
30
+
31
+ # Read .gitignore if it exists
32
+ gitignore_path = path / ".gitignore"
33
+ if gitignore_path.exists():
34
+ try:
35
+ with open(gitignore_path, "r", encoding="utf-8") as f:
36
+ for line in f:
37
+ line = line.strip()
38
+ if line and not line.startswith("#"):
39
+ patterns.append(line)
40
+ except Exception:
41
+ pass # Ignore gitignore read errors
42
+
43
+ # Add common ignore patterns
44
+ common_patterns = [
45
+ ".git/",
46
+ ".git",
47
+ "__pycache__/",
48
+ "*.pyc",
49
+ "*.pyo",
50
+ "*.pyd",
51
+ ".Python",
52
+ "build/",
53
+ "dist/",
54
+ "*.egg-info/",
55
+ ".DS_Store",
56
+ "node_modules/",
57
+ ".env",
58
+ ".env.local",
59
+ ".env.*",
60
+ "*.log",
61
+ ".vscode/",
62
+ ".idea/",
63
+ ".ruff_cache/",
64
+ "*.swp",
65
+ "*.swo",
66
+ "*~",
67
+ ]
68
+
69
+ patterns.extend(common_patterns)
70
+ return cls(patterns)
71
+
72
+ def match(self, path: Path) -> bool:
73
+ """Check if path matches any ignore pattern."""
74
+ path_str = str(path)
75
+
76
+ # Normalize path separators
77
+ path_str = path_str.replace(os.sep, "/")
78
+
79
+ # Check if path matches any pattern
80
+ for pattern in self.patterns:
81
+ if self._matches_pattern(path_str, pattern):
82
+ return True
83
+
84
+ # Also check the path name only (for files in current directory)
85
+ path_name = path.name
86
+ for pattern in self.patterns:
87
+ if self._matches_pattern(path_name, pattern):
88
+ return True
89
+
90
+ return False
91
+
92
+ def _matches_pattern(self, path_str: str, pattern: str) -> bool:
93
+ """Check if path matches a single pattern."""
94
+ # Normalize pattern separators
95
+ pattern = pattern.replace(os.sep, "/")
96
+
97
+ # Handle directory patterns (ending with /)
98
+ if pattern.endswith("/"):
99
+ pattern = pattern[:-1]
100
+ # Match directory or any file/directory inside it
101
+ return path_str == pattern or path_str.startswith(pattern + "/") or path_str == pattern
102
+
103
+ # Handle wildcards
104
+ return fnmatch.fnmatch(path_str, pattern) or fnmatch.fnmatch(path_str, pattern + "/*")
105
+
106
+
107
+ class CodeExplorer:
108
+ """Simple file explorer for SuperQode CLI."""
109
+
110
+ def __init__(self, root_path: Optional[Path] = None):
111
+ self.root_path = root_path or Path.cwd()
112
+ self.path_filter = PathFilter.from_git_root(self.root_path)
113
+ self.console = Console()
114
+
115
+ def explore_directory(self, path: Optional[Path] = None, max_depth: int = 3) -> str:
116
+ """Generate a text-based directory tree."""
117
+ explore_path = path or self.root_path
118
+
119
+ if not explore_path.exists():
120
+ return f"Path does not exist: {explore_path}"
121
+
122
+ if not explore_path.is_dir():
123
+ return f"Not a directory: {explore_path}"
124
+
125
+ tree_lines = []
126
+ tree_lines.append(f"📁 {explore_path.name}/")
127
+ tree_lines.extend(self._build_tree(explore_path, max_depth=max_depth, prefix=""))
128
+
129
+ return "\n".join(tree_lines)
130
+
131
+ def _build_tree(
132
+ self, path: Path, max_depth: int = 3, prefix: str = "", current_depth: int = 0
133
+ ) -> List[str]:
134
+ """Recursively build directory tree."""
135
+ if current_depth >= max_depth:
136
+ return []
137
+
138
+ lines = []
139
+ try:
140
+ items = sorted(path.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower()))
141
+ filtered_items = []
142
+
143
+ for item in items:
144
+ # Filter out items that match ignore patterns
145
+ try:
146
+ rel_path = item.relative_to(self.root_path)
147
+ if not self.path_filter.match(rel_path) and not self.path_filter.match(item):
148
+ filtered_items.append(item)
149
+ except ValueError:
150
+ # Item is not relative to root, check directly
151
+ if not self.path_filter.match(item):
152
+ filtered_items.append(item)
153
+
154
+ for i, item in enumerate(filtered_items):
155
+ is_last = i == len(filtered_items) - 1
156
+ connector = "└── " if is_last else "├── "
157
+
158
+ # Add emoji based on type
159
+ if item.is_dir():
160
+ icon = "📁"
161
+ elif item.suffix.lower() in [".py", ".js", ".ts", ".java", ".cpp", ".c", ".h"]:
162
+ icon = "📄"
163
+ elif item.suffix.lower() in [".md", ".txt", ".rst"]:
164
+ icon = "📝"
165
+ elif item.suffix.lower() in [".json", ".yaml", ".yml", ".toml"]:
166
+ icon = "⚙️"
167
+ elif item.suffix.lower() in [".png", ".jpg", ".jpeg", ".gif", ".svg"]:
168
+ icon = "🖼️"
169
+ else:
170
+ icon = "📄"
171
+
172
+ lines.append(f"{prefix}{connector}{icon} {item.name}")
173
+
174
+ if item.is_dir() and current_depth < max_depth - 1:
175
+ next_prefix = prefix + (" " if is_last else "│ ")
176
+ lines.extend(self._build_tree(item, max_depth, next_prefix, current_depth + 1))
177
+
178
+ except PermissionError:
179
+ lines.append(f"{prefix}└── 🔒 Permission denied")
180
+
181
+ return lines
182
+
183
+
184
+ class FuzzySearch:
185
+ """Fuzzy search implementation for finding files and content."""
186
+
187
+ def __init__(self, case_sensitive: bool = False, cache_size: int = 1024):
188
+ self.case_sensitive = case_sensitive
189
+ self.cache = {}
190
+ self.cache_size = cache_size
191
+
192
+ def match(self, query: str, candidate: str) -> Tuple[float, List[int]]:
193
+ """Match query against candidate with scoring.
194
+
195
+ Returns:
196
+ Tuple of (score, list of matching positions)
197
+ Score of 0 means no match
198
+ """
199
+ cache_key = (query, candidate)
200
+ if cache_key in self.cache:
201
+ return self.cache[cache_key]
202
+
203
+ if not query:
204
+ return 0.0, []
205
+
206
+ # Normalize case
207
+ if not self.case_sensitive:
208
+ candidate = candidate.lower()
209
+ query = query.lower()
210
+
211
+ score, positions = self._calculate_match(query, candidate)
212
+ result = (score, positions)
213
+
214
+ # Simple LRU-style cache
215
+ if len(self.cache) >= self.cache_size:
216
+ # Remove a random item (simple cache eviction)
217
+ self.cache.pop(next(iter(self.cache)))
218
+ self.cache[cache_key] = result
219
+
220
+ return result
221
+
222
+ def _calculate_match(self, query: str, candidate: str) -> Tuple[float, List[int]]:
223
+ """Calculate fuzzy match score and positions."""
224
+ if not query or not candidate:
225
+ return 0.0, []
226
+
227
+ # Find all positions where query characters appear in order
228
+ positions = []
229
+ query_idx = 0
230
+ candidate_idx = 0
231
+
232
+ while query_idx < len(query) and candidate_idx < len(candidate):
233
+ if query[query_idx] == candidate[candidate_idx]:
234
+ positions.append(candidate_idx)
235
+ query_idx += 1
236
+ candidate_idx += 1
237
+
238
+ # Must match all characters in query
239
+ if query_idx < len(query):
240
+ return 0.0, []
241
+
242
+ # Calculate score based on match quality
243
+ if not positions:
244
+ return 0.0, []
245
+
246
+ score = len(positions) # Base score from number of matches
247
+
248
+ # Bonus for consecutive matches
249
+ consecutive_bonus = 0
250
+ for i in range(1, len(positions)):
251
+ if positions[i] == positions[i - 1] + 1:
252
+ consecutive_bonus += 2
253
+ score += consecutive_bonus
254
+
255
+ # Bonus for matches at word boundaries
256
+ word_boundary_bonus = 0
257
+ for pos in positions:
258
+ if pos == 0 or not candidate[pos - 1].isalnum():
259
+ word_boundary_bonus += 3
260
+ score += word_boundary_bonus
261
+
262
+ # Penalty for gaps between matches
263
+ gap_penalty = 0
264
+ for i in range(1, len(positions)):
265
+ gap = positions[i] - positions[i - 1] - 1
266
+ gap_penalty += gap * 0.1
267
+ score -= gap_penalty
268
+
269
+ return max(0, score), positions
270
+
271
+ @classmethod
272
+ @lru_cache(maxsize=1024)
273
+ def get_word_starts(cls, candidate: str) -> List[int]:
274
+ """Get positions of word starts for better scoring."""
275
+ positions = [0] # Start of string
276
+ for i, char in enumerate(candidate):
277
+ if i > 0 and not candidate[i - 1].isalnum() and char.isalnum():
278
+ positions.append(i)
279
+ return positions
280
+
281
+
282
+ class FuzzyFileSearch:
283
+ """Fuzzy search specifically for files."""
284
+
285
+ def __init__(self, root_path: Optional[Path] = None):
286
+ self.root_path = root_path or Path.cwd()
287
+ self.path_filter = PathFilter.from_git_root(self.root_path)
288
+ self.fuzzy_search = FuzzySearch()
289
+ self._file_cache = None
290
+ self._cache_timestamp = 0
291
+
292
+ def search_files(self, query: str, max_results: int = 20) -> List[Tuple[Path, str, float]]:
293
+ """Search for files using fuzzy matching.
294
+
295
+ Returns:
296
+ List of (file_path, relative_path, score) tuples
297
+ """
298
+ if not query.strip():
299
+ return []
300
+
301
+ # Get all project files
302
+ files = self._get_project_files()
303
+
304
+ results = []
305
+ for file_path in files:
306
+ try:
307
+ # Use relative path for searching
308
+ rel_path = file_path.relative_to(self.root_path)
309
+ rel_str = str(rel_path)
310
+
311
+ # Search in filename and full path
312
+ score1, _ = self.fuzzy_search.match(query, rel_path.name)
313
+ score2, _ = self.fuzzy_search.match(query, rel_str)
314
+
315
+ # Use the better score
316
+ score = max(score1, score2)
317
+ if score > 0:
318
+ results.append((file_path, str(rel_path), score))
319
+
320
+ except ValueError:
321
+ # File not relative to root
322
+ continue
323
+
324
+ # Sort by score (highest first)
325
+ results.sort(key=lambda x: x[2], reverse=True)
326
+ return results[:max_results]
327
+
328
+ def _get_project_files(self) -> List[Path]:
329
+ """Get all files in project, respecting gitignore."""
330
+ import time
331
+
332
+ # Simple caching to avoid rescanning on every search
333
+ now = time.time()
334
+ if self._file_cache is not None and now - self._cache_timestamp < 30: # 30 second cache
335
+ return self._file_cache
336
+
337
+ files = []
338
+ try:
339
+ for root, dirs, files_in_dir in os.walk(self.root_path):
340
+ # Filter directories
341
+ dirs[:] = [
342
+ d
343
+ for d in dirs
344
+ if d not in [".git"] and not self.path_filter.match(Path(root) / d)
345
+ ]
346
+
347
+ for file in files_in_dir:
348
+ file_path = Path(root) / file
349
+ if not self.path_filter.match(file_path):
350
+ files.append(file_path)
351
+
352
+ except Exception:
353
+ # Fallback to current directory only
354
+ try:
355
+ files = [
356
+ f
357
+ for f in self.root_path.iterdir()
358
+ if f.is_file() and not self.path_filter.match(f)
359
+ ]
360
+ except Exception:
361
+ files = []
362
+
363
+ self._file_cache = files
364
+ self._cache_timestamp = now
365
+ return files
366
+
367
+
368
+ def fuzzy_find_files(query: str, max_results: int = 20) -> List[Tuple[Path, str, float]]:
369
+ """Convenience function for fuzzy file search."""
370
+ searcher = FuzzyFileSearch()
371
+ return searcher.search_files(query, max_results)
372
+
373
+
374
+ def show_fuzzy_search_results(query: str, results: List[Tuple[Path, str, float]]):
375
+ """Display fuzzy search results in a nice format with git status."""
376
+ if not results:
377
+ console = Console()
378
+ console.print(f"[yellow]No files found matching '{query}'[/yellow]")
379
+ return
380
+
381
+ console = Console()
382
+ console.print(f"\n[bold green]🔍 Found {len(results)} files matching '{query}':[/bold green]\n")
383
+
384
+ # Initialize git status tracker
385
+ git_tracker = GitStatusTracker(Path.cwd())
386
+
387
+ table = Table(show_header=True, header_style="bold blue")
388
+ table.add_column("File", style="cyan", no_wrap=True)
389
+ table.add_column("Path", style="white")
390
+ table.add_column("Status", style="magenta", width=6)
391
+ table.add_column("Score", style="green", justify="right")
392
+
393
+ for file_path, rel_path, score in results:
394
+ # Add file type emoji
395
+ if file_path.suffix.lower() in [".py", ".js", ".ts", ".java", ".cpp", ".c", ".h"]:
396
+ icon = "📄"
397
+ elif file_path.suffix.lower() in [".md", ".txt", ".rst"]:
398
+ icon = "📝"
399
+ elif file_path.suffix.lower() in [".json", ".yaml", ".yml", ".toml"]:
400
+ icon = "⚙️"
401
+ elif file_path.suffix.lower() in [".png", ".jpg", ".jpeg", ".gif", ".svg"]:
402
+ icon = "🖼️"
403
+ else:
404
+ icon = "📄"
405
+
406
+ # Get git status
407
+ git_status = git_tracker.get_status_emoji(file_path)
408
+ if not git_status:
409
+ git_status = "✅" # Clean
410
+
411
+ table.add_row(f"{icon} {file_path.name}", rel_path, git_status, f"{score:.1f}")
412
+
413
+ console.print(table)
414
+ console.print(f"\n[dim]💡 Use ':open <path>' to open a file[/dim]")
415
+ console.print(f"[dim]🔴 Modified 🟢 Added 🟡 Untracked 🔵 Staged ✅ Clean[/dim]")
416
+
417
+
418
+ class GitStatusTracker:
419
+ """Track git status for files in a repository."""
420
+
421
+ def __init__(self, repo_path: Path):
422
+ self.repo_path = repo_path
423
+ self.status_cache = {}
424
+ self.last_update = 0
425
+ self.cache_duration = 10 # seconds
426
+
427
+ def get_status(self, file_path: Path) -> str:
428
+ """Get git status for a file."""
429
+ import time
430
+ import subprocess
431
+
432
+ # Check if cache is fresh
433
+ now = time.time()
434
+ if now - self.last_update > self.cache_duration:
435
+ self._update_status()
436
+
437
+ # Get relative path for lookup
438
+ try:
439
+ rel_path = file_path.relative_to(self.repo_path)
440
+ return self.status_cache.get(str(rel_path), "")
441
+ except ValueError:
442
+ return ""
443
+
444
+ def _update_status(self) -> None:
445
+ """Update git status cache."""
446
+ import time
447
+ import subprocess
448
+
449
+ self.status_cache = {}
450
+ self.last_update = time.time()
451
+
452
+ try:
453
+ # Run git status --porcelain for efficient status
454
+ result = subprocess.run(
455
+ ["git", "status", "--porcelain"],
456
+ cwd=self.repo_path,
457
+ capture_output=True,
458
+ text=True,
459
+ timeout=5,
460
+ )
461
+
462
+ if result.returncode == 0:
463
+ for line in result.stdout.split("\n"):
464
+ if line.strip():
465
+ status = line[:2].strip()
466
+ file_path = line[3:].strip()
467
+
468
+ # Map git status to our status indicators
469
+ if status.startswith("M") or status.endswith("M"):
470
+ self.status_cache[file_path] = "modified"
471
+ elif status.startswith("A") or status == "A":
472
+ self.status_cache[file_path] = "added"
473
+ elif status.startswith("D") or status.endswith("D"):
474
+ self.status_cache[file_path] = "deleted"
475
+ elif status == "??":
476
+ self.status_cache[file_path] = "untracked"
477
+ elif status.startswith("R"):
478
+ self.status_cache[file_path] = "renamed"
479
+
480
+ except (subprocess.TimeoutExpired, subprocess.CalledProcessError, FileNotFoundError):
481
+ # Git not available or not a git repo
482
+ pass
483
+
484
+ def get_status_emoji(self, file_path: Path) -> str:
485
+ """Get status emoji for display."""
486
+ status = self.get_status(file_path)
487
+ return {
488
+ "modified": "🔴",
489
+ "added": "🟢",
490
+ "deleted": "🔴",
491
+ "untracked": "🟡",
492
+ "renamed": "🔵",
493
+ "staged": "🔵",
494
+ }.get(status, "")
495
+
496
+
497
+ class RecentFiles:
498
+ """Track recently accessed files."""
499
+
500
+ def __init__(self, max_files: int = 20):
501
+ self.max_files = max_files
502
+ self.recent_files = []
503
+ self._load_from_disk()
504
+
505
+ def add_file(self, file_path: Path) -> None:
506
+ """Add a file to recent files list."""
507
+ file_str = str(file_path.resolve())
508
+
509
+ # Remove if already exists
510
+ if file_str in self.recent_files:
511
+ self.recent_files.remove(file_str)
512
+
513
+ # Add to beginning
514
+ self.recent_files.insert(0, file_str)
515
+
516
+ # Trim to max size
517
+ self.recent_files = self.recent_files[: self.max_files]
518
+
519
+ # Save to disk
520
+ self._save_to_disk()
521
+
522
+ def get_recent_files(self, limit: int = 10) -> List[Path]:
523
+ """Get list of recent files."""
524
+ return [Path(f) for f in self.recent_files[:limit] if Path(f).exists()]
525
+
526
+ def _load_from_disk(self) -> None:
527
+ """Load recent files from disk."""
528
+ try:
529
+ import json
530
+
531
+ config_dir = Path.home() / ".superqode"
532
+ config_dir.mkdir(exist_ok=True)
533
+ recent_file = config_dir / "recent_files.json"
534
+
535
+ if recent_file.exists():
536
+ with open(recent_file, "r") as f:
537
+ data = json.load(f)
538
+ self.recent_files = data.get("files", [])
539
+ except Exception:
540
+ self.recent_files = []
541
+
542
+ def _save_to_disk(self) -> None:
543
+ """Save recent files to disk."""
544
+ try:
545
+ import json
546
+
547
+ config_dir = Path.home() / ".superqode"
548
+ config_dir.mkdir(exist_ok=True)
549
+ recent_file = config_dir / "recent_files.json"
550
+
551
+ with open(recent_file, "w") as f:
552
+ json.dump({"files": self.recent_files}, f, indent=2)
553
+ except Exception:
554
+ pass
555
+
556
+
557
+ class Bookmarks:
558
+ """Manage bookmarked files."""
559
+
560
+ def __init__(self):
561
+ self.bookmarks = {}
562
+ self._load_from_disk()
563
+
564
+ def add_bookmark(self, file_path: Path, name: str = None) -> bool:
565
+ """Add a file to bookmarks."""
566
+ if not file_path.exists():
567
+ return False
568
+
569
+ if name is None:
570
+ name = file_path.name
571
+
572
+ self.bookmarks[name] = str(file_path.resolve())
573
+ self._save_to_disk()
574
+ return True
575
+
576
+ def remove_bookmark(self, name: str) -> bool:
577
+ """Remove a bookmark."""
578
+ if name in self.bookmarks:
579
+ del self.bookmarks[name]
580
+ self._save_to_disk()
581
+ return True
582
+ return False
583
+
584
+ def get_bookmarks(self):
585
+ """Get all bookmarks as Path objects."""
586
+ result = {}
587
+ for name, path_str in self.bookmarks.items():
588
+ path = Path(path_str)
589
+ if path.exists():
590
+ result[name] = path
591
+ return result
592
+
593
+ def get_bookmark(self, name: str) -> Optional[Path]:
594
+ """Get a specific bookmark."""
595
+ path_str = self.bookmarks.get(name)
596
+ if path_str:
597
+ path = Path(path_str)
598
+ return path if path.exists() else None
599
+ return None
600
+
601
+ def _load_from_disk(self) -> None:
602
+ """Load bookmarks from disk."""
603
+ try:
604
+ import json
605
+
606
+ config_dir = Path.home() / ".superqode"
607
+ config_dir.mkdir(exist_ok=True)
608
+ bookmark_file = config_dir / "bookmarks.json"
609
+
610
+ if bookmark_file.exists():
611
+ with open(bookmark_file, "r") as f:
612
+ self.bookmarks = json.load(f)
613
+ except Exception:
614
+ self.bookmarks = {}
615
+
616
+ def _save_to_disk(self) -> None:
617
+ """Save bookmarks to disk."""
618
+ try:
619
+ import json
620
+
621
+ config_dir = Path.home() / ".superqode"
622
+ config_dir.mkdir(exist_ok=True)
623
+ bookmark_file = config_dir / "bookmarks.json"
624
+
625
+ with open(bookmark_file, "w") as f:
626
+ json.dump(self.bookmarks, f, indent=2)
627
+ except Exception:
628
+ pass
629
+
630
+
631
+ def show_file_content(file_path: Path) -> None:
632
+ """Launch interactive file explorer."""
633
+ if not self.console:
634
+ print("Rich library not available for interactive explorer")
635
+ return
636
+
637
+ current_path = self.root_path
638
+
639
+ while True:
640
+ # Clear screen and show current directory
641
+ self.console.clear()
642
+
643
+ # Show current path
644
+ self.console.print(f"\n[bold blue]📁 File Explorer[/bold blue]")
645
+ self.console.print(f"[dim]Current: {current_path}[/dim]\n")
646
+
647
+ # Show directory tree
648
+ tree = self.explore_directory(current_path, max_depth=2)
649
+ self.console.print(tree)
650
+
651
+ # Show options
652
+ self.console.print("\n[bold cyan]Options:[/bold cyan]")
653
+ self.console.print(" [1] Open file/directory")
654
+ self.console.print(" [2] Search files")
655
+ self.console.print(" [3] Go up (..)")
656
+ self.console.print(" [4] Go to root")
657
+ self.console.print(" [q] Quit")
658
+
659
+ choice = Prompt.ask("\nChoose option", choices=["1", "2", "3", "4", "q"], default="q")
660
+
661
+ if choice == "q":
662
+ break
663
+ elif choice == "1":
664
+ name = Prompt.ask("Enter file/directory name")
665
+ target = current_path / name
666
+ if target.exists():
667
+ if target.is_dir():
668
+ current_path = target
669
+ else:
670
+ # Open file (for now just show path)
671
+ self.console.print(f"[green]Selected file: {target}[/green]")
672
+ input("Press Enter to continue...")
673
+ else:
674
+ self.console.print(f"[red]Not found: {target}[/red]")
675
+ input("Press Enter to continue...")
676
+ elif choice == "2":
677
+ query = Prompt.ask("Search query")
678
+ results = self.find_files(query, max_results=10)
679
+ if results:
680
+ self.console.print(f"\n[bold green]Found {len(results)} files:[/bold green]")
681
+ for i, (path, rel_path) in enumerate(results, 1):
682
+ self.console.print(f" {i}. {rel_path}")
683
+ else:
684
+ self.console.print("[yellow]No files found[/yellow]")
685
+ input("\nPress Enter to continue...")
686
+ elif choice == "3":
687
+ if current_path != self.root_path:
688
+ current_path = current_path.parent
689
+ elif choice == "4":
690
+ current_path = self.root_path
691
+
692
+
693
+ def show_file_explorer():
694
+ """Show file explorer interface."""
695
+ console = Console()
696
+
697
+ try:
698
+ explorer = CodeExplorer()
699
+ tree = explorer.explore_directory(max_depth=3)
700
+
701
+ panel = Panel.fit(
702
+ tree,
703
+ title="[bold blue]📁 Project Files[/bold blue]",
704
+ border_style="blue",
705
+ padding=(1, 2),
706
+ )
707
+
708
+ console.print(panel)
709
+ console.print("\n[dim]Use ':files interactive' for full explorer[/dim]")
710
+
711
+ except Exception as e:
712
+ console.print(f"[red]Error opening file explorer: {e}[/red]")
713
+
714
+
715
+ def show_file_content(file_path: Path, preview_only: bool = False) -> None:
716
+ """Show file content or open in editor with syntax highlighting."""
717
+ console = Console()
718
+
719
+ if not file_path.exists():
720
+ console.print(f"[red]File not found: {file_path}[/red]")
721
+ return
722
+
723
+ if file_path.is_dir():
724
+ console.print(f"[yellow]Cannot open directory: {file_path}[/yellow]")
725
+ console.print(f"[dim]Use ':files interactive' to browse directories[/dim]")
726
+ return
727
+
728
+ # Add to recent files
729
+ recent = RecentFiles()
730
+ recent.add_file(file_path)
731
+
732
+ try:
733
+ # Try to open in editor first (unless preview_only is True)
734
+ if not preview_only:
735
+ import subprocess
736
+ import shutil
737
+
738
+ editors = ["code", "cursor", "subl", "vim", "nano"]
739
+ editor = None
740
+
741
+ for ed in editors:
742
+ if shutil.which(ed):
743
+ editor = ed
744
+ break
745
+
746
+ if editor:
747
+ console.print(f"[green]Opening {file_path} in {editor}...[/green]")
748
+ subprocess.run([editor, str(file_path)], check=False)
749
+ return
750
+
751
+ # Fall back to showing content with syntax highlighting
752
+ console.print(f"[bold cyan]📄 {file_path.name}[/bold cyan]")
753
+ console.print(f"[dim]{file_path}[/dim]")
754
+ console.print()
755
+
756
+ try:
757
+ with open(file_path, "r", encoding="utf-8") as f:
758
+ content = f.read()
759
+
760
+ # Syntax highlighting using pygments
761
+ try:
762
+ from pygments import highlight
763
+ from pygments.lexers import get_lexer_for_filename, TextLexer
764
+ from pygments.formatters import TerminalFormatter
765
+
766
+ try:
767
+ lexer = get_lexer_for_filename(file_path.name)
768
+ except Exception:
769
+ lexer = TextLexer()
770
+
771
+ formatter = TerminalFormatter()
772
+ highlighted = highlight(content, lexer, formatter)
773
+
774
+ # Truncate if too long
775
+ if len(content) > 2000:
776
+ lines = highlighted.split("\n")
777
+ truncated = "\n".join(lines[:50]) # Show first 50 lines
778
+ console.print(truncated)
779
+ console.print(
780
+ f"[dim]... ({len(lines) - 50} more lines, {len(content)} total chars)[/dim]"
781
+ )
782
+ else:
783
+ console.print(highlighted)
784
+
785
+ except ImportError:
786
+ # Fallback without syntax highlighting
787
+ if len(content) > 1000:
788
+ console.print(content[:1000] + "\n[dim]... (truncated)[/dim]")
789
+ else:
790
+ console.print(content)
791
+
792
+ except UnicodeDecodeError:
793
+ console.print("[dim][Binary file - cannot display content][/dim]")
794
+ except Exception as e:
795
+ console.print(f"[red]Error reading file: {e}[/red]")
796
+
797
+ except Exception as e:
798
+ console.print(f"[red]Error opening file: {e}[/red]")
799
+
800
+
801
+ def interactive_file_explorer():
802
+ """Launch full interactive file explorer."""
803
+ console = Console()
804
+
805
+ try:
806
+ explorer = CodeExplorer()
807
+ explorer.interactive_explorer()
808
+ except KeyboardInterrupt:
809
+ console.print("\n[green]File explorer closed.[/green]")
810
+ except Exception as e:
811
+ console.print(f"[red]Error in file explorer: {e}[/red]")