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.
- superqode/__init__.py +33 -0
- superqode/acp/__init__.py +23 -0
- superqode/acp/client.py +913 -0
- superqode/acp/permission_screen.py +457 -0
- superqode/acp/types.py +480 -0
- superqode/acp_discovery.py +856 -0
- superqode/agent/__init__.py +22 -0
- superqode/agent/edit_strategies.py +334 -0
- superqode/agent/loop.py +892 -0
- superqode/agent/qe_report_templates.py +39 -0
- superqode/agent/system_prompts.py +353 -0
- superqode/agent_output.py +721 -0
- superqode/agent_stream.py +953 -0
- superqode/agents/__init__.py +59 -0
- superqode/agents/acp_registry.py +305 -0
- superqode/agents/client.py +249 -0
- superqode/agents/data/augmentcode.com.toml +51 -0
- superqode/agents/data/cagent.dev.toml +51 -0
- superqode/agents/data/claude.com.toml +60 -0
- superqode/agents/data/codeassistant.dev.toml +51 -0
- superqode/agents/data/codex.openai.com.toml +57 -0
- superqode/agents/data/fastagent.ai.toml +66 -0
- superqode/agents/data/geminicli.com.toml +77 -0
- superqode/agents/data/goose.block.xyz.toml +54 -0
- superqode/agents/data/junie.jetbrains.com.toml +56 -0
- superqode/agents/data/kimi.moonshot.cn.toml +57 -0
- superqode/agents/data/llmlingagent.dev.toml +51 -0
- superqode/agents/data/molt.bot.toml +49 -0
- superqode/agents/data/opencode.ai.toml +60 -0
- superqode/agents/data/stakpak.dev.toml +51 -0
- superqode/agents/data/vtcode.dev.toml +51 -0
- superqode/agents/discovery.py +266 -0
- superqode/agents/messaging.py +160 -0
- superqode/agents/persona.py +166 -0
- superqode/agents/registry.py +421 -0
- superqode/agents/schema.py +72 -0
- superqode/agents/unified.py +367 -0
- superqode/app/__init__.py +111 -0
- superqode/app/constants.py +314 -0
- superqode/app/css.py +366 -0
- superqode/app/models.py +118 -0
- superqode/app/suggester.py +125 -0
- superqode/app/widgets.py +1591 -0
- superqode/app_enhanced.py +399 -0
- superqode/app_main.py +17187 -0
- superqode/approval.py +312 -0
- superqode/atomic.py +296 -0
- superqode/commands/__init__.py +1 -0
- superqode/commands/acp.py +965 -0
- superqode/commands/agents.py +180 -0
- superqode/commands/auth.py +278 -0
- superqode/commands/config.py +374 -0
- superqode/commands/init.py +826 -0
- superqode/commands/providers.py +819 -0
- superqode/commands/qe.py +1145 -0
- superqode/commands/roles.py +380 -0
- superqode/commands/serve.py +172 -0
- superqode/commands/suggestions.py +127 -0
- superqode/commands/superqe.py +460 -0
- superqode/config/__init__.py +51 -0
- superqode/config/loader.py +812 -0
- superqode/config/schema.py +498 -0
- superqode/core/__init__.py +111 -0
- superqode/core/roles.py +281 -0
- superqode/danger.py +386 -0
- superqode/data/superqode-template.yaml +1522 -0
- superqode/design_system.py +1080 -0
- superqode/dialogs/__init__.py +6 -0
- superqode/dialogs/base.py +39 -0
- superqode/dialogs/model.py +130 -0
- superqode/dialogs/provider.py +870 -0
- superqode/diff_view.py +919 -0
- superqode/enterprise.py +21 -0
- superqode/evaluation/__init__.py +25 -0
- superqode/evaluation/adapters.py +93 -0
- superqode/evaluation/behaviors.py +89 -0
- superqode/evaluation/engine.py +209 -0
- superqode/evaluation/scenarios.py +96 -0
- superqode/execution/__init__.py +36 -0
- superqode/execution/linter.py +538 -0
- superqode/execution/modes.py +347 -0
- superqode/execution/resolver.py +283 -0
- superqode/execution/runner.py +642 -0
- superqode/file_explorer.py +811 -0
- superqode/file_viewer.py +471 -0
- superqode/flash.py +183 -0
- superqode/guidance/__init__.py +58 -0
- superqode/guidance/config.py +203 -0
- superqode/guidance/prompts.py +71 -0
- superqode/harness/__init__.py +54 -0
- superqode/harness/accelerator.py +291 -0
- superqode/harness/config.py +319 -0
- superqode/harness/validator.py +147 -0
- superqode/history.py +279 -0
- superqode/integrations/superopt_runner.py +124 -0
- superqode/logging/__init__.py +49 -0
- superqode/logging/adapters.py +219 -0
- superqode/logging/formatter.py +923 -0
- superqode/logging/integration.py +341 -0
- superqode/logging/sinks.py +170 -0
- superqode/logging/unified_log.py +417 -0
- superqode/lsp/__init__.py +26 -0
- superqode/lsp/client.py +544 -0
- superqode/main.py +1069 -0
- superqode/mcp/__init__.py +89 -0
- superqode/mcp/auth_storage.py +380 -0
- superqode/mcp/client.py +1236 -0
- superqode/mcp/config.py +319 -0
- superqode/mcp/integration.py +337 -0
- superqode/mcp/oauth.py +436 -0
- superqode/mcp/oauth_callback.py +385 -0
- superqode/mcp/types.py +290 -0
- superqode/memory/__init__.py +31 -0
- superqode/memory/feedback.py +342 -0
- superqode/memory/store.py +522 -0
- superqode/notifications.py +369 -0
- superqode/optimization/__init__.py +5 -0
- superqode/optimization/config.py +33 -0
- superqode/permissions/__init__.py +25 -0
- superqode/permissions/rules.py +488 -0
- superqode/plan.py +323 -0
- superqode/providers/__init__.py +33 -0
- superqode/providers/gateway/__init__.py +165 -0
- superqode/providers/gateway/base.py +228 -0
- superqode/providers/gateway/litellm_gateway.py +1170 -0
- superqode/providers/gateway/openresponses_gateway.py +436 -0
- superqode/providers/health.py +297 -0
- superqode/providers/huggingface/__init__.py +74 -0
- superqode/providers/huggingface/downloader.py +472 -0
- superqode/providers/huggingface/endpoints.py +442 -0
- superqode/providers/huggingface/hub.py +531 -0
- superqode/providers/huggingface/inference.py +394 -0
- superqode/providers/huggingface/transformers_runner.py +516 -0
- superqode/providers/local/__init__.py +100 -0
- superqode/providers/local/base.py +438 -0
- superqode/providers/local/discovery.py +418 -0
- superqode/providers/local/lmstudio.py +256 -0
- superqode/providers/local/mlx.py +457 -0
- superqode/providers/local/ollama.py +486 -0
- superqode/providers/local/sglang.py +268 -0
- superqode/providers/local/tgi.py +260 -0
- superqode/providers/local/tool_support.py +477 -0
- superqode/providers/local/vllm.py +258 -0
- superqode/providers/manager.py +1338 -0
- superqode/providers/models.py +1016 -0
- superqode/providers/models_dev.py +578 -0
- superqode/providers/openresponses/__init__.py +87 -0
- superqode/providers/openresponses/converters/__init__.py +17 -0
- superqode/providers/openresponses/converters/messages.py +343 -0
- superqode/providers/openresponses/converters/tools.py +268 -0
- superqode/providers/openresponses/schema/__init__.py +56 -0
- superqode/providers/openresponses/schema/models.py +585 -0
- superqode/providers/openresponses/streaming/__init__.py +5 -0
- superqode/providers/openresponses/streaming/parser.py +338 -0
- superqode/providers/openresponses/tools/__init__.py +21 -0
- superqode/providers/openresponses/tools/apply_patch.py +352 -0
- superqode/providers/openresponses/tools/code_interpreter.py +290 -0
- superqode/providers/openresponses/tools/file_search.py +333 -0
- superqode/providers/openresponses/tools/mcp_adapter.py +252 -0
- superqode/providers/registry.py +716 -0
- superqode/providers/usage.py +332 -0
- superqode/pure_mode.py +384 -0
- superqode/qr/__init__.py +23 -0
- superqode/qr/dashboard.py +781 -0
- superqode/qr/generator.py +1018 -0
- superqode/qr/templates.py +135 -0
- superqode/safety/__init__.py +41 -0
- superqode/safety/sandbox.py +413 -0
- superqode/safety/warnings.py +256 -0
- superqode/server/__init__.py +33 -0
- superqode/server/lsp_server.py +775 -0
- superqode/server/web.py +250 -0
- superqode/session/__init__.py +25 -0
- superqode/session/persistence.py +580 -0
- superqode/session/sharing.py +477 -0
- superqode/session.py +475 -0
- superqode/sidebar.py +2991 -0
- superqode/stream_view.py +648 -0
- superqode/styles/__init__.py +3 -0
- superqode/superqe/__init__.py +184 -0
- superqode/superqe/acp_runner.py +1064 -0
- superqode/superqe/constitution/__init__.py +62 -0
- superqode/superqe/constitution/evaluator.py +308 -0
- superqode/superqe/constitution/loader.py +432 -0
- superqode/superqe/constitution/schema.py +250 -0
- superqode/superqe/events.py +591 -0
- superqode/superqe/frameworks/__init__.py +65 -0
- superqode/superqe/frameworks/base.py +234 -0
- superqode/superqe/frameworks/e2e.py +263 -0
- superqode/superqe/frameworks/executor.py +237 -0
- superqode/superqe/frameworks/javascript.py +409 -0
- superqode/superqe/frameworks/python.py +373 -0
- superqode/superqe/frameworks/registry.py +92 -0
- superqode/superqe/mcp_tools/__init__.py +47 -0
- superqode/superqe/mcp_tools/core_tools.py +418 -0
- superqode/superqe/mcp_tools/registry.py +230 -0
- superqode/superqe/mcp_tools/testing_tools.py +167 -0
- superqode/superqe/noise.py +89 -0
- superqode/superqe/orchestrator.py +778 -0
- superqode/superqe/roles.py +609 -0
- superqode/superqe/session.py +713 -0
- superqode/superqe/skills/__init__.py +57 -0
- superqode/superqe/skills/base.py +106 -0
- superqode/superqe/skills/core_skills.py +899 -0
- superqode/superqe/skills/registry.py +90 -0
- superqode/superqe/verifier.py +101 -0
- superqode/superqe_cli.py +76 -0
- superqode/tool_call.py +358 -0
- superqode/tools/__init__.py +93 -0
- superqode/tools/agent_tools.py +496 -0
- superqode/tools/base.py +324 -0
- superqode/tools/batch_tool.py +133 -0
- superqode/tools/diagnostics.py +311 -0
- superqode/tools/edit_tools.py +653 -0
- superqode/tools/enhanced_base.py +515 -0
- superqode/tools/file_tools.py +269 -0
- superqode/tools/file_tracking.py +45 -0
- superqode/tools/lsp_tools.py +610 -0
- superqode/tools/network_tools.py +350 -0
- superqode/tools/permissions.py +400 -0
- superqode/tools/question_tool.py +324 -0
- superqode/tools/search_tools.py +598 -0
- superqode/tools/shell_tools.py +259 -0
- superqode/tools/todo_tools.py +121 -0
- superqode/tools/validation.py +80 -0
- superqode/tools/web_tools.py +639 -0
- superqode/tui.py +1152 -0
- superqode/tui_integration.py +875 -0
- superqode/tui_widgets/__init__.py +27 -0
- superqode/tui_widgets/widgets/__init__.py +18 -0
- superqode/tui_widgets/widgets/progress.py +185 -0
- superqode/tui_widgets/widgets/tool_display.py +188 -0
- superqode/undo_manager.py +574 -0
- superqode/utils/__init__.py +5 -0
- superqode/utils/error_handling.py +323 -0
- superqode/utils/fuzzy.py +257 -0
- superqode/widgets/__init__.py +477 -0
- superqode/widgets/agent_collab.py +390 -0
- superqode/widgets/agent_store.py +936 -0
- superqode/widgets/agent_switcher.py +395 -0
- superqode/widgets/animation_manager.py +284 -0
- superqode/widgets/code_context.py +356 -0
- superqode/widgets/command_palette.py +412 -0
- superqode/widgets/connection_status.py +537 -0
- superqode/widgets/conversation_history.py +470 -0
- superqode/widgets/diff_indicator.py +155 -0
- superqode/widgets/enhanced_status_bar.py +385 -0
- superqode/widgets/enhanced_toast.py +476 -0
- superqode/widgets/file_browser.py +809 -0
- superqode/widgets/file_reference.py +585 -0
- superqode/widgets/issue_timeline.py +340 -0
- superqode/widgets/leader_key.py +264 -0
- superqode/widgets/mode_switcher.py +445 -0
- superqode/widgets/model_picker.py +234 -0
- superqode/widgets/permission_preview.py +1205 -0
- superqode/widgets/prompt.py +358 -0
- superqode/widgets/provider_connect.py +725 -0
- superqode/widgets/pty_shell.py +587 -0
- superqode/widgets/qe_dashboard.py +321 -0
- superqode/widgets/resizable_sidebar.py +377 -0
- superqode/widgets/response_changes.py +218 -0
- superqode/widgets/response_display.py +528 -0
- superqode/widgets/rich_tool_display.py +613 -0
- superqode/widgets/sidebar_panels.py +1180 -0
- superqode/widgets/slash_complete.py +356 -0
- superqode/widgets/split_view.py +612 -0
- superqode/widgets/status_bar.py +273 -0
- superqode/widgets/superqode_display.py +786 -0
- superqode/widgets/thinking_display.py +815 -0
- superqode/widgets/throbber.py +87 -0
- superqode/widgets/toast.py +206 -0
- superqode/widgets/unified_output.py +1073 -0
- superqode/workspace/__init__.py +75 -0
- superqode/workspace/artifacts.py +472 -0
- superqode/workspace/coordinator.py +353 -0
- superqode/workspace/diff_tracker.py +429 -0
- superqode/workspace/git_guard.py +373 -0
- superqode/workspace/git_snapshot.py +526 -0
- superqode/workspace/manager.py +750 -0
- superqode/workspace/snapshot.py +357 -0
- superqode/workspace/watcher.py +535 -0
- superqode/workspace/worktree.py +440 -0
- superqode-0.1.5.dist-info/METADATA +204 -0
- superqode-0.1.5.dist-info/RECORD +288 -0
- superqode-0.1.5.dist-info/WHEEL +5 -0
- superqode-0.1.5.dist-info/entry_points.txt +3 -0
- superqode-0.1.5.dist-info/licenses/LICENSE +648 -0
- 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
|
+
]
|