tunacode-cli 0.0.35__py3-none-any.whl → 0.0.37__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 (32) hide show
  1. tunacode/cli/commands/__init__.py +62 -0
  2. tunacode/cli/commands/base.py +99 -0
  3. tunacode/cli/commands/implementations/__init__.py +38 -0
  4. tunacode/cli/commands/implementations/conversation.py +115 -0
  5. tunacode/cli/commands/implementations/debug.py +189 -0
  6. tunacode/cli/commands/implementations/development.py +77 -0
  7. tunacode/cli/commands/implementations/model.py +61 -0
  8. tunacode/cli/commands/implementations/system.py +216 -0
  9. tunacode/cli/commands/registry.py +236 -0
  10. tunacode/cli/repl.py +91 -30
  11. tunacode/configuration/settings.py +9 -2
  12. tunacode/constants.py +1 -1
  13. tunacode/core/agents/main.py +53 -3
  14. tunacode/core/agents/utils.py +304 -0
  15. tunacode/core/setup/config_setup.py +0 -1
  16. tunacode/core/state.py +13 -2
  17. tunacode/setup.py +7 -2
  18. tunacode/tools/read_file.py +8 -2
  19. tunacode/tools/read_file_async_poc.py +18 -10
  20. tunacode/tools/run_command.py +11 -4
  21. tunacode/ui/console.py +31 -4
  22. tunacode/ui/output.py +7 -2
  23. tunacode/ui/panels.py +98 -5
  24. tunacode/ui/utils.py +3 -0
  25. tunacode/utils/text_utils.py +6 -2
  26. {tunacode_cli-0.0.35.dist-info → tunacode_cli-0.0.37.dist-info}/METADATA +17 -17
  27. {tunacode_cli-0.0.35.dist-info → tunacode_cli-0.0.37.dist-info}/RECORD +31 -21
  28. tunacode/cli/commands.py +0 -893
  29. {tunacode_cli-0.0.35.dist-info → tunacode_cli-0.0.37.dist-info}/WHEEL +0 -0
  30. {tunacode_cli-0.0.35.dist-info → tunacode_cli-0.0.37.dist-info}/entry_points.txt +0 -0
  31. {tunacode_cli-0.0.35.dist-info → tunacode_cli-0.0.37.dist-info}/licenses/LICENSE +0 -0
  32. {tunacode_cli-0.0.35.dist-info → tunacode_cli-0.0.37.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,216 @@
1
+ """System-level commands for TunaCode CLI."""
2
+
3
+ import shutil
4
+ import subprocess
5
+ import sys
6
+ from typing import List
7
+
8
+ from ....types import CommandContext
9
+ from ....ui import console as ui
10
+ from ..base import CommandCategory, CommandSpec, SimpleCommand
11
+
12
+
13
+ class HelpCommand(SimpleCommand):
14
+ """Show help information."""
15
+
16
+ spec = CommandSpec(
17
+ name="help",
18
+ aliases=["/help"],
19
+ description="Show help information",
20
+ category=CommandCategory.SYSTEM,
21
+ )
22
+
23
+ def __init__(self, command_registry=None):
24
+ self._command_registry = command_registry
25
+
26
+ async def execute(self, args: List[str], context: CommandContext) -> None:
27
+ await ui.help(self._command_registry)
28
+
29
+
30
+ class ClearCommand(SimpleCommand):
31
+ """Clear screen and message history."""
32
+
33
+ spec = CommandSpec(
34
+ name="clear",
35
+ aliases=["/clear"],
36
+ description="Clear the screen and message history",
37
+ category=CommandCategory.NAVIGATION,
38
+ )
39
+
40
+ async def execute(self, args: List[str], context: CommandContext) -> None:
41
+ # Patch any orphaned tool calls before clearing
42
+ from tunacode.core.agents.main import patch_tool_messages
43
+
44
+ patch_tool_messages("Conversation cleared", context.state_manager)
45
+
46
+ await ui.clear()
47
+ context.state_manager.session.messages = []
48
+ context.state_manager.session.files_in_context.clear()
49
+ await ui.success("Message history and file context cleared")
50
+
51
+
52
+ class RefreshConfigCommand(SimpleCommand):
53
+ """Refresh configuration from defaults."""
54
+
55
+ spec = CommandSpec(
56
+ name="refresh",
57
+ aliases=["/refresh"],
58
+ description="Refresh configuration from defaults (useful after updates)",
59
+ category=CommandCategory.SYSTEM,
60
+ )
61
+
62
+ async def execute(self, args: List[str], context: CommandContext) -> None:
63
+ from tunacode.configuration.defaults import DEFAULT_USER_CONFIG
64
+
65
+ # Update current session config with latest defaults
66
+ for key, value in DEFAULT_USER_CONFIG.items():
67
+ if key not in context.state_manager.session.user_config:
68
+ context.state_manager.session.user_config[key] = value
69
+ elif isinstance(value, dict):
70
+ # Merge dict values, preserving user overrides
71
+ for subkey, subvalue in value.items():
72
+ if subkey not in context.state_manager.session.user_config[key]:
73
+ context.state_manager.session.user_config[key][subkey] = subvalue
74
+
75
+ # Show updated max_iterations
76
+ max_iterations = context.state_manager.session.user_config.get("settings", {}).get(
77
+ "max_iterations", 20
78
+ )
79
+ await ui.success(f"Configuration refreshed - max iterations: {max_iterations}")
80
+
81
+
82
+ class UpdateCommand(SimpleCommand):
83
+ """Update TunaCode to the latest version."""
84
+
85
+ spec = CommandSpec(
86
+ name="update",
87
+ aliases=["/update"],
88
+ description="Update TunaCode to the latest version",
89
+ category=CommandCategory.SYSTEM,
90
+ )
91
+
92
+ async def execute(self, args: List[str], context: CommandContext) -> None:
93
+ await ui.info("Checking for TunaCode updates...")
94
+
95
+ # Detect installation method
96
+ installation_method = None
97
+
98
+ # Check if installed via pipx
99
+ if shutil.which("pipx"):
100
+ try:
101
+ result = subprocess.run(
102
+ ["pipx", "list"], capture_output=True, text=True, timeout=10
103
+ )
104
+ pipx_installed = "tunacode" in result.stdout.lower()
105
+ if pipx_installed:
106
+ installation_method = "pipx"
107
+ except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
108
+ pass
109
+
110
+ # Check if installed via pip
111
+ if not installation_method:
112
+ try:
113
+ result = subprocess.run(
114
+ [sys.executable, "-m", "pip", "show", "tunacode-cli"],
115
+ capture_output=True,
116
+ text=True,
117
+ timeout=10,
118
+ )
119
+ if result.returncode == 0:
120
+ installation_method = "pip"
121
+ except (subprocess.TimeoutExpired, subprocess.CalledProcessError):
122
+ pass
123
+
124
+ if not installation_method:
125
+ await ui.error("Could not detect TunaCode installation method")
126
+ await ui.muted("Manual update options:")
127
+ await ui.muted(" pipx: pipx upgrade tunacode")
128
+ await ui.muted(" pip: pip install --upgrade tunacode-cli")
129
+ return
130
+
131
+ # Perform update based on detected method
132
+ try:
133
+ if installation_method == "pipx":
134
+ await ui.info("Updating via pipx...")
135
+ result = subprocess.run(
136
+ ["pipx", "upgrade", "tunacode"],
137
+ capture_output=True,
138
+ text=True,
139
+ timeout=60,
140
+ )
141
+ else: # pip
142
+ await ui.info("Updating via pip...")
143
+ result = subprocess.run(
144
+ [
145
+ sys.executable,
146
+ "-m",
147
+ "pip",
148
+ "install",
149
+ "--upgrade",
150
+ "tunacode-cli",
151
+ ],
152
+ capture_output=True,
153
+ text=True,
154
+ timeout=60,
155
+ )
156
+
157
+ if result.returncode == 0:
158
+ await ui.success("TunaCode updated successfully!")
159
+ await ui.muted("Restart TunaCode to use the new version")
160
+
161
+ # Show update output if available
162
+ if result.stdout.strip():
163
+ output_lines = result.stdout.strip().split("\n")
164
+ for line in output_lines[-5:]: # Show last 5 lines
165
+ if line.strip():
166
+ await ui.muted(f" {line}")
167
+ else:
168
+ await ui.error("Update failed")
169
+ if result.stderr:
170
+ await ui.muted(f"Error: {result.stderr.strip()}")
171
+
172
+ except subprocess.TimeoutExpired:
173
+ await ui.error("Update timed out")
174
+ except subprocess.CalledProcessError as e:
175
+ await ui.error(f"Update failed: {e}")
176
+ except FileNotFoundError:
177
+ await ui.error(f"Could not find {installation_method} executable")
178
+
179
+
180
+ class StreamingCommand(SimpleCommand):
181
+ """Toggle streaming display on/off."""
182
+
183
+ spec = CommandSpec(
184
+ name="streaming",
185
+ aliases=["/streaming"],
186
+ description="Toggle streaming display on/off",
187
+ category=CommandCategory.SYSTEM,
188
+ )
189
+
190
+ async def execute(self, args: List[str], context: CommandContext) -> None:
191
+ current_setting = context.state_manager.session.user_config.get("settings", {}).get(
192
+ "enable_streaming", True
193
+ )
194
+
195
+ if args and args[0].lower() in ["on", "true", "1", "enable", "enabled"]:
196
+ new_setting = True
197
+ elif args and args[0].lower() in ["off", "false", "0", "disable", "disabled"]:
198
+ new_setting = False
199
+ else:
200
+ # Toggle current setting
201
+ new_setting = not current_setting
202
+
203
+ # Update the configuration
204
+ if "settings" not in context.state_manager.session.user_config:
205
+ context.state_manager.session.user_config["settings"] = {}
206
+ context.state_manager.session.user_config["settings"]["enable_streaming"] = new_setting
207
+
208
+ status = "enabled" if new_setting else "disabled"
209
+ await ui.success(f"Streaming display {status}")
210
+
211
+ if new_setting:
212
+ await ui.muted(
213
+ "Responses will be displayed progressively as they are generated (default)"
214
+ )
215
+ else:
216
+ await ui.muted("Responses will be displayed all at once after completion")
@@ -0,0 +1,236 @@
1
+ """Command registry and factory for TunaCode CLI commands."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Dict, List, Optional, Type
5
+
6
+ from ...exceptions import ValidationError
7
+ from ...types import CommandArgs, CommandContext, ProcessRequestCallback
8
+ from .base import Command, CommandCategory
9
+
10
+ # Import all command implementations
11
+ from .implementations.conversation import CompactCommand
12
+ from .implementations.debug import (
13
+ DumpCommand,
14
+ FixCommand,
15
+ IterationsCommand,
16
+ ParseToolsCommand,
17
+ ThoughtsCommand,
18
+ YoloCommand,
19
+ )
20
+ from .implementations.development import BranchCommand, InitCommand
21
+ from .implementations.model import ModelCommand
22
+ from .implementations.system import (
23
+ ClearCommand,
24
+ HelpCommand,
25
+ RefreshConfigCommand,
26
+ StreamingCommand,
27
+ UpdateCommand,
28
+ )
29
+
30
+
31
+ @dataclass
32
+ class CommandDependencies:
33
+ """Container for command dependencies."""
34
+
35
+ process_request_callback: Optional[ProcessRequestCallback] = None
36
+ command_registry: Optional[Any] = None # Reference to the registry itself
37
+
38
+
39
+ class CommandFactory:
40
+ """Factory for creating commands with proper dependency injection."""
41
+
42
+ def __init__(self, dependencies: Optional[CommandDependencies] = None):
43
+ self.dependencies = dependencies or CommandDependencies()
44
+
45
+ def create_command(self, command_class: Type[Command]) -> Command:
46
+ """Create a command instance with proper dependencies."""
47
+ # Special handling for commands that need dependencies
48
+ if command_class == CompactCommand:
49
+ return CompactCommand(self.dependencies.process_request_callback)
50
+ elif command_class == HelpCommand:
51
+ return HelpCommand(self.dependencies.command_registry)
52
+
53
+ # Default creation for commands without dependencies
54
+ return command_class()
55
+
56
+ def update_dependencies(self, **kwargs) -> None:
57
+ """Update factory dependencies."""
58
+ for key, value in kwargs.items():
59
+ if hasattr(self.dependencies, key):
60
+ setattr(self.dependencies, key, value)
61
+
62
+
63
+ class CommandRegistry:
64
+ """Registry for managing commands with auto-discovery and categories."""
65
+
66
+ def __init__(self, factory: Optional[CommandFactory] = None):
67
+ self._commands: Dict[str, Command] = {}
68
+ self._categories: Dict[CommandCategory, List[Command]] = {
69
+ category: [] for category in CommandCategory
70
+ }
71
+ self._factory = factory or CommandFactory()
72
+ self._discovered = False
73
+
74
+ # Set registry reference in factory dependencies
75
+ self._factory.update_dependencies(command_registry=self)
76
+
77
+ def register(self, command: Command) -> None:
78
+ """Register a command and its aliases."""
79
+ # Register by primary name
80
+ self._commands[command.name] = command
81
+
82
+ # Register all aliases
83
+ for alias in command.aliases:
84
+ self._commands[alias.lower()] = command
85
+
86
+ # Add to category (remove existing instance first to prevent duplicates)
87
+ category_commands = self._categories[command.category]
88
+ # Remove any existing instance of this command class
89
+ self._categories[command.category] = [
90
+ cmd for cmd in category_commands if cmd.__class__ != command.__class__
91
+ ]
92
+ # Add the new instance
93
+ self._categories[command.category].append(command)
94
+
95
+ def register_command_class(self, command_class: Type[Command]) -> None:
96
+ """Register a command class using the factory."""
97
+ command = self._factory.create_command(command_class)
98
+ self.register(command)
99
+
100
+ def discover_commands(self) -> None:
101
+ """Auto-discover and register all command classes."""
102
+ if self._discovered:
103
+ return
104
+
105
+ # List of all command classes to register
106
+ command_classes = [
107
+ YoloCommand,
108
+ DumpCommand,
109
+ ThoughtsCommand,
110
+ IterationsCommand,
111
+ ClearCommand,
112
+ FixCommand,
113
+ ParseToolsCommand,
114
+ RefreshConfigCommand,
115
+ StreamingCommand,
116
+ UpdateCommand,
117
+ HelpCommand,
118
+ BranchCommand,
119
+ CompactCommand,
120
+ ModelCommand,
121
+ InitCommand,
122
+ ]
123
+
124
+ # Register all discovered commands
125
+ for command_class in command_classes:
126
+ self.register_command_class(command_class)
127
+
128
+ self._discovered = True
129
+
130
+ def register_all_default_commands(self) -> None:
131
+ """Register all default commands (backward compatibility)."""
132
+ self.discover_commands()
133
+
134
+ def set_process_request_callback(self, callback: ProcessRequestCallback) -> None:
135
+ """Set the process_request callback for commands that need it."""
136
+ # Only update if callback has changed
137
+ if self._factory.dependencies.process_request_callback == callback:
138
+ return
139
+
140
+ self._factory.update_dependencies(process_request_callback=callback)
141
+
142
+ # Re-register CompactCommand with new dependency if already registered
143
+ if "compact" in self._commands:
144
+ self.register_command_class(CompactCommand)
145
+
146
+ async def execute(self, command_text: str, context: CommandContext) -> Any:
147
+ """
148
+ Execute a command.
149
+
150
+ Args:
151
+ command_text: The full command text
152
+ context: Execution context
153
+
154
+ Returns:
155
+ Command-specific return value, or None if command not found
156
+
157
+ Raises:
158
+ ValidationError: If command is not found or empty
159
+ """
160
+ # Ensure commands are discovered
161
+ self.discover_commands()
162
+
163
+ parts = command_text.split()
164
+ if not parts:
165
+ raise ValidationError("Empty command")
166
+
167
+ command_name = parts[0].lower()
168
+ args = parts[1:]
169
+
170
+ # First try exact match
171
+ if command_name in self._commands:
172
+ command = self._commands[command_name]
173
+ return await command.execute(args, context)
174
+
175
+ # Try partial matching
176
+ matches = self.find_matching_commands(command_name)
177
+
178
+ if not matches:
179
+ raise ValidationError(f"Unknown command: {command_name}")
180
+ elif len(matches) == 1:
181
+ # Unambiguous match
182
+ command = self._commands[matches[0]]
183
+ return await command.execute(args, context)
184
+ else:
185
+ # Ambiguous - show possibilities
186
+ matches_str = ", ".join(sorted(set(matches)))
187
+ raise ValidationError(
188
+ f"Ambiguous command '{command_name}'. Did you mean: {matches_str}?"
189
+ )
190
+
191
+ def find_matching_commands(self, partial_command: str) -> List[str]:
192
+ """
193
+ Find all commands that start with the given partial command.
194
+
195
+ Args:
196
+ partial_command: The partial command to match
197
+
198
+ Returns:
199
+ List of matching command names
200
+ """
201
+ self.discover_commands()
202
+ partial = partial_command.lower()
203
+ return [cmd for cmd in self._commands.keys() if cmd.startswith(partial)]
204
+
205
+ def is_command(self, text: str) -> bool:
206
+ """Check if text starts with a registered command (supports partial matching)."""
207
+ if not text:
208
+ return False
209
+
210
+ parts = text.split()
211
+ if not parts:
212
+ return False
213
+
214
+ command_name = parts[0].lower()
215
+
216
+ # Check exact match first
217
+ if command_name in self._commands:
218
+ return True
219
+
220
+ # Check partial match
221
+ return len(self.find_matching_commands(command_name)) > 0
222
+
223
+ def get_command_names(self) -> CommandArgs:
224
+ """Get all registered command names (including aliases)."""
225
+ self.discover_commands()
226
+ return sorted(self._commands.keys())
227
+
228
+ def get_commands_by_category(self, category: CommandCategory) -> List[Command]:
229
+ """Get all commands in a specific category."""
230
+ self.discover_commands()
231
+ return self._categories.get(category, [])
232
+
233
+ def get_all_categories(self) -> Dict[CommandCategory, List[Command]]:
234
+ """Get all commands organized by category."""
235
+ self.discover_commands()
236
+ return self._categories.copy()
tunacode/cli/repl.py CHANGED
@@ -70,8 +70,9 @@ async def _tool_confirm(tool_call, node, state_manager: StateManager):
70
70
  await _tool_ui.log_mcp(title, args)
71
71
  return
72
72
 
73
- # Stop spinner during user interaction
74
- state_manager.session.spinner.stop()
73
+ # Stop spinner during user interaction (only if not streaming)
74
+ if not state_manager.session.is_streaming_active and state_manager.session.spinner:
75
+ state_manager.session.spinner.stop()
75
76
 
76
77
  # Create confirmation request
77
78
  request = tool_handler.create_confirmation_request(tool_call.tool_name, args)
@@ -84,7 +85,10 @@ async def _tool_confirm(tool_call, node, state_manager: StateManager):
84
85
  raise UserAbortError("User aborted.")
85
86
 
86
87
  await ui.line() # Add line after user input
87
- state_manager.session.spinner.start()
88
+
89
+ # Restart spinner (only if not streaming)
90
+ if not state_manager.session.is_streaming_active and state_manager.session.spinner:
91
+ state_manager.session.spinner.start()
88
92
 
89
93
 
90
94
  async def _tool_handler(part, node, state_manager: StateManager):
@@ -96,7 +100,19 @@ async def _tool_handler(part, node, state_manager: StateManager):
96
100
  if tool_handler.should_confirm(part.tool_name):
97
101
  await ui.info(f"Tool({part.tool_name})")
98
102
 
99
- state_manager.session.spinner.stop()
103
+ # Stop spinner only if not streaming
104
+ if not state_manager.session.is_streaming_active and state_manager.session.spinner:
105
+ state_manager.session.spinner.stop()
106
+
107
+ # Track if we need to stop/restart streaming panel
108
+ streaming_panel = None
109
+ if state_manager.session.is_streaming_active and hasattr(
110
+ state_manager.session, "streaming_panel"
111
+ ):
112
+ streaming_panel = state_manager.session.streaming_panel
113
+ # Stop the streaming panel to prevent UI interference during confirmation
114
+ if streaming_panel and tool_handler.should_confirm(part.tool_name):
115
+ await streaming_panel.stop()
100
116
 
101
117
  try:
102
118
  args = _parse_args(part.args)
@@ -128,7 +144,13 @@ async def _tool_handler(part, node, state_manager: StateManager):
128
144
  patch_tool_messages("Operation aborted by user.", state_manager)
129
145
  raise
130
146
  finally:
131
- state_manager.session.spinner.start()
147
+ # Restart streaming panel if it was stopped
148
+ if streaming_panel and tool_handler.should_confirm(part.tool_name):
149
+ await streaming_panel.start()
150
+
151
+ # Restart spinner only if not streaming
152
+ if not state_manager.session.is_streaming_active and state_manager.session.spinner:
153
+ state_manager.session.spinner.start()
132
154
 
133
155
 
134
156
  # Initialize command registry
@@ -195,38 +217,77 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
195
217
  await ui.error(str(e))
196
218
  return
197
219
 
198
- # Use normal agent processing
199
- res = await agent.process_request(
200
- state_manager.session.current_model,
201
- text,
202
- state_manager,
203
- tool_callback=tool_callback_with_state,
220
+ # Check if streaming is enabled (default: True for better UX)
221
+ enable_streaming = state_manager.session.user_config.get("settings", {}).get(
222
+ "enable_streaming", True
204
223
  )
224
+
225
+ if enable_streaming:
226
+ # Stop spinner before starting streaming display (Rich.Live conflict)
227
+ await ui.spinner(False, state_manager.session.spinner, state_manager)
228
+
229
+ # Mark that streaming is active to prevent spinner conflicts
230
+ state_manager.session.is_streaming_active = True
231
+
232
+ # Use streaming agent processing
233
+ streaming_panel = ui.StreamingAgentPanel()
234
+ await streaming_panel.start()
235
+
236
+ # Store streaming panel reference in session for tool handler access
237
+ state_manager.session.streaming_panel = streaming_panel
238
+
239
+ try:
240
+
241
+ async def streaming_callback(content: str):
242
+ await streaming_panel.update(content)
243
+
244
+ res = await agent.process_request(
245
+ state_manager.session.current_model,
246
+ text,
247
+ state_manager,
248
+ tool_callback=tool_callback_with_state,
249
+ streaming_callback=streaming_callback,
250
+ )
251
+ finally:
252
+ await streaming_panel.stop()
253
+ # Clear streaming panel reference
254
+ state_manager.session.streaming_panel = None
255
+ # Mark streaming as inactive
256
+ state_manager.session.is_streaming_active = False
257
+ # Don't restart spinner - it will be stopped in the outer finally block anyway
258
+ else:
259
+ # Use normal agent processing
260
+ res = await agent.process_request(
261
+ state_manager.session.current_model,
262
+ text,
263
+ state_manager,
264
+ tool_callback=tool_callback_with_state,
265
+ )
205
266
  if output:
206
267
  if state_manager.session.show_thoughts:
207
268
  new_msgs = state_manager.session.messages[start_idx:]
208
269
  for msg in new_msgs:
209
270
  if isinstance(msg, dict) and "thought" in msg:
210
271
  await ui.muted(f"THOUGHT: {msg['thought']}")
211
- # Check if result exists and has output
212
- if hasattr(res, "result") and res.result is not None and hasattr(res.result, "output"):
213
- await ui.agent(res.result.output)
214
- # Always show files in context after agent response
215
- if state_manager.session.files_in_context:
216
- # Extract just filenames from full paths for readability
217
- filenames = [
218
- Path(f).name for f in sorted(state_manager.session.files_in_context)
219
- ]
220
- await ui.muted(f"\nFiles in context: {', '.join(filenames)}")
221
- else:
222
- # Fallback: show that the request was processed
223
- await ui.muted("Request completed")
224
- # Show files in context even for empty responses
225
- if state_manager.session.files_in_context:
226
- filenames = [
227
- Path(f).name for f in sorted(state_manager.session.files_in_context)
228
- ]
229
- await ui.muted(f"Files in context: {', '.join(filenames)}")
272
+
273
+ # Only display result if not streaming (streaming already showed content)
274
+ if not enable_streaming:
275
+ # Check if result exists and has output
276
+ if (
277
+ hasattr(res, "result")
278
+ and res.result is not None
279
+ and hasattr(res.result, "output")
280
+ ):
281
+ await ui.agent(res.result.output)
282
+ else:
283
+ # Fallback: show that the request was processed
284
+ await ui.muted("Request completed")
285
+
286
+ # Always show files in context after agent response
287
+ if state_manager.session.files_in_context:
288
+ # Extract just filenames from full paths for readability
289
+ filenames = [Path(f).name for f in sorted(state_manager.session.files_in_context)]
290
+ await ui.muted(f"\nFiles in context: {', '.join(filenames)}")
230
291
  except CancelledError:
231
292
  await ui.muted("Request cancelled")
232
293
  except UserAbortError:
@@ -7,8 +7,15 @@ Handles configuration paths, model registries, and application metadata.
7
7
 
8
8
  from pathlib import Path
9
9
 
10
- from tunacode.constants import (APP_NAME, APP_VERSION, CONFIG_FILE_NAME, TOOL_READ_FILE,
11
- TOOL_RUN_COMMAND, TOOL_UPDATE_FILE, TOOL_WRITE_FILE)
10
+ from tunacode.constants import (
11
+ APP_NAME,
12
+ APP_VERSION,
13
+ CONFIG_FILE_NAME,
14
+ TOOL_READ_FILE,
15
+ TOOL_RUN_COMMAND,
16
+ TOOL_UPDATE_FILE,
17
+ TOOL_WRITE_FILE,
18
+ )
12
19
  from tunacode.types import ConfigFile, ConfigPath, ToolName
13
20
 
14
21
 
tunacode/constants.py CHANGED
@@ -7,7 +7,7 @@ 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.35"
10
+ APP_VERSION = "0.0.37"
11
11
 
12
12
  # File patterns
13
13
  GUIDE_FILE_PATTERN = "{name}.md"