tunacode-cli 0.0.28__tar.gz → 0.0.30__tar.gz

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 (101) hide show
  1. {tunacode_cli-0.0.28/src/tunacode_cli.egg-info → tunacode_cli-0.0.30}/PKG-INFO +1 -1
  2. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/pyproject.toml +1 -1
  3. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/cli/commands.py +2 -1
  4. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/cli/repl.py +36 -2
  5. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/cli/textual_bridge.py +4 -1
  6. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/configuration/defaults.py +1 -0
  7. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/constants.py +1 -1
  8. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/agents/main.py +225 -41
  9. tunacode_cli-0.0.30/src/tunacode/core/agents/orchestrator.py +213 -0
  10. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/agents/readonly.py +18 -4
  11. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/state.py +5 -0
  12. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/prompts/system.md +38 -8
  13. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/utils/text_utils.py +14 -5
  14. tunacode_cli-0.0.30/src/tunacode/utils/token_counter.py +23 -0
  15. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30/src/tunacode_cli.egg-info}/PKG-INFO +1 -1
  16. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode_cli.egg-info/SOURCES.txt +2 -0
  17. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/tests/test_fallback_responses.py +3 -1
  18. tunacode_cli-0.0.30/tests/test_file_reference_context_tracking.py +147 -0
  19. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/tests/test_file_reference_expansion.py +56 -8
  20. tunacode_cli-0.0.28/src/tunacode/core/agents/orchestrator.py +0 -117
  21. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/CLAUDE.md +0 -0
  22. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/LICENSE +0 -0
  23. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/MANIFEST.in +0 -0
  24. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/README.md +0 -0
  25. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/TUNACODE.md +0 -0
  26. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/setup.cfg +0 -0
  27. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/setup.py +0 -0
  28. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/__init__.py +0 -0
  29. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/cli/__init__.py +0 -0
  30. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/cli/main.py +0 -0
  31. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/cli/textual_app.py +0 -0
  32. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/configuration/__init__.py +0 -0
  33. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/configuration/models.py +0 -0
  34. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/configuration/settings.py +0 -0
  35. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/context.py +0 -0
  36. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/__init__.py +0 -0
  37. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/agents/__init__.py +0 -0
  38. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/agents/planner_schema.py +0 -0
  39. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/background/__init__.py +0 -0
  40. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/background/manager.py +0 -0
  41. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/llm/__init__.py +0 -0
  42. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/llm/planner.py +0 -0
  43. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/setup/__init__.py +0 -0
  44. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/setup/agent_setup.py +0 -0
  45. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/setup/base.py +0 -0
  46. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/setup/config_setup.py +0 -0
  47. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/setup/coordinator.py +0 -0
  48. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/setup/environment_setup.py +0 -0
  49. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/setup/git_safety_setup.py +0 -0
  50. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/core/tool_handler.py +0 -0
  51. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/exceptions.py +0 -0
  52. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/py.typed +0 -0
  53. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/services/__init__.py +0 -0
  54. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/services/mcp.py +0 -0
  55. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/setup.py +0 -0
  56. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/tools/__init__.py +0 -0
  57. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/tools/base.py +0 -0
  58. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/tools/bash.py +0 -0
  59. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/tools/grep.py +0 -0
  60. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/tools/read_file.py +0 -0
  61. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/tools/run_command.py +0 -0
  62. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/tools/update_file.py +0 -0
  63. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/tools/write_file.py +0 -0
  64. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/types.py +0 -0
  65. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/ui/__init__.py +0 -0
  66. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/ui/completers.py +0 -0
  67. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/ui/console.py +0 -0
  68. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/ui/constants.py +0 -0
  69. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/ui/decorators.py +0 -0
  70. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/ui/input.py +0 -0
  71. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/ui/keybindings.py +0 -0
  72. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/ui/lexers.py +0 -0
  73. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/ui/output.py +0 -0
  74. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/ui/panels.py +0 -0
  75. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/ui/prompt_manager.py +0 -0
  76. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/ui/tool_ui.py +0 -0
  77. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/ui/validators.py +0 -0
  78. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/utils/__init__.py +0 -0
  79. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/utils/bm25.py +0 -0
  80. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/utils/diff_utils.py +0 -0
  81. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/utils/file_utils.py +0 -0
  82. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/utils/import_cache.py +0 -0
  83. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/utils/ripgrep.py +0 -0
  84. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/utils/system.py +0 -0
  85. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode/utils/user_configuration.py +0 -0
  86. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode_cli.egg-info/dependency_links.txt +0 -0
  87. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode_cli.egg-info/entry_points.txt +0 -0
  88. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode_cli.egg-info/requires.txt +0 -0
  89. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/src/tunacode_cli.egg-info/top_level.txt +0 -0
  90. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/tests/test_agent_initialization.py +0 -0
  91. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/tests/test_architect_integration.py +0 -0
  92. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/tests/test_architect_simple.py +0 -0
  93. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/tests/test_background_manager.py +0 -0
  94. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/tests/test_config_setup_async.py +0 -0
  95. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/tests/test_fast_glob_search.py +0 -0
  96. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/tests/test_json_tool_parsing.py +0 -0
  97. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/tests/test_orchestrator_file_references.py +0 -0
  98. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/tests/test_orchestrator_import.py +0 -0
  99. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/tests/test_orchestrator_planning_visibility.py +0 -0
  100. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/tests/test_react_thoughts.py +0 -0
  101. {tunacode_cli-0.0.28 → tunacode_cli-0.0.30}/tests/test_update_command.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tunacode-cli
3
- Version: 0.0.28
3
+ Version: 0.0.30
4
4
  Summary: Your agentic CLI developer.
5
5
  Author-email: larock22 <noreply@github.com>
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tunacode-cli"
7
- version = "0.0.28"
7
+ version = "0.0.30"
8
8
  description = "Your agentic CLI developer."
9
9
  keywords = ["cli", "agent", "development", "automation"]
10
10
  readme = "README.md"
@@ -258,7 +258,8 @@ class ClearCommand(SimpleCommand):
258
258
 
259
259
  await ui.clear()
260
260
  context.state_manager.session.messages = []
261
- await ui.success("Message history cleared")
261
+ context.state_manager.session.files_in_context.clear()
262
+ await ui.success("Message history and file context cleared")
262
263
 
263
264
 
264
265
  class FixCommand(SimpleCommand):
@@ -9,6 +9,7 @@ import json
9
9
  import os
10
10
  import subprocess
11
11
  from asyncio.exceptions import CancelledError
12
+ from pathlib import Path
12
13
 
13
14
  from prompt_toolkit.application import run_in_terminal
14
15
  from prompt_toolkit.application.current import get_app
@@ -164,6 +165,13 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
164
165
  # Patch any orphaned tool calls from previous requests before proceeding
165
166
  patch_tool_messages("Tool execution was interrupted", state_manager)
166
167
 
168
+ # Clear tracking for new request when thoughts are enabled
169
+ if state_manager.session.show_thoughts:
170
+ state_manager.session.tool_calls = []
171
+ # Don't clear files_in_context - keep it cumulative for the session
172
+ state_manager.session.iteration_count = 0
173
+ state_manager.session.current_iteration = 0
174
+
167
175
  # Track message start for thoughts display
168
176
  start_idx = len(state_manager.session.messages)
169
177
 
@@ -177,7 +185,10 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
177
185
  try:
178
186
  from tunacode.utils.text_utils import expand_file_refs
179
187
 
180
- text = expand_file_refs(text)
188
+ text, referenced_files = expand_file_refs(text)
189
+ # Track the referenced files
190
+ for file_path in referenced_files:
191
+ state_manager.session.files_in_context.add(file_path)
181
192
  except ValueError as e:
182
193
  await ui.error(str(e))
183
194
  return
@@ -199,12 +210,22 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
199
210
  if not results:
200
211
  # Fallback: show that the request was processed
201
212
  await ui.muted("Request completed")
213
+
214
+ # Always show files in context after orchestrator response
215
+ if state_manager.session.files_in_context:
216
+ filenames = [
217
+ Path(f).name for f in sorted(state_manager.session.files_in_context)
218
+ ]
219
+ await ui.muted(f"\nFiles in context: {', '.join(filenames)}")
202
220
  else:
203
221
  # Expand @file references before sending to the agent
204
222
  try:
205
223
  from tunacode.utils.text_utils import expand_file_refs
206
224
 
207
- text = expand_file_refs(text)
225
+ text, referenced_files = expand_file_refs(text)
226
+ # Track the referenced files
227
+ for file_path in referenced_files:
228
+ state_manager.session.files_in_context.add(file_path)
208
229
  except ValueError as e:
209
230
  await ui.error(str(e))
210
231
  return
@@ -229,9 +250,22 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
229
250
  and hasattr(res.result, "output")
230
251
  ):
231
252
  await ui.agent(res.result.output)
253
+ # Always show files in context after agent response
254
+ if state_manager.session.files_in_context:
255
+ # Extract just filenames from full paths for readability
256
+ filenames = [
257
+ Path(f).name for f in sorted(state_manager.session.files_in_context)
258
+ ]
259
+ await ui.muted(f"\nFiles in context: {', '.join(filenames)}")
232
260
  else:
233
261
  # Fallback: show that the request was processed
234
262
  await ui.muted("Request completed")
263
+ # Show files in context even for empty responses
264
+ if state_manager.session.files_in_context:
265
+ filenames = [
266
+ Path(f).name for f in sorted(state_manager.session.files_in_context)
267
+ ]
268
+ await ui.muted(f"Files in context: {', '.join(filenames)}")
235
269
  except CancelledError:
236
270
  await ui.muted("Request cancelled")
237
271
  except UserAbortError:
@@ -72,7 +72,10 @@ class TextualAgentBridge:
72
72
  try:
73
73
  from tunacode.utils.text_utils import expand_file_refs
74
74
 
75
- text = expand_file_refs(text)
75
+ text, referenced_files = expand_file_refs(text)
76
+ # Track the referenced files
77
+ for file_path in referenced_files:
78
+ self.state_manager.session.files_in_context.add(file_path)
76
79
  except ValueError as e:
77
80
  return f"Error: {str(e)}"
78
81
 
@@ -22,6 +22,7 @@ DEFAULT_USER_CONFIG: UserConfig = {
22
22
  "tool_ignore": [TOOL_READ_FILE],
23
23
  "guide_file": GUIDE_FILE_NAME,
24
24
  "fallback_response": True,
25
+ "fallback_verbosity": "normal", # Options: minimal, normal, detailed
25
26
  },
26
27
  "mcpServers": {},
27
28
  }
@@ -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.28"
10
+ APP_VERSION = "0.0.30"
11
11
 
12
12
  # File patterns
13
13
  GUIDE_FILE_PATTERN = "{name}.md"
@@ -38,6 +38,9 @@ def get_model_messages():
38
38
 
39
39
 
40
40
  async def _process_node(node, tool_callback: Optional[ToolCallback], state_manager: StateManager):
41
+ from tunacode.ui import console as ui
42
+ from tunacode.utils.token_counter import estimate_tokens
43
+
41
44
  if hasattr(node, "request"):
42
45
  state_manager.session.messages.append(node.request)
43
46
 
@@ -45,36 +48,47 @@ async def _process_node(node, tool_callback: Optional[ToolCallback], state_manag
45
48
  state_manager.session.messages.append({"thought": node.thought})
46
49
  # Display thought immediately if show_thoughts is enabled
47
50
  if state_manager.session.show_thoughts:
48
- from tunacode.ui import console as ui
49
-
50
- await ui.muted(f"💭 THOUGHT: {node.thought}")
51
+ await ui.muted(f"THOUGHT: {node.thought}")
51
52
 
52
53
  if hasattr(node, "model_response"):
53
54
  state_manager.session.messages.append(node.model_response)
54
55
 
55
- # Enhanced ReAct thought processing
56
+ # Enhanced display when thoughts are enabled
56
57
  if state_manager.session.show_thoughts:
57
58
  import json
58
59
  import re
59
60
 
60
- from tunacode.ui import console as ui
61
-
61
+ # Display LLM response content
62
62
  for part in node.model_response.parts:
63
63
  if hasattr(part, "content") and isinstance(part.content, str):
64
64
  content = part.content.strip()
65
65
 
66
+ # Skip empty content
67
+ if not content:
68
+ continue
69
+
70
+ # Estimate tokens in this response
71
+ token_count = estimate_tokens(content)
72
+
73
+ # Display non-JSON content as LLM response
74
+ if not content.startswith('{"thought"'):
75
+ # Truncate very long responses for display
76
+ display_content = content[:500] + "..." if len(content) > 500 else content
77
+ await ui.muted(f"\nRESPONSE: {display_content}")
78
+ await ui.muted(f"TOKENS: ~{token_count}")
79
+
66
80
  # Pattern 1: Inline JSON thoughts {"thought": "..."}
67
81
  thought_pattern = r'\{"thought":\s*"([^"]+)"\}'
68
82
  matches = re.findall(thought_pattern, content)
69
83
  for thought in matches:
70
- await ui.muted(f"💭 REASONING: {thought}")
84
+ await ui.muted(f"REASONING: {thought}")
71
85
 
72
86
  # Pattern 2: Standalone thought JSON objects
73
87
  try:
74
88
  if content.startswith('{"thought"'):
75
89
  thought_obj = json.loads(content)
76
90
  if "thought" in thought_obj:
77
- await ui.muted(f"💭 REASONING: {thought_obj['thought']}")
91
+ await ui.muted(f"REASONING: {thought_obj['thought']}")
78
92
  except (json.JSONDecodeError, KeyError):
79
93
  pass
80
94
 
@@ -85,36 +99,86 @@ async def _process_node(node, tool_callback: Optional[ToolCallback], state_manag
85
99
  if thought not in [m for m in matches]: # Avoid duplicates
86
100
  # Clean up escaped characters
87
101
  cleaned_thought = thought.replace('\\"', '"').replace("\\n", " ")
88
- await ui.muted(f"💭 REASONING: {cleaned_thought}")
89
-
90
- # Pattern 4: Text-based reasoning indicators
91
- reasoning_indicators = [
92
- (r"I need to (.+?)\.", "PLANNING"),
93
- (r"Let me (.+?)\.", "ACTION"),
94
- (r"The output shows (.+?)\.", "OBSERVATION"),
95
- (r"Based on (.+?), I should (.+?)\.", "DECISION"),
96
- ]
97
-
98
- for pattern, label in reasoning_indicators:
99
- indicator_matches = re.findall(pattern, content, re.IGNORECASE)
100
- for match in indicator_matches:
101
- if isinstance(match, tuple):
102
- match_text = " ".join(match)
103
- else:
104
- match_text = match
105
- await ui.muted(f"🎯 {label}: {match_text}")
106
- break # Only show first match per pattern
102
+ await ui.muted(f"REASONING: {cleaned_thought}")
107
103
 
108
104
  # Check for tool calls and fallback to JSON parsing if needed
109
105
  has_tool_calls = False
110
106
  for part in node.model_response.parts:
111
107
  if part.part_kind == "tool-call" and tool_callback:
112
108
  has_tool_calls = True
109
+
110
+ # Display tool call details when thoughts are enabled
111
+ if state_manager.session.show_thoughts:
112
+ await ui.muted(f"\nTOOL: {part.tool_name}")
113
+ if hasattr(part, "args"):
114
+ # Check if args is a dictionary before accessing keys
115
+ if isinstance(part.args, dict):
116
+ # Simplify display based on tool type
117
+ if part.tool_name == "read_file" and "file_path" in part.args:
118
+ file_path = part.args["file_path"]
119
+ filename = Path(file_path).name
120
+ await ui.muted(f"Reading: {filename}")
121
+ elif part.tool_name == "write_file" and "file_path" in part.args:
122
+ file_path = part.args["file_path"]
123
+ filename = Path(file_path).name
124
+ await ui.muted(f"Writing: {filename}")
125
+ elif part.tool_name == "update_file" and "file_path" in part.args:
126
+ file_path = part.args["file_path"]
127
+ filename = Path(file_path).name
128
+ await ui.muted(f"Updating: {filename}")
129
+ elif (
130
+ part.tool_name in ["run_command", "bash"] and "command" in part.args
131
+ ):
132
+ command = part.args["command"]
133
+ # Truncate long commands
134
+ display_cmd = (
135
+ command if len(command) <= 60 else command[:57] + "..."
136
+ )
137
+ await ui.muted(f"Command: {display_cmd}")
138
+ else:
139
+ # For other tools, show full args but more compact
140
+ args_str = json.dumps(part.args, indent=2)
141
+ await ui.muted(f"ARGS: {args_str}")
142
+ else:
143
+ # If args is not a dict (e.g., a string), just display it as is
144
+ await ui.muted(f"ARGS: {part.args}")
145
+
146
+ # Track this tool call (moved outside thoughts block)
147
+ state_manager.session.tool_calls.append(
148
+ {
149
+ "tool": part.tool_name,
150
+ "args": part.args if hasattr(part, "args") else {},
151
+ "iteration": state_manager.session.current_iteration,
152
+ }
153
+ )
154
+
155
+ # Track files if this is read_file (moved outside thoughts block)
156
+ if (
157
+ part.tool_name == "read_file"
158
+ and hasattr(part, "args")
159
+ and "file_path" in part.args
160
+ ):
161
+ state_manager.session.files_in_context.add(part.args["file_path"])
162
+ # Show files in context when thoughts are enabled
163
+ if state_manager.session.show_thoughts:
164
+ await ui.muted(
165
+ f"\nFILES IN CONTEXT: {list(state_manager.session.files_in_context)}"
166
+ )
167
+
113
168
  await tool_callback(part, node)
169
+
114
170
  elif part.part_kind == "tool-return":
115
171
  obs_msg = f"OBSERVATION[{part.tool_name}]: {part.content[:2_000]}"
116
172
  state_manager.session.messages.append(obs_msg)
117
173
 
174
+ # Display tool return when thoughts are enabled
175
+ if state_manager.session.show_thoughts:
176
+ # Truncate for display
177
+ display_content = (
178
+ part.content[:200] + "..." if len(part.content) > 200 else part.content
179
+ )
180
+ await ui.muted(f"TOOL RESULT: {display_content}")
181
+
118
182
  # If no structured tool calls found, try parsing JSON from text content
119
183
  if not has_tool_calls and tool_callback:
120
184
  for part in node.model_response.parts:
@@ -276,13 +340,13 @@ async def parse_json_tool_calls(
276
340
  if state_manager.session.show_thoughts:
277
341
  from tunacode.ui import console as ui
278
342
 
279
- await ui.muted(f"🔧 FALLBACK: Executed {tool_name} via JSON parsing")
343
+ await ui.muted(f"FALLBACK: Executed {tool_name} via JSON parsing")
280
344
 
281
345
  except Exception as e:
282
346
  if state_manager.session.show_thoughts:
283
347
  from tunacode.ui import console as ui
284
348
 
285
- await ui.error(f"Error executing fallback tool {tool_name}: {str(e)}")
349
+ await ui.error(f"Error executing fallback tool {tool_name}: {str(e)}")
286
350
 
287
351
 
288
352
  async def extract_and_execute_tool_calls(
@@ -324,13 +388,13 @@ async def extract_and_execute_tool_calls(
324
388
  if state_manager.session.show_thoughts:
325
389
  from tunacode.ui import console as ui
326
390
 
327
- await ui.muted(f"🔧 FALLBACK: Executed {tool_data['tool']} from code block")
391
+ await ui.muted(f"FALLBACK: Executed {tool_data['tool']} from code block")
328
392
 
329
393
  except (json.JSONDecodeError, KeyError, Exception) as e:
330
394
  if state_manager.session.show_thoughts:
331
395
  from tunacode.ui import console as ui
332
396
 
333
- await ui.error(f"Error parsing code block tool call: {str(e)}")
397
+ await ui.error(f"Error parsing code block tool call: {str(e)}")
334
398
 
335
399
 
336
400
  async def process_request(
@@ -349,49 +413,160 @@ async def process_request(
349
413
 
350
414
  response_state = ResponseState()
351
415
 
416
+ # Reset iteration tracking for this request
417
+ state_manager.session.iteration_count = 0
418
+
352
419
  async with agent.iter(message, message_history=mh) as agent_run:
353
420
  i = 0
354
421
  async for node in agent_run:
422
+ state_manager.session.current_iteration = i + 1
355
423
  await _process_node(node, tool_callback, state_manager)
356
424
  if hasattr(node, "result") and node.result and hasattr(node.result, "output"):
357
425
  if node.result.output:
358
426
  response_state.has_user_response = True
359
427
  i += 1
428
+ state_manager.session.iteration_count = i
360
429
 
361
430
  # Display iteration progress if thoughts are enabled
362
- if state_manager.session.show_thoughts and i > 1:
431
+ if state_manager.session.show_thoughts:
363
432
  from tunacode.ui import console as ui
364
433
 
365
- await ui.muted(f"🔄 Iteration {i}/{max_iterations}")
434
+ await ui.muted(f"\nITERATION: {i}/{max_iterations}")
435
+
436
+ # Show summary of tools used so far
437
+ if state_manager.session.tool_calls:
438
+ tool_summary = {}
439
+ for tc in state_manager.session.tool_calls:
440
+ tool_name = tc.get("tool", "unknown")
441
+ tool_summary[tool_name] = tool_summary.get(tool_name, 0) + 1
442
+
443
+ summary_str = ", ".join(
444
+ [f"{name}: {count}" for name, count in tool_summary.items()]
445
+ )
446
+ await ui.muted(f"TOOLS USED: {summary_str}")
366
447
 
367
448
  if i >= max_iterations:
368
449
  if state_manager.session.show_thoughts:
369
450
  from tunacode.ui import console as ui
370
451
 
371
- await ui.warning(f"⚠️ Reached maximum iterations ({max_iterations})")
452
+ await ui.warning(f"Reached maximum iterations ({max_iterations})")
372
453
  break
373
454
 
374
455
  # If we need to add a fallback response, create a wrapper
375
456
  if not response_state.has_user_response and i >= max_iterations and fallback_enabled:
376
457
  patch_tool_messages("Task incomplete", state_manager=state_manager)
377
458
  response_state.has_final_synthesis = True
459
+
460
+ # Extract context from the agent run
461
+ tool_calls_summary = []
462
+ files_modified = set()
463
+ commands_run = []
464
+
465
+ # Analyze message history for context
466
+ for msg in state_manager.session.messages:
467
+ if hasattr(msg, "parts"):
468
+ for part in msg.parts:
469
+ if hasattr(part, "part_kind") and part.part_kind == "tool-call":
470
+ tool_name = getattr(part, "tool_name", "unknown")
471
+ tool_calls_summary.append(tool_name)
472
+
473
+ # Track specific operations
474
+ if tool_name in ["write_file", "update_file"] and hasattr(part, "args"):
475
+ if "file_path" in part.args:
476
+ files_modified.add(part.args["file_path"])
477
+ elif tool_name in ["run_command", "bash"] and hasattr(part, "args"):
478
+ if "command" in part.args:
479
+ commands_run.append(part.args["command"])
480
+
481
+ # Build fallback response with context
378
482
  fallback = FallbackResponse(
379
483
  summary="Reached maximum iterations without producing a final response.",
380
- progress=f"{i}/{max_iterations} iterations completed",
484
+ progress=f"Completed {i} iterations (limit: {max_iterations})",
485
+ )
486
+
487
+ # Get verbosity setting
488
+ verbosity = state_manager.session.user_config.get("settings", {}).get(
489
+ "fallback_verbosity", "normal"
490
+ )
491
+
492
+ if verbosity in ["normal", "detailed"]:
493
+ # Add what was attempted
494
+ if tool_calls_summary:
495
+ tool_counts = {}
496
+ for tool in tool_calls_summary:
497
+ tool_counts[tool] = tool_counts.get(tool, 0) + 1
498
+
499
+ fallback.issues.append(f"Executed {len(tool_calls_summary)} tool calls:")
500
+ for tool, count in sorted(tool_counts.items()):
501
+ fallback.issues.append(f" • {tool}: {count}x")
502
+
503
+ if verbosity == "detailed":
504
+ if files_modified:
505
+ fallback.issues.append(f"\nFiles modified ({len(files_modified)}):")
506
+ for f in sorted(files_modified)[:5]: # Limit to 5 files
507
+ fallback.issues.append(f" • {f}")
508
+ if len(files_modified) > 5:
509
+ fallback.issues.append(f" • ... and {len(files_modified) - 5} more")
510
+
511
+ if commands_run:
512
+ fallback.issues.append(f"\nCommands executed ({len(commands_run)}):")
513
+ for cmd in commands_run[:3]: # Limit to 3 commands
514
+ # Truncate long commands
515
+ display_cmd = cmd if len(cmd) <= 60 else cmd[:57] + "..."
516
+ fallback.issues.append(f" • {display_cmd}")
517
+ if len(commands_run) > 3:
518
+ fallback.issues.append(f" • ... and {len(commands_run) - 3} more")
519
+
520
+ # Add helpful next steps
521
+ fallback.next_steps.append(
522
+ "The task may be too complex - try breaking it into smaller steps"
381
523
  )
524
+ fallback.next_steps.append("Check the output above for any errors or partial progress")
525
+ if files_modified:
526
+ fallback.next_steps.append("Review modified files to see what changes were made")
527
+
528
+ # Create comprehensive output
529
+ output_parts = [fallback.summary, ""]
530
+
531
+ if fallback.progress:
532
+ output_parts.append(f"Progress: {fallback.progress}")
533
+
534
+ if fallback.issues:
535
+ output_parts.append("\nWhat happened:")
536
+ output_parts.extend(fallback.issues)
537
+
538
+ if fallback.next_steps:
539
+ output_parts.append("\nSuggested next steps:")
540
+ for step in fallback.next_steps:
541
+ output_parts.append(f" • {step}")
542
+
543
+ comprehensive_output = "\n".join(output_parts)
382
544
 
383
545
  # Create a wrapper object that mimics AgentRun with the required attributes
384
546
  class AgentRunWrapper:
385
547
  def __init__(self, wrapped_run, fallback_result):
386
548
  self._wrapped = wrapped_run
387
- self.result = fallback_result
549
+ self._result = fallback_result
388
550
  self.response_state = response_state
389
551
 
390
- def __getattr__(self, name):
552
+ def __getattribute__(self, name):
553
+ # Handle special attributes first to avoid conflicts
554
+ if name in ["_wrapped", "_result", "response_state"]:
555
+ return object.__getattribute__(self, name)
556
+
557
+ # Explicitly handle 'result' to return our fallback result
558
+ if name == "result":
559
+ return object.__getattribute__(self, "_result")
560
+
391
561
  # Delegate all other attributes to the wrapped object
392
- return getattr(self._wrapped, name)
562
+ try:
563
+ return getattr(object.__getattribute__(self, "_wrapped"), name)
564
+ except AttributeError:
565
+ raise AttributeError(
566
+ f"'{type(self).__name__}' object has no attribute '{name}'"
567
+ )
393
568
 
394
- return AgentRunWrapper(agent_run, SimpleResult(fallback.summary))
569
+ return AgentRunWrapper(agent_run, SimpleResult(comprehensive_output))
395
570
 
396
571
  # For non-fallback cases, we still need to handle the response_state
397
572
  # Create a minimal wrapper just to add response_state
@@ -400,8 +575,17 @@ async def process_request(
400
575
  self._wrapped = wrapped_run
401
576
  self.response_state = response_state
402
577
 
403
- def __getattr__(self, name):
578
+ def __getattribute__(self, name):
579
+ # Handle special attributes first
580
+ if name in ["_wrapped", "response_state"]:
581
+ return object.__getattribute__(self, name)
582
+
404
583
  # Delegate all other attributes to the wrapped object
405
- return getattr(self._wrapped, name)
584
+ try:
585
+ return getattr(object.__getattribute__(self, "_wrapped"), name)
586
+ except AttributeError:
587
+ raise AttributeError(
588
+ f"'{type(self).__name__}' object has no attribute '{name}'"
589
+ )
406
590
 
407
591
  return AgentRunWithState(agent_run)