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,585 @@
1
+ """
2
+ SuperQode File Reference Widget - @ file mentions with fuzzy search.
3
+
4
+ Enables @filename syntax for including files in context.
5
+
6
+ Features:
7
+ - Fuzzy file search when typing @
8
+ - Autocomplete popup with file suggestions
9
+ - Highlights matched characters
10
+ - Automatically includes file content in message
11
+
12
+ Usage:
13
+ > Fix the bug in @utils/parser.py
14
+ > Review @src/main.py and @tests/test_main.py
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import os
20
+ import re
21
+ from pathlib import Path
22
+ from typing import Callable, List, Optional, Tuple, TYPE_CHECKING
23
+
24
+ from rich.text import Text
25
+
26
+ from textual.widgets import Static, Input, OptionList
27
+ from textual.containers import Container, Vertical
28
+ from textual.reactive import reactive
29
+ from textual.message import Message
30
+ from textual import on
31
+ from textual.binding import Binding
32
+
33
+ if TYPE_CHECKING:
34
+ from textual.app import App
35
+
36
+
37
+ # ============================================================================
38
+ # DESIGN CONSTANTS
39
+ # ============================================================================
40
+
41
+ try:
42
+ from superqode.design_system import COLORS as SQ_COLORS
43
+ except ImportError:
44
+
45
+ class SQ_COLORS:
46
+ primary = "#7c3aed"
47
+ primary_light = "#a855f7"
48
+ success = "#10b981"
49
+ text_primary = "#fafafa"
50
+ text_secondary = "#e4e4e7"
51
+ text_muted = "#a1a1aa"
52
+ text_dim = "#71717a"
53
+ bg_surface = "#0a0a0a"
54
+ border_default = "#27272a"
55
+
56
+
57
+ # ============================================================================
58
+ # FILE REFERENCE PARSER
59
+ # ============================================================================
60
+
61
+ # Pattern to match @filename references
62
+ FILE_REFERENCE_PATTERN = re.compile(r"@([\w./\-_]+)")
63
+
64
+
65
+ def parse_file_references(text: str) -> List[str]:
66
+ """
67
+ Extract all @filename references from text.
68
+
69
+ Args:
70
+ text: Input text possibly containing @references
71
+
72
+ Returns:
73
+ List of file paths referenced
74
+ """
75
+ matches = FILE_REFERENCE_PATTERN.findall(text)
76
+ return matches
77
+
78
+
79
+ def expand_file_references(text: str, root_path: Path) -> Tuple[str, List[Tuple[str, str]]]:
80
+ """
81
+ Expand @filename references to include file content.
82
+
83
+ Args:
84
+ text: Input text with @references
85
+ root_path: Root directory for resolving files
86
+
87
+ Returns:
88
+ Tuple of (clean_text, [(path, content), ...])
89
+ """
90
+ references = parse_file_references(text)
91
+ file_contents = []
92
+
93
+ for ref in references:
94
+ # Try to resolve the file path
95
+ file_path = root_path / ref
96
+
97
+ # Also try without leading path components
98
+ if not file_path.exists():
99
+ # Search for the file
100
+ for candidate in root_path.rglob(f"*{ref}"):
101
+ if candidate.is_file():
102
+ file_path = candidate
103
+ break
104
+
105
+ if file_path.exists() and file_path.is_file():
106
+ try:
107
+ content = file_path.read_text(errors="replace")
108
+ # Limit content size
109
+ if len(content) > 50000:
110
+ content = content[:50000] + "\n... (truncated)"
111
+ file_contents.append((str(file_path.relative_to(root_path)), content))
112
+ except Exception:
113
+ pass
114
+
115
+ # Remove @ prefixes from text for clean display
116
+ clean_text = FILE_REFERENCE_PATTERN.sub(r"\1", text)
117
+
118
+ return clean_text, file_contents
119
+
120
+
121
+ # ============================================================================
122
+ # FILE SCANNER
123
+ # ============================================================================
124
+
125
+
126
+ class FileScanner:
127
+ """
128
+ Scans and caches files in a directory for quick fuzzy search.
129
+ """
130
+
131
+ # File extensions to include
132
+ CODE_EXTENSIONS = {
133
+ ".py",
134
+ ".js",
135
+ ".ts",
136
+ ".tsx",
137
+ ".jsx",
138
+ ".go",
139
+ ".rs",
140
+ ".rb",
141
+ ".java",
142
+ ".kt",
143
+ ".c",
144
+ ".cpp",
145
+ ".h",
146
+ ".hpp",
147
+ ".cs",
148
+ ".swift",
149
+ ".vue",
150
+ ".svelte",
151
+ ".html",
152
+ ".css",
153
+ ".scss",
154
+ ".sass",
155
+ ".less",
156
+ ".json",
157
+ ".yaml",
158
+ ".yml",
159
+ ".toml",
160
+ ".xml",
161
+ ".md",
162
+ ".txt",
163
+ ".sh",
164
+ ".bash",
165
+ ".zsh",
166
+ ".fish",
167
+ ".sql",
168
+ ".graphql",
169
+ }
170
+
171
+ # Directories to exclude
172
+ EXCLUDE_DIRS = {
173
+ ".git",
174
+ "node_modules",
175
+ "__pycache__",
176
+ ".venv",
177
+ "venv",
178
+ ".env",
179
+ "dist",
180
+ "build",
181
+ ".next",
182
+ ".nuxt",
183
+ "coverage",
184
+ ".pytest_cache",
185
+ ".mypy_cache",
186
+ ".tox",
187
+ "eggs",
188
+ "*.egg-info",
189
+ }
190
+
191
+ def __init__(self, root_path: Path, max_files: int = 5000):
192
+ self.root_path = root_path
193
+ self.max_files = max_files
194
+ self._files: List[str] = []
195
+ self._scanned = False
196
+
197
+ def scan(self, force: bool = False) -> List[str]:
198
+ """Scan directory for files."""
199
+ if self._scanned and not force:
200
+ return self._files
201
+
202
+ self._files = []
203
+ count = 0
204
+
205
+ try:
206
+ for item in self.root_path.rglob("*"):
207
+ if count >= self.max_files:
208
+ break
209
+
210
+ # Skip excluded directories
211
+ if any(excl in item.parts for excl in self.EXCLUDE_DIRS):
212
+ continue
213
+
214
+ if item.is_file():
215
+ # Check extension
216
+ if item.suffix.lower() in self.CODE_EXTENSIONS or item.suffix == "":
217
+ rel_path = str(item.relative_to(self.root_path))
218
+ self._files.append(rel_path)
219
+ count += 1
220
+ except Exception:
221
+ pass
222
+
223
+ self._scanned = True
224
+ return self._files
225
+
226
+ def search(self, query: str, max_results: int = 10) -> List[Tuple[str, float, List[int]]]:
227
+ """
228
+ Fuzzy search files matching query.
229
+
230
+ Returns:
231
+ List of (path, score, match_positions)
232
+ """
233
+ from superqode.utils.fuzzy import path_fuzzy_search
234
+
235
+ files = self.scan()
236
+ if not query:
237
+ return [(f, 0.0, []) for f in files[:max_results]]
238
+
239
+ matches = path_fuzzy_search.search(query, files, max_results=max_results)
240
+ return [(m.text, m.score, m.positions) for m in matches]
241
+
242
+
243
+ # ============================================================================
244
+ # FILE AUTOCOMPLETE WIDGET
245
+ # ============================================================================
246
+
247
+
248
+ class FileAutocomplete(Container):
249
+ """
250
+ Dropdown autocomplete widget for file references.
251
+
252
+ Shows fuzzy-matched files when user types @.
253
+ """
254
+
255
+ DEFAULT_CSS = """
256
+ FileAutocomplete {
257
+ layer: overlay;
258
+ width: auto;
259
+ max-width: 60;
260
+ height: auto;
261
+ max-height: 12;
262
+ background: #0a0a0a;
263
+ border: round #7c3aed;
264
+ padding: 0;
265
+ display: none;
266
+ }
267
+
268
+ FileAutocomplete.visible {
269
+ display: block;
270
+ }
271
+
272
+ FileAutocomplete OptionList {
273
+ height: auto;
274
+ max-height: 10;
275
+ background: #0a0a0a;
276
+ border: none;
277
+ padding: 0;
278
+ }
279
+
280
+ FileAutocomplete OptionList:focus {
281
+ border: none;
282
+ }
283
+
284
+ FileAutocomplete OptionList > .option-list--option {
285
+ padding: 0 1;
286
+ }
287
+
288
+ FileAutocomplete OptionList > .option-list--option-highlighted {
289
+ background: #7c3aed40;
290
+ }
291
+
292
+ FileAutocomplete .header {
293
+ height: 1;
294
+ background: #1a1a1a;
295
+ padding: 0 1;
296
+ color: #71717a;
297
+ }
298
+ """
299
+
300
+ class FileSelected(Message):
301
+ """Posted when a file is selected."""
302
+
303
+ def __init__(self, path: str) -> None:
304
+ self.path = path
305
+ super().__init__()
306
+
307
+ class Dismissed(Message):
308
+ """Posted when autocomplete is dismissed."""
309
+
310
+ pass
311
+
312
+ visible: reactive[bool] = reactive(False)
313
+
314
+ def __init__(self, root_path: Path, **kwargs):
315
+ super().__init__(**kwargs)
316
+ self._scanner = FileScanner(root_path)
317
+ self._query = ""
318
+ self._results: List[Tuple[str, float, List[int]]] = []
319
+
320
+ def compose(self):
321
+ """Compose the autocomplete widget."""
322
+ yield Static("◇ Files", classes="header")
323
+ yield OptionList(id="file-options")
324
+
325
+ def on_mount(self) -> None:
326
+ """Start file scan in background."""
327
+ self._scanner.scan()
328
+
329
+ def watch_visible(self, visible: bool) -> None:
330
+ """Toggle visibility."""
331
+ if visible:
332
+ self.add_class("visible")
333
+ else:
334
+ self.remove_class("visible")
335
+
336
+ def show(self, query: str = "") -> None:
337
+ """Show autocomplete with query."""
338
+ self._query = query
339
+ self._update_results()
340
+ self.visible = True
341
+ try:
342
+ self.query_one("#file-options", OptionList).focus()
343
+ except Exception:
344
+ pass
345
+
346
+ def hide(self) -> None:
347
+ """Hide autocomplete."""
348
+ self.visible = False
349
+ self.post_message(self.Dismissed())
350
+
351
+ def _update_results(self) -> None:
352
+ """Update search results."""
353
+ from superqode.utils.fuzzy import path_fuzzy_search
354
+
355
+ self._results = self._scanner.search(self._query, max_results=10)
356
+
357
+ # Update option list
358
+ try:
359
+ options = self.query_one("#file-options", OptionList)
360
+ options.clear_options()
361
+
362
+ for path, score, positions in self._results:
363
+ # Highlight matched characters
364
+ display = path_fuzzy_search.highlight_match(
365
+ path, positions, highlight_start="[bold cyan]", highlight_end="[/bold cyan]"
366
+ )
367
+ options.add_option(Text.from_markup(f"↳ {display}"))
368
+ except Exception:
369
+ pass
370
+
371
+ def update_query(self, query: str) -> None:
372
+ """Update search query."""
373
+ self._query = query
374
+ self._update_results()
375
+
376
+ @on(OptionList.OptionSelected)
377
+ def _on_option_selected(self, event: OptionList.OptionSelected) -> None:
378
+ """Handle file selection."""
379
+ event.stop()
380
+ if 0 <= event.option_index < len(self._results):
381
+ path = self._results[event.option_index][0]
382
+ self.post_message(self.FileSelected(path))
383
+ self.hide()
384
+
385
+ def select_highlighted(self) -> None:
386
+ """Select the currently highlighted option."""
387
+ try:
388
+ options = self.query_one("#file-options", OptionList)
389
+ if options.highlighted is not None and 0 <= options.highlighted < len(self._results):
390
+ path = self._results[options.highlighted][0]
391
+ self.post_message(self.FileSelected(path))
392
+ self.hide()
393
+ except Exception:
394
+ pass
395
+
396
+ def move_up(self) -> None:
397
+ """Move selection up."""
398
+ try:
399
+ options = self.query_one("#file-options", OptionList)
400
+ options.action_cursor_up()
401
+ except Exception:
402
+ pass
403
+
404
+ def move_down(self) -> None:
405
+ """Move selection down."""
406
+ try:
407
+ options = self.query_one("#file-options", OptionList)
408
+ options.action_cursor_down()
409
+ except Exception:
410
+ pass
411
+
412
+
413
+ # ============================================================================
414
+ # ENHANCED INPUT WITH FILE REFERENCES
415
+ # ============================================================================
416
+
417
+
418
+ class FileReferenceInput(Input):
419
+ """
420
+ Enhanced input that supports @file references.
421
+
422
+ Shows autocomplete when typing @ and includes file content
423
+ in the final message.
424
+ """
425
+
426
+ BINDINGS = [
427
+ Binding("tab", "complete", "Complete", show=False),
428
+ Binding("escape", "cancel_complete", "Cancel", show=False),
429
+ Binding("up", "move_up", "Up", show=False),
430
+ Binding("down", "move_down", "Down", show=False),
431
+ ]
432
+
433
+ class MessageWithFiles(Message):
434
+ """Posted when message is submitted with file references."""
435
+
436
+ def __init__(self, text: str, files: List[Tuple[str, str]]) -> None:
437
+ self.text = text # Clean text without @ prefixes
438
+ self.files = files # List of (path, content) tuples
439
+ super().__init__()
440
+
441
+ def __init__(self, root_path: Path = None, **kwargs):
442
+ super().__init__(**kwargs)
443
+ self._root_path = root_path or Path.cwd()
444
+ self._autocomplete: Optional[FileAutocomplete] = None
445
+ self._at_position: int = -1 # Position of @ that triggered autocomplete
446
+
447
+ def on_mount(self) -> None:
448
+ """Mount autocomplete widget."""
449
+ # Note: Autocomplete is mounted by parent app
450
+ pass
451
+
452
+ def set_autocomplete(self, autocomplete: FileAutocomplete) -> None:
453
+ """Set the autocomplete widget to use."""
454
+ self._autocomplete = autocomplete
455
+
456
+ def _check_for_trigger(self) -> None:
457
+ """Check if we should show autocomplete."""
458
+ value = self.value
459
+ cursor = self.cursor_position
460
+
461
+ # Look for @ before cursor
462
+ before_cursor = value[:cursor]
463
+ at_pos = before_cursor.rfind("@")
464
+
465
+ if at_pos >= 0:
466
+ # Check if @ is at start or after whitespace
467
+ if at_pos == 0 or before_cursor[at_pos - 1] in " \t":
468
+ # Get query after @
469
+ query = before_cursor[at_pos + 1 :]
470
+
471
+ # Don't trigger if query contains space (completed reference)
472
+ if " " not in query:
473
+ self._at_position = at_pos
474
+ if self._autocomplete:
475
+ self._autocomplete.show(query)
476
+ return
477
+
478
+ # No trigger, hide autocomplete
479
+ self._at_position = -1
480
+ if self._autocomplete and self._autocomplete.visible:
481
+ self._autocomplete.hide()
482
+
483
+ def watch_value(self, value: str) -> None:
484
+ """Watch for @ triggers."""
485
+ self._check_for_trigger()
486
+
487
+ def on_file_autocomplete_file_selected(self, event: FileAutocomplete.FileSelected) -> None:
488
+ """Handle file selection from autocomplete."""
489
+ if self._at_position >= 0:
490
+ # Replace @query with @full_path
491
+ before = self.value[: self._at_position]
492
+ after_at = self.value[self._at_position + 1 :]
493
+
494
+ # Find end of current query (next space or end)
495
+ space_pos = after_at.find(" ")
496
+ if space_pos >= 0:
497
+ after = after_at[space_pos:]
498
+ else:
499
+ after = ""
500
+
501
+ # Insert selected file
502
+ self.value = f"{before}@{event.path}{after}"
503
+ self.cursor_position = len(before) + 1 + len(event.path)
504
+
505
+ self._at_position = -1
506
+
507
+ def action_complete(self) -> None:
508
+ """Tab to select highlighted autocomplete option."""
509
+ if self._autocomplete and self._autocomplete.visible:
510
+ self._autocomplete.select_highlighted()
511
+
512
+ def action_cancel_complete(self) -> None:
513
+ """Escape to cancel autocomplete."""
514
+ if self._autocomplete and self._autocomplete.visible:
515
+ self._autocomplete.hide()
516
+ self._at_position = -1
517
+
518
+ def action_move_up(self) -> None:
519
+ """Move autocomplete selection up."""
520
+ if self._autocomplete and self._autocomplete.visible:
521
+ self._autocomplete.move_up()
522
+
523
+ def action_move_down(self) -> None:
524
+ """Move autocomplete selection down."""
525
+ if self._autocomplete and self._autocomplete.visible:
526
+ self._autocomplete.move_down()
527
+
528
+ def get_message_with_files(self) -> Tuple[str, List[Tuple[str, str]]]:
529
+ """
530
+ Get the message text and any referenced files.
531
+
532
+ Returns:
533
+ (clean_text, [(path, content), ...])
534
+ """
535
+ return expand_file_references(self.value, self._root_path)
536
+
537
+
538
+ # ============================================================================
539
+ # HELPER FUNCTIONS
540
+ # ============================================================================
541
+
542
+
543
+ def format_file_context(files: List[Tuple[str, str]]) -> str:
544
+ """
545
+ Format file contents for inclusion in AI context.
546
+
547
+ Args:
548
+ files: List of (path, content) tuples
549
+
550
+ Returns:
551
+ Formatted string with file contents
552
+ """
553
+ if not files:
554
+ return ""
555
+
556
+ parts = []
557
+ for path, content in files:
558
+ parts.append(f'<file path="{path}">\n{content}\n</file>')
559
+
560
+ return "\n\n".join(parts)
561
+
562
+
563
+ def count_file_tokens(files: List[Tuple[str, str]]) -> int:
564
+ """
565
+ Estimate token count for files.
566
+
567
+ Rough estimate: ~4 chars per token
568
+ """
569
+ total_chars = sum(len(content) for _, content in files)
570
+ return total_chars // 4
571
+
572
+
573
+ # ============================================================================
574
+ # EXPORTS
575
+ # ============================================================================
576
+
577
+ __all__ = [
578
+ "parse_file_references",
579
+ "expand_file_references",
580
+ "FileScanner",
581
+ "FileAutocomplete",
582
+ "FileReferenceInput",
583
+ "format_file_context",
584
+ "count_file_tokens",
585
+ ]