tunacode-cli 0.0.9__py3-none-any.whl → 0.0.10__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 (46) hide show
  1. tunacode/cli/commands.py +34 -165
  2. tunacode/cli/main.py +15 -38
  3. tunacode/cli/repl.py +24 -18
  4. tunacode/configuration/defaults.py +1 -1
  5. tunacode/configuration/models.py +4 -11
  6. tunacode/configuration/settings.py +10 -3
  7. tunacode/constants.py +6 -4
  8. tunacode/context.py +3 -1
  9. tunacode/core/agents/main.py +94 -52
  10. tunacode/core/setup/agent_setup.py +1 -1
  11. tunacode/core/setup/config_setup.py +148 -78
  12. tunacode/core/setup/coordinator.py +4 -2
  13. tunacode/core/setup/environment_setup.py +1 -1
  14. tunacode/core/setup/git_safety_setup.py +51 -39
  15. tunacode/exceptions.py +2 -0
  16. tunacode/prompts/system.txt +1 -1
  17. tunacode/services/undo_service.py +16 -13
  18. tunacode/setup.py +6 -2
  19. tunacode/tools/base.py +20 -11
  20. tunacode/tools/update_file.py +14 -24
  21. tunacode/tools/write_file.py +7 -9
  22. tunacode/ui/completers.py +33 -98
  23. tunacode/ui/input.py +9 -13
  24. tunacode/ui/keybindings.py +3 -1
  25. tunacode/ui/lexers.py +17 -16
  26. tunacode/ui/output.py +8 -14
  27. tunacode/ui/panels.py +7 -5
  28. tunacode/ui/prompt_manager.py +4 -8
  29. tunacode/ui/tool_ui.py +3 -3
  30. tunacode/utils/system.py +0 -40
  31. tunacode_cli-0.0.10.dist-info/METADATA +366 -0
  32. tunacode_cli-0.0.10.dist-info/RECORD +65 -0
  33. {tunacode_cli-0.0.9.dist-info → tunacode_cli-0.0.10.dist-info}/licenses/LICENSE +1 -1
  34. tunacode/cli/model_selector.py +0 -178
  35. tunacode/core/agents/tinyagent_main.py +0 -194
  36. tunacode/core/setup/optimized_coordinator.py +0 -73
  37. tunacode/services/enhanced_undo_service.py +0 -322
  38. tunacode/services/project_undo_service.py +0 -311
  39. tunacode/tools/tinyagent_tools.py +0 -103
  40. tunacode/utils/lazy_imports.py +0 -59
  41. tunacode/utils/regex_cache.py +0 -33
  42. tunacode_cli-0.0.9.dist-info/METADATA +0 -321
  43. tunacode_cli-0.0.9.dist-info/RECORD +0 -73
  44. {tunacode_cli-0.0.9.dist-info → tunacode_cli-0.0.10.dist-info}/WHEEL +0 -0
  45. {tunacode_cli-0.0.9.dist-info → tunacode_cli-0.0.10.dist-info}/entry_points.txt +0 -0
  46. {tunacode_cli-0.0.9.dist-info → tunacode_cli-0.0.10.dist-info}/top_level.txt +0 -0
tunacode/ui/completers.py CHANGED
@@ -10,117 +10,54 @@ from ..cli.commands import CommandRegistry
10
10
 
11
11
 
12
12
  class CommandCompleter(Completer):
13
- """Completer for slash commands and their arguments."""
14
-
13
+ """Completer for slash commands."""
14
+
15
15
  def __init__(self, command_registry: Optional[CommandRegistry] = None):
16
16
  self.command_registry = command_registry
17
- self._model_selector = None
18
-
17
+
19
18
  def get_completions(
20
19
  self, document: Document, complete_event: CompleteEvent
21
20
  ) -> Iterable[Completion]:
22
- """Get completions for slash commands and model names."""
21
+ """Get completions for slash commands."""
23
22
  # Get the text before cursor
24
23
  text = document.text_before_cursor
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
-
24
+
31
25
  # Check if we're at the start of a line or after whitespace
32
- if text and not text.isspace() and text[-1] != "\n":
26
+ if text and not text.isspace() and text[-1] != '\n':
33
27
  # Only complete commands at the start of input or after a newline
34
- last_newline = text.rfind("\n")
35
- line_start = text[last_newline + 1 :] if last_newline >= 0 else text # noqa: E203
36
-
28
+ last_newline = text.rfind('\n')
29
+ line_start = text[last_newline + 1:] if last_newline >= 0 else text
30
+
37
31
  # Skip if not at the beginning of a line
38
- if line_start and not line_start.startswith("/"):
32
+ if line_start and not line_start.startswith('/'):
39
33
  return
40
-
34
+
41
35
  # Get the word before cursor
42
36
  word_before_cursor = document.get_word_before_cursor(WORD=True)
43
-
37
+
44
38
  # Only complete if word starts with /
45
- if not word_before_cursor.startswith("/"):
39
+ if not word_before_cursor.startswith('/'):
46
40
  return
47
-
41
+
48
42
  # Get command names from registry
49
43
  if self.command_registry:
50
44
  command_names = self.command_registry.get_command_names()
51
45
  else:
52
46
  # Fallback list of commands
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
-
47
+ command_names = ['/help', '/clear', '/dump', '/yolo', '/undo',
48
+ '/branch', '/compact', '/model']
49
+
66
50
  # Get the partial command (without /)
67
51
  partial = word_before_cursor[1:].lower()
68
-
52
+
69
53
  # Yield completions for matching commands
70
54
  for cmd in command_names:
71
- if cmd.startswith("/") and cmd[1:].lower().startswith(partial):
55
+ if cmd.startswith('/') and cmd[1:].lower().startswith(partial):
72
56
  yield Completion(
73
57
  text=cmd,
74
58
  start_position=-len(word_before_cursor),
75
59
  display=cmd,
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],
60
+ display_meta='command'
124
61
  )
125
62
 
126
63
 
@@ -133,14 +70,14 @@ class FileReferenceCompleter(Completer):
133
70
  """Get completions for @file references."""
134
71
  # Get the word before cursor
135
72
  word_before_cursor = document.get_word_before_cursor(WORD=True)
136
-
73
+
137
74
  # Check if we're in an @file reference
138
75
  if not word_before_cursor.startswith("@"):
139
76
  return
140
-
77
+
141
78
  # Get the path part after @
142
79
  path_part = word_before_cursor[1:] # Remove @
143
-
80
+
144
81
  # Determine directory and prefix
145
82
  if "/" in path_part:
146
83
  # Path includes directory
@@ -150,18 +87,18 @@ class FileReferenceCompleter(Completer):
150
87
  # Just filename, search in current directory
151
88
  dir_path = "."
152
89
  prefix = path_part
153
-
90
+
154
91
  # Get matching files
155
92
  try:
156
93
  if os.path.exists(dir_path) and os.path.isdir(dir_path):
157
94
  for item in sorted(os.listdir(dir_path)):
158
95
  if item.startswith(prefix):
159
96
  full_path = os.path.join(dir_path, item) if dir_path != "." else item
160
-
97
+
161
98
  # Skip hidden files unless explicitly requested
162
99
  if item.startswith(".") and not prefix.startswith("."):
163
100
  continue
164
-
101
+
165
102
  # Add / for directories
166
103
  if os.path.isdir(full_path):
167
104
  display = item + "/"
@@ -169,15 +106,15 @@ class FileReferenceCompleter(Completer):
169
106
  else:
170
107
  display = item
171
108
  completion = full_path
172
-
109
+
173
110
  # Calculate how much to replace
174
111
  start_position = -len(path_part)
175
-
112
+
176
113
  yield Completion(
177
114
  text=completion,
178
115
  start_position=start_position,
179
116
  display=display,
180
- display_meta="dir" if os.path.isdir(full_path) else "file",
117
+ display_meta="dir" if os.path.isdir(full_path) else "file"
181
118
  )
182
119
  except (OSError, PermissionError):
183
120
  # Silently ignore inaccessible directories
@@ -186,9 +123,7 @@ class FileReferenceCompleter(Completer):
186
123
 
187
124
  def create_completer(command_registry: Optional[CommandRegistry] = None) -> Completer:
188
125
  """Create a merged completer for both commands and file references."""
189
- return merge_completers(
190
- [
191
- CommandCompleter(command_registry),
192
- FileReferenceCompleter(),
193
- ]
194
- )
126
+ return merge_completers([
127
+ CommandCompleter(command_registry),
128
+ FileReferenceCompleter(),
129
+ ])
tunacode/ui/input.py CHANGED
@@ -4,9 +4,10 @@ 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
7
8
  from prompt_toolkit.validation import Validator
8
9
 
9
- from tunacode.constants import UI_PROMPT_PREFIX
10
+ from tunacode.constants import UI_COLORS, UI_PROMPT_PREFIX
10
11
  from tunacode.core.state import StateManager
11
12
 
12
13
  from .completers import create_completer
@@ -32,7 +33,6 @@ async def input(
32
33
  lexer=None,
33
34
  timeoutlen: float = 0.05,
34
35
  state_manager: Optional[StateManager] = None,
35
- default: str = "",
36
36
  ) -> str:
37
37
  """
38
38
  Prompt for user input using simplified prompt management.
@@ -49,7 +49,6 @@ async def input(
49
49
  lexer: Optional lexer for syntax highlighting
50
50
  timeoutlen: Timeout length for input
51
51
  state_manager: The state manager for session storage
52
- default: Default value to pre-fill in the input
53
52
 
54
53
  Returns:
55
54
  User input string
@@ -64,7 +63,6 @@ async def input(
64
63
  completer=completer,
65
64
  lexer=lexer,
66
65
  timeoutlen=timeoutlen,
67
- default=default,
68
66
  )
69
67
 
70
68
  # Create prompt manager
@@ -74,26 +72,24 @@ async def input(
74
72
  return await manager.get_input(session_key, pretext, config)
75
73
 
76
74
 
77
- async def multiline_input(
78
- state_manager: Optional[StateManager] = None, command_registry=None
79
- ) -> str:
75
+ async def multiline_input(state_manager: Optional[StateManager] = None, command_registry=None) -> str:
80
76
  """Get multiline input from the user with @file completion and highlighting."""
81
77
  kb = create_key_bindings()
82
78
  placeholder = formatted_text(
83
79
  (
84
80
  "<darkgrey>"
85
- "<bold>Enter</bold> to submit, "
86
- "<bold>Esc + Enter</bold> for new line, "
81
+ "<bold>Enter</bold> to submit "
82
+ "<bold>Esc + Enter</bold> for new line "
87
83
  "<bold>/help</bold> for commands"
88
84
  "</darkgrey>"
89
85
  )
90
86
  )
91
87
  return await input(
92
- "multiline",
93
- key_bindings=kb,
94
- multiline=True,
88
+ "multiline",
89
+ key_bindings=kb,
90
+ multiline=True,
95
91
  placeholder=placeholder,
96
92
  completer=create_completer(command_registry),
97
93
  lexer=FileReferenceLexer(),
98
- state_manager=state_manager,
94
+ state_manager=state_manager
99
95
  )
@@ -7,6 +7,8 @@ def create_key_bindings() -> KeyBindings:
7
7
  """Create and configure key bindings for the UI."""
8
8
  kb = KeyBindings()
9
9
 
10
+
11
+
10
12
  @kb.add("enter")
11
13
  def _submit(event):
12
14
  """Submit the current buffer."""
@@ -16,7 +18,7 @@ def create_key_bindings() -> KeyBindings:
16
18
  def _newline(event):
17
19
  """Insert a newline character."""
18
20
  event.current_buffer.insert_text("\n")
19
-
21
+
20
22
  @kb.add("escape", "enter")
21
23
  def _escape_enter(event):
22
24
  """Insert a newline when escape then enter is pressed."""
tunacode/ui/lexers.py CHANGED
@@ -2,44 +2,45 @@
2
2
 
3
3
  import re
4
4
 
5
+ from prompt_toolkit.formatted_text import FormattedText
5
6
  from prompt_toolkit.lexers import Lexer
6
7
 
7
8
 
8
9
  class FileReferenceLexer(Lexer):
9
10
  """Lexer that highlights @file references in light blue."""
10
-
11
+
11
12
  # Pattern to match @file references
12
- FILE_REF_PATTERN = re.compile(r"@([\w./_-]+)")
13
-
13
+ FILE_REF_PATTERN = re.compile(r'@([\w./_-]+)')
14
+
14
15
  def lex_document(self, document):
15
16
  """Return a formatted text list for the given document."""
16
- lines = document.text.split("\n")
17
-
17
+ lines = document.text.split('\n')
18
+
18
19
  def get_line_tokens(line_number):
19
20
  """Get tokens for a specific line."""
20
21
  if line_number >= len(lines):
21
22
  return []
22
-
23
+
23
24
  line = lines[line_number]
24
25
  tokens = []
25
26
  last_end = 0
26
-
27
+
27
28
  # Find all @file references in the line
28
29
  for match in self.FILE_REF_PATTERN.finditer(line):
29
30
  start, end = match.span()
30
-
31
+
31
32
  # Add text before the match
32
33
  if start > last_end:
33
- tokens.append(("", line[last_end:start]))
34
-
34
+ tokens.append(('', line[last_end:start]))
35
+
35
36
  # Add the @file reference with styling
36
- tokens.append(("class:file-reference", match.group(0)))
37
+ tokens.append(('class:file-reference', match.group(0)))
37
38
  last_end = end
38
-
39
+
39
40
  # Add remaining text
40
41
  if last_end < len(line):
41
- tokens.append(("", line[last_end:]))
42
-
42
+ tokens.append(('', line[last_end:]))
43
+
43
44
  return tokens
44
-
45
- return get_line_tokens
45
+
46
+ return get_line_tokens
tunacode/ui/output.py CHANGED
@@ -16,15 +16,9 @@ from .decorators import create_sync_wrapper
16
16
  console = Console()
17
17
  colors = DotDict(UI_COLORS)
18
18
 
19
- BANNER = (
20
- "[bold #00d7ff]┌─────────────────────────────────────────────────────────────────┐"
21
- "[/bold #00d7ff]\n"
22
- "[bold #00d7ff]│[/bold #00d7ff] [bold white]T U N A C O D E[/bold white] "
23
- "[dim #64748b]• Agentic AI Development Environment[/dim #64748b] "
24
- "[bold #00d7ff]│[/bold #00d7ff]\n"
25
- "[bold #00d7ff]└─────────────────────────────────────────────────────────────────┘"
26
- "[/bold #00d7ff]"
27
- )
19
+ BANNER = """[bold #00d7ff]╭─────────────────────────────────────────────────────────────────╮[/bold #00d7ff]
20
+ [bold #00d7ff]│[/bold #00d7ff] [bold white]T U N A C O D E[/bold white] [dim #64748b]• Agentic AI Development Environment[/dim #64748b] [bold #00d7ff]│[/bold #00d7ff]
21
+ [bold #00d7ff]╰─────────────────────────────────────────────────────────────────╯[/bold #00d7ff]"""
28
22
 
29
23
 
30
24
  @create_sync_wrapper
@@ -34,8 +28,8 @@ async def print(message, **kwargs) -> None:
34
28
 
35
29
 
36
30
  async def line() -> None:
37
- """Print a line to the console."""
38
- await run_in_terminal(lambda: console.line())
31
+ """Print an empty line for spacing."""
32
+ await print("")
39
33
 
40
34
 
41
35
  async def info(text: str) -> None:
@@ -53,9 +47,9 @@ async def warning(text: str) -> None:
53
47
  await print(f"[{colors.warning}]⚠[/{colors.warning}] {text}")
54
48
 
55
49
 
56
- async def muted(text: str, spaces: int = 0) -> None:
57
- """Print a muted message."""
58
- await print(f"{' ' * spaces}[{colors.muted}]•[/{colors.muted}] [dim]{text}[/dim]")
50
+ async def muted(text: str) -> None:
51
+ """Print muted text."""
52
+ await print(text, style=colors.muted)
59
53
 
60
54
 
61
55
  async def usage(usage: str) -> None:
tunacode/ui/panels.py CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  from typing import Any, Optional, Union
4
4
 
5
+ from rich.box import ROUNDED
5
6
  from rich.markdown import Markdown
6
7
  from rich.padding import Padding
7
8
  from rich.panel import Panel
@@ -38,11 +39,12 @@ async def panel(
38
39
  """Display a rich panel with modern styling."""
39
40
  border_style = border_style or kwargs.get("style") or colors.border
40
41
  panel_obj = Panel(
41
- Padding(text, (0, 1, 0, 1)),
42
- title=f"[bold]{title}[/bold]",
43
- title_align="left",
42
+ Padding(text, (0, 1, 0, 1)),
43
+ title=f"[bold]{title}[/bold]",
44
+ title_align="left",
44
45
  border_style=border_style,
45
46
  padding=(0, 1),
47
+ box=ROUNDED # Use ROUNDED box style
46
48
  )
47
49
  await print(Padding(panel_obj, (top, right, bottom, left)), **kwargs)
48
50
 
@@ -83,8 +85,8 @@ async def models(state_manager: StateManager = None) -> None:
83
85
 
84
86
  async def help(command_registry=None) -> None:
85
87
  """Display the available commands organized by category."""
86
- table = Table(show_header=False, box=None, padding=(0, 3, 0, 0))
87
- table.add_column("Command", style=f"bold {colors.primary}", justify="right", min_width=16)
88
+ table = Table(show_header=False, box=None, padding=(0, 2, 0, 0))
89
+ table.add_column("Command", style=f"bold {colors.primary}", justify="right", min_width=18)
88
90
  table.add_column("Description", style=colors.muted)
89
91
 
90
92
  if command_registry:
@@ -28,7 +28,6 @@ class PromptConfig:
28
28
  completer: Optional[Completer] = None
29
29
  lexer: Optional[Lexer] = None
30
30
  timeoutlen: float = 0.05
31
- default: str = ""
32
31
 
33
32
 
34
33
  class PromptManager:
@@ -43,14 +42,12 @@ class PromptManager:
43
42
  self.state_manager = state_manager
44
43
  self._temp_sessions = {} # For when no state manager is available
45
44
  self._style = self._create_style()
46
-
45
+
47
46
  def _create_style(self) -> Style:
48
47
  """Create the style for the prompt with file reference highlighting."""
49
- return Style.from_dict(
50
- {
51
- "file-reference": UI_COLORS.get("file_ref", "light_blue"),
52
- }
53
- )
48
+ return Style.from_dict({
49
+ 'file-reference': UI_COLORS.get('file_ref', 'light_blue'),
50
+ })
54
51
 
55
52
  def get_session(self, session_key: str, config: PromptConfig) -> PromptSession:
56
53
  """Get or create a prompt session.
@@ -108,7 +105,6 @@ class PromptManager:
108
105
  is_password=config.is_password,
109
106
  validator=config.validator,
110
107
  multiline=config.multiline,
111
- default=config.default,
112
108
  )
113
109
 
114
110
  # Clean up response
tunacode/ui/tool_ui.py CHANGED
@@ -154,9 +154,9 @@ class ToolUI:
154
154
  if request.filepath:
155
155
  ui.console.print(f"File: {request.filepath}", style=self.colors.muted)
156
156
 
157
- ui.console.print(" 1. Yes (default)")
158
- ui.console.print(" 2. Yes, and don't ask again for commands like this")
159
- ui.console.print(f" 3. No, and tell {APP_NAME} what to do differently")
157
+ ui.console.print(" [1] Yes (default)")
158
+ ui.console.print(" [2] Yes, and don't ask again for commands like this")
159
+ ui.console.print(f" [3] No, and tell {APP_NAME} what to do differently")
160
160
  resp = input(" Choose an option [1/2/3]: ").strip() or "1"
161
161
 
162
162
  # Add spacing after user choice for better readability
tunacode/utils/system.py CHANGED
@@ -277,46 +277,6 @@ def check_for_updates():
277
277
  return False, current_version
278
278
 
279
279
 
280
- async def update_tunacode():
281
- """
282
- Update TunaCode to the latest version using pip.
283
- """
284
- from ..ui import console as ui
285
-
286
- await ui.info("🔄 Checking for updates...")
287
-
288
- has_update, latest_version = check_for_updates()
289
-
290
- if not has_update:
291
- await ui.success("✅ TunaCode is already up to date!")
292
- return
293
-
294
- app_settings = ApplicationSettings()
295
- current_version = app_settings.version
296
-
297
- await ui.info(f"📦 Updating from v{current_version} to v{latest_version}...")
298
-
299
- try:
300
- # Run pip install --upgrade tunacode-cli
301
- result = subprocess.run(
302
- ["pip", "install", "--upgrade", "tunacode-cli"],
303
- capture_output=True,
304
- text=True,
305
- check=True
306
- )
307
-
308
- if result.returncode == 0:
309
- await ui.success(f"🎉 Successfully updated to v{latest_version}!")
310
- await ui.info("Please restart TunaCode to use the new version.")
311
- else:
312
- await ui.error(f"❌ Update failed: {result.stderr}")
313
-
314
- except subprocess.CalledProcessError as e:
315
- await ui.error(f"❌ Update failed: {e.stderr}")
316
- except Exception as e:
317
- await ui.error(f"❌ Update failed: {str(e)}")
318
-
319
-
320
280
  def list_cwd(max_depth=3):
321
281
  """
322
282
  Lists files in the current working directory up to a specified depth,