tunacode-cli 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of tunacode-cli might be problematic. Click here for more details.

Files changed (65) hide show
  1. tunacode/__init__.py +0 -0
  2. tunacode/cli/__init__.py +4 -0
  3. tunacode/cli/commands.py +632 -0
  4. tunacode/cli/main.py +47 -0
  5. tunacode/cli/repl.py +251 -0
  6. tunacode/configuration/__init__.py +1 -0
  7. tunacode/configuration/defaults.py +26 -0
  8. tunacode/configuration/models.py +69 -0
  9. tunacode/configuration/settings.py +32 -0
  10. tunacode/constants.py +129 -0
  11. tunacode/context.py +83 -0
  12. tunacode/core/__init__.py +0 -0
  13. tunacode/core/agents/__init__.py +0 -0
  14. tunacode/core/agents/main.py +119 -0
  15. tunacode/core/setup/__init__.py +17 -0
  16. tunacode/core/setup/agent_setup.py +41 -0
  17. tunacode/core/setup/base.py +37 -0
  18. tunacode/core/setup/config_setup.py +179 -0
  19. tunacode/core/setup/coordinator.py +45 -0
  20. tunacode/core/setup/environment_setup.py +62 -0
  21. tunacode/core/setup/git_safety_setup.py +188 -0
  22. tunacode/core/setup/undo_setup.py +32 -0
  23. tunacode/core/state.py +43 -0
  24. tunacode/core/tool_handler.py +57 -0
  25. tunacode/exceptions.py +105 -0
  26. tunacode/prompts/system.txt +71 -0
  27. tunacode/py.typed +0 -0
  28. tunacode/services/__init__.py +1 -0
  29. tunacode/services/mcp.py +86 -0
  30. tunacode/services/undo_service.py +244 -0
  31. tunacode/setup.py +50 -0
  32. tunacode/tools/__init__.py +0 -0
  33. tunacode/tools/base.py +244 -0
  34. tunacode/tools/read_file.py +89 -0
  35. tunacode/tools/run_command.py +107 -0
  36. tunacode/tools/update_file.py +117 -0
  37. tunacode/tools/write_file.py +82 -0
  38. tunacode/types.py +259 -0
  39. tunacode/ui/__init__.py +1 -0
  40. tunacode/ui/completers.py +129 -0
  41. tunacode/ui/console.py +74 -0
  42. tunacode/ui/constants.py +16 -0
  43. tunacode/ui/decorators.py +59 -0
  44. tunacode/ui/input.py +95 -0
  45. tunacode/ui/keybindings.py +27 -0
  46. tunacode/ui/lexers.py +46 -0
  47. tunacode/ui/output.py +109 -0
  48. tunacode/ui/panels.py +156 -0
  49. tunacode/ui/prompt_manager.py +117 -0
  50. tunacode/ui/tool_ui.py +187 -0
  51. tunacode/ui/validators.py +23 -0
  52. tunacode/utils/__init__.py +0 -0
  53. tunacode/utils/bm25.py +55 -0
  54. tunacode/utils/diff_utils.py +69 -0
  55. tunacode/utils/file_utils.py +41 -0
  56. tunacode/utils/ripgrep.py +17 -0
  57. tunacode/utils/system.py +336 -0
  58. tunacode/utils/text_utils.py +87 -0
  59. tunacode/utils/user_configuration.py +54 -0
  60. tunacode_cli-0.0.1.dist-info/METADATA +242 -0
  61. tunacode_cli-0.0.1.dist-info/RECORD +65 -0
  62. tunacode_cli-0.0.1.dist-info/WHEEL +5 -0
  63. tunacode_cli-0.0.1.dist-info/entry_points.txt +2 -0
  64. tunacode_cli-0.0.1.dist-info/licenses/LICENSE +21 -0
  65. tunacode_cli-0.0.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,107 @@
1
+ """
2
+ Module: sidekick.tools.run_command
3
+
4
+ Command execution tool for agent operations in the Sidekick application.
5
+ Provides controlled shell command execution with output capture and truncation.
6
+ """
7
+
8
+ import subprocess
9
+
10
+ from tunacode.constants import (CMD_OUTPUT_FORMAT, CMD_OUTPUT_NO_ERRORS, CMD_OUTPUT_NO_OUTPUT,
11
+ CMD_OUTPUT_TRUNCATED, COMMAND_OUTPUT_END_SIZE,
12
+ COMMAND_OUTPUT_START_INDEX, COMMAND_OUTPUT_THRESHOLD,
13
+ ERROR_COMMAND_EXECUTION, MAX_COMMAND_OUTPUT)
14
+ from tunacode.exceptions import ToolExecutionError
15
+ from tunacode.tools.base import BaseTool
16
+ from tunacode.types import ToolResult
17
+ from tunacode.ui import console as default_ui
18
+
19
+
20
+ class RunCommandTool(BaseTool):
21
+ """Tool for running shell commands."""
22
+
23
+ @property
24
+ def tool_name(self) -> str:
25
+ return "Shell"
26
+
27
+ async def _execute(self, command: str) -> ToolResult:
28
+ """Run a shell command and return the output.
29
+
30
+ Args:
31
+ command: The command to run.
32
+
33
+ Returns:
34
+ ToolResult: The output of the command (stdout and stderr).
35
+
36
+ Raises:
37
+ FileNotFoundError: If command not found
38
+ Exception: Any command execution errors
39
+ """
40
+ process = subprocess.Popen(
41
+ command,
42
+ shell=True,
43
+ stdout=subprocess.PIPE,
44
+ stderr=subprocess.PIPE,
45
+ text=True,
46
+ )
47
+ stdout, stderr = process.communicate()
48
+ output = stdout.strip() or CMD_OUTPUT_NO_OUTPUT
49
+ error = stderr.strip() or CMD_OUTPUT_NO_ERRORS
50
+ resp = CMD_OUTPUT_FORMAT.format(output=output, error=error).strip()
51
+
52
+ # Truncate if the output is too long to prevent issues
53
+ if len(resp) > MAX_COMMAND_OUTPUT:
54
+ # Include both the beginning and end of the output
55
+ start_part = resp[:COMMAND_OUTPUT_START_INDEX]
56
+ end_part = (
57
+ resp[-COMMAND_OUTPUT_END_SIZE:]
58
+ if len(resp) > COMMAND_OUTPUT_THRESHOLD
59
+ else resp[COMMAND_OUTPUT_START_INDEX:]
60
+ )
61
+ truncated_resp = start_part + CMD_OUTPUT_TRUNCATED + end_part
62
+ return truncated_resp
63
+
64
+ return resp
65
+
66
+ async def _handle_error(self, error: Exception, command: str = None) -> ToolResult:
67
+ """Handle errors with specific messages for common cases.
68
+
69
+ Raises:
70
+ ToolExecutionError: Always raised with structured error information
71
+ """
72
+ if isinstance(error, FileNotFoundError):
73
+ err_msg = ERROR_COMMAND_EXECUTION.format(command=command, error=error)
74
+ else:
75
+ # Use parent class handling for other errors
76
+ await super()._handle_error(error, command)
77
+ return # super() will raise, this is unreachable
78
+
79
+ if self.ui:
80
+ await self.ui.error(err_msg)
81
+
82
+ raise ToolExecutionError(tool_name=self.tool_name, message=err_msg, original_error=error)
83
+
84
+ def _get_error_context(self, command: str = None) -> str:
85
+ """Get error context for command execution."""
86
+ if command:
87
+ return f"running command '{command}'"
88
+ return super()._get_error_context()
89
+
90
+
91
+ # Create the function that maintains the existing interface
92
+ async def run_command(command: str) -> ToolResult:
93
+ """
94
+ Run a shell command and return the output. User must confirm risky commands.
95
+
96
+ Args:
97
+ command (str): The command to run.
98
+
99
+ Returns:
100
+ ToolResult: The output of the command (stdout and stderr) or an error message.
101
+ """
102
+ tool = RunCommandTool(default_ui)
103
+ try:
104
+ return await tool.execute(command)
105
+ except ToolExecutionError as e:
106
+ # Return error message for pydantic-ai compatibility
107
+ return str(e)
@@ -0,0 +1,117 @@
1
+ """
2
+ Module: sidekick.tools.update_file
3
+
4
+ File update tool for agent operations in the Sidekick application.
5
+ Enables safe text replacement in existing files with target/patch semantics.
6
+ """
7
+
8
+ import os
9
+
10
+ from pydantic_ai.exceptions import ModelRetry
11
+
12
+ from tunacode.exceptions import ToolExecutionError
13
+ from tunacode.tools.base import FileBasedTool
14
+ from tunacode.types import FileContent, FilePath, ToolResult
15
+ from tunacode.ui import console as default_ui
16
+
17
+
18
+ class UpdateFileTool(FileBasedTool):
19
+ """Tool for updating existing files by replacing text blocks."""
20
+
21
+ @property
22
+ def tool_name(self) -> str:
23
+ return "Update"
24
+
25
+ async def _execute(
26
+ self, filepath: FilePath, target: FileContent, patch: FileContent
27
+ ) -> ToolResult:
28
+ """Update an existing file by replacing a target text block with a patch.
29
+
30
+ Args:
31
+ filepath: The path to the file to update.
32
+ target: The entire, exact block of text to be replaced.
33
+ patch: The new block of text to insert.
34
+
35
+ Returns:
36
+ ToolResult: A message indicating success.
37
+
38
+ Raises:
39
+ ModelRetry: If file not found or target not found
40
+ Exception: Any file operation errors
41
+ """
42
+ if not os.path.exists(filepath):
43
+ raise ModelRetry(
44
+ f"File '{filepath}' not found. Cannot update. "
45
+ "Verify the filepath or use `write_file` if it's a new file."
46
+ )
47
+
48
+ with open(filepath, "r", encoding="utf-8") as f:
49
+ original = f.read()
50
+
51
+ if target not in original:
52
+ # Provide context to help the LLM find the target
53
+ context_lines = 10
54
+ lines = original.splitlines()
55
+ snippet = "\n".join(lines[:context_lines])
56
+ # Use ModelRetry to guide the LLM
57
+ raise ModelRetry(
58
+ f"Target block not found in '{filepath}'. "
59
+ "Ensure the `target` argument exactly matches the content you want to replace. "
60
+ f"File starts with:\n---\n{snippet}\n---"
61
+ )
62
+
63
+ new_content = original.replace(target, patch, 1) # Replace only the first occurrence
64
+
65
+ if original == new_content:
66
+ # This could happen if target and patch are identical
67
+ raise ModelRetry(
68
+ f"Update target found, but replacement resulted in no changes to '{filepath}'. "
69
+ "Was the `target` identical to the `patch`? Please check the file content."
70
+ )
71
+
72
+ with open(filepath, "w", encoding="utf-8") as f:
73
+ f.write(new_content)
74
+
75
+ return f"File '{filepath}' updated successfully."
76
+
77
+ def _format_args(
78
+ self, filepath: FilePath, target: FileContent = None, patch: FileContent = None
79
+ ) -> str:
80
+ """Format arguments, truncating target and patch for display."""
81
+ args = [repr(filepath)]
82
+
83
+ if target is not None:
84
+ if len(target) > 50:
85
+ args.append(f"target='{target[:47]}...'")
86
+ else:
87
+ args.append(f"target={repr(target)}")
88
+
89
+ if patch is not None:
90
+ if len(patch) > 50:
91
+ args.append(f"patch='{patch[:47]}...'")
92
+ else:
93
+ args.append(f"patch={repr(patch)}")
94
+
95
+ return ", ".join(args)
96
+
97
+
98
+ # Create the function that maintains the existing interface
99
+ async def update_file(filepath: FilePath, target: FileContent, patch: FileContent) -> ToolResult:
100
+ """
101
+ Update an existing file by replacing a target text block with a patch.
102
+ Requires confirmation with diff before applying.
103
+
104
+ Args:
105
+ filepath (FilePath): The path to the file to update.
106
+ target (FileContent): The entire, exact block of text to be replaced.
107
+ patch (FileContent): The new block of text to insert.
108
+
109
+ Returns:
110
+ ToolResult: A message indicating the success or failure of the operation.
111
+ """
112
+ tool = UpdateFileTool(default_ui)
113
+ try:
114
+ return await tool.execute(filepath, target, patch)
115
+ except ToolExecutionError as e:
116
+ # Return error message for pydantic-ai compatibility
117
+ return str(e)
@@ -0,0 +1,82 @@
1
+ """
2
+ Module: sidekick.tools.write_file
3
+
4
+ File writing tool for agent operations in the Sidekick application.
5
+ Creates new files with automatic directory creation and overwrite protection.
6
+ """
7
+
8
+ import os
9
+
10
+ from pydantic_ai.exceptions import ModelRetry
11
+
12
+ from tunacode.exceptions import ToolExecutionError
13
+ from tunacode.tools.base import FileBasedTool
14
+ from tunacode.types import FileContent, FilePath, ToolResult
15
+ from tunacode.ui import console as default_ui
16
+
17
+
18
+ class WriteFileTool(FileBasedTool):
19
+ """Tool for writing content to new files."""
20
+
21
+ @property
22
+ def tool_name(self) -> str:
23
+ return "Write"
24
+
25
+ async def _execute(self, filepath: FilePath, content: FileContent) -> ToolResult:
26
+ """Write content to a new file. Fails if the file already exists.
27
+
28
+ Args:
29
+ filepath: The path to the file to write to.
30
+ content: The content to write to the file.
31
+
32
+ Returns:
33
+ ToolResult: A message indicating success.
34
+
35
+ Raises:
36
+ ModelRetry: If the file already exists
37
+ Exception: Any file writing errors
38
+ """
39
+ # Prevent overwriting existing files with this tool.
40
+ if os.path.exists(filepath):
41
+ # Use ModelRetry to guide the LLM
42
+ raise ModelRetry(
43
+ f"File '{filepath}' already exists. "
44
+ "Use the `update_file` tool to modify it, or choose a different filepath."
45
+ )
46
+
47
+ # Create directories if they don't exist
48
+ dirpath = os.path.dirname(filepath)
49
+ if dirpath and not os.path.exists(dirpath):
50
+ os.makedirs(dirpath, exist_ok=True)
51
+
52
+ with open(filepath, "w", encoding="utf-8") as file:
53
+ file.write(content)
54
+
55
+ return f"Successfully wrote to new file: {filepath}"
56
+
57
+ def _format_args(self, filepath: FilePath, content: FileContent = None) -> str:
58
+ """Format arguments, truncating content for display."""
59
+ if content is not None and len(content) > 50:
60
+ return f"{repr(filepath)}, content='{content[:47]}...'"
61
+ return super()._format_args(filepath, content)
62
+
63
+
64
+ # Create the function that maintains the existing interface
65
+ async def write_file(filepath: FilePath, content: FileContent) -> ToolResult:
66
+ """
67
+ Write content to a new file. Fails if the file already exists.
68
+ Requires confirmation before writing.
69
+
70
+ Args:
71
+ filepath (FilePath): The path to the file to write to.
72
+ content (FileContent): The content to write to the file.
73
+
74
+ Returns:
75
+ ToolResult: A message indicating the success or failure of the operation.
76
+ """
77
+ tool = WriteFileTool(default_ui)
78
+ try:
79
+ return await tool.execute(filepath, content)
80
+ except ToolExecutionError as e:
81
+ # Return error message for pydantic-ai compatibility
82
+ return str(e)
tunacode/types.py ADDED
@@ -0,0 +1,259 @@
1
+ """
2
+ Centralized type definitions for Sidekick CLI.
3
+
4
+ This module contains all type aliases, protocols, and type definitions
5
+ used throughout the Sidekick codebase.
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Any, Awaitable, Callable, Dict, List, Optional, Protocol, Tuple, Union
11
+
12
+ # Try to import pydantic-ai types if available
13
+ try:
14
+ from pydantic_ai import Agent
15
+ from pydantic_ai.messages import ModelRequest, ModelResponse, ToolReturnPart
16
+
17
+ PydanticAgent = Agent
18
+ MessagePart = Union[ToolReturnPart, Any]
19
+ except ImportError:
20
+ # Fallback if pydantic-ai is not available
21
+ PydanticAgent = Any
22
+ MessagePart = Any
23
+ ModelRequest = Any
24
+ ModelResponse = Any
25
+
26
+ # =============================================================================
27
+ # Core Types
28
+ # =============================================================================
29
+
30
+ # Basic type aliases
31
+ UserConfig = Dict[str, Any]
32
+ EnvConfig = Dict[str, str]
33
+ ModelName = str
34
+ ToolName = str
35
+ SessionId = str
36
+ DeviceId = str
37
+ InputSessions = Dict[str, Any]
38
+
39
+ # =============================================================================
40
+ # Configuration Types
41
+ # =============================================================================
42
+
43
+
44
+ @dataclass
45
+ class ModelPricing:
46
+ """Pricing information for a model."""
47
+
48
+ input: float
49
+ cached_input: float
50
+ output: float
51
+
52
+
53
+ @dataclass
54
+ class ModelConfig:
55
+ """Configuration for a model including pricing."""
56
+
57
+ pricing: ModelPricing
58
+
59
+
60
+ ModelRegistry = Dict[str, ModelConfig]
61
+
62
+ # Path configuration
63
+ ConfigPath = Path
64
+ ConfigFile = Path
65
+
66
+ # =============================================================================
67
+ # Tool Types
68
+ # =============================================================================
69
+
70
+ # Tool execution types
71
+ ToolArgs = Dict[str, Any]
72
+ ToolResult = str
73
+ ToolCallback = Callable[[Any, Any], Awaitable[None]]
74
+ ToolCallId = str
75
+
76
+
77
+ class ToolFunction(Protocol):
78
+ """Protocol for tool functions."""
79
+
80
+ async def __call__(self, *args, **kwargs) -> str: ...
81
+
82
+
83
+ @dataclass
84
+ class ToolConfirmationRequest:
85
+ """Request for tool execution confirmation."""
86
+
87
+ tool_name: str
88
+ args: Dict[str, Any]
89
+ filepath: Optional[str] = None
90
+
91
+
92
+ @dataclass
93
+ class ToolConfirmationResponse:
94
+ """Response from tool confirmation dialog."""
95
+
96
+ approved: bool
97
+ skip_future: bool = False
98
+ abort: bool = False
99
+
100
+
101
+ # =============================================================================
102
+ # UI Types
103
+ # =============================================================================
104
+
105
+
106
+ class UILogger(Protocol):
107
+ """Protocol for UI logging operations."""
108
+
109
+ async def info(self, message: str) -> None: ...
110
+ async def error(self, message: str) -> None: ...
111
+ async def warning(self, message: str) -> None: ...
112
+ async def debug(self, message: str) -> None: ...
113
+ async def success(self, message: str) -> None: ...
114
+
115
+
116
+ # UI callback types
117
+ UICallback = Callable[[str], Awaitable[None]]
118
+ UIInputCallback = Callable[[str, str], Awaitable[str]]
119
+
120
+ # =============================================================================
121
+ # Agent Types
122
+ # =============================================================================
123
+
124
+ # Agent response types
125
+ AgentResponse = Any # Replace with proper pydantic-ai types when available
126
+ MessageHistory = List[Any]
127
+ AgentRun = Any # pydantic_ai.RunContext or similar
128
+
129
+ # Agent configuration
130
+ AgentConfig = Dict[str, Any]
131
+ AgentName = str
132
+
133
+ # =============================================================================
134
+ # Session and State Types
135
+ # =============================================================================
136
+
137
+
138
+ @dataclass
139
+ class SessionState:
140
+ """Complete session state for the application."""
141
+
142
+ user_config: Dict[str, Any]
143
+ agents: Dict[str, Any]
144
+ messages: List[Any]
145
+ total_cost: float
146
+ current_model: str
147
+ spinner: Optional[Any]
148
+ tool_ignore: List[str]
149
+ yolo: bool
150
+ undo_initialized: bool
151
+ session_id: str
152
+ device_id: Optional[str]
153
+ input_sessions: Dict[str, Any]
154
+ current_task: Optional[Any]
155
+
156
+
157
+ # Forward reference for StateManager to avoid circular imports
158
+ StateManager = Any # Will be replaced with actual StateManager type
159
+
160
+ # =============================================================================
161
+ # Command Types
162
+ # =============================================================================
163
+
164
+ # Command execution types
165
+ CommandArgs = List[str]
166
+ CommandResult = Optional[Any]
167
+ ProcessRequestCallback = Callable[[str, StateManager, bool], Awaitable[Any]]
168
+
169
+
170
+ @dataclass
171
+ class CommandContext:
172
+ """Context passed to command handlers."""
173
+
174
+ state_manager: StateManager
175
+ process_request: Optional[ProcessRequestCallback] = None
176
+
177
+
178
+ # =============================================================================
179
+ # Service Types
180
+ # =============================================================================
181
+
182
+ # MCP (Model Context Protocol) types
183
+ MCPServerConfig = Dict[str, Any]
184
+ MCPServers = Dict[str, MCPServerConfig]
185
+
186
+
187
+ # =============================================================================
188
+ # File Operation Types
189
+ # =============================================================================
190
+
191
+ # File-related types
192
+ FilePath = Union[str, Path]
193
+ FileContent = str
194
+ FileEncoding = str
195
+ FileDiff = Tuple[str, str] # (original, modified)
196
+ FileSize = int
197
+ LineNumber = int
198
+
199
+ # =============================================================================
200
+ # Error Handling Types
201
+ # =============================================================================
202
+
203
+ # Error context types
204
+ ErrorContext = Dict[str, Any]
205
+ OriginalError = Optional[Exception]
206
+ ErrorMessage = str
207
+
208
+ # =============================================================================
209
+ # Async Types
210
+ # =============================================================================
211
+
212
+ # Async function types
213
+ AsyncFunc = Callable[..., Awaitable[Any]]
214
+ AsyncToolFunc = Callable[..., Awaitable[str]]
215
+ AsyncVoidFunc = Callable[..., Awaitable[None]]
216
+
217
+ # =============================================================================
218
+ # Diff and Update Types
219
+ # =============================================================================
220
+
221
+ # Types for file updates and diffs
222
+ UpdateOperation = Dict[str, Any]
223
+ DiffLine = str
224
+ DiffHunk = List[DiffLine]
225
+
226
+ # =============================================================================
227
+ # Validation Types
228
+ # =============================================================================
229
+
230
+ # Input validation types
231
+ ValidationResult = Union[bool, str] # True for valid, error message for invalid
232
+ Validator = Callable[[Any], ValidationResult]
233
+
234
+ # =============================================================================
235
+ # Cost Tracking Types
236
+ # =============================================================================
237
+
238
+ # Cost calculation types
239
+ TokenCount = int
240
+ CostAmount = float
241
+
242
+
243
+ @dataclass
244
+ class TokenUsage:
245
+ """Token usage for a request."""
246
+
247
+ input_tokens: int
248
+ cached_tokens: int
249
+ output_tokens: int
250
+
251
+
252
+ @dataclass
253
+ class CostBreakdown:
254
+ """Breakdown of costs for a request."""
255
+
256
+ input_cost: float
257
+ cached_cost: float
258
+ output_cost: float
259
+ total_cost: float
@@ -0,0 +1 @@
1
+ # UI package
@@ -0,0 +1,129 @@
1
+ """Completers for file references and commands."""
2
+
3
+ import os
4
+ from typing import Iterable, Optional
5
+
6
+ from prompt_toolkit.completion import CompleteEvent, Completer, Completion, merge_completers
7
+ from prompt_toolkit.document import Document
8
+
9
+ from ..cli.commands import CommandRegistry
10
+
11
+
12
+ class CommandCompleter(Completer):
13
+ """Completer for slash commands."""
14
+
15
+ def __init__(self, command_registry: Optional[CommandRegistry] = None):
16
+ self.command_registry = command_registry
17
+
18
+ def get_completions(
19
+ self, document: Document, complete_event: CompleteEvent
20
+ ) -> Iterable[Completion]:
21
+ """Get completions for slash commands."""
22
+ # Get the text before cursor
23
+ text = document.text_before_cursor
24
+
25
+ # Check if we're at the start of a line or after whitespace
26
+ if text and not text.isspace() and text[-1] != '\n':
27
+ # Only complete commands at the start of input or after a newline
28
+ last_newline = text.rfind('\n')
29
+ line_start = text[last_newline + 1:] if last_newline >= 0 else text
30
+
31
+ # Skip if not at the beginning of a line
32
+ if line_start and not line_start.startswith('/'):
33
+ return
34
+
35
+ # Get the word before cursor
36
+ word_before_cursor = document.get_word_before_cursor(WORD=True)
37
+
38
+ # Only complete if word starts with /
39
+ if not word_before_cursor.startswith('/'):
40
+ return
41
+
42
+ # Get command names from registry
43
+ if self.command_registry:
44
+ command_names = self.command_registry.get_command_names()
45
+ else:
46
+ # Fallback list of commands
47
+ command_names = ['/help', '/clear', '/dump', '/yolo', '/undo',
48
+ '/branch', '/compact', '/model', '/init']
49
+
50
+ # Get the partial command (without /)
51
+ partial = word_before_cursor[1:].lower()
52
+
53
+ # Yield completions for matching commands
54
+ for cmd in command_names:
55
+ if cmd.startswith('/') and cmd[1:].lower().startswith(partial):
56
+ yield Completion(
57
+ text=cmd,
58
+ start_position=-len(word_before_cursor),
59
+ display=cmd,
60
+ display_meta='command'
61
+ )
62
+
63
+
64
+ class FileReferenceCompleter(Completer):
65
+ """Completer for @file references that provides file path suggestions."""
66
+
67
+ def get_completions(
68
+ self, document: Document, complete_event: CompleteEvent
69
+ ) -> Iterable[Completion]:
70
+ """Get completions for @file references."""
71
+ # Get the word before cursor
72
+ word_before_cursor = document.get_word_before_cursor(WORD=True)
73
+
74
+ # Check if we're in an @file reference
75
+ if not word_before_cursor.startswith("@"):
76
+ return
77
+
78
+ # Get the path part after @
79
+ path_part = word_before_cursor[1:] # Remove @
80
+
81
+ # Determine directory and prefix
82
+ if "/" in path_part:
83
+ # Path includes directory
84
+ dir_path = os.path.dirname(path_part)
85
+ prefix = os.path.basename(path_part)
86
+ else:
87
+ # Just filename, search in current directory
88
+ dir_path = "."
89
+ prefix = path_part
90
+
91
+ # Get matching files
92
+ try:
93
+ if os.path.exists(dir_path) and os.path.isdir(dir_path):
94
+ for item in sorted(os.listdir(dir_path)):
95
+ if item.startswith(prefix):
96
+ full_path = os.path.join(dir_path, item) if dir_path != "." else item
97
+
98
+ # Skip hidden files unless explicitly requested
99
+ if item.startswith(".") and not prefix.startswith("."):
100
+ continue
101
+
102
+ # Add / for directories
103
+ if os.path.isdir(full_path):
104
+ display = item + "/"
105
+ completion = full_path + "/"
106
+ else:
107
+ display = item
108
+ completion = full_path
109
+
110
+ # Calculate how much to replace
111
+ start_position = -len(path_part)
112
+
113
+ yield Completion(
114
+ text=completion,
115
+ start_position=start_position,
116
+ display=display,
117
+ display_meta="dir" if os.path.isdir(full_path) else "file"
118
+ )
119
+ except (OSError, PermissionError):
120
+ # Silently ignore inaccessible directories
121
+ pass
122
+
123
+
124
+ def create_completer(command_registry: Optional[CommandRegistry] = None) -> Completer:
125
+ """Create a merged completer for both commands and file references."""
126
+ return merge_completers([
127
+ CommandCompleter(command_registry),
128
+ FileReferenceCompleter(),
129
+ ])