tunacode-cli 0.0.5__py3-none-any.whl → 0.0.7__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/main.py +6 -0
  3. tunacode/cli/model_selector.py +178 -0
  4. tunacode/cli/repl.py +11 -10
  5. tunacode/configuration/models.py +11 -1
  6. tunacode/constants.py +10 -10
  7. tunacode/context.py +1 -3
  8. tunacode/core/agents/main.py +52 -94
  9. tunacode/core/agents/tinyagent_main.py +173 -0
  10. tunacode/core/setup/git_safety_setup.py +39 -51
  11. tunacode/core/setup/optimized_coordinator.py +73 -0
  12. tunacode/exceptions.py +0 -2
  13. tunacode/services/enhanced_undo_service.py +322 -0
  14. tunacode/services/project_undo_service.py +311 -0
  15. tunacode/services/undo_service.py +13 -16
  16. tunacode/tools/base.py +11 -20
  17. tunacode/tools/tinyagent_tools.py +103 -0
  18. tunacode/tools/update_file.py +24 -14
  19. tunacode/tools/write_file.py +9 -7
  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 +40 -0
  30. tunacode_cli-0.0.7.dist-info/METADATA +262 -0
  31. {tunacode_cli-0.0.5.dist-info → tunacode_cli-0.0.7.dist-info}/RECORD +35 -27
  32. tunacode_cli-0.0.5.dist-info/METADATA +0 -247
  33. {tunacode_cli-0.0.5.dist-info → tunacode_cli-0.0.7.dist-info}/WHEEL +0 -0
  34. {tunacode_cli-0.0.5.dist-info → tunacode_cli-0.0.7.dist-info}/entry_points.txt +0 -0
  35. {tunacode_cli-0.0.5.dist-info → tunacode_cli-0.0.7.dist-info}/licenses/LICENSE +0 -0
  36. {tunacode_cli-0.0.5.dist-info → tunacode_cli-0.0.7.dist-info}/top_level.txt +0 -0
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 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/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."""
tunacode/ui/lexers.py CHANGED
@@ -2,45 +2,44 @@
2
2
 
3
3
  import re
4
4
 
5
- from prompt_toolkit.formatted_text import FormattedText
6
5
  from prompt_toolkit.lexers import Lexer
7
6
 
8
7
 
9
8
  class FileReferenceLexer(Lexer):
10
9
  """Lexer that highlights @file references in light blue."""
11
-
10
+
12
11
  # Pattern to match @file references
13
- FILE_REF_PATTERN = re.compile(r'@([\w./_-]+)')
14
-
12
+ FILE_REF_PATTERN = re.compile(r"@([\w./_-]+)")
13
+
15
14
  def lex_document(self, document):
16
15
  """Return a formatted text list for the given document."""
17
- lines = document.text.split('\n')
18
-
16
+ lines = document.text.split("\n")
17
+
19
18
  def get_line_tokens(line_number):
20
19
  """Get tokens for a specific line."""
21
20
  if line_number >= len(lines):
22
21
  return []
23
-
22
+
24
23
  line = lines[line_number]
25
24
  tokens = []
26
25
  last_end = 0
27
-
26
+
28
27
  # Find all @file references in the line
29
28
  for match in self.FILE_REF_PATTERN.finditer(line):
30
29
  start, end = match.span()
31
-
30
+
32
31
  # Add text before the match
33
32
  if start > last_end:
34
- tokens.append(('', line[last_end:start]))
35
-
33
+ tokens.append(("", line[last_end:start]))
34
+
36
35
  # Add the @file reference with styling
37
- tokens.append(('class:file-reference', match.group(0)))
36
+ tokens.append(("class:file-reference", match.group(0)))
38
37
  last_end = end
39
-
38
+
40
39
  # Add remaining text
41
40
  if last_end < len(line):
42
- tokens.append(('', line[last_end:]))
43
-
41
+ tokens.append(("", line[last_end:]))
42
+
44
43
  return tokens
45
-
46
- return get_line_tokens
44
+
45
+ return get_line_tokens
tunacode/ui/output.py CHANGED
@@ -16,9 +16,15 @@ from .decorators import create_sync_wrapper
16
16
  console = Console()
17
17
  colors = DotDict(UI_COLORS)
18
18
 
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]"""
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
+ )
22
28
 
23
29
 
24
30
  @create_sync_wrapper
tunacode/ui/panels.py CHANGED
@@ -38,11 +38,11 @@ async def panel(
38
38
  """Display a rich panel with modern styling."""
39
39
  border_style = border_style or kwargs.get("style") or colors.border
40
40
  panel_obj = Panel(
41
- Padding(text, (0, 1, 0, 1)),
42
- title=f"[bold]{title}[/bold]",
43
- title_align="left",
41
+ Padding(text, (0, 1, 0, 1)),
42
+ title=f"[bold]{title}[/bold]",
43
+ title_align="left",
44
44
  border_style=border_style,
45
- padding=(0, 1)
45
+ padding=(0, 1),
46
46
  )
47
47
  await print(Padding(panel_obj, (top, right, bottom, left)), **kwargs)
48
48