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

tunacode/cli/repl.py CHANGED
@@ -344,7 +344,7 @@ async def process_request(text: str, state_manager: StateManager, output: bool =
344
344
  except CancelledError:
345
345
  await ui.muted(MSG_REQUEST_CANCELLED)
346
346
  except UserAbortError:
347
- await ui.muted(MSG_OPERATION_ABORTED)
347
+ await ui.muted(MSG_OPERATION_ABORTED_BY_USER)
348
348
  except UnexpectedModelBehavior as e:
349
349
  error_message = str(e)
350
350
  await ui.muted(error_message)
@@ -463,7 +463,6 @@ async def repl(state_manager: StateManager):
463
463
  state_manager.session.current_task = get_app().create_background_task(
464
464
  process_request(line, state_manager)
465
465
  )
466
- await state_manager.session.current_task
467
466
 
468
467
  state_manager.session.update_token_count()
469
468
  context_display = get_context_window_display(
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.45"
10
+ APP_VERSION = "0.0.47"
11
11
 
12
12
  # File patterns
13
13
  GUIDE_FILE_PATTERN = "{name}.md"
@@ -34,7 +34,7 @@ from tunacode.core.recursive import RecursiveTaskExecutor
34
34
  from tunacode.core.state import StateManager
35
35
  from tunacode.core.token_usage.api_response_parser import ApiResponseParser
36
36
  from tunacode.core.token_usage.cost_calculator import CostCalculator
37
- from tunacode.exceptions import ToolBatchingJSONError
37
+ from tunacode.exceptions import ToolBatchingJSONError, UserAbortError
38
38
  from tunacode.services.mcp import get_mcp_servers
39
39
  from tunacode.tools.bash import bash
40
40
  from tunacode.tools.glob import glob
@@ -752,300 +752,335 @@ async def process_request(
752
752
  tool_callback: Optional[ToolCallback] = None,
753
753
  streaming_callback: Optional[callable] = None,
754
754
  ) -> AgentRun:
755
- agent = get_or_create_agent(model, state_manager)
756
- mh = state_manager.session.messages.copy()
757
- # Get max iterations from config (default: 40)
758
- max_iterations = state_manager.session.user_config.get("settings", {}).get("max_iterations", 40)
759
- fallback_enabled = state_manager.session.user_config.get("settings", {}).get(
760
- "fallback_response", True
761
- )
755
+ try:
756
+ agent = get_or_create_agent(model, state_manager)
757
+ mh = state_manager.session.messages.copy()
758
+ # Get max iterations from config (default: 40)
759
+ max_iterations = state_manager.session.user_config.get("settings", {}).get(
760
+ "max_iterations", 40
761
+ )
762
+ fallback_enabled = state_manager.session.user_config.get("settings", {}).get(
763
+ "fallback_response", True
764
+ )
762
765
 
763
- # Check if recursive execution is enabled
764
- use_recursive = state_manager.session.user_config.get("settings", {}).get(
765
- "use_recursive_execution", True
766
- )
767
- recursive_threshold = state_manager.session.user_config.get("settings", {}).get(
768
- "recursive_complexity_threshold", 0.7
769
- )
766
+ # Check if recursive execution is enabled
767
+ use_recursive = state_manager.session.user_config.get("settings", {}).get(
768
+ "use_recursive_execution", True
769
+ )
770
+ recursive_threshold = state_manager.session.user_config.get("settings", {}).get(
771
+ "recursive_complexity_threshold", 0.7
772
+ )
770
773
 
771
- # Check if recursive execution should be used
772
- if use_recursive and state_manager.session.current_recursion_depth == 0:
773
- try:
774
- # Initialize recursive executor
775
- recursive_executor = RecursiveTaskExecutor(
776
- state_manager=state_manager,
777
- max_depth=state_manager.session.max_recursion_depth,
778
- min_complexity_threshold=recursive_threshold,
779
- default_iteration_budget=max_iterations,
780
- )
774
+ # Check if recursive execution should be used
775
+ if use_recursive and state_manager.session.current_recursion_depth == 0:
776
+ try:
777
+ # Initialize recursive executor
778
+ recursive_executor = RecursiveTaskExecutor(
779
+ state_manager=state_manager,
780
+ max_depth=state_manager.session.max_recursion_depth,
781
+ min_complexity_threshold=recursive_threshold,
782
+ default_iteration_budget=max_iterations,
783
+ )
784
+
785
+ # Analyze task complexity
786
+ complexity_result = await recursive_executor.decomposer.analyze_and_decompose(
787
+ message
788
+ )
781
789
 
782
- # Analyze task complexity
783
- complexity_result = await recursive_executor.decomposer.analyze_and_decompose(message)
790
+ if (
791
+ complexity_result.should_decompose
792
+ and complexity_result.total_complexity >= recursive_threshold
793
+ ):
794
+ if state_manager.session.show_thoughts:
795
+ from tunacode.ui import console as ui
784
796
 
785
- if (
786
- complexity_result.should_decompose
787
- and complexity_result.total_complexity >= recursive_threshold
788
- ):
789
- if state_manager.session.show_thoughts:
790
- from tunacode.ui import console as ui
797
+ await ui.muted(
798
+ f"\n🔄 RECURSIVE EXECUTION: Task complexity {complexity_result.total_complexity:.2f} >= {recursive_threshold}"
799
+ )
800
+ await ui.muted(f"Reasoning: {complexity_result.reasoning}")
801
+ await ui.muted(f"Subtasks: {len(complexity_result.subtasks)}")
791
802
 
792
- await ui.muted(
793
- f"\n🔄 RECURSIVE EXECUTION: Task complexity {complexity_result.total_complexity:.2f} >= {recursive_threshold}"
803
+ # Execute recursively
804
+ success, result, error = await recursive_executor.execute_task(
805
+ request=message, parent_task_id=None, depth=0
794
806
  )
795
- await ui.muted(f"Reasoning: {complexity_result.reasoning}")
796
- await ui.muted(f"Subtasks: {len(complexity_result.subtasks)}")
797
807
 
798
- # Execute recursively
799
- success, result, error = await recursive_executor.execute_task(
800
- request=message, parent_task_id=None, depth=0
801
- )
808
+ # For now, fall back to normal execution
809
+ # TODO: Properly integrate recursive execution results
810
+ pass
811
+ except Exception as e:
812
+ logger.warning(f"Recursive execution failed, falling back to normal: {e}")
813
+ # Continue with normal execution
802
814
 
803
- # For now, fall back to normal execution
804
- # TODO: Properly integrate recursive execution results
805
- pass
806
- except Exception as e:
807
- logger.warning(f"Recursive execution failed, falling back to normal: {e}")
808
- # Continue with normal execution
809
-
810
- from tunacode.configuration.models import ModelRegistry
811
- from tunacode.core.token_usage.usage_tracker import UsageTracker
812
-
813
- parser = ApiResponseParser()
814
- registry = ModelRegistry()
815
- calculator = CostCalculator(registry)
816
- usage_tracker = UsageTracker(parser, calculator, state_manager)
817
- response_state = ResponseState()
818
-
819
- # Reset iteration tracking for this request
820
- state_manager.session.iteration_count = 0
821
-
822
- # Create a request-level buffer for batching read-only tools across nodes
823
- tool_buffer = ToolBuffer()
824
-
825
- # Show TUNACODE.md preview if it was loaded and thoughts are enabled
826
- if state_manager.session.show_thoughts and hasattr(state_manager, "tunacode_preview"):
827
- from tunacode.ui import console as ui
828
-
829
- await ui.muted(state_manager.tunacode_preview)
830
- # Clear the preview after displaying it once
831
- delattr(state_manager, "tunacode_preview")
832
-
833
- # Show what we're sending to the API when thoughts are enabled
834
- if state_manager.session.show_thoughts:
835
- from tunacode.ui import console as ui
836
-
837
- await ui.muted("\n" + "=" * 60)
838
- await ui.muted("📤 SENDING TO API:")
839
- await ui.muted(f"Message: {message}")
840
- await ui.muted(f"Model: {model}")
841
- await ui.muted(f"Message History Length: {len(mh)}")
842
- await ui.muted("=" * 60)
843
-
844
- async with agent.iter(message, message_history=mh) as agent_run:
845
- i = 0
846
- async for node in agent_run:
847
- state_manager.session.current_iteration = i + 1
848
-
849
- # Handle token-level streaming for model request nodes
850
- if streaming_callback and STREAMING_AVAILABLE and Agent.is_model_request_node(node):
851
- async with node.stream(agent_run.ctx) as request_stream:
852
- async for event in request_stream:
853
- if isinstance(event, PartDeltaEvent) and isinstance(
854
- event.delta, TextPartDelta
855
- ):
856
- # Stream individual token deltas
857
- if event.delta.content_delta:
858
- await streaming_callback(event.delta.content_delta)
859
-
860
- await _process_node(
861
- node,
862
- tool_callback,
863
- state_manager,
864
- tool_buffer,
865
- streaming_callback,
866
- usage_tracker,
867
- )
868
- if hasattr(node, "result") and node.result and hasattr(node.result, "output"):
869
- if node.result.output:
870
- response_state.has_user_response = True
871
- i += 1
872
- state_manager.session.iteration_count = i
815
+ from tunacode.configuration.models import ModelRegistry
816
+ from tunacode.core.token_usage.usage_tracker import UsageTracker
873
817
 
874
- # Display iteration progress if thoughts are enabled
875
- if state_manager.session.show_thoughts:
876
- from tunacode.ui import console as ui
818
+ parser = ApiResponseParser()
819
+ registry = ModelRegistry()
820
+ calculator = CostCalculator(registry)
821
+ usage_tracker = UsageTracker(parser, calculator, state_manager)
822
+ response_state = ResponseState()
877
823
 
878
- await ui.muted(f"\nITERATION: {i}/{max_iterations}")
824
+ # Reset iteration tracking for this request
825
+ state_manager.session.iteration_count = 0
879
826
 
880
- # Show summary of tools used so far
881
- if state_manager.session.tool_calls:
882
- tool_summary = {}
883
- for tc in state_manager.session.tool_calls:
884
- tool_name = tc.get("tool", "unknown")
885
- tool_summary[tool_name] = tool_summary.get(tool_name, 0) + 1
827
+ # Create a request-level buffer for batching read-only tools across nodes
828
+ tool_buffer = ToolBuffer()
886
829
 
887
- summary_str = ", ".join(
888
- [f"{name}: {count}" for name, count in tool_summary.items()]
889
- )
890
- await ui.muted(f"TOOLS USED: {summary_str}")
830
+ # Show TUNACODE.md preview if it was loaded and thoughts are enabled
831
+ if state_manager.session.show_thoughts and hasattr(state_manager, "tunacode_preview"):
832
+ from tunacode.ui import console as ui
833
+
834
+ await ui.muted(state_manager.tunacode_preview)
835
+ # Clear the preview after displaying it once
836
+ delattr(state_manager, "tunacode_preview")
837
+
838
+ # Show what we're sending to the API when thoughts are enabled
839
+ if state_manager.session.show_thoughts:
840
+ from tunacode.ui import console as ui
841
+
842
+ await ui.muted("\n" + "=" * 60)
843
+ await ui.muted("📤 SENDING TO API:")
844
+ await ui.muted(f"Message: {message}")
845
+ await ui.muted(f"Model: {model}")
846
+ await ui.muted(f"Message History Length: {len(mh)}")
847
+ await ui.muted("=" * 60)
848
+
849
+ async with agent.iter(message, message_history=mh) as agent_run:
850
+ i = 0
851
+ async for node in agent_run:
852
+ state_manager.session.current_iteration = i + 1
853
+
854
+ # Handle token-level streaming for model request nodes
855
+ if streaming_callback and STREAMING_AVAILABLE and Agent.is_model_request_node(node):
856
+ async with node.stream(agent_run.ctx) as request_stream:
857
+ async for event in request_stream:
858
+ if isinstance(event, PartDeltaEvent) and isinstance(
859
+ event.delta, TextPartDelta
860
+ ):
861
+ # Stream individual token deltas
862
+ if event.delta.content_delta:
863
+ await streaming_callback(event.delta.content_delta)
864
+
865
+ await _process_node(
866
+ node,
867
+ tool_callback,
868
+ state_manager,
869
+ tool_buffer,
870
+ streaming_callback,
871
+ usage_tracker,
872
+ )
873
+ if hasattr(node, "result") and node.result and hasattr(node.result, "output"):
874
+ if node.result.output:
875
+ response_state.has_user_response = True
876
+ i += 1
877
+ state_manager.session.iteration_count = i
891
878
 
892
- if i >= max_iterations:
879
+ # Display iteration progress if thoughts are enabled
893
880
  if state_manager.session.show_thoughts:
894
881
  from tunacode.ui import console as ui
895
882
 
896
- await ui.warning(f"Reached maximum iterations ({max_iterations})")
897
- break
883
+ await ui.muted(f"\nITERATION: {i}/{max_iterations}")
898
884
 
899
- # Final flush: execute any remaining buffered read-only tools
900
- if tool_callback and tool_buffer.has_tasks():
901
- import time
885
+ # Show summary of tools used so far
886
+ if state_manager.session.tool_calls:
887
+ tool_summary = {}
888
+ for tc in state_manager.session.tool_calls:
889
+ tool_name = tc.get("tool", "unknown")
890
+ tool_summary[tool_name] = tool_summary.get(tool_name, 0) + 1
902
891
 
903
- from tunacode.ui import console as ui
904
-
905
- buffered_tasks = tool_buffer.flush()
906
- start_time = time.time()
892
+ summary_str = ", ".join(
893
+ [f"{name}: {count}" for name, count in tool_summary.items()]
894
+ )
895
+ await ui.muted(f"TOOLS USED: {summary_str}")
907
896
 
908
- if state_manager.session.show_thoughts:
909
- await ui.muted("\n" + "=" * 60)
910
- await ui.muted(
911
- f"🚀 FINAL BATCH: Executing {len(buffered_tasks)} buffered read-only tools"
912
- )
913
- await ui.muted("=" * 60)
897
+ if i >= max_iterations:
898
+ if state_manager.session.show_thoughts:
899
+ from tunacode.ui import console as ui
914
900
 
915
- for idx, (part, node) in enumerate(buffered_tasks, 1):
916
- tool_desc = f" [{idx}] {part.tool_name}"
917
- if hasattr(part, "args") and isinstance(part.args, dict):
918
- if part.tool_name == "read_file" and "file_path" in part.args:
919
- tool_desc += f" → {part.args['file_path']}"
920
- elif part.tool_name == "grep" and "pattern" in part.args:
921
- tool_desc += f" → pattern: '{part.args['pattern']}'"
922
- if "include_files" in part.args:
923
- tool_desc += f", files: '{part.args['include_files']}'"
924
- elif part.tool_name == "list_dir" and "directory" in part.args:
925
- tool_desc += f" → {part.args['directory']}"
926
- elif part.tool_name == "glob" and "pattern" in part.args:
927
- tool_desc += f" → pattern: '{part.args['pattern']}'"
928
- await ui.muted(tool_desc)
929
- await ui.muted("=" * 60)
901
+ await ui.warning(f"Reached maximum iterations ({max_iterations})")
902
+ break
930
903
 
931
- await execute_tools_parallel(buffered_tasks, tool_callback)
904
+ # Final flush: execute any remaining buffered read-only tools
905
+ if tool_callback and tool_buffer.has_tasks():
906
+ import time
932
907
 
933
- elapsed_time = (time.time() - start_time) * 1000
934
- sequential_estimate = len(buffered_tasks) * 100
935
- speedup = sequential_estimate / elapsed_time if elapsed_time > 0 else 1.0
908
+ from tunacode.ui import console as ui
936
909
 
937
- if state_manager.session.show_thoughts:
938
- await ui.muted(
939
- f"✅ Final batch completed in {elapsed_time:.0f}ms "
940
- f"(~{speedup:.1f}x faster than sequential)\n"
941
- )
910
+ buffered_tasks = tool_buffer.flush()
911
+ start_time = time.time()
942
912
 
943
- # If we need to add a fallback response, create a wrapper
944
- if not response_state.has_user_response and i >= max_iterations and fallback_enabled:
945
- patch_tool_messages("Task incomplete", state_manager=state_manager)
946
- response_state.has_final_synthesis = True
947
-
948
- # Extract context from the agent run
949
- tool_calls_summary = []
950
- files_modified = set()
951
- commands_run = []
952
-
953
- # Analyze message history for context
954
- for msg in state_manager.session.messages:
955
- if hasattr(msg, "parts"):
956
- for part in msg.parts:
957
- if hasattr(part, "part_kind") and part.part_kind == "tool-call":
958
- tool_name = getattr(part, "tool_name", "unknown")
959
- tool_calls_summary.append(tool_name)
960
-
961
- # Track specific operations
962
- if tool_name in ["write_file", "update_file"] and hasattr(part, "args"):
963
- if isinstance(part.args, dict) and "file_path" in part.args:
964
- files_modified.add(part.args["file_path"])
965
- elif tool_name in ["run_command", "bash"] and hasattr(part, "args"):
966
- if isinstance(part.args, dict) and "command" in part.args:
967
- commands_run.append(part.args["command"])
968
-
969
- # Build fallback response with context
970
- fallback = FallbackResponse(
971
- summary="Reached maximum iterations without producing a final response.",
972
- progress=f"Completed {i} iterations (limit: {max_iterations})",
973
- )
913
+ if state_manager.session.show_thoughts:
914
+ await ui.muted("\n" + "=" * 60)
915
+ await ui.muted(
916
+ f"🚀 FINAL BATCH: Executing {len(buffered_tasks)} buffered read-only tools"
917
+ )
918
+ await ui.muted("=" * 60)
974
919
 
975
- # Get verbosity setting
976
- verbosity = state_manager.session.user_config.get("settings", {}).get(
977
- "fallback_verbosity", "normal"
978
- )
920
+ for idx, (part, node) in enumerate(buffered_tasks, 1):
921
+ tool_desc = f" [{idx}] {part.tool_name}"
922
+ if hasattr(part, "args") and isinstance(part.args, dict):
923
+ if part.tool_name == "read_file" and "file_path" in part.args:
924
+ tool_desc += f" → {part.args['file_path']}"
925
+ elif part.tool_name == "grep" and "pattern" in part.args:
926
+ tool_desc += f" → pattern: '{part.args['pattern']}'"
927
+ if "include_files" in part.args:
928
+ tool_desc += f", files: '{part.args['include_files']}'"
929
+ elif part.tool_name == "list_dir" and "directory" in part.args:
930
+ tool_desc += f" → {part.args['directory']}"
931
+ elif part.tool_name == "glob" and "pattern" in part.args:
932
+ tool_desc += f" → pattern: '{part.args['pattern']}'"
933
+ await ui.muted(tool_desc)
934
+ await ui.muted("=" * 60)
979
935
 
980
- if verbosity in ["normal", "detailed"]:
981
- # Add what was attempted
982
- if tool_calls_summary:
983
- tool_counts = {}
984
- for tool in tool_calls_summary:
985
- tool_counts[tool] = tool_counts.get(tool, 0) + 1
986
-
987
- fallback.issues.append(f"Executed {len(tool_calls_summary)} tool calls:")
988
- for tool, count in sorted(tool_counts.items()):
989
- fallback.issues.append(f" • {tool}: {count}x")
990
-
991
- if verbosity == "detailed":
992
- if files_modified:
993
- fallback.issues.append(f"\nFiles modified ({len(files_modified)}):")
994
- for f in sorted(files_modified)[:5]: # Limit to 5 files
995
- fallback.issues.append(f" • {f}")
996
- if len(files_modified) > 5:
997
- fallback.issues.append(f" • ... and {len(files_modified) - 5} more")
998
-
999
- if commands_run:
1000
- fallback.issues.append(f"\nCommands executed ({len(commands_run)}):")
1001
- for cmd in commands_run[:3]: # Limit to 3 commands
1002
- # Truncate long commands
1003
- display_cmd = cmd if len(cmd) <= 60 else cmd[:57] + "..."
1004
- fallback.issues.append(f" • {display_cmd}")
1005
- if len(commands_run) > 3:
1006
- fallback.issues.append(f" • ... and {len(commands_run) - 3} more")
1007
-
1008
- # Add helpful next steps
1009
- fallback.next_steps.append(
1010
- "The task may be too complex - try breaking it into smaller steps"
1011
- )
1012
- fallback.next_steps.append("Check the output above for any errors or partial progress")
1013
- if files_modified:
1014
- fallback.next_steps.append("Review modified files to see what changes were made")
936
+ await execute_tools_parallel(buffered_tasks, tool_callback)
1015
937
 
1016
- # Create comprehensive output
1017
- output_parts = [fallback.summary, ""]
938
+ elapsed_time = (time.time() - start_time) * 1000
939
+ sequential_estimate = len(buffered_tasks) * 100
940
+ speedup = sequential_estimate / elapsed_time if elapsed_time > 0 else 1.0
1018
941
 
1019
- if fallback.progress:
1020
- output_parts.append(f"Progress: {fallback.progress}")
942
+ if state_manager.session.show_thoughts:
943
+ await ui.muted(
944
+ f"✅ Final batch completed in {elapsed_time:.0f}ms "
945
+ f"(~{speedup:.1f}x faster than sequential)\n"
946
+ )
1021
947
 
1022
- if fallback.issues:
1023
- output_parts.append("\nWhat happened:")
1024
- output_parts.extend(fallback.issues)
948
+ # If we need to add a fallback response, create a wrapper
949
+ if not response_state.has_user_response and i >= max_iterations and fallback_enabled:
950
+ patch_tool_messages("Task incomplete", state_manager=state_manager)
951
+ response_state.has_final_synthesis = True
952
+
953
+ # Extract context from the agent run
954
+ tool_calls_summary = []
955
+ files_modified = set()
956
+ commands_run = []
957
+
958
+ # Analyze message history for context
959
+ for msg in state_manager.session.messages:
960
+ if hasattr(msg, "parts"):
961
+ for part in msg.parts:
962
+ if hasattr(part, "part_kind") and part.part_kind == "tool-call":
963
+ tool_name = getattr(part, "tool_name", "unknown")
964
+ tool_calls_summary.append(tool_name)
965
+
966
+ # Track specific operations
967
+ if tool_name in ["write_file", "update_file"] and hasattr(
968
+ part, "args"
969
+ ):
970
+ if isinstance(part.args, dict) and "file_path" in part.args:
971
+ files_modified.add(part.args["file_path"])
972
+ elif tool_name in ["run_command", "bash"] and hasattr(part, "args"):
973
+ if isinstance(part.args, dict) and "command" in part.args:
974
+ commands_run.append(part.args["command"])
975
+
976
+ # Build fallback response with context
977
+ fallback = FallbackResponse(
978
+ summary="Reached maximum iterations without producing a final response.",
979
+ progress=f"Completed {i} iterations (limit: {max_iterations})",
980
+ )
1025
981
 
1026
- if fallback.next_steps:
1027
- output_parts.append("\nSuggested next steps:")
1028
- for step in fallback.next_steps:
1029
- output_parts.append(f" • {step}")
982
+ # Get verbosity setting
983
+ verbosity = state_manager.session.user_config.get("settings", {}).get(
984
+ "fallback_verbosity", "normal"
985
+ )
1030
986
 
1031
- comprehensive_output = "\n".join(output_parts)
987
+ if verbosity in ["normal", "detailed"]:
988
+ # Add what was attempted
989
+ if tool_calls_summary:
990
+ tool_counts = {}
991
+ for tool in tool_calls_summary:
992
+ tool_counts[tool] = tool_counts.get(tool, 0) + 1
993
+
994
+ fallback.issues.append(f"Executed {len(tool_calls_summary)} tool calls:")
995
+ for tool, count in sorted(tool_counts.items()):
996
+ fallback.issues.append(f" • {tool}: {count}x")
997
+
998
+ if verbosity == "detailed":
999
+ if files_modified:
1000
+ fallback.issues.append(f"\nFiles modified ({len(files_modified)}):")
1001
+ for f in sorted(files_modified)[:5]: # Limit to 5 files
1002
+ fallback.issues.append(f" • {f}")
1003
+ if len(files_modified) > 5:
1004
+ fallback.issues.append(
1005
+ f" • ... and {len(files_modified) - 5} more"
1006
+ )
1007
+
1008
+ if commands_run:
1009
+ fallback.issues.append(f"\nCommands executed ({len(commands_run)}):")
1010
+ for cmd in commands_run[:3]: # Limit to 3 commands
1011
+ # Truncate long commands
1012
+ display_cmd = cmd if len(cmd) <= 60 else cmd[:57] + "..."
1013
+ fallback.issues.append(f" • {display_cmd}")
1014
+ if len(commands_run) > 3:
1015
+ fallback.issues.append(f" • ... and {len(commands_run) - 3} more")
1016
+
1017
+ # Add helpful next steps
1018
+ fallback.next_steps.append(
1019
+ "The task may be too complex - try breaking it into smaller steps"
1020
+ )
1021
+ fallback.next_steps.append(
1022
+ "Check the output above for any errors or partial progress"
1023
+ )
1024
+ if files_modified:
1025
+ fallback.next_steps.append(
1026
+ "Review modified files to see what changes were made"
1027
+ )
1032
1028
 
1033
- # Create a wrapper object that mimics AgentRun with the required attributes
1034
- class AgentRunWrapper:
1035
- def __init__(self, wrapped_run, fallback_result):
1029
+ # Create comprehensive output
1030
+ output_parts = [fallback.summary, ""]
1031
+
1032
+ if fallback.progress:
1033
+ output_parts.append(f"Progress: {fallback.progress}")
1034
+
1035
+ if fallback.issues:
1036
+ output_parts.append("\nWhat happened:")
1037
+ output_parts.extend(fallback.issues)
1038
+
1039
+ if fallback.next_steps:
1040
+ output_parts.append("\nSuggested next steps:")
1041
+ for step in fallback.next_steps:
1042
+ output_parts.append(f" • {step}")
1043
+
1044
+ comprehensive_output = "\n".join(output_parts)
1045
+
1046
+ # Create a wrapper object that mimics AgentRun with the required attributes
1047
+ class AgentRunWrapper:
1048
+ def __init__(self, wrapped_run, fallback_result):
1049
+ self._wrapped = wrapped_run
1050
+ self._result = fallback_result
1051
+ self.response_state = response_state
1052
+
1053
+ def __getattribute__(self, name):
1054
+ # Handle special attributes first to avoid conflicts
1055
+ if name in ["_wrapped", "_result", "response_state"]:
1056
+ return object.__getattribute__(self, name)
1057
+
1058
+ # Explicitly handle 'result' to return our fallback result
1059
+ if name == "result":
1060
+ return object.__getattribute__(self, "_result")
1061
+
1062
+ # Delegate all other attributes to the wrapped object
1063
+ try:
1064
+ return getattr(object.__getattribute__(self, "_wrapped"), name)
1065
+ except AttributeError:
1066
+ raise AttributeError(
1067
+ f"'{type(self).__name__}' object has no attribute '{name}'"
1068
+ )
1069
+
1070
+ return AgentRunWrapper(agent_run, SimpleResult(comprehensive_output))
1071
+
1072
+ # For non-fallback cases, we still need to handle the response_state
1073
+ # Create a minimal wrapper just to add response_state
1074
+ class AgentRunWithState:
1075
+ def __init__(self, wrapped_run):
1036
1076
  self._wrapped = wrapped_run
1037
- self._result = fallback_result
1038
1077
  self.response_state = response_state
1039
1078
 
1040
1079
  def __getattribute__(self, name):
1041
- # Handle special attributes first to avoid conflicts
1042
- if name in ["_wrapped", "_result", "response_state"]:
1080
+ # Handle special attributes first
1081
+ if name in ["_wrapped", "response_state"]:
1043
1082
  return object.__getattribute__(self, name)
1044
1083
 
1045
- # Explicitly handle 'result' to return our fallback result
1046
- if name == "result":
1047
- return object.__getattribute__(self, "_result")
1048
-
1049
1084
  # Delegate all other attributes to the wrapped object
1050
1085
  try:
1051
1086
  return getattr(object.__getattribute__(self, "_wrapped"), name)
@@ -1054,26 +1089,6 @@ async def process_request(
1054
1089
  f"'{type(self).__name__}' object has no attribute '{name}'"
1055
1090
  )
1056
1091
 
1057
- return AgentRunWrapper(agent_run, SimpleResult(comprehensive_output))
1058
-
1059
- # For non-fallback cases, we still need to handle the response_state
1060
- # Create a minimal wrapper just to add response_state
1061
- class AgentRunWithState:
1062
- def __init__(self, wrapped_run):
1063
- self._wrapped = wrapped_run
1064
- self.response_state = response_state
1065
-
1066
- def __getattribute__(self, name):
1067
- # Handle special attributes first
1068
- if name in ["_wrapped", "response_state"]:
1069
- return object.__getattribute__(self, name)
1070
-
1071
- # Delegate all other attributes to the wrapped object
1072
- try:
1073
- return getattr(object.__getattribute__(self, "_wrapped"), name)
1074
- except AttributeError:
1075
- raise AttributeError(
1076
- f"'{type(self).__name__}' object has no attribute '{name}'"
1077
- )
1078
-
1079
- return AgentRunWithState(agent_run)
1092
+ return AgentRunWithState(agent_run)
1093
+ except asyncio.CancelledError:
1094
+ raise UserAbortError("User aborted the request.")
@@ -49,6 +49,14 @@ class CostCalculator:
49
49
 
50
50
  pricing = model_config.pricing
51
51
 
52
+ # Safety check for None pricing
53
+ if not pricing:
54
+ return 0.0
55
+
56
+ # Safety check for None pricing attributes
57
+ if pricing.input is None or pricing.output is None:
58
+ return 0.0
59
+
52
60
  input_cost = (prompt_tokens / TOKENS_PER_MILLION) * pricing.input
53
61
 
54
62
  output_cost = (completion_tokens / TOKENS_PER_MILLION) * pricing.output
@@ -60,18 +60,27 @@ class UsageTracker(UsageTrackerProtocol):
60
60
  if not api_model_name.startswith(provider_prefix + ":"):
61
61
  final_model_name = f"{provider_prefix}:{api_model_name}"
62
62
 
63
- return self.calculator.calculate_cost(
63
+ cost = self.calculator.calculate_cost(
64
64
  prompt_tokens=parsed_data.get("prompt_tokens", 0),
65
65
  completion_tokens=parsed_data.get("completion_tokens", 0),
66
66
  model_name=final_model_name,
67
67
  )
68
68
 
69
+ # Ensure cost is never None
70
+ return cost if cost is not None else 0.0
71
+
69
72
  def _update_state(self, parsed_data: dict, cost: float):
70
73
  """Updates the last_call and session_total usage in the state."""
71
74
  session = self.state_manager.session
72
75
  prompt_tokens = parsed_data.get("prompt_tokens", 0)
73
76
  completion_tokens = parsed_data.get("completion_tokens", 0)
74
77
 
78
+ # Initialize usage dicts if they're None
79
+ if session.last_call_usage is None:
80
+ session.last_call_usage = {"prompt_tokens": 0, "completion_tokens": 0, "cost": 0.0}
81
+ if session.session_total_usage is None:
82
+ session.session_total_usage = {"prompt_tokens": 0, "completion_tokens": 0, "cost": 0.0}
83
+
75
84
  # Update last call usage
76
85
  session.last_call_usage["prompt_tokens"] = prompt_tokens
77
86
  session.last_call_usage["completion_tokens"] = completion_tokens
@@ -85,6 +94,13 @@ class UsageTracker(UsageTrackerProtocol):
85
94
  async def _display_summary(self):
86
95
  """Formats and prints the usage summary to the console."""
87
96
  session = self.state_manager.session
97
+
98
+ # Initialize usage dicts if they're None
99
+ if session.last_call_usage is None:
100
+ session.last_call_usage = {"prompt_tokens": 0, "completion_tokens": 0, "cost": 0.0}
101
+ if session.session_total_usage is None:
102
+ session.session_total_usage = {"prompt_tokens": 0, "completion_tokens": 0, "cost": 0.0}
103
+
88
104
  prompt = session.last_call_usage["prompt_tokens"]
89
105
  completion = session.last_call_usage["completion_tokens"]
90
106
  last_cost = session.last_call_usage["cost"]
tunacode/ui/input.py CHANGED
@@ -75,12 +75,13 @@ async def multiline_input(
75
75
  state_manager: Optional[StateManager] = None, command_registry=None
76
76
  ) -> str:
77
77
  """Get multiline input from the user with @file completion and highlighting."""
78
- kb = create_key_bindings()
78
+ kb = create_key_bindings(state_manager)
79
79
  placeholder = formatted_text(
80
80
  (
81
81
  "<darkgrey>"
82
82
  "<bold>Enter</bold> to submit • "
83
83
  "<bold>Esc + Enter</bold> for new line • "
84
+ "<bold>Esc</bold> to cancel • "
84
85
  "<bold>/help</bold> for commands"
85
86
  "</darkgrey>"
86
87
  )
@@ -1,9 +1,15 @@
1
1
  """Key binding handlers for TunaCode UI."""
2
2
 
3
+ import logging
4
+
3
5
  from prompt_toolkit.key_binding import KeyBindings
4
6
 
7
+ from ..core.state import StateManager
8
+
9
+ logger = logging.getLogger(__name__)
5
10
 
6
- def create_key_bindings() -> KeyBindings:
11
+
12
+ def create_key_bindings(state_manager: StateManager = None) -> KeyBindings:
7
13
  """Create and configure key bindings for the UI."""
8
14
  kb = KeyBindings()
9
15
 
@@ -22,4 +28,14 @@ def create_key_bindings() -> KeyBindings:
22
28
  """Insert a newline when escape then enter is pressed."""
23
29
  event.current_buffer.insert_text("\n")
24
30
 
31
+ @kb.add("escape")
32
+ def _escape(event):
33
+ """Immediately interrupts the current operation."""
34
+ current_task = state_manager.session.current_task if state_manager else None
35
+ if current_task and not current_task.done():
36
+ logger.debug("Interrupting current task")
37
+ current_task.cancel()
38
+ else:
39
+ logger.debug("Escape key pressed outside task context")
40
+
25
41
  return kb
@@ -1,11 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tunacode-cli
3
- Version: 0.0.45
3
+ Version: 0.0.47
4
4
  Summary: Your agentic CLI developer.
5
5
  Author-email: larock22 <noreply@github.com>
6
- License-Expression: MIT
7
- Project-URL: Homepage, https://github.com/larock22/tunacode
8
- Project-URL: Repository, https://github.com/larock22/tunacode
6
+ License: MIT
7
+ Project-URL: Homepage, https://tunacode.xyz/
8
+ Project-URL: Repository, https://github.com/alchemiststudiosDOTai/tunacode
9
+ Project-URL: Issues, https://github.com/alchemiststudiosDOTai/tunacode/issues
10
+ Project-URL: Documentation, https://github.com/alchemiststudiosDOTai/tunacode#readme
9
11
  Keywords: cli,agent,development,automation
10
12
  Classifier: Development Status :: 4 - Beta
11
13
  Classifier: Intended Audience :: Developers
@@ -16,7 +18,7 @@ Classifier: Programming Language :: Python :: 3.12
16
18
  Classifier: Programming Language :: Python :: 3.13
17
19
  Classifier: Topic :: Software Development
18
20
  Classifier: Topic :: Utilities
19
- Requires-Python: >=3.10
21
+ Requires-Python: <3.14,>=3.10
20
22
  Description-Content-Type: text/markdown
21
23
  License-File: LICENSE
22
24
  Requires-Dist: typer==0.15.3
@@ -46,7 +48,7 @@ Dynamic: license-file
46
48
 
47
49
  **AI-powered CLI coding assistant**
48
50
 
49
- ![Demo](docs/assets/demo.gif)
51
+ ![TunaCode Example](assets/tunacode_example.png)
50
52
 
51
53
  </div>
52
54
 
@@ -62,6 +64,29 @@ wget -qO- https://raw.githubusercontent.com/alchemiststudiosDOTai/tunacode/maste
62
64
  pip install tunacode-cli
63
65
  ```
64
66
 
67
+ ## Development Installation
68
+
69
+ For contributors and developers who want to work on TunaCode:
70
+
71
+ ```bash
72
+ # Clone the repository
73
+ git clone https://github.com/alchemiststudiosDOTai/tunacode.git
74
+ cd tunacode
75
+
76
+ # Quick setup (recommended)
77
+ ./scripts/setup_dev_env.sh
78
+
79
+ # Or manual setup
80
+ python3 -m venv venv
81
+ source venv/bin/activate # On Windows: venv\Scripts\activate
82
+ pip install -e ".[dev]"
83
+
84
+ # Verify installation
85
+ python -m tunacode --version
86
+ ```
87
+
88
+ See [Development Guide](docs/DEVELOPMENT.md) for detailed instructions.
89
+
65
90
  ## Configuration
66
91
 
67
92
  Choose your AI provider and set your API key:
@@ -139,6 +164,7 @@ _Note: While the tool is fully functional, we're focusing on stability and core
139
164
  - [**Advanced Configuration**](docs/ADVANCED-CONFIG.md) - Provider setup, MCP, customization
140
165
  - [**Architecture**](docs/ARCHITECTURE.md) - Source code organization and design
141
166
  - [**Development**](docs/DEVELOPMENT.md) - Contributing and development setup
167
+ - [**Troubleshooting**](docs/TROUBLESHOOTING.md) - Common issues and solutions
142
168
 
143
169
  ## Links
144
170
 
@@ -1,5 +1,5 @@
1
1
  tunacode/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- tunacode/constants.py,sha256=RivEbBYCMFVkeAO8WwknYut7dIRzAbDjtpVIWMYhdS8,5168
2
+ tunacode/constants.py,sha256=833fgfAmkcJmU1RmSYDhGCOzoU-eUMdgbcfBBwUQOZ4,5168
3
3
  tunacode/context.py,sha256=_gXVCyjU052jlyRAl9tklZSwl5U_zI_EIX8XN87VVWE,2786
4
4
  tunacode/exceptions.py,sha256=oDO1SVKOgjcKIylwqdbqh_g5my4roU5mB9Nv4n_Vb0s,3877
5
5
  tunacode/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -7,7 +7,7 @@ tunacode/setup.py,sha256=XPt4eAK-qcIZQv64jGZ_ryxcImDwps9OmXjJfIS1xcs,1899
7
7
  tunacode/types.py,sha256=Czq7jYXHq7fZQtyqkCN5_7eEu1wyYUcB50C6v3sTWDw,8188
8
8
  tunacode/cli/__init__.py,sha256=zgs0UbAck8hfvhYsWhWOfBe5oK09ug2De1r4RuQZREA,55
9
9
  tunacode/cli/main.py,sha256=erP6jNXcxVQOVn8sm6uNaLEAYevniVXl6Sem872mW68,2755
10
- tunacode/cli/repl.py,sha256=58UJbKUcKCd1D1x0uOA5ipudZiiYOr-NXuJ6RmEn5_w,18962
10
+ tunacode/cli/repl.py,sha256=rSsUievdOdTkxzHNIEgUHbuWbk0ki78743Wz5hVFe9s,18917
11
11
  tunacode/cli/commands/__init__.py,sha256=zmE9JcJ9Qd2xJhgdS4jMDJOoZsrAZmL5MAFxbKkk7F8,1670
12
12
  tunacode/cli/commands/base.py,sha256=GxUuDsDSpz0iXryy8MrEw88UM3C3yxL__kDK1QhshoA,2517
13
13
  tunacode/cli/commands/registry.py,sha256=XVuLpp5S4Fw7GfIZfLrVZFo4jMLMNmYNpYN7xWgXyOk,8223
@@ -27,7 +27,7 @@ tunacode/core/code_index.py,sha256=jgAx3lSWP_DwnyiP5Jkm1YvX4JJyI4teMzlNrJSpEOA,1
27
27
  tunacode/core/state.py,sha256=2lsDgq0uIIc_MnXPE9SG_1fYFBDWWlgqgqm2Ik1iFBs,5599
28
28
  tunacode/core/tool_handler.py,sha256=BPjR013OOO0cLXPdLeL2FDK0ixUwOYu59FfHdcdFhp4,2277
29
29
  tunacode/core/agents/__init__.py,sha256=UUJiPYb91arwziSpjd7vIk7XNGA_4HQbsOIbskSqevA,149
30
- tunacode/core/agents/main.py,sha256=jaEGGyqlBdu_GWGAQSIz0jUMkBxUebcgza4VrlrdQls,45967
30
+ tunacode/core/agents/main.py,sha256=BRy3joJnn4GpUW47PyhiKK1JLMSamlK7wDMLwAGzSG0,47441
31
31
  tunacode/core/agents/utils.py,sha256=7kJAiUlkyWO3-b4T07XsGgycVrcNhv3NEPLdaktBnP4,12847
32
32
  tunacode/core/background/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
33
33
  tunacode/core/background/manager.py,sha256=rJdl3eDLTQwjbT7VhxXcJbZopCNR3M8ZGMbmeVnwwMc,1126
@@ -46,8 +46,8 @@ tunacode/core/setup/coordinator.py,sha256=oVTN2xIeJERXitVJpkIk9tDGLs1D1bxIRmaogJ
46
46
  tunacode/core/setup/environment_setup.py,sha256=n3IrObKEynHZSwtUJ1FddMg2C4sHz7ca42awemImV8s,2225
47
47
  tunacode/core/setup/git_safety_setup.py,sha256=CRIqrQt0QUJQRS344njty_iCqTorrDhHlXRuET7w0Tk,6714
48
48
  tunacode/core/token_usage/api_response_parser.py,sha256=CTtqGaFaxpkzkW3TEbe00QJzyRULpWN1EQxIYMleseg,1622
49
- tunacode/core/token_usage/cost_calculator.py,sha256=oQPMphGhqIt7NKdOg1o5Zbo59_nwFWmRJMQ30ViiCWs,1835
50
- tunacode/core/token_usage/usage_tracker.py,sha256=uKCpdZgmDfLauwsawSCifMu0kJE3lAnK7Sjd-0KYUgA,3894
49
+ tunacode/core/token_usage/cost_calculator.py,sha256=RjO-O0JENBuGOrWP7QgBZlZxeXC-PAIr8tj_9p_BxOU,2058
50
+ tunacode/core/token_usage/usage_tracker.py,sha256=kuAjUCyQkFykPy5mqsLRbKhZW298pyiCuFGn-ptBpy4,4657
51
51
  tunacode/prompts/system.md,sha256=hXpjZ8Yiv2Acr2_6EmC2uOklP8FbmvyYR9oais-1KLk,16290
52
52
  tunacode/services/__init__.py,sha256=w_E8QK6RnvKSvU866eDe8BCRV26rAm4d3R-Yg06OWCU,19
53
53
  tunacode/services/mcp.py,sha256=R48X73KQjQ9vwhBrtbWHSBl-4K99QXmbIhh5J_1Gezo,3046
@@ -68,8 +68,8 @@ tunacode/ui/completers.py,sha256=Jx1zyCESwdm_4ZopvCBtb0bCJF-bRy8aBWG2yhPQtDc,487
68
68
  tunacode/ui/console.py,sha256=icb7uYrV8XmZg9glreEy5MrvDkmrKxbf_ZkNqElN1uE,2120
69
69
  tunacode/ui/constants.py,sha256=A76B_KpM8jCuBYRg4cPmhi8_j6LLyWttO7_jjv47r3w,421
70
70
  tunacode/ui/decorators.py,sha256=e2KM-_pI5EKHa2M045IjUe4rPkTboxaKHXJT0K3461g,1914
71
- tunacode/ui/input.py,sha256=E_zAJqNYoAVFA-j4xE9Qgs22y-GrdSZNqiseX-Or0ho,2955
72
- tunacode/ui/keybindings.py,sha256=h0MlD73CW_3i2dQzb9EFSPkqy0raZ_isgjxUiA9u6ts,691
71
+ tunacode/ui/input.py,sha256=x_7G9VVdvydpEk2kcyG-OBKND5lL5ADbiGcDXF1n5UA,3014
72
+ tunacode/ui/keybindings.py,sha256=tn0q0eRw72j8xPWX0673Xc-vmwlvFEyuhy3C451ntfE,1233
73
73
  tunacode/ui/lexers.py,sha256=tmg4ic1enyTRLzanN5QPP7D_0n12YjX_8ZhsffzhXA4,1340
74
74
  tunacode/ui/output.py,sha256=51O0VHajte4dXHK5Az5SSP4IOb2q5SbCwvqdAoxyg7c,5665
75
75
  tunacode/ui/panels.py,sha256=dBZEVIJqliWreY-hz6HpW5rdBmPOJZ6sPrv8KQ3eNhk,8570
@@ -91,9 +91,9 @@ tunacode/utils/system.py,sha256=FSoibTIH0eybs4oNzbYyufIiV6gb77QaeY2yGqW39AY,1138
91
91
  tunacode/utils/text_utils.py,sha256=6YBD9QfkDO44-6jxnwRWIpmfIifPG-NqMzy_O2NAouc,7277
92
92
  tunacode/utils/token_counter.py,sha256=lLbkrNUraRQn5RMhwnGurqq1RHFDyn4AaFhruONWIxo,2690
93
93
  tunacode/utils/user_configuration.py,sha256=Ilz8dpGVJDBE2iLWHAPT0xR8D51VRKV3kIbsAz8Bboc,3275
94
- tunacode_cli-0.0.45.dist-info/licenses/LICENSE,sha256=Btzdu2kIoMbdSp6OyCLupB1aRgpTCJ_szMimgEnpkkE,1056
95
- tunacode_cli-0.0.45.dist-info/METADATA,sha256=ZEVJDRAxDDjMG8DR64Hj9_Txw7h0lD9YwrhAAwft_nY,5137
96
- tunacode_cli-0.0.45.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
97
- tunacode_cli-0.0.45.dist-info/entry_points.txt,sha256=hbkytikj4dGu6rizPuAd_DGUPBGF191RTnhr9wdhORY,51
98
- tunacode_cli-0.0.45.dist-info/top_level.txt,sha256=lKy2P6BWNi5XSA4DHFvyjQ14V26lDZctwdmhEJrxQbU,9
99
- tunacode_cli-0.0.45.dist-info/RECORD,,
94
+ tunacode_cli-0.0.47.dist-info/licenses/LICENSE,sha256=Btzdu2kIoMbdSp6OyCLupB1aRgpTCJ_szMimgEnpkkE,1056
95
+ tunacode_cli-0.0.47.dist-info/METADATA,sha256=oHfTMPtEUgbgVd7Nm0bb39wAKAuPKRHskBpZmDnBxMM,5902
96
+ tunacode_cli-0.0.47.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
97
+ tunacode_cli-0.0.47.dist-info/entry_points.txt,sha256=hbkytikj4dGu6rizPuAd_DGUPBGF191RTnhr9wdhORY,51
98
+ tunacode_cli-0.0.47.dist-info/top_level.txt,sha256=lKy2P6BWNi5XSA4DHFvyjQ14V26lDZctwdmhEJrxQbU,9
99
+ tunacode_cli-0.0.47.dist-info/RECORD,,