tunacode-cli 0.0.50__py3-none-any.whl → 0.0.53__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 (87) hide show
  1. tunacode/cli/commands/base.py +2 -2
  2. tunacode/cli/commands/implementations/__init__.py +7 -1
  3. tunacode/cli/commands/implementations/conversation.py +1 -1
  4. tunacode/cli/commands/implementations/debug.py +1 -1
  5. tunacode/cli/commands/implementations/development.py +4 -1
  6. tunacode/cli/commands/implementations/template.py +132 -0
  7. tunacode/cli/commands/registry.py +28 -1
  8. tunacode/cli/commands/template_shortcut.py +93 -0
  9. tunacode/cli/main.py +6 -0
  10. tunacode/cli/repl.py +29 -174
  11. tunacode/cli/repl_components/__init__.py +10 -0
  12. tunacode/cli/repl_components/command_parser.py +34 -0
  13. tunacode/cli/repl_components/error_recovery.py +88 -0
  14. tunacode/cli/repl_components/output_display.py +33 -0
  15. tunacode/cli/repl_components/tool_executor.py +84 -0
  16. tunacode/configuration/defaults.py +2 -2
  17. tunacode/configuration/settings.py +11 -14
  18. tunacode/constants.py +57 -23
  19. tunacode/context.py +0 -14
  20. tunacode/core/agents/agent_components/__init__.py +27 -0
  21. tunacode/core/agents/agent_components/agent_config.py +109 -0
  22. tunacode/core/agents/agent_components/json_tool_parser.py +109 -0
  23. tunacode/core/agents/agent_components/message_handler.py +100 -0
  24. tunacode/core/agents/agent_components/node_processor.py +480 -0
  25. tunacode/core/agents/agent_components/response_state.py +13 -0
  26. tunacode/core/agents/agent_components/result_wrapper.py +50 -0
  27. tunacode/core/agents/agent_components/task_completion.py +28 -0
  28. tunacode/core/agents/agent_components/tool_buffer.py +24 -0
  29. tunacode/core/agents/agent_components/tool_executor.py +49 -0
  30. tunacode/core/agents/main.py +421 -778
  31. tunacode/core/agents/utils.py +42 -2
  32. tunacode/core/background/manager.py +3 -3
  33. tunacode/core/logging/__init__.py +4 -3
  34. tunacode/core/logging/config.py +29 -16
  35. tunacode/core/logging/formatters.py +1 -1
  36. tunacode/core/logging/handlers.py +41 -7
  37. tunacode/core/setup/__init__.py +2 -0
  38. tunacode/core/setup/agent_setup.py +2 -2
  39. tunacode/core/setup/base.py +2 -2
  40. tunacode/core/setup/config_setup.py +10 -6
  41. tunacode/core/setup/git_safety_setup.py +13 -2
  42. tunacode/core/setup/template_setup.py +75 -0
  43. tunacode/core/state.py +13 -2
  44. tunacode/core/token_usage/api_response_parser.py +6 -2
  45. tunacode/core/token_usage/usage_tracker.py +37 -7
  46. tunacode/core/tool_handler.py +24 -1
  47. tunacode/prompts/system.md +289 -4
  48. tunacode/setup.py +2 -0
  49. tunacode/templates/__init__.py +9 -0
  50. tunacode/templates/loader.py +210 -0
  51. tunacode/tools/glob.py +3 -3
  52. tunacode/tools/grep.py +26 -276
  53. tunacode/tools/grep_components/__init__.py +9 -0
  54. tunacode/tools/grep_components/file_filter.py +93 -0
  55. tunacode/tools/grep_components/pattern_matcher.py +152 -0
  56. tunacode/tools/grep_components/result_formatter.py +45 -0
  57. tunacode/tools/grep_components/search_result.py +35 -0
  58. tunacode/tools/todo.py +27 -21
  59. tunacode/types.py +19 -4
  60. tunacode/ui/completers.py +6 -1
  61. tunacode/ui/decorators.py +2 -2
  62. tunacode/ui/keybindings.py +1 -1
  63. tunacode/ui/panels.py +13 -5
  64. tunacode/ui/prompt_manager.py +1 -1
  65. tunacode/ui/tool_ui.py +8 -2
  66. tunacode/utils/bm25.py +4 -4
  67. tunacode/utils/file_utils.py +2 -2
  68. tunacode/utils/message_utils.py +3 -1
  69. tunacode/utils/system.py +0 -4
  70. tunacode/utils/text_utils.py +1 -1
  71. tunacode/utils/token_counter.py +2 -2
  72. {tunacode_cli-0.0.50.dist-info → tunacode_cli-0.0.53.dist-info}/METADATA +146 -1
  73. tunacode_cli-0.0.53.dist-info/RECORD +123 -0
  74. {tunacode_cli-0.0.50.dist-info → tunacode_cli-0.0.53.dist-info}/top_level.txt +0 -1
  75. api/auth.py +0 -13
  76. api/users.py +0 -8
  77. tunacode/core/recursive/__init__.py +0 -18
  78. tunacode/core/recursive/aggregator.py +0 -467
  79. tunacode/core/recursive/budget.py +0 -414
  80. tunacode/core/recursive/decomposer.py +0 -398
  81. tunacode/core/recursive/executor.py +0 -470
  82. tunacode/core/recursive/hierarchy.py +0 -488
  83. tunacode/ui/recursive_progress.py +0 -380
  84. tunacode_cli-0.0.50.dist-info/RECORD +0 -107
  85. {tunacode_cli-0.0.50.dist-info → tunacode_cli-0.0.53.dist-info}/WHEEL +0 -0
  86. {tunacode_cli-0.0.50.dist-info → tunacode_cli-0.0.53.dist-info}/entry_points.txt +0 -0
  87. {tunacode_cli-0.0.50.dist-info → tunacode_cli-0.0.53.dist-info}/licenses/LICENSE +0 -0
@@ -44,12 +44,12 @@ class Command(ABC):
44
44
  return CommandCategory.SYSTEM
45
45
 
46
46
  @abstractmethod
47
- async def execute(self, args: CommandArgs, context: CommandContext) -> CommandResult:
47
+ async def execute(self, _args: CommandArgs, context: CommandContext) -> CommandResult:
48
48
  """
49
49
  Execute the command.
50
50
 
51
51
  Args:
52
- args: Command arguments (excluding the command name)
52
+ _args: Command arguments (excluding the command name)
53
53
  context: Execution context with state and config
54
54
 
55
55
  Returns:
@@ -12,7 +12,13 @@ from .debug import (
12
12
  )
13
13
  from .development import BranchCommand, InitCommand
14
14
  from .model import ModelCommand
15
- from .system import ClearCommand, HelpCommand, RefreshConfigCommand, StreamingCommand, UpdateCommand
15
+ from .system import (
16
+ ClearCommand,
17
+ HelpCommand,
18
+ RefreshConfigCommand,
19
+ StreamingCommand,
20
+ UpdateCommand,
21
+ )
16
22
  from .todo import TodoCommand
17
23
 
18
24
  __all__ = [
@@ -40,7 +40,7 @@ class CompactCommand(SimpleCommand):
40
40
  result = await process_request(
41
41
  summary_prompt,
42
42
  context.state_manager,
43
- output=False, # We'll handle the output ourselves
43
+ False, # We'll handle the output ourselves
44
44
  )
45
45
 
46
46
  # Extract summary text from result
@@ -169,7 +169,7 @@ class ParseToolsCommand(SimpleCommand):
169
169
  # Create tool callback
170
170
  from tunacode.cli.repl import _tool_handler
171
171
 
172
- def tool_callback_with_state(part, node):
172
+ def tool_callback_with_state(part, _node):
173
173
  return _tool_handler(part, context.state_manager)
174
174
 
175
175
  try:
@@ -72,6 +72,9 @@ If there are Cursor rules (in .cursor/rules/ or .cursorrules) or Copilot rules (
72
72
  make sure to include them."""
73
73
 
74
74
  # Call the agent to analyze and create/update the file
75
- await context.process_request(prompt, context.state_manager)
75
+ if context.process_request:
76
+ await context.process_request(
77
+ prompt, context.state_manager.session.current_model, context.state_manager
78
+ )
76
79
 
77
80
  return None
@@ -0,0 +1,132 @@
1
+ """Template management commands for TunaCode CLI."""
2
+
3
+ from typing import Optional
4
+
5
+ from ....templates.loader import TemplateLoader
6
+ from ....types import CommandArgs, CommandContext
7
+ from ....ui import console as ui
8
+ from ..base import CommandCategory, CommandSpec, SimpleCommand
9
+
10
+
11
+ class TemplateCommand(SimpleCommand):
12
+ """Manage and use templates for pre-approved tools."""
13
+
14
+ spec = CommandSpec(
15
+ name="template",
16
+ aliases=["/template", "/tpl"],
17
+ description="Manage and use templates",
18
+ category=CommandCategory.SYSTEM,
19
+ )
20
+
21
+ async def execute(self, args: CommandArgs, context: CommandContext) -> Optional[str]:
22
+ """Execute template command with subcommands."""
23
+ if not args:
24
+ await self._show_help()
25
+ return None
26
+
27
+ subcommand = args[0]
28
+
29
+ if subcommand == "list":
30
+ await self._list_templates()
31
+ elif subcommand == "load":
32
+ if len(args) < 2:
33
+ await ui.error("Please specify a template name")
34
+ await ui.muted("Usage: /template load <template-name>")
35
+ return None
36
+ await self._load_template(args[1], context)
37
+ elif subcommand == "create":
38
+ await self._create_template()
39
+ elif subcommand == "clear":
40
+ await self._clear_template(context)
41
+ else:
42
+ await ui.error(f"Unknown subcommand: {subcommand}")
43
+ await self._show_help()
44
+
45
+ return None
46
+
47
+ async def _show_help(self) -> None:
48
+ """Show help for template command."""
49
+ await ui.info("Template management commands:")
50
+ await ui.muted(" /template list - List available templates")
51
+ await ui.muted(" /template load <name> - Load and activate a template")
52
+ await ui.muted(" /template create - Create a new template interactively")
53
+ await ui.muted(" /template clear - Clear the active template")
54
+
55
+ async def _list_templates(self) -> None:
56
+ """List all available templates."""
57
+ loader = TemplateLoader()
58
+ template_names = loader.list_templates()
59
+
60
+ if not template_names:
61
+ await ui.info("No templates found.")
62
+ await ui.muted("Create one with: /template create")
63
+ return
64
+
65
+ await ui.info("Available templates:")
66
+ for name in template_names:
67
+ template = loader.load_template(name)
68
+ if template:
69
+ tools_count = len(template.allowed_tools) if template.allowed_tools else 0
70
+ shortcut_info = f" ({template.shortcut})" if template.shortcut else ""
71
+ await ui.muted(
72
+ f" • {name}{shortcut_info}: {template.description} ({tools_count} tools)"
73
+ )
74
+
75
+ async def _load_template(self, name: str, context: CommandContext) -> None:
76
+ """Load and activate a template."""
77
+ loader = TemplateLoader()
78
+ template = loader.load_template(name)
79
+
80
+ if not template:
81
+ await ui.error(f"Template '{name}' not found")
82
+ await ui.muted("Use '/template list' to see available templates")
83
+ return
84
+
85
+ # Set active template in tool handler
86
+ if hasattr(context.state_manager, "tool_handler") and context.state_manager.tool_handler:
87
+ context.state_manager.tool_handler.set_active_template(template)
88
+ else:
89
+ await ui.error("Tool handler not initialized. Please restart TunaCode.")
90
+ return
91
+
92
+ await ui.success(f"Loaded template: {template.name}")
93
+ await ui.muted(f"Description: {template.description}")
94
+
95
+ if template.shortcut:
96
+ await ui.muted(f"Shortcut: {template.shortcut}")
97
+
98
+ if template.allowed_tools:
99
+ await ui.info(f"Allowed tools ({len(template.allowed_tools)}):")
100
+ tools_str = ", ".join(template.allowed_tools)
101
+ await ui.muted(f" {tools_str}")
102
+
103
+ # Execute the prompt if one is defined
104
+ if template.prompt:
105
+ await ui.info("Template has a default prompt:")
106
+ await ui.muted(f" {template.prompt}")
107
+ await ui.muted("Submit this prompt to execute it")
108
+
109
+ async def _create_template(self) -> None:
110
+ """Create a new template interactively."""
111
+ await ui.info("Interactive template creation is not yet implemented")
112
+ await ui.muted("Please create templates manually as JSON files in:")
113
+ await ui.muted(" ~/.config/tunacode/templates/")
114
+ await ui.muted("")
115
+ await ui.muted("Example template format:")
116
+ await ui.muted("{")
117
+ await ui.muted(' "name": "debug",')
118
+ await ui.muted(' "description": "Debugging and analysis",')
119
+ await ui.muted(' "shortcut": "/debug",')
120
+ await ui.muted(' "prompt": "Debug the following issue: {argument}",')
121
+ await ui.muted(' "allowed_tools": ["read_file", "grep", "list_dir", "run_command"]')
122
+ await ui.muted("}")
123
+
124
+ # TODO: Implement interactive creation when proper input handling is available
125
+
126
+ async def _clear_template(self, context: CommandContext) -> None:
127
+ """Clear the currently active template."""
128
+ if hasattr(context.state_manager, "tool_handler") and context.state_manager.tool_handler:
129
+ context.state_manager.tool_handler.set_active_template(None)
130
+ await ui.success("Cleared active template")
131
+ else:
132
+ await ui.error("Unable to clear template - tool handler not found")
@@ -4,6 +4,7 @@ from dataclasses import dataclass
4
4
  from typing import Any, Dict, List, Optional, Type
5
5
 
6
6
  from ...exceptions import ValidationError
7
+ from ...templates.loader import TemplateLoader
7
8
  from ...types import CommandArgs, CommandContext, ProcessRequestCallback
8
9
  from .base import Command, CommandCategory
9
10
 
@@ -26,7 +27,9 @@ from .implementations.system import (
26
27
  StreamingCommand,
27
28
  UpdateCommand,
28
29
  )
30
+ from .implementations.template import TemplateCommand
29
31
  from .implementations.todo import TodoCommand
32
+ from .template_shortcut import TemplateShortcutCommand
30
33
 
31
34
 
32
35
  @dataclass
@@ -71,6 +74,7 @@ class CommandRegistry:
71
74
  }
72
75
  self._factory = factory or CommandFactory()
73
76
  self._discovered = False
77
+ self._shortcuts_loaded = False
74
78
 
75
79
  # Set registry reference in factory dependencies
76
80
  self._factory.update_dependencies(command_registry=self)
@@ -120,12 +124,13 @@ class CommandRegistry:
120
124
  CompactCommand,
121
125
  ModelCommand,
122
126
  InitCommand,
127
+ TemplateCommand,
123
128
  TodoCommand,
124
129
  ]
125
130
 
126
131
  # Register all discovered commands
127
132
  for command_class in command_classes:
128
- self.register_command_class(command_class)
133
+ self.register_command_class(command_class) # type: ignore[arg-type]
129
134
 
130
135
  self._discovered = True
131
136
 
@@ -133,6 +138,26 @@ class CommandRegistry:
133
138
  """Register all default commands (backward compatibility)."""
134
139
  self.discover_commands()
135
140
 
141
+ def load_template_shortcuts(self) -> None:
142
+ """Load and register template shortcuts dynamically."""
143
+ if self._shortcuts_loaded:
144
+ return
145
+
146
+ try:
147
+ loader = TemplateLoader()
148
+ shortcuts = loader.get_templates_with_shortcuts()
149
+
150
+ for shortcut, template in shortcuts.items():
151
+ # Create a template shortcut command instance
152
+ shortcut_command = TemplateShortcutCommand(template)
153
+ self.register(shortcut_command)
154
+
155
+ self._shortcuts_loaded = True
156
+
157
+ except Exception as e:
158
+ # Don't fail if templates can't be loaded
159
+ print(f"Warning: Failed to load template shortcuts: {str(e)}")
160
+
136
161
  def set_process_request_callback(self, callback: ProcessRequestCallback) -> None:
137
162
  """Set the process_request callback for commands that need it."""
138
163
  # Only update if callback has changed
@@ -161,6 +186,8 @@ class CommandRegistry:
161
186
  """
162
187
  # Ensure commands are discovered
163
188
  self.discover_commands()
189
+ # Load template shortcuts
190
+ self.load_template_shortcuts()
164
191
 
165
192
  parts = command_text.split()
166
193
  if not parts:
@@ -0,0 +1,93 @@
1
+ """Template shortcut command base class for dynamic template-based commands."""
2
+
3
+ from typing import List, Optional
4
+
5
+ from ...templates.loader import Template
6
+ from ...types import CommandArgs, CommandContext
7
+ from ...ui import console as ui
8
+ from .base import Command, CommandCategory
9
+
10
+
11
+ class TemplateShortcutCommand(Command):
12
+ """Base class for template shortcut commands that auto-load templates and process arguments."""
13
+
14
+ def __init__(self, template: Template):
15
+ """Initialize with a specific template.
16
+
17
+ Args:
18
+ template: The template instance this shortcut represents
19
+ """
20
+ self.template = template
21
+ self._name = template.shortcut.lstrip("/") if template.shortcut else template.name
22
+ self._aliases = [template.shortcut] if template.shortcut else []
23
+ self._description = f"{template.description} (Template: {template.name})"
24
+
25
+ @property
26
+ def name(self) -> str:
27
+ """The primary name of the command."""
28
+ return self._name
29
+
30
+ @property
31
+ def aliases(self) -> List[str]:
32
+ """Alternative names for the command."""
33
+ return self._aliases
34
+
35
+ @property
36
+ def description(self) -> str:
37
+ """Description of what the command does."""
38
+ return self._description
39
+
40
+ @property
41
+ def category(self) -> CommandCategory:
42
+ """Category this command belongs to."""
43
+ return CommandCategory.DEVELOPMENT
44
+
45
+ async def execute(self, args: CommandArgs, context: CommandContext) -> Optional[str]:
46
+ """Execute the template shortcut command.
47
+
48
+ Args:
49
+ args: Command arguments passed by the user
50
+ context: Command execution context
51
+
52
+ Returns:
53
+ Optional string result
54
+ """
55
+ # Set the template as active in the tool handler
56
+ if hasattr(context.state_manager, "tool_handler") and context.state_manager.tool_handler:
57
+ context.state_manager.tool_handler.set_active_template(self.template)
58
+ else:
59
+ await ui.error("Tool handler not initialized. Please restart TunaCode.")
60
+ return None
61
+
62
+ # Show template activation
63
+ await ui.success(f"Activated template: {self.template.name}")
64
+ if self.template.allowed_tools:
65
+ await ui.muted(f"Auto-approved tools: {', '.join(self.template.allowed_tools)}")
66
+
67
+ # Process the prompt with arguments
68
+ if self.template.prompt:
69
+ # Join all arguments as a single string
70
+ argument = " ".join(args) if args else ""
71
+
72
+ # Substitute {argument} placeholder in the prompt
73
+ prompt = self.template.prompt.replace("{argument}", argument)
74
+
75
+ # If there are additional parameters, substitute them too
76
+ if self.template.parameters:
77
+ for key, value in self.template.parameters.items():
78
+ placeholder = f"{{{key}}}"
79
+ prompt = prompt.replace(placeholder, value)
80
+
81
+ # Show the expanded prompt
82
+ await ui.info("Executing prompt:")
83
+ await ui.muted(f" {prompt}")
84
+
85
+ # Execute the prompt via process_request
86
+ if context.process_request:
87
+ await context.process_request(
88
+ prompt, context.state_manager.session.current_model, context.state_manager
89
+ )
90
+ else:
91
+ await ui.muted("Template activated. No default prompt defined.")
92
+
93
+ return None
tunacode/cli/main.py CHANGED
@@ -12,6 +12,7 @@ import typer
12
12
  from tunacode.cli.repl import repl
13
13
  from tunacode.configuration.settings import ApplicationSettings
14
14
  from tunacode.core.state import StateManager
15
+ from tunacode.core.tool_handler import ToolHandler
15
16
  from tunacode.exceptions import UserAbortError
16
17
  from tunacode.setup import setup
17
18
  from tunacode.ui import console as ui
@@ -60,6 +61,11 @@ def main(
60
61
 
61
62
  try:
62
63
  await setup(run_setup, state_manager, cli_config)
64
+
65
+ # Initialize ToolHandler after setup
66
+ tool_handler = ToolHandler(state_manager)
67
+ state_manager.set_tool_handler(tool_handler)
68
+
63
69
  await repl(state_manager)
64
70
  except (KeyboardInterrupt, UserAbortError):
65
71
  update_task.cancel()
tunacode/cli/repl.py CHANGED
@@ -9,7 +9,6 @@ Handles user input, command processing, and agent interaction in an interactive
9
9
  # IMPORTS AND DEPENDENCIES
10
10
  # ============================================================================
11
11
 
12
- import json
13
12
  import logging
14
13
  import os
15
14
  import subprocess
@@ -23,28 +22,23 @@ from pydantic_ai.exceptions import UnexpectedModelBehavior
23
22
  from tunacode.constants import DEFAULT_CONTEXT_WINDOW
24
23
  from tunacode.core.agents import main as agent
25
24
  from tunacode.core.agents.main import patch_tool_messages
26
- from tunacode.core.tool_handler import ToolHandler
27
25
  from tunacode.exceptions import AgentError, UserAbortError, ValidationError
28
26
  from tunacode.ui import console as ui
29
27
  from tunacode.ui.output import get_context_window_display
30
- from tunacode.ui.tool_ui import ToolUI
31
28
  from tunacode.utils.security import CommandSecurityError, safe_subprocess_run
32
29
 
33
- from ..types import CommandContext, CommandResult, StateManager, ToolArgs
30
+ from ..types import CommandContext, CommandResult, StateManager
34
31
  from .commands import CommandRegistry
35
32
 
36
33
  # ============================================================================
37
34
  # MODULE-LEVEL CONSTANTS AND CONFIGURATION
38
35
  # ============================================================================
39
-
40
- _tool_ui = ToolUI()
36
+ from .repl_components import attempt_tool_recovery, display_agent_output, tool_handler
37
+ from .repl_components.output_display import MSG_REQUEST_COMPLETED
41
38
 
42
39
  MSG_OPERATION_ABORTED = "Operation aborted."
43
- MSG_OPERATION_ABORTED_BY_USER = "Operation aborted by user."
44
40
  MSG_TOOL_INTERRUPTED = "Tool execution was interrupted"
45
41
  MSG_REQUEST_CANCELLED = "Request cancelled"
46
- MSG_REQUEST_COMPLETED = "Request completed"
47
- MSG_JSON_RECOVERY = "Recovered using JSON tool parsing"
48
42
  MSG_SESSION_ENDED = "Session ended. Happy coding!"
49
43
  MSG_AGENT_BUSY = "Agent is busy, press Ctrl+C to interrupt."
50
44
  MSG_HIT_CTRL_C = "Hit Ctrl+C again to exit"
@@ -54,92 +48,8 @@ DEFAULT_SHELL = "bash"
54
48
  # Configure logging
55
49
  logger = logging.getLogger(__name__)
56
50
 
57
- # ============================================================================
58
- # UTILITY FUNCTIONS
59
- # ============================================================================
60
-
61
-
62
- def _parse_args(args) -> ToolArgs:
63
- """
64
- Parse tool arguments from a JSON string or dictionary.
65
-
66
- Args:
67
- args (str or dict): A JSON-formatted string or a dictionary containing tool arguments.
68
-
69
- Returns:
70
- dict: The parsed arguments.
71
-
72
- Raises:
73
- ValueError: If 'args' is not a string or dictionary, or if the string is not valid JSON.
74
- """
75
- if isinstance(args, str):
76
- try:
77
- return json.loads(args)
78
- except json.JSONDecodeError:
79
- raise ValidationError(f"Invalid JSON: {args}")
80
- elif isinstance(args, dict):
81
- return args
82
- else:
83
- raise ValidationError(f"Invalid args type: {type(args)}")
84
-
85
-
86
- # ============================================================================
87
- # TOOL EXECUTION AND CONFIRMATION HANDLERS
88
- # ============================================================================
89
-
90
-
91
- async def _tool_handler(part, state_manager: StateManager):
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
-
99
- tool_handler = ToolHandler(state_manager)
100
-
101
- if tool_handler.should_confirm(part.tool_name):
102
- await ui.info(f"Tool({part.tool_name})")
103
-
104
- if not state_manager.session.is_streaming_active and state_manager.session.spinner:
105
- state_manager.session.spinner.stop()
106
-
107
- streaming_panel = None
108
- if state_manager.session.is_streaming_active and hasattr(
109
- state_manager.session, "streaming_panel"
110
- ):
111
- streaming_panel = state_manager.session.streaming_panel
112
- if streaming_panel and tool_handler.should_confirm(part.tool_name):
113
- await streaming_panel.stop()
114
-
115
- try:
116
- args = _parse_args(part.args)
117
-
118
- def confirm_func():
119
- if not tool_handler.should_confirm(part.tool_name):
120
- return False
121
- request = tool_handler.create_confirmation_request(part.tool_name, args)
122
-
123
- response = _tool_ui.show_sync_confirmation(request)
124
-
125
- if not tool_handler.process_confirmation(response, part.tool_name):
126
- return True # Abort
127
- return False # Continue
128
-
129
- should_abort = await run_in_terminal(confirm_func)
130
-
131
- if should_abort:
132
- raise UserAbortError("User aborted.")
133
-
134
- except UserAbortError:
135
- patch_tool_messages(MSG_OPERATION_ABORTED_BY_USER, state_manager)
136
- raise
137
- finally:
138
- if streaming_panel and tool_handler.should_confirm(part.tool_name):
139
- await streaming_panel.start()
140
-
141
- if not state_manager.session.is_streaming_active and state_manager.session.spinner:
142
- state_manager.session.spinner.start()
51
+ # The _parse_args function has been moved to repl_components.command_parser
52
+ # The _tool_handler function has been moved to repl_components.tool_executor
143
53
 
144
54
 
145
55
  # ============================================================================
@@ -169,82 +79,13 @@ async def _handle_command(command: str, state_manager: StateManager) -> CommandR
169
79
  return await _command_registry.execute(command, context)
170
80
  except ValidationError as e:
171
81
  await ui.error(str(e))
82
+ return None
172
83
 
173
84
 
174
- # ============================================================================
175
- # ERROR RECOVERY
176
- # ============================================================================
177
-
178
-
179
- async def _attempt_tool_recovery(e: Exception, state_manager: StateManager) -> bool:
180
- """
181
- Attempt to recover from tool calling failures using guard clauses.
85
+ # The _attempt_tool_recovery function has been moved to repl_components.error_recovery
182
86
 
183
- Returns:
184
- bool: True if recovery was successful, False otherwise
185
- """
186
- error_str = str(e).lower()
187
- tool_keywords = ["tool", "function", "call", "schema"]
188
- if not any(keyword in error_str for keyword in tool_keywords):
189
- return False
190
87
 
191
- if not state_manager.session.messages:
192
- return False
193
-
194
- last_msg = state_manager.session.messages[-1]
195
- if not hasattr(last_msg, "parts"):
196
- return False
197
-
198
- for part in last_msg.parts:
199
- if not hasattr(part, "content") or not isinstance(part.content, str):
200
- continue
201
-
202
- try:
203
- from tunacode.core.agents.main import extract_and_execute_tool_calls
204
-
205
- def tool_callback_with_state(part, node):
206
- return _tool_handler(part, state_manager)
207
-
208
- await extract_and_execute_tool_calls(
209
- part.content, tool_callback_with_state, state_manager
210
- )
211
-
212
- await ui.warning(f" {MSG_JSON_RECOVERY}")
213
- return True
214
-
215
- except Exception as e:
216
- logger.debug(f"Failed to check triple quotes: {e}")
217
- continue
218
-
219
- return False
220
-
221
-
222
- # ============================================================================
223
- # AGENT OUTPUT DISPLAY
224
- # ============================================================================
225
-
226
-
227
- async def _display_agent_output(res, enable_streaming: bool) -> None:
228
- """Display agent output using guard clauses to flatten nested conditionals."""
229
- if enable_streaming:
230
- return
231
-
232
- if not hasattr(res, "result") or res.result is None or not hasattr(res.result, "output"):
233
- await ui.muted(MSG_REQUEST_COMPLETED)
234
- return
235
-
236
- output = res.result.output
237
-
238
- if not isinstance(output, str):
239
- return
240
-
241
- if output.strip().startswith('{"thought"'):
242
- return
243
-
244
- if '"tool_uses"' in output:
245
- return
246
-
247
- await ui.agent(output)
88
+ # The _display_agent_output function has been moved to repl_components.output_display
248
89
 
249
90
 
250
91
  # ============================================================================
@@ -254,6 +95,14 @@ async def _display_agent_output(res, enable_streaming: bool) -> None:
254
95
 
255
96
  async def process_request(text: str, state_manager: StateManager, output: bool = True):
256
97
  """Process input using the agent, handling cancellation safely."""
98
+ import uuid
99
+
100
+ # Generate a unique ID for this request for correlated logging
101
+ request_id = str(uuid.uuid4())
102
+ logger.debug(
103
+ "Processing new request", extra={"request_id": request_id, "input_text": text[:100]}
104
+ )
105
+ state_manager.session.request_id = request_id
257
106
 
258
107
  # Check for cancellation before starting (only if explicitly set to True)
259
108
  operation_cancelled = getattr(state_manager.session, "operation_cancelled", False)
@@ -274,8 +123,8 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
274
123
 
275
124
  start_idx = len(state_manager.session.messages)
276
125
 
277
- def tool_callback_with_state(part, node):
278
- return _tool_handler(part, state_manager)
126
+ def tool_callback_with_state(part, _node):
127
+ return tool_handler(part, state_manager)
279
128
 
280
129
  try:
281
130
  from tunacode.utils.text_utils import expand_file_refs
@@ -313,8 +162,8 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
313
162
  await streaming_panel.update(content)
314
163
 
315
164
  res = await agent.process_request(
316
- state_manager.session.current_model,
317
165
  text,
166
+ state_manager.session.current_model,
318
167
  state_manager,
319
168
  tool_callback=tool_callback_with_state,
320
169
  streaming_callback=streaming_callback,
@@ -326,8 +175,8 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
326
175
  else:
327
176
  # Use normal agent processing
328
177
  res = await agent.process_request(
329
- state_manager.session.current_model,
330
178
  text,
179
+ state_manager.session.current_model,
331
180
  state_manager,
332
181
  tool_callback=tool_callback_with_state,
333
182
  )
@@ -351,7 +200,7 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
351
200
  await ui.muted(MSG_REQUEST_COMPLETED)
352
201
  else:
353
202
  # Use the dedicated function for displaying agent output
354
- await _display_agent_output(res, enable_streaming)
203
+ await display_agent_output(res, enable_streaming)
355
204
 
356
205
  # Always show files in context after agent response
357
206
  if state_manager.session.files_in_context:
@@ -369,7 +218,7 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
369
218
  patch_tool_messages(error_message, state_manager)
370
219
  except Exception as e:
371
220
  # Try tool recovery for tool-related errors
372
- if await _attempt_tool_recovery(e, state_manager):
221
+ if await attempt_tool_recovery(e, state_manager):
373
222
  return # Successfully recovered
374
223
 
375
224
  agent_error = AgentError(f"Agent processing failed: {str(e)}")
@@ -439,7 +288,13 @@ async def repl(state_manager: StateManager):
439
288
  action = await _handle_command(line, state_manager)
440
289
  if action == "restart":
441
290
  break
442
- continue
291
+ elif isinstance(action, str) and action:
292
+ # If the command returned a string (e.g., from template shortcut),
293
+ # process it as a prompt
294
+ line = action
295
+ # Fall through to process as normal text
296
+ else:
297
+ continue
443
298
 
444
299
  if line.startswith("!"):
445
300
  command = line[1:].strip()