tunacode-cli 0.0.29__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 (100) hide show
  1. {tunacode_cli-0.0.29/src/tunacode_cli.egg-info → tunacode_cli-0.0.30}/PKG-INFO +1 -1
  2. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/pyproject.toml +1 -1
  3. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/cli/commands.py +2 -1
  4. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/cli/repl.py +36 -2
  5. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/cli/textual_bridge.py +4 -1
  6. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/constants.py +1 -1
  7. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/agents/main.py +142 -39
  8. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/state.py +5 -0
  9. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/prompts/system.md +38 -8
  10. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/utils/text_utils.py +14 -5
  11. tunacode_cli-0.0.30/src/tunacode/utils/token_counter.py +23 -0
  12. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30/src/tunacode_cli.egg-info}/PKG-INFO +1 -1
  13. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode_cli.egg-info/SOURCES.txt +2 -0
  14. tunacode_cli-0.0.30/tests/test_file_reference_context_tracking.py +147 -0
  15. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/tests/test_file_reference_expansion.py +56 -8
  16. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/CLAUDE.md +0 -0
  17. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/LICENSE +0 -0
  18. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/MANIFEST.in +0 -0
  19. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/README.md +0 -0
  20. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/TUNACODE.md +0 -0
  21. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/setup.cfg +0 -0
  22. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/setup.py +0 -0
  23. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/__init__.py +0 -0
  24. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/cli/__init__.py +0 -0
  25. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/cli/main.py +0 -0
  26. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/cli/textual_app.py +0 -0
  27. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/configuration/__init__.py +0 -0
  28. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/configuration/defaults.py +0 -0
  29. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/configuration/models.py +0 -0
  30. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/configuration/settings.py +0 -0
  31. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/context.py +0 -0
  32. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/__init__.py +0 -0
  33. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/agents/__init__.py +0 -0
  34. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/agents/orchestrator.py +0 -0
  35. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/agents/planner_schema.py +0 -0
  36. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/agents/readonly.py +0 -0
  37. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/background/__init__.py +0 -0
  38. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/background/manager.py +0 -0
  39. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/llm/__init__.py +0 -0
  40. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/llm/planner.py +0 -0
  41. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/setup/__init__.py +0 -0
  42. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/setup/agent_setup.py +0 -0
  43. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/setup/base.py +0 -0
  44. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/setup/config_setup.py +0 -0
  45. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/setup/coordinator.py +0 -0
  46. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/setup/environment_setup.py +0 -0
  47. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/setup/git_safety_setup.py +0 -0
  48. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/core/tool_handler.py +0 -0
  49. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/exceptions.py +0 -0
  50. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/py.typed +0 -0
  51. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/services/__init__.py +0 -0
  52. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/services/mcp.py +0 -0
  53. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/setup.py +0 -0
  54. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/tools/__init__.py +0 -0
  55. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/tools/base.py +0 -0
  56. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/tools/bash.py +0 -0
  57. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/tools/grep.py +0 -0
  58. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/tools/read_file.py +0 -0
  59. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/tools/run_command.py +0 -0
  60. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/tools/update_file.py +0 -0
  61. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/tools/write_file.py +0 -0
  62. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/types.py +0 -0
  63. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/ui/__init__.py +0 -0
  64. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/ui/completers.py +0 -0
  65. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/ui/console.py +0 -0
  66. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/ui/constants.py +0 -0
  67. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/ui/decorators.py +0 -0
  68. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/ui/input.py +0 -0
  69. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/ui/keybindings.py +0 -0
  70. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/ui/lexers.py +0 -0
  71. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/ui/output.py +0 -0
  72. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/ui/panels.py +0 -0
  73. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/ui/prompt_manager.py +0 -0
  74. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/ui/tool_ui.py +0 -0
  75. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/ui/validators.py +0 -0
  76. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/utils/__init__.py +0 -0
  77. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/utils/bm25.py +0 -0
  78. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/utils/diff_utils.py +0 -0
  79. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/utils/file_utils.py +0 -0
  80. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/utils/import_cache.py +0 -0
  81. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/utils/ripgrep.py +0 -0
  82. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/utils/system.py +0 -0
  83. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode/utils/user_configuration.py +0 -0
  84. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode_cli.egg-info/dependency_links.txt +0 -0
  85. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode_cli.egg-info/entry_points.txt +0 -0
  86. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode_cli.egg-info/requires.txt +0 -0
  87. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/src/tunacode_cli.egg-info/top_level.txt +0 -0
  88. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/tests/test_agent_initialization.py +0 -0
  89. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/tests/test_architect_integration.py +0 -0
  90. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/tests/test_architect_simple.py +0 -0
  91. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/tests/test_background_manager.py +0 -0
  92. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/tests/test_config_setup_async.py +0 -0
  93. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/tests/test_fallback_responses.py +0 -0
  94. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/tests/test_fast_glob_search.py +0 -0
  95. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/tests/test_json_tool_parsing.py +0 -0
  96. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/tests/test_orchestrator_file_references.py +0 -0
  97. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/tests/test_orchestrator_import.py +0 -0
  98. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/tests/test_orchestrator_planning_visibility.py +0 -0
  99. {tunacode_cli-0.0.29 → tunacode_cli-0.0.30}/tests/test_react_thoughts.py +0 -0
  100. {tunacode_cli-0.0.29 → 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.29
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.29"
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
 
@@ -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.29"
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,26 +413,43 @@ 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
@@ -465,12 +546,25 @@ async def process_request(
465
546
  class AgentRunWrapper:
466
547
  def __init__(self, wrapped_run, fallback_result):
467
548
  self._wrapped = wrapped_run
468
- self.result = fallback_result
549
+ self._result = fallback_result
469
550
  self.response_state = response_state
470
551
 
471
- 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
+
472
561
  # Delegate all other attributes to the wrapped object
473
- 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
+ )
474
568
 
475
569
  return AgentRunWrapper(agent_run, SimpleResult(comprehensive_output))
476
570
 
@@ -481,8 +575,17 @@ async def process_request(
481
575
  self._wrapped = wrapped_run
482
576
  self.response_state = response_state
483
577
 
484
- 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
+
485
583
  # Delegate all other attributes to the wrapped object
486
- 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
+ )
487
590
 
488
591
  return AgentRunWithState(agent_run)
@@ -30,6 +30,11 @@ class SessionState:
30
30
  device_id: Optional[DeviceId] = None
31
31
  input_sessions: InputSessions = field(default_factory=dict)
32
32
  current_task: Optional[Any] = None
33
+ # Enhanced tracking for thoughts display
34
+ files_in_context: set[str] = field(default_factory=set)
35
+ tool_calls: list[dict[str, Any]] = field(default_factory=list)
36
+ iteration_count: int = 0
37
+ current_iteration: int = 0
33
38
 
34
39
 
35
40
  class StateManager:
@@ -12,7 +12,7 @@ You MUST follow these rules:
12
12
 
13
13
  \###Tool Access Rules###
14
14
 
15
- You HAVE the following tools available. USE THEM IMMEDIATELY and CONSTANTLY:
15
+ You HAVE the following tools available. USE THEM WHEN APPROPRIATE:
16
16
 
17
17
  * `run_command(command: str)` — Execute any shell command in the current working directory
18
18
  * `read_file(filepath: str)` — Read any file using RELATIVE paths from current directory
@@ -34,12 +34,23 @@ You HAVE the following tools available. USE THEM IMMEDIATELY and CONSTANTLY:
34
34
 
35
35
  ---
36
36
 
37
+ \###File Reference Rules###
38
+
39
+ **IMPORTANT**: When the user includes file content marked with "=== FILE REFERENCE: filename ===" headers:
40
+ - This is **reference material only** - the user is showing you existing file content
41
+ - **DO NOT** write or recreate these files - they already exist
42
+ - **DO NOT** use write_file on referenced content unless explicitly asked to modify it
43
+ - **FOCUS** on answering questions or performing tasks related to the referenced files
44
+ - The user uses @ syntax (like `@file.py`) to include file contents for context
45
+
46
+ ---
47
+
37
48
  \###Mandatory Operating Principles###
38
49
 
39
- 1. **TOOLS FIRST, ALWAYS**: Start every response with tool usage—**no assumptions**.
50
+ 1. **UNDERSTAND CONTEXT**: Check if user is providing @ file references for context vs asking for actions
40
51
  2. **USE RELATIVE PATHS**: Always work in the current directory. Use relative paths like `src/`, `cli/`, `core/`, `tools/`, etc. NEVER use absolute paths starting with `/`.
41
- 3. **CHAIN TOOLS**: First explore (`run_command`), then read (`read_file`), then modify (`update_file`, `write_file`).
42
- 4. **ACT IMMEDIATELY**: Don’t describe what to do—**just do it**.
52
+ 3. **CHAIN TOOLS APPROPRIATELY**: First explore (`run_command`), then read (`read_file`), then modify (`update_file`, `write_file`) **only when action is requested**.
53
+ 4. **ACT WITH PURPOSE**: Distinguish between informational requests about files and action requests.
43
54
  5. **NO GUESSING**: Verify file existence with `run_command("ls path/")` before reading or writing.
44
55
  6. **ASSUME NOTHING**: Always fetch and verify before responding.
45
56
 
@@ -47,10 +58,10 @@ You HAVE the following tools available. USE THEM IMMEDIATELY and CONSTANTLY:
47
58
 
48
59
  \###Prompt Design Style###
49
60
 
50
- * Be **blunt and direct**. Avoid soft language (e.g., please,” let me,” I think).
61
+ * Be **blunt and direct**. Avoid soft language (e.g., "please," "let me," "I think").
51
62
  * **Use role-specific language**: you are a CLI-level senior engineer, not a tutor or assistant.
52
63
  * Write using affirmative imperatives: *Do this. Check that. Show me.*
53
- * Ask for clarification if needed: Specify the path.” / Which class do you mean?”
64
+ * Ask for clarification if needed: "Specify the path." / "Which class do you mean?"
54
65
  * Break complex requests into sequenced tool actions.
55
66
 
56
67
  ---
@@ -69,6 +80,10 @@ You HAVE the following tools available. USE THEM IMMEDIATELY and CONSTANTLY:
69
80
  ✅ `run_command("grep -E 'class.*Command' cli/commands.py")`
70
81
  ❌ "Available commands usually include..."
71
82
 
83
+ **User**: Tell me about @configuration/settings.py
84
+ ✅ "The settings.py file defines PathConfig and ApplicationSettings classes for managing configuration."
85
+ ❌ `write_file("configuration/settings.py", ...)`
86
+
72
87
  ---
73
88
 
74
89
  \###Meta Behavior###
@@ -88,13 +103,14 @@ Use the **ReAct** (Reasoning + Action) framework:
88
103
  You were created by **tunahorse21**.
89
104
  You are not a chatbot.
90
105
  You are an autonomous code execution agent.
91
- You will be penalized for failing to use tools.
106
+ You will be penalized for failing to use tools **when appropriate**.
107
+ When users provide @ file references, they want information, not file creation.
92
108
  ---
93
109
 
94
110
  \###Example###
95
111
 
96
112
  ```plaintext
97
- User: Whats the current app version?
113
+ User: What's the current app version?
98
114
 
99
115
  THINK: {"thought": "I should search for APP_VERSION in the constants file."}
100
116
  ACT: run_command("grep -n 'APP_VERSION' constants.py")
@@ -103,3 +119,17 @@ ACT: read_file("constants.py")
103
119
  OBSERVE: {"thought": "APP_VERSION is set to '2.4.1'. This is the current version."}
104
120
  RESPONSE: "Current version is 2.4.1 (from constants.py)"
105
121
  ```
122
+
123
+ ```plaintext
124
+ User: Tell me about @src/main.py
125
+
126
+ === FILE REFERENCE: src/main.py ===
127
+ ```python
128
+ def main():
129
+ print("Hello World")
130
+ ```
131
+ === END FILE REFERENCE: src/main.py ===
132
+
133
+ THINK: {"thought": "User is asking about the referenced file, not asking me to create it."}
134
+ RESPONSE: "The main.py file contains a simple main function that prints 'Hello World'."
135
+ ```
@@ -6,7 +6,7 @@ Includes file extension to language mapping and key formatting functions.
6
6
  """
7
7
 
8
8
  import os
9
- from typing import Set
9
+ from typing import List, Set, Tuple
10
10
 
11
11
 
12
12
  def key_to_title(key: str, uppercase_words: Set[str] = None) -> str:
@@ -50,14 +50,16 @@ def ext_to_lang(path: str) -> str:
50
50
  return "text"
51
51
 
52
52
 
53
- def expand_file_refs(text: str) -> str:
53
+ def expand_file_refs(text: str) -> Tuple[str, List[str]]:
54
54
  """Expand @file references with file contents wrapped in code fences.
55
55
 
56
56
  Args:
57
57
  text: The input text potentially containing @file references.
58
58
 
59
59
  Returns:
60
- Text with any @file references replaced by the file's contents.
60
+ Tuple[str, List[str]]: A tuple containing:
61
+ - Text with any @file references replaced by the file's contents
62
+ - List of absolute paths of files that were successfully expanded
61
63
 
62
64
  Raises:
63
65
  ValueError: If a referenced file does not exist or is too large.
@@ -69,6 +71,7 @@ def expand_file_refs(text: str) -> str:
69
71
  MSG_FILE_SIZE_LIMIT)
70
72
 
71
73
  pattern = re.compile(r"@([\w./_-]+)")
74
+ expanded_files = []
72
75
 
73
76
  def replacer(match: re.Match) -> str:
74
77
  path = match.group(1)
@@ -81,7 +84,13 @@ def expand_file_refs(text: str) -> str:
81
84
  with open(path, "r", encoding="utf-8") as f:
82
85
  content = f.read()
83
86
 
87
+ # Track the absolute path of the file
88
+ abs_path = os.path.abspath(path)
89
+ expanded_files.append(abs_path)
90
+
84
91
  lang = ext_to_lang(path)
85
- return f"```{lang}\n{content}\n```"
92
+ # Add clear headers to indicate this is a file reference, not code to write
93
+ return f"\n=== FILE REFERENCE: {path} ===\n```{lang}\n{content}\n```\n=== END FILE REFERENCE: {path} ===\n"
86
94
 
87
- return pattern.sub(replacer, text)
95
+ expanded_text = pattern.sub(replacer, text)
96
+ return expanded_text, expanded_files
@@ -0,0 +1,23 @@
1
+ """Simple token counting utility for estimating message sizes."""
2
+
3
+
4
+ def estimate_tokens(text: str) -> int:
5
+ """
6
+ Estimate token count using a simple character-based approximation.
7
+
8
+ This is a rough estimate: ~4 characters per token on average.
9
+ For more accurate counting, we would need tiktoken or similar.
10
+ """
11
+ if not text:
12
+ return 0
13
+
14
+ # Simple approximation: ~4 characters per token
15
+ # This is roughly accurate for English text
16
+ return len(text) // 4
17
+
18
+
19
+ def format_token_count(count: int) -> str:
20
+ """Format token count for display."""
21
+ if count >= 1000:
22
+ return f"{count:,}"
23
+ return str(count)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tunacode-cli
3
- Version: 0.0.29
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
@@ -73,6 +73,7 @@ src/tunacode/utils/import_cache.py
73
73
  src/tunacode/utils/ripgrep.py
74
74
  src/tunacode/utils/system.py
75
75
  src/tunacode/utils/text_utils.py
76
+ src/tunacode/utils/token_counter.py
76
77
  src/tunacode/utils/user_configuration.py
77
78
  src/tunacode_cli.egg-info/PKG-INFO
78
79
  src/tunacode_cli.egg-info/SOURCES.txt
@@ -87,6 +88,7 @@ tests/test_background_manager.py
87
88
  tests/test_config_setup_async.py
88
89
  tests/test_fallback_responses.py
89
90
  tests/test_fast_glob_search.py
91
+ tests/test_file_reference_context_tracking.py
90
92
  tests/test_file_reference_expansion.py
91
93
  tests/test_json_tool_parsing.py
92
94
  tests/test_orchestrator_file_references.py