tunacode-cli 0.0.4__py3-none-any.whl → 0.0.6__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 (36) hide show
  1. tunacode/cli/commands.py +91 -33
  2. tunacode/cli/model_selector.py +178 -0
  3. tunacode/cli/repl.py +11 -10
  4. tunacode/configuration/models.py +11 -1
  5. tunacode/constants.py +11 -11
  6. tunacode/context.py +1 -3
  7. tunacode/core/agents/main.py +52 -94
  8. tunacode/core/agents/tinyagent_main.py +171 -0
  9. tunacode/core/setup/git_safety_setup.py +39 -51
  10. tunacode/core/setup/optimized_coordinator.py +73 -0
  11. tunacode/exceptions.py +13 -15
  12. tunacode/services/enhanced_undo_service.py +322 -0
  13. tunacode/services/project_undo_service.py +311 -0
  14. tunacode/services/undo_service.py +18 -21
  15. tunacode/tools/base.py +11 -20
  16. tunacode/tools/tinyagent_tools.py +103 -0
  17. tunacode/tools/update_file.py +24 -14
  18. tunacode/tools/write_file.py +9 -7
  19. tunacode/types.py +2 -2
  20. tunacode/ui/completers.py +98 -33
  21. tunacode/ui/input.py +8 -7
  22. tunacode/ui/keybindings.py +1 -3
  23. tunacode/ui/lexers.py +16 -17
  24. tunacode/ui/output.py +9 -3
  25. tunacode/ui/panels.py +4 -4
  26. tunacode/ui/prompt_manager.py +6 -4
  27. tunacode/utils/lazy_imports.py +59 -0
  28. tunacode/utils/regex_cache.py +33 -0
  29. tunacode/utils/system.py +13 -13
  30. tunacode_cli-0.0.6.dist-info/METADATA +235 -0
  31. {tunacode_cli-0.0.4.dist-info → tunacode_cli-0.0.6.dist-info}/RECORD +35 -27
  32. tunacode_cli-0.0.4.dist-info/METADATA +0 -247
  33. {tunacode_cli-0.0.4.dist-info → tunacode_cli-0.0.6.dist-info}/WHEEL +0 -0
  34. {tunacode_cli-0.0.4.dist-info → tunacode_cli-0.0.6.dist-info}/entry_points.txt +0 -0
  35. {tunacode_cli-0.0.4.dist-info → tunacode_cli-0.0.6.dist-info}/licenses/LICENSE +0 -0
  36. {tunacode_cli-0.0.4.dist-info → tunacode_cli-0.0.6.dist-info}/top_level.txt +0 -0
@@ -7,18 +7,18 @@ Manages automatic commits and rollback operations.
7
7
 
8
8
  import subprocess
9
9
  import time
10
+ from datetime import datetime, timezone
10
11
  from pathlib import Path
11
12
  from typing import Optional, Tuple
12
13
 
13
- from pydantic_ai.messages import ModelResponse, TextPart
14
-
15
- from tunacode.constants import (ERROR_UNDO_INIT, UNDO_DISABLED_HOME, UNDO_DISABLED_NO_GIT,
16
- UNDO_GIT_TIMEOUT, UNDO_INITIAL_COMMIT)
14
+ from tunacode.constants import (ERROR_UNDO_INIT, UNDO_DISABLED_HOME, UNDO_GIT_TIMEOUT,
15
+ UNDO_INITIAL_COMMIT)
17
16
  from tunacode.core.state import StateManager
18
17
  from tunacode.exceptions import GitOperationError
19
- from tunacode.ui import console as ui
20
18
  from tunacode.utils.system import get_session_dir
21
19
 
20
+ # Removed pydantic_ai import - using dict-based messages now
21
+
22
22
 
23
23
  def is_in_git_project(directory: Optional[Path] = None) -> bool:
24
24
  """
@@ -59,7 +59,7 @@ def init_undo_system(state_manager: StateManager) -> bool:
59
59
  home_dir = Path.home()
60
60
 
61
61
  if cwd == home_dir:
62
- ui.warning(UNDO_DISABLED_HOME)
62
+ print(f"⚠️ {UNDO_DISABLED_HOME}")
63
63
  return False
64
64
 
65
65
  if not is_in_git_project():
@@ -100,11 +100,11 @@ def init_undo_system(state_manager: StateManager) -> bool:
100
100
  return True
101
101
  except subprocess.TimeoutExpired as e:
102
102
  error = GitOperationError(operation="init", message=UNDO_GIT_TIMEOUT, original_error=e)
103
- ui.warning(str(error))
103
+ print(f"⚠️ {str(error)}")
104
104
  return False
105
105
  except Exception as e:
106
106
  error = GitOperationError(operation="init", message=str(e), original_error=e)
107
- ui.warning(ERROR_UNDO_INIT.format(e=e))
107
+ print(f"⚠️ {ERROR_UNDO_INIT.format(e=e)}")
108
108
  return False
109
109
 
110
110
 
@@ -156,11 +156,11 @@ def commit_for_undo(
156
156
  error = GitOperationError(
157
157
  operation="commit", message="Git commit timed out", original_error=e
158
158
  )
159
- ui.warning(str(error))
159
+ print(f"⚠️ {str(error)}")
160
160
  return False
161
161
  except Exception as e:
162
162
  error = GitOperationError(operation="commit", message=str(e), original_error=e)
163
- ui.warning(f"Error creating undo commit: {e}")
163
+ print(f"⚠️ Error creating undo commit: {e}")
164
164
  return False
165
165
 
166
166
 
@@ -220,17 +220,14 @@ def perform_undo(state_manager: StateManager) -> Tuple[bool, str]:
220
220
  # Add a system message to the chat history to inform the AI
221
221
  # about the undo operation
222
222
  state_manager.session.messages.append(
223
- ModelResponse(
224
- parts=[
225
- TextPart(
226
- content=(
227
- "The last changes were undone. "
228
- f"Commit message of undone changes: {commit_msg}"
229
- )
230
- )
231
- ],
232
- kind="response",
233
- )
223
+ {
224
+ "role": "system",
225
+ "content": (
226
+ f"The last changes were undone. "
227
+ f"Commit message of undone changes: {commit_msg}"
228
+ ),
229
+ "timestamp": datetime.now(timezone.utc).isoformat(),
230
+ }
234
231
  )
235
232
 
236
233
  return True, "Successfully undid last change"
tunacode/tools/base.py CHANGED
@@ -1,13 +1,11 @@
1
1
  """Base tool class for all Sidekick tools.
2
2
 
3
3
  This module provides a base class that implements common patterns
4
- for all tools including error handling, UI logging, and ModelRetry support.
4
+ for all tools including error handling and UI logging.
5
5
  """
6
6
 
7
7
  from abc import ABC, abstractmethod
8
8
 
9
- from pydantic_ai.exceptions import ModelRetry
10
-
11
9
  from tunacode.exceptions import FileOperationError, ToolExecutionError
12
10
  from tunacode.types import FilePath, ToolName, ToolResult, UILogger
13
11
 
@@ -28,31 +26,25 @@ class BaseTool(ABC):
28
26
 
29
27
  This method wraps the tool-specific logic with:
30
28
  - UI logging of the operation
31
- - Exception handling (except ModelRetry and ToolExecutionError)
29
+ - Exception handling
32
30
  - Consistent error message formatting
33
31
 
34
32
  Returns:
35
33
  str: Success message
36
34
 
37
35
  Raises:
38
- ModelRetry: Re-raised to guide the LLM
39
- ToolExecutionError: Raised for all other errors with structured information
36
+ ToolExecutionError: Raised for all errors with structured information
40
37
  """
41
38
  try:
42
39
  if self.ui:
43
40
  await self.ui.info(f"{self.tool_name}({self._format_args(*args, **kwargs)})")
44
41
  result = await self._execute(*args, **kwargs)
45
-
42
+
46
43
  # For file operations, try to create a git commit for undo tracking
47
44
  if isinstance(self, FileBasedTool):
48
45
  await self._commit_for_undo()
49
-
46
+
50
47
  return result
51
- except ModelRetry as e:
52
- # Log as warning and re-raise for pydantic-ai
53
- if self.ui:
54
- await self.ui.warning(str(e))
55
- raise
56
48
  except ToolExecutionError:
57
49
  # Already properly formatted, just re-raise
58
50
  raise
@@ -76,8 +68,7 @@ class BaseTool(ABC):
76
68
  str: Success message describing what was done
77
69
 
78
70
  Raises:
79
- ModelRetry: When the LLM needs guidance
80
- Exception: Any other errors will be caught and handled
71
+ Exception: Any errors will be caught and handled
81
72
  """
82
73
  pass
83
74
 
@@ -148,10 +139,10 @@ class FileBasedTool(BaseTool):
148
139
  - Encoding handling
149
140
  - Git commit for undo tracking
150
141
  """
151
-
142
+
152
143
  async def _commit_for_undo(self) -> None:
153
144
  """Create a git commit for undo tracking after file operations.
154
-
145
+
155
146
  This method gracefully handles cases where git is not available:
156
147
  - No git repository: Warns user about limited undo functionality
157
148
  - Git command fails: Warns but doesn't break the main operation
@@ -160,13 +151,13 @@ class FileBasedTool(BaseTool):
160
151
  try:
161
152
  # Import here to avoid circular imports
162
153
  from tunacode.services.undo_service import commit_for_undo, is_in_git_project
163
-
154
+
164
155
  # Check if we're in a git project first
165
156
  if not is_in_git_project():
166
157
  if self.ui:
167
158
  await self.ui.muted("⚠️ No git repository - undo functionality limited")
168
159
  return
169
-
160
+
170
161
  # Try to create commit with tool name as prefix
171
162
  success = commit_for_undo(message_prefix=f"tunacode {self.tool_name.lower()}")
172
163
  if success and self.ui:
@@ -179,7 +170,7 @@ class FileBasedTool(BaseTool):
179
170
  if self.ui:
180
171
  try:
181
172
  await self.ui.muted("⚠️ Git commit failed - undo functionality limited")
182
- except:
173
+ except Exception:
183
174
  # Even the warning failed, just continue silently
184
175
  pass
185
176
 
@@ -0,0 +1,103 @@
1
+ """TinyAgent tool implementations with decorators."""
2
+
3
+ from typing import Optional
4
+
5
+ from tinyagent.decorators import tool
6
+
7
+ from tunacode.exceptions import ToolExecutionError
8
+ from tunacode.ui import console as ui
9
+
10
+ # Import the existing tool classes to reuse their logic
11
+ from .read_file import ReadFileTool
12
+ from .run_command import RunCommandTool
13
+ from .update_file import UpdateFileTool
14
+ from .write_file import WriteFileTool
15
+
16
+
17
+ @tool
18
+ async def read_file(filepath: str) -> str:
19
+ """Read the contents of a file.
20
+
21
+ Args:
22
+ filepath: The path to the file to read.
23
+
24
+ Returns:
25
+ The contents of the file.
26
+
27
+ Raises:
28
+ Exception: If file cannot be read.
29
+ """
30
+ tool_instance = ReadFileTool(ui)
31
+ try:
32
+ result = await tool_instance.execute(filepath)
33
+ return result
34
+ except ToolExecutionError as e:
35
+ # tinyAgent expects exceptions to be raised, not returned as strings
36
+ raise Exception(str(e))
37
+
38
+
39
+ @tool
40
+ async def write_file(filepath: str, content: str) -> str:
41
+ """Write content to a file.
42
+
43
+ Args:
44
+ filepath: The path to the file to write.
45
+ content: The content to write to the file.
46
+
47
+ Returns:
48
+ Success message.
49
+
50
+ Raises:
51
+ Exception: If file cannot be written.
52
+ """
53
+ tool_instance = WriteFileTool(ui)
54
+ try:
55
+ result = await tool_instance.execute(filepath, content)
56
+ return result
57
+ except ToolExecutionError as e:
58
+ raise Exception(str(e))
59
+
60
+
61
+ @tool
62
+ async def update_file(filepath: str, old_content: str, new_content: str) -> str:
63
+ """Update specific content in a file.
64
+
65
+ Args:
66
+ filepath: The path to the file to update.
67
+ old_content: The content to find and replace.
68
+ new_content: The new content to insert.
69
+
70
+ Returns:
71
+ Success message.
72
+
73
+ Raises:
74
+ Exception: If file cannot be updated.
75
+ """
76
+ tool_instance = UpdateFileTool(ui)
77
+ try:
78
+ result = await tool_instance.execute(filepath, old_content, new_content)
79
+ return result
80
+ except ToolExecutionError as e:
81
+ raise Exception(str(e))
82
+
83
+
84
+ @tool
85
+ async def run_command(command: str, timeout: Optional[int] = None) -> str:
86
+ """Run a shell command.
87
+
88
+ Args:
89
+ command: The command to run.
90
+ timeout: Optional timeout in seconds.
91
+
92
+ Returns:
93
+ The command output.
94
+
95
+ Raises:
96
+ Exception: If command fails.
97
+ """
98
+ tool_instance = RunCommandTool(ui)
99
+ try:
100
+ result = await tool_instance.execute(command, timeout)
101
+ return result
102
+ except ToolExecutionError as e:
103
+ raise Exception(str(e))
@@ -7,8 +7,6 @@ Enables safe text replacement in existing files with target/patch semantics.
7
7
 
8
8
  import os
9
9
 
10
- from pydantic_ai.exceptions import ModelRetry
11
-
12
10
  from tunacode.exceptions import ToolExecutionError
13
11
  from tunacode.tools.base import FileBasedTool
14
12
  from tunacode.types import FileContent, FilePath, ToolResult
@@ -36,13 +34,17 @@ class UpdateFileTool(FileBasedTool):
36
34
  ToolResult: A message indicating success.
37
35
 
38
36
  Raises:
39
- ModelRetry: If file not found or target not found
37
+ ToolExecutionError: If file not found or target not found
40
38
  Exception: Any file operation errors
41
39
  """
42
40
  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."
41
+ raise ToolExecutionError(
42
+ tool_name=self.tool_name,
43
+ message=(
44
+ f"File '{filepath}' not found. Cannot update. "
45
+ "Verify the filepath or use `write_file` if it's a new file."
46
+ ),
47
+ original_error=None,
46
48
  )
47
49
 
48
50
  with open(filepath, "r", encoding="utf-8") as f:
@@ -53,20 +55,28 @@ class UpdateFileTool(FileBasedTool):
53
55
  context_lines = 10
54
56
  lines = original.splitlines()
55
57
  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---"
58
+ # Raise error to guide the LLM
59
+ raise ToolExecutionError(
60
+ tool_name=self.tool_name,
61
+ message=(
62
+ f"Target block not found in '{filepath}'. "
63
+ "Ensure the `target` argument exactly matches the content you want to replace. "
64
+ f"File starts with:\n---\n{snippet}\n---"
65
+ ),
66
+ original_error=None,
61
67
  )
62
68
 
63
69
  new_content = original.replace(target, patch, 1) # Replace only the first occurrence
64
70
 
65
71
  if original == new_content:
66
72
  # 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."
73
+ raise ToolExecutionError(
74
+ tool_name=self.tool_name,
75
+ message=(
76
+ f"Update target found, but replacement resulted in no changes to '{filepath}'. "
77
+ "Was the `target` identical to the `patch`? Please check the file content."
78
+ ),
79
+ original_error=None,
70
80
  )
71
81
 
72
82
  with open(filepath, "w", encoding="utf-8") as f:
@@ -7,8 +7,6 @@ Creates new files with automatic directory creation and overwrite protection.
7
7
 
8
8
  import os
9
9
 
10
- from pydantic_ai.exceptions import ModelRetry
11
-
12
10
  from tunacode.exceptions import ToolExecutionError
13
11
  from tunacode.tools.base import FileBasedTool
14
12
  from tunacode.types import FileContent, FilePath, ToolResult
@@ -33,15 +31,19 @@ class WriteFileTool(FileBasedTool):
33
31
  ToolResult: A message indicating success.
34
32
 
35
33
  Raises:
36
- ModelRetry: If the file already exists
34
+ ToolExecutionError: If the file already exists
37
35
  Exception: Any file writing errors
38
36
  """
39
37
  # Prevent overwriting existing files with this tool.
40
38
  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."
39
+ # Raise error to guide the LLM
40
+ raise ToolExecutionError(
41
+ tool_name=self.tool_name,
42
+ message=(
43
+ f"File '{filepath}' already exists. Use the `update_file` tool "
44
+ "to modify it, or choose a different filepath."
45
+ ),
46
+ original_error=None,
45
47
  )
46
48
 
47
49
  # Create directories if they don't exist
tunacode/types.py CHANGED
@@ -1,8 +1,8 @@
1
1
  """
2
- Centralized type definitions for Sidekick CLI.
2
+ Centralized type definitions for TunaCode CLI.
3
3
 
4
4
  This module contains all type aliases, protocols, and type definitions
5
- used throughout the Sidekick codebase.
5
+ used throughout the TunaCode codebase.
6
6
  """
7
7
 
8
8
  from dataclasses import dataclass
tunacode/ui/completers.py CHANGED
@@ -10,54 +10,117 @@ from ..cli.commands import CommandRegistry
10
10
 
11
11
 
12
12
  class CommandCompleter(Completer):
13
- """Completer for slash commands."""
14
-
13
+ """Completer for slash commands and their arguments."""
14
+
15
15
  def __init__(self, command_registry: Optional[CommandRegistry] = None):
16
16
  self.command_registry = command_registry
17
-
17
+ self._model_selector = None
18
+
18
19
  def get_completions(
19
20
  self, document: Document, complete_event: CompleteEvent
20
21
  ) -> Iterable[Completion]:
21
- """Get completions for slash commands."""
22
+ """Get completions for slash commands and model names."""
22
23
  # Get the text before cursor
23
24
  text = document.text_before_cursor
24
-
25
+
26
+ # Check if we're completing model names after /model or /m command
27
+ if self._should_complete_model_names(text):
28
+ yield from self._get_model_completions(document, text)
29
+ return
30
+
25
31
  # Check if we're at the start of a line or after whitespace
26
- if text and not text.isspace() and text[-1] != '\n':
32
+ if text and not text.isspace() and text[-1] != "\n":
27
33
  # 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
-
34
+ last_newline = text.rfind("\n")
35
+ line_start = text[last_newline + 1 :] if last_newline >= 0 else text # noqa: E203
36
+
31
37
  # Skip if not at the beginning of a line
32
- if line_start and not line_start.startswith('/'):
38
+ if line_start and not line_start.startswith("/"):
33
39
  return
34
-
40
+
35
41
  # Get the word before cursor
36
42
  word_before_cursor = document.get_word_before_cursor(WORD=True)
37
-
43
+
38
44
  # Only complete if word starts with /
39
- if not word_before_cursor.startswith('/'):
45
+ if not word_before_cursor.startswith("/"):
40
46
  return
41
-
47
+
42
48
  # Get command names from registry
43
49
  if self.command_registry:
44
50
  command_names = self.command_registry.get_command_names()
45
51
  else:
46
52
  # Fallback list of commands
47
- command_names = ['/help', '/clear', '/dump', '/yolo', '/undo',
48
- '/branch', '/compact', '/model', '/init']
49
-
53
+ command_names = [
54
+ "/help",
55
+ "/clear",
56
+ "/dump",
57
+ "/yolo",
58
+ "/undo",
59
+ "/branch",
60
+ "/compact",
61
+ "/model",
62
+ "/m",
63
+ "/init",
64
+ ]
65
+
50
66
  # Get the partial command (without /)
51
67
  partial = word_before_cursor[1:].lower()
52
-
68
+
53
69
  # Yield completions for matching commands
54
70
  for cmd in command_names:
55
- if cmd.startswith('/') and cmd[1:].lower().startswith(partial):
71
+ if cmd.startswith("/") and cmd[1:].lower().startswith(partial):
56
72
  yield Completion(
57
73
  text=cmd,
58
74
  start_position=-len(word_before_cursor),
59
75
  display=cmd,
60
- display_meta='command'
76
+ display_meta="command",
77
+ )
78
+
79
+ def _should_complete_model_names(self, text: str) -> bool:
80
+ """Check if we should complete model names."""
81
+ # Look for /model or /m command followed by space
82
+ import re
83
+
84
+ pattern = r"(?:^|\n)\s*(?:/model|/m)\s+\S*$"
85
+ return bool(re.search(pattern, text))
86
+
87
+ def _get_model_completions(self, document: Document, text: str) -> Iterable[Completion]:
88
+ """Get completions for model names."""
89
+ # Lazy import and cache
90
+ if self._model_selector is None:
91
+ try:
92
+ from ..cli.model_selector import ModelSelector
93
+
94
+ self._model_selector = ModelSelector()
95
+ except ImportError:
96
+ return
97
+
98
+ # Get the partial model name
99
+ word_before_cursor = document.get_word_before_cursor(WORD=True)
100
+ partial = word_before_cursor.lower()
101
+
102
+ # Yield model short names and indices
103
+ seen = set()
104
+ for i, model in enumerate(self._model_selector.models):
105
+ # Complete by index
106
+ index_str = str(i)
107
+ if index_str.startswith(partial) and index_str not in seen:
108
+ seen.add(index_str)
109
+ yield Completion(
110
+ text=index_str,
111
+ start_position=-len(word_before_cursor),
112
+ display=f"{index_str} - {model.display_name}",
113
+ display_meta=model.provider.value[2],
114
+ )
115
+
116
+ # Complete by short name
117
+ if model.short_name.lower().startswith(partial) and model.short_name not in seen:
118
+ seen.add(model.short_name)
119
+ yield Completion(
120
+ text=model.short_name,
121
+ start_position=-len(word_before_cursor),
122
+ display=f"{model.short_name} - {model.display_name}",
123
+ display_meta=model.provider.value[2],
61
124
  )
62
125
 
63
126
 
@@ -70,14 +133,14 @@ class FileReferenceCompleter(Completer):
70
133
  """Get completions for @file references."""
71
134
  # Get the word before cursor
72
135
  word_before_cursor = document.get_word_before_cursor(WORD=True)
73
-
136
+
74
137
  # Check if we're in an @file reference
75
138
  if not word_before_cursor.startswith("@"):
76
139
  return
77
-
140
+
78
141
  # Get the path part after @
79
142
  path_part = word_before_cursor[1:] # Remove @
80
-
143
+
81
144
  # Determine directory and prefix
82
145
  if "/" in path_part:
83
146
  # Path includes directory
@@ -87,18 +150,18 @@ class FileReferenceCompleter(Completer):
87
150
  # Just filename, search in current directory
88
151
  dir_path = "."
89
152
  prefix = path_part
90
-
153
+
91
154
  # Get matching files
92
155
  try:
93
156
  if os.path.exists(dir_path) and os.path.isdir(dir_path):
94
157
  for item in sorted(os.listdir(dir_path)):
95
158
  if item.startswith(prefix):
96
159
  full_path = os.path.join(dir_path, item) if dir_path != "." else item
97
-
160
+
98
161
  # Skip hidden files unless explicitly requested
99
162
  if item.startswith(".") and not prefix.startswith("."):
100
163
  continue
101
-
164
+
102
165
  # Add / for directories
103
166
  if os.path.isdir(full_path):
104
167
  display = item + "/"
@@ -106,15 +169,15 @@ class FileReferenceCompleter(Completer):
106
169
  else:
107
170
  display = item
108
171
  completion = full_path
109
-
172
+
110
173
  # Calculate how much to replace
111
174
  start_position = -len(path_part)
112
-
175
+
113
176
  yield Completion(
114
177
  text=completion,
115
178
  start_position=start_position,
116
179
  display=display,
117
- display_meta="dir" if os.path.isdir(full_path) else "file"
180
+ display_meta="dir" if os.path.isdir(full_path) else "file",
118
181
  )
119
182
  except (OSError, PermissionError):
120
183
  # Silently ignore inaccessible directories
@@ -123,7 +186,9 @@ class FileReferenceCompleter(Completer):
123
186
 
124
187
  def create_completer(command_registry: Optional[CommandRegistry] = None) -> Completer:
125
188
  """Create a merged completer for both commands and file references."""
126
- return merge_completers([
127
- CommandCompleter(command_registry),
128
- FileReferenceCompleter(),
129
- ])
189
+ return merge_completers(
190
+ [
191
+ CommandCompleter(command_registry),
192
+ FileReferenceCompleter(),
193
+ ]
194
+ )
tunacode/ui/input.py CHANGED
@@ -4,10 +4,9 @@ from typing import Optional
4
4
 
5
5
  from prompt_toolkit.formatted_text import HTML
6
6
  from prompt_toolkit.key_binding import KeyBindings
7
- from prompt_toolkit.styles import Style
8
7
  from prompt_toolkit.validation import Validator
9
8
 
10
- from tunacode.constants import UI_COLORS, UI_PROMPT_PREFIX
9
+ from tunacode.constants import UI_PROMPT_PREFIX
11
10
  from tunacode.core.state import StateManager
12
11
 
13
12
  from .completers import create_completer
@@ -72,7 +71,9 @@ async def input(
72
71
  return await manager.get_input(session_key, pretext, config)
73
72
 
74
73
 
75
- async def multiline_input(state_manager: Optional[StateManager] = None, command_registry=None) -> str:
74
+ async def multiline_input(
75
+ state_manager: Optional[StateManager] = None, command_registry=None
76
+ ) -> str:
76
77
  """Get multiline input from the user with @file completion and highlighting."""
77
78
  kb = create_key_bindings()
78
79
  placeholder = formatted_text(
@@ -85,11 +86,11 @@ async def multiline_input(state_manager: Optional[StateManager] = None, command_
85
86
  )
86
87
  )
87
88
  return await input(
88
- "multiline",
89
- key_bindings=kb,
90
- multiline=True,
89
+ "multiline",
90
+ key_bindings=kb,
91
+ multiline=True,
91
92
  placeholder=placeholder,
92
93
  completer=create_completer(command_registry),
93
94
  lexer=FileReferenceLexer(),
94
- state_manager=state_manager
95
+ state_manager=state_manager,
95
96
  )
@@ -7,8 +7,6 @@ def create_key_bindings() -> KeyBindings:
7
7
  """Create and configure key bindings for the UI."""
8
8
  kb = KeyBindings()
9
9
 
10
-
11
-
12
10
  @kb.add("enter")
13
11
  def _submit(event):
14
12
  """Submit the current buffer."""
@@ -18,7 +16,7 @@ def create_key_bindings() -> KeyBindings:
18
16
  def _newline(event):
19
17
  """Insert a newline character."""
20
18
  event.current_buffer.insert_text("\n")
21
-
19
+
22
20
  @kb.add("escape", "enter")
23
21
  def _escape_enter(event):
24
22
  """Insert a newline when escape then enter is pressed."""