tunacode-cli 0.0.47__py3-none-any.whl → 0.0.49__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.

api/auth.py ADDED
@@ -0,0 +1,13 @@
1
+ import jwt
2
+
3
+
4
+ def authenticate(username, password):
5
+ # TODO: Add password hashing
6
+ if username == "admin" and password == "admin":
7
+ return generate_token(username)
8
+ return None
9
+
10
+
11
+ def generate_token(username):
12
+ # TODO: Add expiration
13
+ return jwt.encode({"user": username}, "secret")
api/users.py ADDED
@@ -0,0 +1,8 @@
1
+ from .auth import authenticate
2
+
3
+
4
+ class UserManager:
5
+ def login(self, username, password):
6
+ token = authenticate(username, password)
7
+ # TODO: Store session
8
+ return token
tunacode/__init__.py CHANGED
@@ -0,0 +1,4 @@
1
+ # Initialize unified logging system on package import
2
+ from tunacode.core.logging import setup_logging
3
+
4
+ setup_logging()
tunacode/cli/repl.py CHANGED
@@ -90,6 +90,12 @@ def _parse_args(args) -> ToolArgs:
90
90
 
91
91
  async def _tool_handler(part, state_manager: StateManager):
92
92
  """Handle tool execution with separated business logic and UI."""
93
+ # Check for cancellation before tool execution (only if explicitly set to True)
94
+ operation_cancelled = getattr(state_manager.session, "operation_cancelled", False)
95
+ if operation_cancelled is True:
96
+ logger.debug("Tool execution cancelled")
97
+ raise CancelledError("Operation was cancelled")
98
+
93
99
  tool_handler = ToolHandler(state_manager)
94
100
 
95
101
  if tool_handler.should_confirm(part.tool_name):
@@ -249,6 +255,12 @@ async def _display_agent_output(res, enable_streaming: bool) -> None:
249
255
  async def process_request(text: str, state_manager: StateManager, output: bool = True):
250
256
  """Process input using the agent, handling cancellation safely."""
251
257
 
258
+ # Check for cancellation before starting (only if explicitly set to True)
259
+ operation_cancelled = getattr(state_manager.session, "operation_cancelled", False)
260
+ if operation_cancelled is True:
261
+ logger.debug("Operation cancelled before processing started")
262
+ raise CancelledError("Operation was cancelled")
263
+
252
264
  state_manager.session.spinner = await ui.spinner(
253
265
  True, state_manager.session.spinner, state_manager
254
266
  )
@@ -275,6 +287,12 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
275
287
  await ui.error(str(e))
276
288
  return
277
289
 
290
+ # Check for cancellation before proceeding with agent call (only if explicitly set to True)
291
+ operation_cancelled = getattr(state_manager.session, "operation_cancelled", False)
292
+ if operation_cancelled is True:
293
+ logger.debug("Operation cancelled before agent processing")
294
+ raise CancelledError("Operation was cancelled")
295
+
278
296
  enable_streaming = state_manager.session.user_config.get("settings", {}).get(
279
297
  "enable_streaming", True
280
298
  )
@@ -338,13 +356,13 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
338
356
  # Always show files in context after agent response
339
357
  if state_manager.session.files_in_context:
340
358
  filenames = [Path(f).name for f in sorted(state_manager.session.files_in_context)]
341
- await ui.muted(f"\nFiles in context: {', '.join(filenames)}")
359
+ await ui.muted(f"Files in context: {', '.join(filenames)}")
342
360
 
343
361
  # --- ERROR HANDLING ---
344
362
  except CancelledError:
345
363
  await ui.muted(MSG_REQUEST_CANCELLED)
346
364
  except UserAbortError:
347
- await ui.muted(MSG_OPERATION_ABORTED_BY_USER)
365
+ await ui.muted(MSG_OPERATION_ABORTED)
348
366
  except UnexpectedModelBehavior as e:
349
367
  error_message = str(e)
350
368
  await ui.muted(error_message)
@@ -360,6 +378,9 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
360
378
  finally:
361
379
  await ui.spinner(False, state_manager.session.spinner, state_manager)
362
380
  state_manager.session.current_task = None
381
+ # Reset cancellation flag when task completes (if attribute exists)
382
+ if hasattr(state_manager.session, "operation_cancelled"):
383
+ state_manager.session.operation_cancelled = False
363
384
 
364
385
  if "multiline" in state_manager.session.input_sessions:
365
386
  await run_in_terminal(
@@ -460,9 +481,14 @@ async def repl(state_manager: StateManager):
460
481
  await ui.muted(MSG_AGENT_BUSY)
461
482
  continue
462
483
 
484
+ # Reset cancellation flag for new operations (if attribute exists)
485
+ if hasattr(state_manager.session, "operation_cancelled"):
486
+ state_manager.session.operation_cancelled = False
487
+
463
488
  state_manager.session.current_task = get_app().create_background_task(
464
489
  process_request(line, state_manager)
465
490
  )
491
+ await state_manager.session.current_task
466
492
 
467
493
  state_manager.session.update_token_count()
468
494
  context_display = get_context_window_display(
tunacode/constants.py CHANGED
@@ -7,7 +7,8 @@ Centralizes all magic strings, UI text, error messages, and application constant
7
7
 
8
8
  # Application info
9
9
  APP_NAME = "TunaCode"
10
- APP_VERSION = "0.0.47"
10
+ APP_VERSION = "0.0.49"
11
+
11
12
 
12
13
  # File patterns
13
14
  GUIDE_FILE_PATTERN = "{name}.md"
@@ -6,7 +6,6 @@ Handles agent creation, configuration, and request processing.
6
6
 
7
7
  import asyncio
8
8
  import json
9
- import logging
10
9
  import os
11
10
  import re
12
11
  from datetime import datetime, timezone
@@ -15,6 +14,8 @@ from typing import Any, Iterator, List, Optional, Tuple
15
14
 
16
15
  from pydantic_ai import Agent
17
16
 
17
+ from tunacode.core.logging.logger import get_logger
18
+
18
19
  # Import streaming types with fallback for older versions
19
20
  try:
20
21
  from pydantic_ai.messages import (
@@ -30,7 +31,6 @@ except ImportError:
30
31
  STREAMING_AVAILABLE = False
31
32
 
32
33
  from tunacode.constants import READ_ONLY_TOOLS
33
- from tunacode.core.recursive import RecursiveTaskExecutor
34
34
  from tunacode.core.state import StateManager
35
35
  from tunacode.core.token_usage.api_response_parser import ApiResponseParser
36
36
  from tunacode.core.token_usage.cost_calculator import CostCalculator
@@ -60,7 +60,7 @@ from tunacode.types import (
60
60
  )
61
61
 
62
62
  # Configure logging
63
- logger = logging.getLogger(__name__)
63
+ logger = get_logger(__name__)
64
64
 
65
65
 
66
66
  class ToolBuffer:
@@ -528,12 +528,12 @@ def get_or_create_agent(model: ModelName, state_manager: StateManager) -> Pydant
528
528
  tunacode_content = tunacode_path.read_text(encoding="utf-8")
529
529
  if tunacode_content.strip():
530
530
  # Log that we found TUNACODE.md
531
- print("📄 TUNACODE.md located: Loading context...")
531
+ logger.info("📄 TUNACODE.md located: Loading context...")
532
532
 
533
533
  system_prompt += "\n\n# Project Context from TUNACODE.md\n" + tunacode_content
534
534
  else:
535
535
  # Log that TUNACODE.md was not found
536
- print("📄 TUNACODE.md not found: Using default context")
536
+ logger.info("📄 TUNACODE.md not found: Using default context")
537
537
  except Exception as e:
538
538
  # Log errors loading TUNACODE.md at debug level
539
539
  logger.debug(f"Error loading TUNACODE.md: {e}")
@@ -547,9 +547,8 @@ def get_or_create_agent(model: ModelName, state_manager: StateManager) -> Pydant
547
547
  system_prompt += f'\n\n# Current Todo List\n\nYou have existing todos that need attention:\n\n{current_todos}\n\nRemember to check progress on these todos and update them as you work. Use todo("list") to see current status anytime.'
548
548
  except Exception as e:
549
549
  # Log error but don't fail agent creation
550
- import sys
551
550
 
552
- print(f"Warning: Failed to load todos: {e}", file=sys.stderr)
551
+ logger.warning(f"Warning: Failed to load todos: {e}")
553
552
 
554
553
  state_manager.session.agents[model] = Agent(
555
554
  model=model,
@@ -763,55 +762,6 @@ async def process_request(
763
762
  "fallback_response", True
764
763
  )
765
764
 
766
- # Check if recursive execution is enabled
767
- use_recursive = state_manager.session.user_config.get("settings", {}).get(
768
- "use_recursive_execution", True
769
- )
770
- recursive_threshold = state_manager.session.user_config.get("settings", {}).get(
771
- "recursive_complexity_threshold", 0.7
772
- )
773
-
774
- # Check if recursive execution should be used
775
- if use_recursive and state_manager.session.current_recursion_depth == 0:
776
- try:
777
- # Initialize recursive executor
778
- recursive_executor = RecursiveTaskExecutor(
779
- state_manager=state_manager,
780
- max_depth=state_manager.session.max_recursion_depth,
781
- min_complexity_threshold=recursive_threshold,
782
- default_iteration_budget=max_iterations,
783
- )
784
-
785
- # Analyze task complexity
786
- complexity_result = await recursive_executor.decomposer.analyze_and_decompose(
787
- message
788
- )
789
-
790
- if (
791
- complexity_result.should_decompose
792
- and complexity_result.total_complexity >= recursive_threshold
793
- ):
794
- if state_manager.session.show_thoughts:
795
- from tunacode.ui import console as ui
796
-
797
- await ui.muted(
798
- f"\n🔄 RECURSIVE EXECUTION: Task complexity {complexity_result.total_complexity:.2f} >= {recursive_threshold}"
799
- )
800
- await ui.muted(f"Reasoning: {complexity_result.reasoning}")
801
- await ui.muted(f"Subtasks: {len(complexity_result.subtasks)}")
802
-
803
- # Execute recursively
804
- success, result, error = await recursive_executor.execute_task(
805
- request=message, parent_task_id=None, depth=0
806
- )
807
-
808
- # For now, fall back to normal execution
809
- # TODO: Properly integrate recursive execution results
810
- pass
811
- except Exception as e:
812
- logger.warning(f"Recursive execution failed, falling back to normal: {e}")
813
- # Continue with normal execution
814
-
815
765
  from tunacode.configuration.models import ModelRegistry
816
766
  from tunacode.core.token_usage.usage_tracker import UsageTracker
817
767
 
@@ -910,28 +860,27 @@ async def process_request(
910
860
  buffered_tasks = tool_buffer.flush()
911
861
  start_time = time.time()
912
862
 
913
- if state_manager.session.show_thoughts:
914
- await ui.muted("\n" + "=" * 60)
915
- await ui.muted(
916
- f"🚀 FINAL BATCH: Executing {len(buffered_tasks)} buffered read-only tools"
917
- )
918
- await ui.muted("=" * 60)
863
+ await ui.muted("\n" + "=" * 60)
864
+ await ui.muted(
865
+ f"🚀 FINAL BATCH: Executing {len(buffered_tasks)} buffered read-only tools"
866
+ )
867
+ await ui.muted("=" * 60)
919
868
 
920
- for idx, (part, node) in enumerate(buffered_tasks, 1):
921
- tool_desc = f" [{idx}] {part.tool_name}"
922
- if hasattr(part, "args") and isinstance(part.args, dict):
923
- if part.tool_name == "read_file" and "file_path" in part.args:
924
- tool_desc += f" → {part.args['file_path']}"
925
- elif part.tool_name == "grep" and "pattern" in part.args:
926
- tool_desc += f" → pattern: '{part.args['pattern']}'"
927
- if "include_files" in part.args:
928
- tool_desc += f", files: '{part.args['include_files']}'"
929
- elif part.tool_name == "list_dir" and "directory" in part.args:
930
- tool_desc += f" → {part.args['directory']}"
931
- elif part.tool_name == "glob" and "pattern" in part.args:
932
- tool_desc += f" → pattern: '{part.args['pattern']}'"
933
- await ui.muted(tool_desc)
934
- await ui.muted("=" * 60)
869
+ for idx, (part, node) in enumerate(buffered_tasks, 1):
870
+ tool_desc = f" [{idx}] {part.tool_name}"
871
+ if hasattr(part, "args") and isinstance(part.args, dict):
872
+ if part.tool_name == "read_file" and "file_path" in part.args:
873
+ tool_desc += f" → {part.args['file_path']}"
874
+ elif part.tool_name == "grep" and "pattern" in part.args:
875
+ tool_desc += f" → pattern: '{part.args['pattern']}'"
876
+ if "include_files" in part.args:
877
+ tool_desc += f", files: '{part.args['include_files']}'"
878
+ elif part.tool_name == "list_dir" and "directory" in part.args:
879
+ tool_desc += f" → {part.args['directory']}"
880
+ elif part.tool_name == "glob" and "pattern" in part.args:
881
+ tool_desc += f" → pattern: '{part.args['pattern']}'"
882
+ await ui.muted(tool_desc)
883
+ await ui.muted("=" * 60)
935
884
 
936
885
  await execute_tools_parallel(buffered_tasks, tool_callback)
937
886
 
@@ -939,11 +888,10 @@ async def process_request(
939
888
  sequential_estimate = len(buffered_tasks) * 100
940
889
  speedup = sequential_estimate / elapsed_time if elapsed_time > 0 else 1.0
941
890
 
942
- if state_manager.session.show_thoughts:
943
- await ui.muted(
944
- f"✅ Final batch completed in {elapsed_time:.0f}ms "
945
- f"(~{speedup:.1f}x faster than sequential)\n"
946
- )
891
+ await ui.muted(
892
+ f"✅ Final batch completed in {elapsed_time:.0f}ms "
893
+ f"(~{speedup:.1f}x faster than sequential)\n"
894
+ )
947
895
 
948
896
  # If we need to add a fallback response, create a wrapper
949
897
  if not response_state.has_user_response and i >= max_iterations and fallback_enabled:
@@ -1089,6 +1037,7 @@ async def process_request(
1089
1037
  f"'{type(self).__name__}' object has no attribute '{name}'"
1090
1038
  )
1091
1039
 
1092
- return AgentRunWithState(agent_run)
1040
+ return AgentRunWithState(agent_run)
1093
1041
  except asyncio.CancelledError:
1094
- raise UserAbortError("User aborted the request.")
1042
+ # When task is cancelled, raise UserAbortError instead
1043
+ raise UserAbortError("Operation was cancelled by user")
@@ -0,0 +1,29 @@
1
+ import logging
2
+
3
+ # Custom log level: THOUGHT
4
+ THOUGHT = 25
5
+ logging.addLevelName(THOUGHT, "THOUGHT")
6
+
7
+
8
+ def thought(self, message, *args, **kwargs):
9
+ if self.isEnabledFor(THOUGHT):
10
+ self._log(THOUGHT, message, args, **kwargs)
11
+
12
+
13
+ logging.Logger.thought = thought
14
+
15
+
16
+ # RichHandler for UI output (stub, real implementation in handlers.py)
17
+ class RichHandler(logging.Handler):
18
+ def emit(self, record):
19
+ # Actual implementation in handlers.py
20
+ pass
21
+
22
+
23
+ def setup_logging(config_path=None):
24
+ """
25
+ Set up logging configuration from YAML file.
26
+ """
27
+ from .config import LogConfig
28
+
29
+ LogConfig.load(config_path)
@@ -0,0 +1,28 @@
1
+ import logging
2
+ import logging.config
3
+ import os
4
+
5
+ import yaml
6
+
7
+ DEFAULT_CONFIG_PATH = os.path.join(
8
+ os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "config", "logging.yaml"
9
+ )
10
+
11
+
12
+ class LogConfig:
13
+ @staticmethod
14
+ def load(config_path=None):
15
+ """
16
+ Load logging configuration from YAML file and apply it.
17
+ """
18
+ path = config_path or DEFAULT_CONFIG_PATH
19
+ if not os.path.exists(path):
20
+ raise FileNotFoundError(f"Logging config file not found: {path}")
21
+ with open(path, "r") as f:
22
+ config = yaml.safe_load(f)
23
+ logging_config = config.get("logging", config)
24
+ try:
25
+ logging.config.dictConfig(logging_config)
26
+ except Exception as e:
27
+ print(f"Failed to configure logging: {e}")
28
+ logging.basicConfig(level=logging.INFO)
@@ -0,0 +1,48 @@
1
+ import logging
2
+
3
+
4
+ class SimpleFormatter(logging.Formatter):
5
+ """
6
+ Simple formatter for UI output.
7
+ """
8
+
9
+ def __init__(self):
10
+ super().__init__("[%(levelname)s] %(message)s")
11
+
12
+
13
+ class DetailedFormatter(logging.Formatter):
14
+ """
15
+ Detailed formatter for backend text logs.
16
+ """
17
+
18
+ def __init__(self):
19
+ super().__init__("[%(asctime)s] [%(levelname)s] [%(name)s:%(lineno)d] - %(message)s")
20
+
21
+
22
+ try:
23
+ from pythonjsonlogger import jsonlogger
24
+
25
+ class JSONFormatter(jsonlogger.JsonFormatter):
26
+ """
27
+ JSON formatter for structured logs.
28
+ """
29
+
30
+ def __init__(self):
31
+ super().__init__("%(asctime)s %(name)s %(levelname)s %(message)s")
32
+ except ImportError:
33
+ import json
34
+
35
+ class JSONFormatter(logging.Formatter):
36
+ """
37
+ Fallback JSON formatter if pythonjsonlogger is not installed.
38
+ """
39
+
40
+ def format(self, record):
41
+ log_entry = {
42
+ "timestamp": self.formatTime(record),
43
+ "level": record.levelname,
44
+ "name": record.name,
45
+ "line": record.lineno,
46
+ "message": record.getMessage(),
47
+ }
48
+ return json.dumps(log_entry)
@@ -0,0 +1,83 @@
1
+ import json
2
+ import logging
3
+
4
+ from rich.console import Console
5
+ from rich.text import Text
6
+
7
+ # Global context for streaming state
8
+ _streaming_context = {"just_finished": False}
9
+
10
+
11
+ class RichHandler(logging.Handler):
12
+ """
13
+ Handler that outputs logs to the console using rich formatting.
14
+ """
15
+
16
+ level_icons = {
17
+ "INFO": "",
18
+ "WARNING": "⚠️",
19
+ "ERROR": "❌",
20
+ "CRITICAL": "🚨",
21
+ "THOUGHT": "🤔",
22
+ "DEBUG": "",
23
+ }
24
+
25
+ def __init__(self, level=logging.NOTSET):
26
+ super().__init__(level)
27
+ self.console = Console()
28
+
29
+ def emit(self, record):
30
+ try:
31
+ icon = self.level_icons.get(record.levelname, "")
32
+ timestamp = self.formatTime(record)
33
+ msg = self.format(record)
34
+ if icon:
35
+ output = f"[{timestamp}] {icon} {msg}"
36
+ else:
37
+ output = f"[{timestamp}] {msg}"
38
+
39
+ # Check if we just finished streaming to avoid extra newlines
40
+ just_finished_streaming = _streaming_context.get("just_finished", False)
41
+ if just_finished_streaming:
42
+ _streaming_context["just_finished"] = False # Reset after use
43
+ # Don't add extra newline when transitioning from streaming
44
+ self.console.print(Text(output), end="\n")
45
+ else:
46
+ self.console.print(Text(output))
47
+ except Exception:
48
+ self.handleError(record)
49
+
50
+ def formatTime(self, record, datefmt=None):
51
+ from datetime import datetime
52
+
53
+ ct = datetime.fromtimestamp(record.created)
54
+ if datefmt:
55
+ return ct.strftime(datefmt)
56
+ return ct.strftime("%Y-%m-%d %H:%M:%S")
57
+
58
+
59
+ class StructuredFileHandler(logging.FileHandler):
60
+ """
61
+ Handler that outputs logs as structured JSON lines.
62
+ """
63
+
64
+ def emit(self, record):
65
+ try:
66
+ log_entry = {
67
+ "timestamp": self.formatTime(record),
68
+ "level": record.levelname,
69
+ "name": record.name,
70
+ "line": record.lineno,
71
+ "message": record.getMessage(),
72
+ "extra_data": getattr(record, "extra", {}),
73
+ }
74
+ self.stream.write(json.dumps(log_entry) + "\n")
75
+ self.flush()
76
+ except Exception:
77
+ self.handleError(record)
78
+
79
+ def formatTime(self, record, datefmt=None):
80
+ from datetime import datetime, timezone
81
+
82
+ ct = datetime.fromtimestamp(record.created, tz=timezone.utc)
83
+ return ct.isoformat()
@@ -0,0 +1,8 @@
1
+ import logging
2
+
3
+
4
+ def get_logger(name=None):
5
+ """
6
+ Get a logger instance with the given name.
7
+ """
8
+ return logging.getLogger(name)
@@ -332,10 +332,11 @@ class TaskHierarchy:
332
332
  """Propagate context from one task to another.
333
333
 
334
334
  Args:
335
- from_task: Source task ID
335
+ from_task: Source task ID (unused, kept for API consistency)
336
336
  to_task: Target task ID
337
337
  context_update: Context to propagate
338
338
  """
339
+ _ = from_task # Unused but kept for API consistency
339
340
  if to_task in self._execution_contexts:
340
341
  self._execution_contexts[to_task].inherited_context.update(context_update)
341
342
 
tunacode/core/state.py CHANGED
@@ -41,6 +41,10 @@ class SessionState:
41
41
  input_sessions: InputSessions = field(default_factory=dict)
42
42
  current_task: Optional[Any] = None
43
43
  todos: list[TodoItem] = field(default_factory=list)
44
+ # ESC key tracking for double-press functionality
45
+ esc_press_count: int = 0
46
+ last_esc_time: Optional[float] = None
47
+ operation_cancelled: bool = False
44
48
  # Enhanced tracking for thoughts display
45
49
  files_in_context: set[str] = field(default_factory=set)
46
50
  tool_calls: list[dict[str, Any]] = field(default_factory=list)
tunacode/tools/base.py CHANGED
@@ -8,6 +8,7 @@ from abc import ABC, abstractmethod
8
8
 
9
9
  from pydantic_ai.exceptions import ModelRetry
10
10
 
11
+ from tunacode.core.logging.logger import get_logger
11
12
  from tunacode.exceptions import FileOperationError, ToolExecutionError
12
13
  from tunacode.types import FilePath, ToolName, ToolResult, UILogger
13
14
 
@@ -22,6 +23,7 @@ class BaseTool(ABC):
22
23
  ui_logger: UI logger instance for displaying messages
23
24
  """
24
25
  self.ui = ui_logger
26
+ self.logger = get_logger(self.__class__.__name__)
25
27
 
26
28
  async def execute(self, *args, **kwargs) -> ToolResult:
27
29
  """Execute the tool with error handling and logging.
@@ -39,14 +41,17 @@ class BaseTool(ABC):
39
41
  ToolExecutionError: Raised for all other errors with structured information
40
42
  """
41
43
  try:
44
+ msg = f"{self.tool_name}({self._format_args(*args, **kwargs)})"
42
45
  if self.ui:
43
- await self.ui.info(f"{self.tool_name}({self._format_args(*args, **kwargs)})")
46
+ await self.ui.info(msg)
47
+ self.logger.info(msg)
44
48
  result = await self._execute(*args, **kwargs)
45
49
  return result
46
50
  except ModelRetry as e:
47
51
  # Log as warning and re-raise for pydantic-ai
48
52
  if self.ui:
49
53
  await self.ui.warning(str(e))
54
+ self.logger.warning(f"ModelRetry: {e}")
50
55
  raise
51
56
  except ToolExecutionError:
52
57
  # Already properly formatted, just re-raise
@@ -90,6 +95,7 @@ class BaseTool(ABC):
90
95
  err_msg = f"Error {self._get_error_context(*args, **kwargs)}: {error}"
91
96
  if self.ui:
92
97
  await self.ui.error(err_msg)
98
+ self.logger.error(err_msg)
93
99
 
94
100
  # Raise proper exception instead of returning string
95
101
  raise ToolExecutionError(tool_name=self.tool_name, message=str(error), original_error=error)
tunacode/types.py CHANGED
@@ -13,7 +13,7 @@ from typing import Any, Awaitable, Callable, Dict, List, Literal, Optional, Prot
13
13
  # Try to import pydantic-ai types if available
14
14
  try:
15
15
  from pydantic_ai import Agent
16
- from pydantic_ai.messages import ModelRequest, ModelResponse, ToolReturnPart
16
+ from pydantic_ai.messages import ModelRequest, ToolReturnPart
17
17
 
18
18
  PydanticAgent = Agent
19
19
  MessagePart = Union[ToolReturnPart, Any]
tunacode/ui/completers.py CHANGED
@@ -17,7 +17,7 @@ class CommandCompleter(Completer):
17
17
  self.command_registry = command_registry
18
18
 
19
19
  def get_completions(
20
- self, document: Document, complete_event: CompleteEvent
20
+ self, document: Document, _complete_event: CompleteEvent
21
21
  ) -> Iterable[Completion]:
22
22
  """Get completions for slash commands."""
23
23
  # Get the text before cursor
@@ -65,7 +65,7 @@ class FileReferenceCompleter(Completer):
65
65
  """Completer for @file references that provides file path suggestions."""
66
66
 
67
67
  def get_completions(
68
- self, document: Document, complete_event: CompleteEvent
68
+ self, document: Document, _complete_event: CompleteEvent
69
69
  ) -> Iterable[Completion]:
70
70
  """Get completions for @file references."""
71
71
  # Get the word before cursor
tunacode/ui/console.py CHANGED
@@ -9,29 +9,26 @@ from rich.markdown import Markdown
9
9
  # Import and re-export all functions from specialized modules
10
10
  from .input import formatted_text, input, multiline_input
11
11
  from .keybindings import create_key_bindings
12
+
13
+ # Unified UI logger compatibility layer
14
+ from .logging_compat import ui_logger
12
15
  from .output import (
13
16
  banner,
14
17
  clear,
15
- info,
16
18
  line,
17
19
  muted,
18
20
  print,
19
21
  spinner,
20
- success,
21
22
  sync_print,
22
23
  update_available,
23
24
  usage,
24
25
  version,
25
- warning,
26
26
  )
27
-
28
- # Patch banner to use sync fast version
29
27
  from .panels import (
30
28
  StreamingAgentPanel,
31
29
  agent,
32
30
  agent_streaming,
33
31
  dump_messages,
34
- error,
35
32
  help,
36
33
  models,
37
34
  panel,
@@ -42,6 +39,28 @@ from .panels import (
42
39
  from .prompt_manager import PromptConfig, PromptManager
43
40
  from .validators import ModelValidator
44
41
 
42
+
43
+ # Async wrappers for UI logging
44
+ async def info(message: str) -> None:
45
+ await ui_logger.info(message)
46
+
47
+
48
+ async def warning(message: str) -> None:
49
+ await ui_logger.warning(message)
50
+
51
+
52
+ async def error(message: str) -> None:
53
+ await ui_logger.error(message)
54
+
55
+
56
+ async def debug(message: str) -> None:
57
+ await ui_logger.debug(message)
58
+
59
+
60
+ async def success(message: str) -> None:
61
+ await ui_logger.success(message)
62
+
63
+
45
64
  # Create console object for backward compatibility
46
65
  console = RichConsole(force_terminal=True, legacy_windows=False)
47
66
 
@@ -68,22 +87,24 @@ __all__ = [
68
87
  "banner",
69
88
  "clear",
70
89
  "console",
71
- "info",
72
90
  "line",
73
91
  "muted",
74
92
  "print",
75
93
  "spinner",
76
- "success",
77
94
  "sync_print",
78
95
  "update_available",
79
96
  "usage",
80
97
  "version",
98
+ # Unified logging wrappers
99
+ "info",
81
100
  "warning",
101
+ "error",
102
+ "debug",
103
+ "success",
82
104
  # From panels module
83
105
  "agent",
84
106
  "agent_streaming",
85
107
  "dump_messages",
86
- "error",
87
108
  "help",
88
109
  "models",
89
110
  "panel",
tunacode/ui/input.py CHANGED
@@ -81,7 +81,7 @@ async def multiline_input(
81
81
  "<darkgrey>"
82
82
  "<bold>Enter</bold> to submit • "
83
83
  "<bold>Esc + Enter</bold> for new line • "
84
- "<bold>Esc</bold> to cancel • "
84
+ "<bold>Esc twice</bold> to cancel • "
85
85
  "<bold>/help</bold> for commands"
86
86
  "</darkgrey>"
87
87
  )
@@ -1,7 +1,9 @@
1
1
  """Key binding handlers for TunaCode UI."""
2
2
 
3
3
  import logging
4
+ import time
4
5
 
6
+ from prompt_toolkit.application import run_in_terminal
5
7
  from prompt_toolkit.key_binding import KeyBindings
6
8
 
7
9
  from ..core.state import StateManager
@@ -30,12 +32,51 @@ def create_key_bindings(state_manager: StateManager = None) -> KeyBindings:
30
32
 
31
33
  @kb.add("escape")
32
34
  def _escape(event):
33
- """Immediately interrupts the current operation."""
34
- current_task = state_manager.session.current_task if state_manager else None
35
- if current_task and not current_task.done():
36
- logger.debug("Interrupting current task")
37
- current_task.cancel()
35
+ """Handle ESC key with double-press logic: first press warns, second cancels."""
36
+ if not state_manager:
37
+ logger.debug("Escape key pressed without state manager")
38
+ return
39
+
40
+ current_time = time.time()
41
+ session = state_manager.session
42
+
43
+ # Reset counter if too much time has passed (3 seconds timeout)
44
+ if session.last_esc_time and (current_time - session.last_esc_time) > 3.0:
45
+ session.esc_press_count = 0
46
+
47
+ session.esc_press_count += 1
48
+ session.last_esc_time = current_time
49
+
50
+ logger.debug(f"ESC key pressed: count={session.esc_press_count}, time={current_time}")
51
+
52
+ if session.esc_press_count == 1:
53
+ # First ESC press - show warning message
54
+ from ..ui.output import warning
55
+
56
+ run_in_terminal(lambda: warning("Hit ESC again within 3 seconds to cancel operation"))
57
+ logger.debug("First ESC press - showing warning")
38
58
  else:
39
- logger.debug("Escape key pressed outside task context")
59
+ # Second ESC press - cancel operation
60
+ session.esc_press_count = 0 # Reset counter
61
+ logger.debug("Second ESC press - initiating cancellation")
62
+
63
+ # Mark the session as being cancelled to prevent new operations
64
+ session.operation_cancelled = True
65
+
66
+ current_task = session.current_task
67
+ if current_task and not current_task.done():
68
+ logger.debug(f"Cancelling current task: {current_task}")
69
+ try:
70
+ current_task.cancel()
71
+ logger.debug("Task cancellation initiated successfully")
72
+ except Exception as e:
73
+ logger.debug(f"Failed to cancel task: {e}")
74
+ else:
75
+ logger.debug(f"No active task to cancel: current_task={current_task}")
76
+
77
+ # Force exit the current input by raising KeyboardInterrupt
78
+ # This will be caught by the prompt manager and converted to UserAbortError
79
+ logger.debug("Raising KeyboardInterrupt to abort current operation")
80
+ raise KeyboardInterrupt()
40
81
 
41
82
  return kb
@@ -0,0 +1,44 @@
1
+ """
2
+ UILogger compatibility layer for unified logging.
3
+
4
+ Implements the UILogger protocol using the unified logging system,
5
+ preserving UI formatting and behavior for all log levels.
6
+ """
7
+
8
+ from tunacode.core.logging.logger import get_logger
9
+ from tunacode.types import UILogger
10
+
11
+
12
+ class UnifiedUILogger(UILogger):
13
+ """
14
+ UILogger implementation that routes all UI log calls through the unified logging system.
15
+ Preserves UI conventions for info, error, warning, debug, and success.
16
+ """
17
+
18
+ def __init__(self, name: str = "ui"):
19
+ self.logger = get_logger(name)
20
+
21
+ async def info(self, message: str) -> None:
22
+ # Standard info log
23
+ self.logger.info(message)
24
+
25
+ async def error(self, message: str) -> None:
26
+ # Standard error log
27
+ self.logger.error(message)
28
+
29
+ async def warning(self, message: str) -> None:
30
+ # Standard warning log
31
+ self.logger.warning(message)
32
+
33
+ async def debug(self, message: str) -> None:
34
+ # Standard debug log
35
+ self.logger.debug(message)
36
+
37
+ async def success(self, message: str) -> None:
38
+ # "Success" is a UI convention; log as info with a marker for UI formatting
39
+ # Add a special prefix or extra field for downstream handlers/formatters
40
+ self.logger.info(f"[SUCCESS] {message}", extra={"ui_success": True})
41
+
42
+
43
+ # Singleton instance for convenience
44
+ ui_logger: UILogger = UnifiedUILogger()
tunacode/ui/output.py CHANGED
@@ -18,6 +18,7 @@ from tunacode.utils.token_counter import format_token_count
18
18
 
19
19
  from .constants import SPINNER_TYPE
20
20
  from .decorators import create_sync_wrapper
21
+ from .logging_compat import ui_logger
21
22
 
22
23
  # Create console with explicit settings to ensure ANSI codes work properly
23
24
  console = Console(force_terminal=True, legacy_windows=False)
@@ -54,18 +55,18 @@ async def line() -> None:
54
55
 
55
56
 
56
57
  async def info(text: str) -> None:
57
- """Print an informational message."""
58
- await print(f"[{colors.primary}]●[/{colors.primary}] {text}", style=colors.muted)
58
+ """Unified logging: informational message."""
59
+ await ui_logger.info(text)
59
60
 
60
61
 
61
62
  async def success(message: str) -> None:
62
- """Print a success message."""
63
- await print(f"[{colors.success}]✓[/{colors.success}] {message}")
63
+ """Unified logging: success message."""
64
+ await ui_logger.success(message)
64
65
 
65
66
 
66
67
  async def warning(text: str) -> None:
67
- """Print a warning message."""
68
- await print(f"[{colors.warning}]⚠[/{colors.warning}] {text}")
68
+ """Unified logging: warning message."""
69
+ await ui_logger.warning(text)
69
70
 
70
71
 
71
72
  async def muted(text: str) -> None:
tunacode/ui/panels.py CHANGED
@@ -9,7 +9,6 @@ from rich.padding import Padding
9
9
  from rich.panel import Panel
10
10
  from rich.pretty import Pretty
11
11
  from rich.table import Table
12
- from rich.text import Text
13
12
 
14
13
  from tunacode.configuration.models import ModelRegistry
15
14
  from tunacode.constants import (
@@ -31,7 +30,6 @@ from tunacode.constants import (
31
30
  DESC_MODEL_SWITCH,
32
31
  DESC_YOLO,
33
32
  PANEL_AVAILABLE_COMMANDS,
34
- PANEL_ERROR,
35
33
  PANEL_MESSAGE_HISTORY,
36
34
  PANEL_MODELS,
37
35
  UI_COLORS,
@@ -88,17 +86,17 @@ class StreamingAgentPanel:
88
86
  def _create_panel(self) -> Panel:
89
87
  """Create a Rich panel with current content."""
90
88
  # Use the UI_THINKING_MESSAGE constant instead of hardcoded text
89
+ from rich.text import Text
90
+
91
91
  from tunacode.constants import UI_THINKING_MESSAGE
92
92
 
93
- # If no content, show thinking message with Rich markup
93
+ # Handle the default thinking message with Rich markup
94
94
  if not self.content:
95
- content_to_display = Text.from_markup(UI_THINKING_MESSAGE)
95
+ content_renderable = Text.from_markup(UI_THINKING_MESSAGE)
96
96
  else:
97
- # Normal content is markdown
98
- content_to_display = Markdown(self.content)
99
-
97
+ content_renderable = Markdown(self.content)
100
98
  panel_obj = Panel(
101
- Padding(content_to_display, (0, 1, 0, 1)),
99
+ Padding(content_renderable, (0, 1, 0, 1)),
102
100
  title=f"[bold]{self.title}[/bold]",
103
101
  title_align="left",
104
102
  border_style=colors.primary,
@@ -137,7 +135,25 @@ class StreamingAgentPanel:
137
135
  async def stop(self):
138
136
  """Stop the live streaming display."""
139
137
  if self.live:
138
+ # Get the console before stopping the live display
139
+ from .output import console
140
+
141
+ # Stop the live display
140
142
  self.live.stop()
143
+
144
+ # Comprehensive cleanup to prevent extra lines
145
+ console.print("", end="") # Reset the current line without newline
146
+ if hasattr(console, "file") and hasattr(console.file, "flush"):
147
+ console.file.flush() # Ensure output is flushed
148
+
149
+ # Mark that we just finished streaming (for output control)
150
+ try:
151
+ from tunacode.core.logging.handlers import _streaming_context
152
+
153
+ _streaming_context["just_finished"] = True
154
+ except ImportError:
155
+ pass # If we can't import, continue without the optimization
156
+
141
157
  self.live = None
142
158
 
143
159
 
@@ -159,8 +175,10 @@ async def agent_streaming(content_stream, bottom: int = 1):
159
175
 
160
176
 
161
177
  async def error(text: str) -> None:
162
- """Display an error panel."""
163
- await panel(PANEL_ERROR, text, style=colors.error)
178
+ """Unified logging: error message."""
179
+ from .logging_compat import ui_logger
180
+
181
+ await ui_logger.error(text)
164
182
 
165
183
 
166
184
  async def dump_messages(messages_list=None, state_manager: StateManager = None) -> None:
@@ -3,13 +3,14 @@ Security utilities for safe command execution and input validation.
3
3
  Provides defensive measures against command injection attacks.
4
4
  """
5
5
 
6
- import logging
7
6
  import re
8
7
  import shlex
9
8
  import subprocess
10
9
  from typing import List, Optional
11
10
 
12
- logger = logging.getLogger(__name__)
11
+ from tunacode.core.logging.logger import get_logger
12
+
13
+ logger = get_logger(__name__)
13
14
 
14
15
  # Dangerous shell metacharacters that indicate potential injection
15
16
  DANGEROUS_CHARS = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tunacode-cli
3
- Version: 0.0.47
3
+ Version: 0.0.49
4
4
  Summary: Your agentic CLI developer.
5
5
  Author-email: larock22 <noreply@github.com>
6
6
  License: MIT
@@ -37,7 +37,7 @@ Requires-Dist: textual-dev; extra == "dev"
37
37
  Requires-Dist: pre-commit; extra == "dev"
38
38
  Dynamic: license-file
39
39
 
40
- # TunaCode
40
+ # TunaCode CLI
41
41
 
42
42
  <div align="center">
43
43
 
@@ -1,13 +1,15 @@
1
- tunacode/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- tunacode/constants.py,sha256=833fgfAmkcJmU1RmSYDhGCOzoU-eUMdgbcfBBwUQOZ4,5168
1
+ api/auth.py,sha256=_ysF1RCXvtJR1S35lbYQZexES1lif4J6VVzEyqNK74Q,303
2
+ api/users.py,sha256=WRcy1Vsr4cEC8CW2qeN3XrA9EMyIk47ufpMyvQ4nLuw,193
3
+ tunacode/__init__.py,sha256=yUul8igNYMfUrHnYfioIGAqvrH8b5BKiO_pt1wVnmd0,119
4
+ tunacode/constants.py,sha256=oxUB7zL0wk998eBXu9xV85PKp18FtYSbHlqxRy8TtbI,5169
3
5
  tunacode/context.py,sha256=_gXVCyjU052jlyRAl9tklZSwl5U_zI_EIX8XN87VVWE,2786
4
6
  tunacode/exceptions.py,sha256=oDO1SVKOgjcKIylwqdbqh_g5my4roU5mB9Nv4n_Vb0s,3877
5
7
  tunacode/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
8
  tunacode/setup.py,sha256=XPt4eAK-qcIZQv64jGZ_ryxcImDwps9OmXjJfIS1xcs,1899
7
- tunacode/types.py,sha256=Czq7jYXHq7fZQtyqkCN5_7eEu1wyYUcB50C6v3sTWDw,8188
9
+ tunacode/types.py,sha256=y8jqLvdSDdi2Y4YECVG2uctpBjjBh9MJ4w1EDEZdYf0,8173
8
10
  tunacode/cli/__init__.py,sha256=zgs0UbAck8hfvhYsWhWOfBe5oK09ug2De1r4RuQZREA,55
9
11
  tunacode/cli/main.py,sha256=erP6jNXcxVQOVn8sm6uNaLEAYevniVXl6Sem872mW68,2755
10
- tunacode/cli/repl.py,sha256=rSsUievdOdTkxzHNIEgUHbuWbk0ki78743Wz5hVFe9s,18917
12
+ tunacode/cli/repl.py,sha256=jg2lnr-ZuYUKiSIJBDR9uFsIP7b_gBA6EoxMDgTSwCw,20385
11
13
  tunacode/cli/commands/__init__.py,sha256=zmE9JcJ9Qd2xJhgdS4jMDJOoZsrAZmL5MAFxbKkk7F8,1670
12
14
  tunacode/cli/commands/base.py,sha256=GxUuDsDSpz0iXryy8MrEw88UM3C3yxL__kDK1QhshoA,2517
13
15
  tunacode/cli/commands/registry.py,sha256=XVuLpp5S4Fw7GfIZfLrVZFo4jMLMNmYNpYN7xWgXyOk,8223
@@ -24,20 +26,25 @@ tunacode/configuration/models.py,sha256=buH8ZquvcYI3OQBDIZeJ08cu00rSCeNABtUwl3VQ
24
26
  tunacode/configuration/settings.py,sha256=KoN0u6GG3Hh_TWt02D_wpRfbACYri3gCDTXHtJfHl2w,994
25
27
  tunacode/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
28
  tunacode/core/code_index.py,sha256=jgAx3lSWP_DwnyiP5Jkm1YvX4JJyI4teMzlNrJSpEOA,15661
27
- tunacode/core/state.py,sha256=2lsDgq0uIIc_MnXPE9SG_1fYFBDWWlgqgqm2Ik1iFBs,5599
29
+ tunacode/core/state.py,sha256=gYFP6MIwlfYLxT6NFvqkJ3tEHkdxGdexunwXZwmRv0E,5762
28
30
  tunacode/core/tool_handler.py,sha256=BPjR013OOO0cLXPdLeL2FDK0ixUwOYu59FfHdcdFhp4,2277
29
31
  tunacode/core/agents/__init__.py,sha256=UUJiPYb91arwziSpjd7vIk7XNGA_4HQbsOIbskSqevA,149
30
- tunacode/core/agents/main.py,sha256=BRy3joJnn4GpUW47PyhiKK1JLMSamlK7wDMLwAGzSG0,47441
32
+ tunacode/core/agents/main.py,sha256=htxekWzljg0nrbY3xf6PApxsBHUMOSQxVaDGxP0GwUs,44968
31
33
  tunacode/core/agents/utils.py,sha256=7kJAiUlkyWO3-b4T07XsGgycVrcNhv3NEPLdaktBnP4,12847
32
34
  tunacode/core/background/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
35
  tunacode/core/background/manager.py,sha256=rJdl3eDLTQwjbT7VhxXcJbZopCNR3M8ZGMbmeVnwwMc,1126
34
36
  tunacode/core/llm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
+ tunacode/core/logging/__init__.py,sha256=pGr3EXxS4Yyz6Gasqg-m9fUENWnnbyaQqMaxR56WW4c,635
38
+ tunacode/core/logging/config.py,sha256=EC5JLPq6JcF-zYNxJ6ZTizrxttY3Jp56zb8TGTWaUjQ,845
39
+ tunacode/core/logging/formatters.py,sha256=lDex7P5eFzu-r9VEMshohj1zxWqANWSS581CJoMp-HA,1210
40
+ tunacode/core/logging/handlers.py,sha256=SxmgH7yWc8bbCKcDeBtkbkHJy5saRqbIoDoTQh4jiYQ,2538
41
+ tunacode/core/logging/logger.py,sha256=9RjRuX0GoUojRJ8WnJGQPFdXiluiJMCoFmvc8xEioB8,142
35
42
  tunacode/core/recursive/__init__.py,sha256=S9_dN0faJqam3Pnaum9PRC8Hd90bpES8syFgAD8-QbI,446
36
43
  tunacode/core/recursive/aggregator.py,sha256=KlWajEyT8OokWeY6ZwQ4EsVu05V4KeH3o2cYUz-Ce1Y,15725
37
44
  tunacode/core/recursive/budget.py,sha256=0wY6xSrZKmudUwthwX1mlcF5yat2_y3fNu5shX_IzvA,14131
38
45
  tunacode/core/recursive/decomposer.py,sha256=k8rgaPEMMb2mZ9W0m_Yg0oEBqbYpqDnK0zyTkx6eTPs,14550
39
46
  tunacode/core/recursive/executor.py,sha256=7taFsjYjSfL8OUQLM1ZVn9Xix7YoputFBAZSo9tDtDM,16646
40
- tunacode/core/recursive/hierarchy.py,sha256=67tdDCKB5mTSn6VIwm-knEHB_VKUkeljFdwH62U5chE,15648
47
+ tunacode/core/recursive/hierarchy.py,sha256=YKYIt1APsZviVmhjxfQ9p441rVkfEHRcmuh3cFV-vqU,15744
41
48
  tunacode/core/setup/__init__.py,sha256=lzdpY6rIGf9DDlDBDGFvQZaSOQeFsNglHbkpq1-GtU8,376
42
49
  tunacode/core/setup/agent_setup.py,sha256=trELO8cPnWo36BBnYmXDEnDPdhBg0p-VLnx9A8hSSSQ,1401
43
50
  tunacode/core/setup/base.py,sha256=cbyT2-xK2mWgH4EO17VfM_OM2bj0kT895NW2jSXbe3c,968
@@ -52,7 +59,7 @@ tunacode/prompts/system.md,sha256=hXpjZ8Yiv2Acr2_6EmC2uOklP8FbmvyYR9oais-1KLk,16
52
59
  tunacode/services/__init__.py,sha256=w_E8QK6RnvKSvU866eDe8BCRV26rAm4d3R-Yg06OWCU,19
53
60
  tunacode/services/mcp.py,sha256=R48X73KQjQ9vwhBrtbWHSBl-4K99QXmbIhh5J_1Gezo,3046
54
61
  tunacode/tools/__init__.py,sha256=ECBuUWWF1JjHW42CCceaPKgVTQyuljbz3RlhuA2fe2s,314
55
- tunacode/tools/base.py,sha256=TF71ZE66-W-7GLY8QcPpPJ5CVjod6FHL1caBOTCssvU,7044
62
+ tunacode/tools/base.py,sha256=DhlanZZZxU2JJaBOwwyGFKMUcoCWR_CzLuwVeSXC0Go,7297
56
63
  tunacode/tools/bash.py,sha256=mgZqugfDFevZ4BETuUv90pzXvtq7qKGUGFiuDxzmytk,8766
57
64
  tunacode/tools/glob.py,sha256=TSgVK79ewZgGw8ucYkkiHgVqRgkw-wZrhP8j52nm_gQ,10334
58
65
  tunacode/tools/grep.py,sha256=jboEVA2ATv0YI8zg9dF89emZ_HWy2vVtsQ_-hDhlr7g,26337
@@ -64,15 +71,16 @@ tunacode/tools/todo.py,sha256=bVbohgwKqvvTe8efxXrMZDQU8vdk4E3jF9Cj38dRq7k,12727
64
71
  tunacode/tools/update_file.py,sha256=bW1MhTzRjBDjJzqQ6A1yCVEbkr1oIqtEC8uqcg_rfY4,3957
65
72
  tunacode/tools/write_file.py,sha256=prL6u8XOi9ZyPU-YNlG9YMLbSLrDJXDRuDX73ncXh-k,2699
66
73
  tunacode/ui/__init__.py,sha256=aRNE2pS50nFAX6y--rSGMNYwhz905g14gRd6g4BolYU,13
67
- tunacode/ui/completers.py,sha256=Jx1zyCESwdm_4ZopvCBtb0bCJF-bRy8aBWG2yhPQtDc,4878
68
- tunacode/ui/console.py,sha256=icb7uYrV8XmZg9glreEy5MrvDkmrKxbf_ZkNqElN1uE,2120
74
+ tunacode/ui/completers.py,sha256=40wkF1nqG9HNVmP8MRWEPya0zV6GbyFXAuT-k7uXmxg,4880
75
+ tunacode/ui/console.py,sha256=wLiJ9cVrWHS0zNadqbcXnsg0T2wj4xLACg17YAYLmU4,2574
69
76
  tunacode/ui/constants.py,sha256=A76B_KpM8jCuBYRg4cPmhi8_j6LLyWttO7_jjv47r3w,421
70
77
  tunacode/ui/decorators.py,sha256=e2KM-_pI5EKHa2M045IjUe4rPkTboxaKHXJT0K3461g,1914
71
- tunacode/ui/input.py,sha256=x_7G9VVdvydpEk2kcyG-OBKND5lL5ADbiGcDXF1n5UA,3014
72
- tunacode/ui/keybindings.py,sha256=tn0q0eRw72j8xPWX0673Xc-vmwlvFEyuhy3C451ntfE,1233
78
+ tunacode/ui/input.py,sha256=NCZlj5qzNPy0gsSeGKeDNdAOMKZVGph8Z-UBXhX-Sbk,3020
79
+ tunacode/ui/keybindings.py,sha256=ACUofGGs3xFvrNWi1cQ_Tkyy0lhIrZAEIOPlK6vZFVY,3014
73
80
  tunacode/ui/lexers.py,sha256=tmg4ic1enyTRLzanN5QPP7D_0n12YjX_8ZhsffzhXA4,1340
74
- tunacode/ui/output.py,sha256=51O0VHajte4dXHK5Az5SSP4IOb2q5SbCwvqdAoxyg7c,5665
75
- tunacode/ui/panels.py,sha256=dBZEVIJqliWreY-hz6HpW5rdBmPOJZ6sPrv8KQ3eNhk,8570
81
+ tunacode/ui/logging_compat.py,sha256=5v6lcjVaG1CxdY1Zm9FAGr9H7Sy-tP6ihGfhP-5YvAY,1406
82
+ tunacode/ui/output.py,sha256=vXfkPfQWmQzkmfGkY6snAk7auLgN8E7XEGUc-YVjTlM,5604
83
+ tunacode/ui/panels.py,sha256=usGbzBDZdBY2pS2zHdlY0s5NKf9AUw7llOHKWoaNIlU,9307
76
84
  tunacode/ui/prompt_manager.py,sha256=U2cntB34vm-YwOj3gzFRUK362zccrz8pigQfpxr5sv8,4650
77
85
  tunacode/ui/recursive_progress.py,sha256=V0dGpJWt19TVArOYcQ3Lki8cR3ZepFT6iGwnChSFhFI,12906
78
86
  tunacode/ui/tool_ui.py,sha256=qp1aZUpLO5UOdJziY8tw0URC8gjoWoSKdGu5y2wuTUU,7013
@@ -86,14 +94,14 @@ tunacode/utils/import_cache.py,sha256=q_xjJbtju05YbFopLDSkIo1hOtCx3DOTl3GQE5FFDg
86
94
  tunacode/utils/message_utils.py,sha256=kM6VSS2Dudjplie009khHgmIRjDoBUzv6tvHcYNDAAE,586
87
95
  tunacode/utils/retry.py,sha256=AHdUzY6m-mwlT4OPXdtWWMAafL_NeS7JAMORGyM8c5k,4931
88
96
  tunacode/utils/ripgrep.py,sha256=AXUs2FFt0A7KBV996deS8wreIlUzKOlAHJmwrcAr4No,583
89
- tunacode/utils/security.py,sha256=e_zo9VmcOKFjgFMr9GOBIFhAmND4PBlJZgY7zqnsGjI,6548
97
+ tunacode/utils/security.py,sha256=i3eGKg4o-qY2S_ObTlEaHO93q14iBfiPXR5O7srHn58,6579
90
98
  tunacode/utils/system.py,sha256=FSoibTIH0eybs4oNzbYyufIiV6gb77QaeY2yGqW39AY,11381
91
99
  tunacode/utils/text_utils.py,sha256=6YBD9QfkDO44-6jxnwRWIpmfIifPG-NqMzy_O2NAouc,7277
92
100
  tunacode/utils/token_counter.py,sha256=lLbkrNUraRQn5RMhwnGurqq1RHFDyn4AaFhruONWIxo,2690
93
101
  tunacode/utils/user_configuration.py,sha256=Ilz8dpGVJDBE2iLWHAPT0xR8D51VRKV3kIbsAz8Bboc,3275
94
- tunacode_cli-0.0.47.dist-info/licenses/LICENSE,sha256=Btzdu2kIoMbdSp6OyCLupB1aRgpTCJ_szMimgEnpkkE,1056
95
- tunacode_cli-0.0.47.dist-info/METADATA,sha256=oHfTMPtEUgbgVd7Nm0bb39wAKAuPKRHskBpZmDnBxMM,5902
96
- tunacode_cli-0.0.47.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
97
- tunacode_cli-0.0.47.dist-info/entry_points.txt,sha256=hbkytikj4dGu6rizPuAd_DGUPBGF191RTnhr9wdhORY,51
98
- tunacode_cli-0.0.47.dist-info/top_level.txt,sha256=lKy2P6BWNi5XSA4DHFvyjQ14V26lDZctwdmhEJrxQbU,9
99
- tunacode_cli-0.0.47.dist-info/RECORD,,
102
+ tunacode_cli-0.0.49.dist-info/licenses/LICENSE,sha256=Btzdu2kIoMbdSp6OyCLupB1aRgpTCJ_szMimgEnpkkE,1056
103
+ tunacode_cli-0.0.49.dist-info/METADATA,sha256=YdmUijOV5ybOmyMWEYJHEtAWpVur1dhi8ezMKKS81BM,5906
104
+ tunacode_cli-0.0.49.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
105
+ tunacode_cli-0.0.49.dist-info/entry_points.txt,sha256=hbkytikj4dGu6rizPuAd_DGUPBGF191RTnhr9wdhORY,51
106
+ tunacode_cli-0.0.49.dist-info/top_level.txt,sha256=GuU751acRvOhM5yLKFW0-gBg62JGh5zycDSq4tRFOYE,13
107
+ tunacode_cli-0.0.49.dist-info/RECORD,,