tunacode-cli 0.0.35__py3-none-any.whl → 0.0.37__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of tunacode-cli might be problematic. Click here for more details.

Files changed (32) hide show
  1. tunacode/cli/commands/__init__.py +62 -0
  2. tunacode/cli/commands/base.py +99 -0
  3. tunacode/cli/commands/implementations/__init__.py +38 -0
  4. tunacode/cli/commands/implementations/conversation.py +115 -0
  5. tunacode/cli/commands/implementations/debug.py +189 -0
  6. tunacode/cli/commands/implementations/development.py +77 -0
  7. tunacode/cli/commands/implementations/model.py +61 -0
  8. tunacode/cli/commands/implementations/system.py +216 -0
  9. tunacode/cli/commands/registry.py +236 -0
  10. tunacode/cli/repl.py +91 -30
  11. tunacode/configuration/settings.py +9 -2
  12. tunacode/constants.py +1 -1
  13. tunacode/core/agents/main.py +53 -3
  14. tunacode/core/agents/utils.py +304 -0
  15. tunacode/core/setup/config_setup.py +0 -1
  16. tunacode/core/state.py +13 -2
  17. tunacode/setup.py +7 -2
  18. tunacode/tools/read_file.py +8 -2
  19. tunacode/tools/read_file_async_poc.py +18 -10
  20. tunacode/tools/run_command.py +11 -4
  21. tunacode/ui/console.py +31 -4
  22. tunacode/ui/output.py +7 -2
  23. tunacode/ui/panels.py +98 -5
  24. tunacode/ui/utils.py +3 -0
  25. tunacode/utils/text_utils.py +6 -2
  26. {tunacode_cli-0.0.35.dist-info → tunacode_cli-0.0.37.dist-info}/METADATA +17 -17
  27. {tunacode_cli-0.0.35.dist-info → tunacode_cli-0.0.37.dist-info}/RECORD +31 -21
  28. tunacode/cli/commands.py +0 -893
  29. {tunacode_cli-0.0.35.dist-info → tunacode_cli-0.0.37.dist-info}/WHEEL +0 -0
  30. {tunacode_cli-0.0.35.dist-info → tunacode_cli-0.0.37.dist-info}/entry_points.txt +0 -0
  31. {tunacode_cli-0.0.35.dist-info → tunacode_cli-0.0.37.dist-info}/licenses/LICENSE +0 -0
  32. {tunacode_cli-0.0.35.dist-info → tunacode_cli-0.0.37.dist-info}/top_level.txt +0 -0
@@ -12,6 +12,22 @@ from datetime import datetime, timezone
12
12
  from pathlib import Path
13
13
  from typing import Any, Iterator, List, Optional, Tuple
14
14
 
15
+ from pydantic_ai import Agent
16
+
17
+ # Import streaming types with fallback for older versions
18
+ try:
19
+ from pydantic_ai.messages import (
20
+ PartDeltaEvent,
21
+ TextPartDelta,
22
+ )
23
+
24
+ STREAMING_AVAILABLE = True
25
+ except ImportError:
26
+ # Fallback for older pydantic-ai versions
27
+ PartDeltaEvent = None
28
+ TextPartDelta = None
29
+ STREAMING_AVAILABLE = False
30
+
15
31
  from tunacode.constants import READ_ONLY_TOOLS
16
32
  from tunacode.core.state import StateManager
17
33
  from tunacode.services.mcp import get_mcp_servers
@@ -23,8 +39,18 @@ from tunacode.tools.read_file import read_file
23
39
  from tunacode.tools.run_command import run_command
24
40
  from tunacode.tools.update_file import update_file
25
41
  from tunacode.tools.write_file import write_file
26
- from tunacode.types import (AgentRun, ErrorMessage, FallbackResponse, ModelName, PydanticAgent,
27
- ResponseState, SimpleResult, ToolCallback, ToolCallId, ToolName)
42
+ from tunacode.types import (
43
+ AgentRun,
44
+ ErrorMessage,
45
+ FallbackResponse,
46
+ ModelName,
47
+ PydanticAgent,
48
+ ResponseState,
49
+ SimpleResult,
50
+ ToolCallback,
51
+ ToolCallId,
52
+ ToolName,
53
+ )
28
54
 
29
55
 
30
56
  class ToolBuffer:
@@ -187,6 +213,7 @@ async def _process_node(
187
213
  tool_callback: Optional[ToolCallback],
188
214
  state_manager: StateManager,
189
215
  tool_buffer: Optional[ToolBuffer] = None,
216
+ streaming_callback: Optional[callable] = None,
190
217
  ):
191
218
  from tunacode.ui import console as ui
192
219
  from tunacode.utils.token_counter import estimate_tokens
@@ -206,6 +233,16 @@ async def _process_node(
206
233
  if hasattr(node, "model_response"):
207
234
  state_manager.session.messages.append(node.model_response)
208
235
 
236
+ # Stream content to callback if provided
237
+ # Use this as fallback when true token streaming is not available
238
+ if streaming_callback and not STREAMING_AVAILABLE:
239
+ for part in node.model_response.parts:
240
+ if hasattr(part, "content") and isinstance(part.content, str):
241
+ content = part.content.strip()
242
+ if content and not content.startswith('{"thought"'):
243
+ # Stream non-JSON content (actual response content)
244
+ await streaming_callback(content)
245
+
209
246
  # Enhanced display when thoughts are enabled
210
247
  if state_manager.session.show_thoughts:
211
248
  # Show raw API response data
@@ -673,6 +710,7 @@ async def process_request(
673
710
  message: str,
674
711
  state_manager: StateManager,
675
712
  tool_callback: Optional[ToolCallback] = None,
713
+ streaming_callback: Optional[callable] = None,
676
714
  ) -> AgentRun:
677
715
  agent = get_or_create_agent(model, state_manager)
678
716
  mh = state_manager.session.messages.copy()
@@ -713,7 +751,19 @@ async def process_request(
713
751
  i = 0
714
752
  async for node in agent_run:
715
753
  state_manager.session.current_iteration = i + 1
716
- await _process_node(node, tool_callback, state_manager, tool_buffer)
754
+
755
+ # Handle token-level streaming for model request nodes
756
+ if streaming_callback and STREAMING_AVAILABLE and Agent.is_model_request_node(node):
757
+ async with node.stream(agent_run.ctx) as request_stream:
758
+ async for event in request_stream:
759
+ if isinstance(event, PartDeltaEvent) and isinstance(
760
+ event.delta, TextPartDelta
761
+ ):
762
+ # Stream individual token deltas
763
+ if event.delta.content_delta:
764
+ await streaming_callback(event.delta.content_delta)
765
+
766
+ await _process_node(node, tool_callback, state_manager, tool_buffer, streaming_callback)
717
767
  if hasattr(node, "result") and node.result and hasattr(node.result, "output"):
718
768
  if node.result.output:
719
769
  response_state.has_user_response = True
@@ -0,0 +1,304 @@
1
+ import asyncio
2
+ import importlib
3
+ import json
4
+ import os
5
+ import re
6
+ from collections.abc import Iterator
7
+ from datetime import datetime, timezone
8
+ from typing import Any
9
+
10
+ from tunacode.constants import READ_ONLY_TOOLS
11
+ from tunacode.types import (
12
+ ErrorMessage,
13
+ StateManager,
14
+ ToolCallback,
15
+ ToolCallId,
16
+ ToolName,
17
+ )
18
+ from tunacode.ui import console as ui
19
+
20
+
21
+ # Lazy import for Agent and Tool
22
+ def get_agent_tool():
23
+ pydantic_ai = importlib.import_module("pydantic_ai")
24
+ return pydantic_ai.Agent, pydantic_ai.Tool
25
+
26
+
27
+ def get_model_messages():
28
+ messages = importlib.import_module("pydantic_ai.messages")
29
+ return messages.ModelRequest, messages.ToolReturnPart
30
+
31
+
32
+ async def execute_tools_parallel(
33
+ tool_calls: list[tuple[Any, Any]], callback: ToolCallback, return_exceptions: bool = True
34
+ ) -> list[Any]:
35
+ """Execute multiple tool calls in parallel using asyncio.
36
+
37
+ Args:
38
+ tool_calls: List of (part, node) tuples
39
+ callback: The tool callback function to execute
40
+ return_exceptions: Whether to return exceptions or raise them
41
+
42
+ Returns:
43
+ List of results in the same order as input, with exceptions for failed calls
44
+ """
45
+ # Get max parallel from environment or default to CPU count
46
+ max_parallel = int(os.environ.get("TUNACODE_MAX_PARALLEL", os.cpu_count() or 4))
47
+
48
+ async def execute_with_error_handling(part, node):
49
+ try:
50
+ return await callback(part, node)
51
+ except Exception as e:
52
+ return e
53
+
54
+ # If we have more tools than max_parallel, execute in batches
55
+ if len(tool_calls) > max_parallel:
56
+ results = []
57
+ for i in range(0, len(tool_calls), max_parallel):
58
+ batch = tool_calls[i : i + max_parallel]
59
+ batch_tasks = [execute_with_error_handling(part, node) for part, node in batch]
60
+ batch_results = await asyncio.gather(*batch_tasks, return_exceptions=return_exceptions)
61
+ results.extend(batch_results)
62
+ return results
63
+ tasks = [execute_with_error_handling(part, node) for part, node in tool_calls]
64
+ return await asyncio.gather(*tasks, return_exceptions=return_exceptions)
65
+
66
+
67
+ def batch_read_only_tools(tool_calls: list[Any]) -> Iterator[list[Any]]:
68
+ """Batch tool calls so read-only tools can be executed in parallel.
69
+
70
+ Yields batches where:
71
+ - Read-only tools are grouped together
72
+ - Write/execute tools are in their own batch (single item)
73
+ - Order within each batch is preserved
74
+
75
+ Args:
76
+ tool_calls: List of tool call objects with 'tool' attribute
77
+
78
+ Yields:
79
+ Batches of tool calls
80
+ """
81
+ if not tool_calls:
82
+ return
83
+
84
+ current_batch = []
85
+
86
+ for tool_call in tool_calls:
87
+ tool_name = tool_call.tool_name if hasattr(tool_call, "tool_name") else None
88
+
89
+ if tool_name in READ_ONLY_TOOLS:
90
+ # Add to current batch
91
+ current_batch.append(tool_call)
92
+ else:
93
+ # Yield any pending read-only batch
94
+ if current_batch:
95
+ yield current_batch
96
+ current_batch = []
97
+
98
+ # Yield write/execute tool as single-item batch
99
+ yield [tool_call]
100
+
101
+ # Yield any remaining read-only tools
102
+ if current_batch:
103
+ yield current_batch
104
+
105
+
106
+ async def create_buffering_callback(
107
+ original_callback: ToolCallback, buffer: Any, state_manager: StateManager
108
+ ) -> ToolCallback:
109
+ """Create a callback wrapper that buffers read-only tools for parallel execution.
110
+
111
+ Args:
112
+ original_callback: The original tool callback
113
+ buffer: ToolBuffer instance to store read-only tools
114
+ state_manager: StateManager for UI access
115
+
116
+ Returns:
117
+ A wrapped callback function
118
+ """
119
+
120
+ async def buffering_callback(part, node):
121
+ tool_name = getattr(part, "tool_name", None)
122
+
123
+ if tool_name in READ_ONLY_TOOLS:
124
+ # Buffer read-only tools
125
+ buffer.add(part, node)
126
+ # Don't execute yet - will be executed in parallel batch
127
+ return None
128
+
129
+ # Non-read-only tool encountered - flush buffer first
130
+ if buffer.has_tasks():
131
+ buffered_tasks = buffer.flush()
132
+
133
+ # Execute buffered read-only tools in parallel
134
+ if state_manager.session.show_thoughts:
135
+ await ui.muted(f"Executing {len(buffered_tasks)} read-only tools in parallel")
136
+
137
+ await execute_tools_parallel(buffered_tasks, original_callback)
138
+
139
+ # Execute the non-read-only tool
140
+ return await original_callback(part, node)
141
+
142
+ return buffering_callback
143
+
144
+
145
+ async def parse_json_tool_calls(
146
+ text: str, tool_callback: ToolCallback | None, state_manager: StateManager
147
+ ):
148
+ """Parse JSON tool calls from text when structured tool calling fails.
149
+ Fallback for when API providers don't support proper tool calling.
150
+ """
151
+ if not tool_callback:
152
+ return
153
+
154
+ # Pattern for JSON tool calls: {"tool": "tool_name", "args": {...}}
155
+ # Find potential JSON objects and parse them
156
+ potential_jsons = []
157
+ brace_count = 0
158
+ start_pos = -1
159
+
160
+ for i, char in enumerate(text):
161
+ if char == "{":
162
+ if brace_count == 0:
163
+ start_pos = i
164
+ brace_count += 1
165
+ elif char == "}":
166
+ brace_count -= 1
167
+ if brace_count == 0 and start_pos != -1:
168
+ potential_json = text[start_pos : i + 1]
169
+ try:
170
+ parsed = json.loads(potential_json)
171
+ if isinstance(parsed, dict) and "tool" in parsed and "args" in parsed:
172
+ potential_jsons.append((parsed["tool"], parsed["args"]))
173
+ except json.JSONDecodeError:
174
+ pass
175
+ start_pos = -1
176
+
177
+ matches = potential_jsons
178
+
179
+ for tool_name, args in matches:
180
+ try:
181
+ # Create a mock tool call object
182
+ class MockToolCall:
183
+ def __init__(self, tool_name: str, args: dict):
184
+ self.tool_name = tool_name
185
+ self.args = args
186
+ self.tool_call_id = f"fallback_{datetime.now().timestamp()}"
187
+
188
+ class MockNode:
189
+ pass
190
+
191
+ # Execute the tool through the callback
192
+ mock_call = MockToolCall(tool_name, args)
193
+ mock_node = MockNode()
194
+
195
+ await tool_callback(mock_call, mock_node)
196
+
197
+ if state_manager.session.show_thoughts:
198
+ await ui.muted(f"FALLBACK: Executed {tool_name} via JSON parsing")
199
+
200
+ except Exception as e:
201
+ if state_manager.session.show_thoughts:
202
+ await ui.error(f"Error executing fallback tool {tool_name}: {e!s}")
203
+
204
+
205
+ async def extract_and_execute_tool_calls(
206
+ text: str, tool_callback: ToolCallback | None, state_manager: StateManager
207
+ ):
208
+ """Extract tool calls from text content and execute them.
209
+ Supports multiple formats for maximum compatibility.
210
+ """
211
+ if not tool_callback:
212
+ return
213
+
214
+ # Format 1: {"tool": "name", "args": {...}}
215
+ await parse_json_tool_calls(text, tool_callback, state_manager)
216
+
217
+ # Format 2: Tool calls in code blocks
218
+ code_block_pattern = r'```json\s*(\{(?:[^{}]|"[^"]*"|(?:\{[^}]*\}))*"tool"(?:[^{}]|"[^"]*"|(?:\{[^}]*\}))*\})\s*```'
219
+ code_matches = re.findall(code_block_pattern, text, re.MULTILINE | re.DOTALL)
220
+
221
+ for match in code_matches:
222
+ try:
223
+ tool_data = json.loads(match)
224
+ if "tool" in tool_data and "args" in tool_data:
225
+
226
+ class MockToolCall:
227
+ def __init__(self, tool_name: str, args: dict):
228
+ self.tool_name = tool_name
229
+ self.args = args
230
+ self.tool_call_id = f"codeblock_{datetime.now().timestamp()}"
231
+
232
+ class MockNode:
233
+ pass
234
+
235
+ mock_call = MockToolCall(tool_data["tool"], tool_data["args"])
236
+ mock_node = MockNode()
237
+
238
+ await tool_callback(mock_call, mock_node)
239
+
240
+ if state_manager.session.show_thoughts:
241
+ await ui.muted(f"FALLBACK: Executed {tool_data['tool']} from code block")
242
+
243
+ except (json.JSONDecodeError, KeyError, Exception) as e:
244
+ if state_manager.session.show_thoughts:
245
+ await ui.error(f"Error parsing code block tool call: {e!s}")
246
+
247
+
248
+ def patch_tool_messages(
249
+ error_message: ErrorMessage = "Tool operation failed",
250
+ state_manager: StateManager = None,
251
+ ):
252
+ """Find any tool calls without responses and add synthetic error responses for them.
253
+ Takes an error message to use in the synthesized tool response.
254
+
255
+ Ignores tools that have corresponding retry prompts as the model is already
256
+ addressing them.
257
+ """
258
+ if state_manager is None:
259
+ raise ValueError("state_manager is required for patch_tool_messages")
260
+
261
+ messages = state_manager.session.messages
262
+
263
+ if not messages:
264
+ return
265
+
266
+ # Map tool calls to their tool returns
267
+ tool_calls: dict[ToolCallId, ToolName] = {} # tool_call_id -> tool_name
268
+ tool_returns: set[ToolCallId] = set() # set of tool_call_ids with returns
269
+ retry_prompts: set[ToolCallId] = set() # set of tool_call_ids with retry prompts
270
+
271
+ for message in messages:
272
+ if hasattr(message, "parts"):
273
+ for part in message.parts:
274
+ if (
275
+ hasattr(part, "part_kind")
276
+ and hasattr(part, "tool_call_id")
277
+ and part.tool_call_id
278
+ ):
279
+ if part.part_kind == "tool-call":
280
+ tool_calls[part.tool_call_id] = part.tool_name
281
+ elif part.part_kind == "tool-return":
282
+ tool_returns.add(part.tool_call_id)
283
+ elif part.part_kind == "retry-prompt":
284
+ retry_prompts.add(part.tool_call_id)
285
+
286
+ # Identify orphaned tools (those without responses and not being retried)
287
+ for tool_call_id, tool_name in list(tool_calls.items()):
288
+ if tool_call_id not in tool_returns and tool_call_id not in retry_prompts:
289
+ # Import ModelRequest and ToolReturnPart lazily
290
+ model_request_cls, tool_return_part_cls = get_model_messages()
291
+ messages.append(
292
+ model_request_cls(
293
+ parts=[
294
+ tool_return_part_cls(
295
+ tool_name=tool_name,
296
+ content=error_message,
297
+ tool_call_id=tool_call_id,
298
+ timestamp=datetime.now(timezone.utc),
299
+ part_kind="tool-return",
300
+ )
301
+ ],
302
+ kind="request",
303
+ )
304
+ )
@@ -107,7 +107,6 @@ class ConfigSetup(BaseSetup):
107
107
  "--key 'your-key' --baseurl 'https://openrouter.ai/api/v1'[/green]"
108
108
  )
109
109
  console.print("\n[yellow]Run 'tunacode --help' for more options[/yellow]\n")
110
- from tunacode.exceptions import ConfigurationError
111
110
 
112
111
  raise ConfigurationError(
113
112
  "No configuration found. Please use CLI flags to configure."
tunacode/core/state.py CHANGED
@@ -8,8 +8,15 @@ import uuid
8
8
  from dataclasses import dataclass, field
9
9
  from typing import Any, Optional
10
10
 
11
- from tunacode.types import (DeviceId, InputSessions, MessageHistory, ModelName, SessionId, ToolName,
12
- UserConfig)
11
+ from tunacode.types import (
12
+ DeviceId,
13
+ InputSessions,
14
+ MessageHistory,
15
+ ModelName,
16
+ SessionId,
17
+ ToolName,
18
+ UserConfig,
19
+ )
13
20
 
14
21
 
15
22
  @dataclass
@@ -35,6 +42,10 @@ class SessionState:
35
42
  tool_calls: list[dict[str, Any]] = field(default_factory=list)
36
43
  iteration_count: int = 0
37
44
  current_iteration: int = 0
45
+ # Track streaming state to prevent spinner conflicts
46
+ is_streaming_active: bool = False
47
+ # Track streaming panel reference for tool handler access
48
+ streaming_panel: Optional[Any] = None
38
49
 
39
50
 
40
51
  class StateManager:
tunacode/setup.py CHANGED
@@ -7,8 +7,13 @@ Provides high-level setup functions for initializing the application and its age
7
7
 
8
8
  from typing import Any, Optional
9
9
 
10
- from tunacode.core.setup import (AgentSetup, ConfigSetup, EnvironmentSetup, GitSafetySetup,
11
- SetupCoordinator)
10
+ from tunacode.core.setup import (
11
+ AgentSetup,
12
+ ConfigSetup,
13
+ EnvironmentSetup,
14
+ GitSafetySetup,
15
+ SetupCoordinator,
16
+ )
12
17
  from tunacode.core.state import StateManager
13
18
 
14
19
 
@@ -8,8 +8,14 @@ Provides safe file reading with size limits and proper error handling.
8
8
  import asyncio
9
9
  import os
10
10
 
11
- from tunacode.constants import (ERROR_FILE_DECODE, ERROR_FILE_DECODE_DETAILS, ERROR_FILE_NOT_FOUND,
12
- ERROR_FILE_TOO_LARGE, MAX_FILE_SIZE, MSG_FILE_SIZE_LIMIT)
11
+ from tunacode.constants import (
12
+ ERROR_FILE_DECODE,
13
+ ERROR_FILE_DECODE_DETAILS,
14
+ ERROR_FILE_NOT_FOUND,
15
+ ERROR_FILE_TOO_LARGE,
16
+ MAX_FILE_SIZE,
17
+ MSG_FILE_SIZE_LIMIT,
18
+ )
13
19
  from tunacode.exceptions import ToolExecutionError
14
20
  from tunacode.tools.base import FileBasedTool
15
21
  from tunacode.types import ToolResult
@@ -11,8 +11,14 @@ import sys
11
11
  from concurrent.futures import ThreadPoolExecutor
12
12
  from typing import Optional
13
13
 
14
- from tunacode.constants import (ERROR_FILE_DECODE, ERROR_FILE_DECODE_DETAILS, ERROR_FILE_NOT_FOUND,
15
- ERROR_FILE_TOO_LARGE, MAX_FILE_SIZE, MSG_FILE_SIZE_LIMIT)
14
+ from tunacode.constants import (
15
+ ERROR_FILE_DECODE,
16
+ ERROR_FILE_DECODE_DETAILS,
17
+ ERROR_FILE_NOT_FOUND,
18
+ ERROR_FILE_TOO_LARGE,
19
+ MAX_FILE_SIZE,
20
+ MSG_FILE_SIZE_LIMIT,
21
+ )
16
22
  from tunacode.exceptions import ToolExecutionError
17
23
  from tunacode.tools.base import FileBasedTool
18
24
  from tunacode.types import ToolResult
@@ -150,17 +156,18 @@ async def read_file_async(filepath: str) -> str:
150
156
  # Benchmarking utilities for testing
151
157
  async def benchmark_read_performance():
152
158
  """Benchmark the performance difference between sync and async reads."""
159
+ import contextlib
160
+ import tempfile
153
161
  import time
154
162
 
155
163
  from tunacode.tools.read_file import read_file as read_file_sync
156
164
 
157
- # Create some test files
165
+ # Create some test files using tempfile for secure temporary file creation
158
166
  test_files = []
159
- for i in range(10):
160
- filepath = f"/tmp/test_file_{i}.txt"
161
- with open(filepath, "w") as f:
162
- f.write("x" * 10000) # 10KB file
163
- test_files.append(filepath)
167
+ for _ in range(10):
168
+ with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".txt") as temp_file:
169
+ temp_file.write("x" * 10000) # 10KB file
170
+ test_files.append(temp_file.name)
164
171
 
165
172
  # Test synchronous reads (sequential)
166
173
  start_time = time.time()
@@ -174,9 +181,10 @@ async def benchmark_read_performance():
174
181
  await asyncio.gather(*tasks)
175
182
  async_time = time.time() - start_time
176
183
 
177
- # Cleanup
184
+ # Cleanup using safe file removal
178
185
  for filepath in test_files:
179
- os.unlink(filepath)
186
+ with contextlib.suppress(OSError):
187
+ os.unlink(filepath)
180
188
 
181
189
  print(f"Synchronous reads: {sync_time:.3f}s")
182
190
  print(f"Async reads: {async_time:.3f}s")
@@ -7,10 +7,17 @@ Provides controlled shell command execution with output capture and truncation.
7
7
 
8
8
  import subprocess
9
9
 
10
- from tunacode.constants import (CMD_OUTPUT_FORMAT, CMD_OUTPUT_NO_ERRORS, CMD_OUTPUT_NO_OUTPUT,
11
- CMD_OUTPUT_TRUNCATED, COMMAND_OUTPUT_END_SIZE,
12
- COMMAND_OUTPUT_START_INDEX, COMMAND_OUTPUT_THRESHOLD,
13
- ERROR_COMMAND_EXECUTION, MAX_COMMAND_OUTPUT)
10
+ from tunacode.constants import (
11
+ CMD_OUTPUT_FORMAT,
12
+ CMD_OUTPUT_NO_ERRORS,
13
+ CMD_OUTPUT_NO_OUTPUT,
14
+ CMD_OUTPUT_TRUNCATED,
15
+ COMMAND_OUTPUT_END_SIZE,
16
+ COMMAND_OUTPUT_START_INDEX,
17
+ COMMAND_OUTPUT_THRESHOLD,
18
+ ERROR_COMMAND_EXECUTION,
19
+ MAX_COMMAND_OUTPUT,
20
+ )
14
21
  from tunacode.exceptions import ToolExecutionError
15
22
  from tunacode.tools.base import BaseTool
16
23
  from tunacode.types import ToolResult
tunacode/ui/console.py CHANGED
@@ -9,11 +9,36 @@ from rich.markdown import Markdown
9
9
  # Import and re-export all functions from specialized modules
10
10
  from .input import formatted_text, input, multiline_input
11
11
  from .keybindings import create_key_bindings
12
- from .output import (banner, clear, info, line, muted, print, spinner, success, sync_print,
13
- update_available, usage, version, warning)
12
+ from .output import (
13
+ banner,
14
+ clear,
15
+ info,
16
+ line,
17
+ muted,
18
+ print,
19
+ spinner,
20
+ success,
21
+ sync_print,
22
+ update_available,
23
+ usage,
24
+ version,
25
+ warning,
26
+ )
27
+
14
28
  # Patch banner to use sync fast version
15
- from .panels import (agent, dump_messages, error, help, models, panel, sync_panel,
16
- sync_tool_confirm, tool_confirm)
29
+ from .panels import (
30
+ StreamingAgentPanel,
31
+ agent,
32
+ agent_streaming,
33
+ dump_messages,
34
+ error,
35
+ help,
36
+ models,
37
+ panel,
38
+ sync_panel,
39
+ sync_tool_confirm,
40
+ tool_confirm,
41
+ )
17
42
  from .prompt_manager import PromptConfig, PromptManager
18
43
  from .validators import ModelValidator
19
44
 
@@ -56,11 +81,13 @@ __all__ = [
56
81
  "warning",
57
82
  # From panels module
58
83
  "agent",
84
+ "agent_streaming",
59
85
  "dump_messages",
60
86
  "error",
61
87
  "help",
62
88
  "models",
63
89
  "panel",
90
+ "StreamingAgentPanel",
64
91
  "sync_panel",
65
92
  "sync_tool_confirm",
66
93
  "tool_confirm",
tunacode/ui/output.py CHANGED
@@ -5,8 +5,13 @@ from rich.console import Console
5
5
  from rich.padding import Padding
6
6
 
7
7
  from tunacode.configuration.settings import ApplicationSettings
8
- from tunacode.constants import (MSG_UPDATE_AVAILABLE, MSG_UPDATE_INSTRUCTION, MSG_VERSION_DISPLAY,
9
- UI_COLORS, UI_THINKING_MESSAGE)
8
+ from tunacode.constants import (
9
+ MSG_UPDATE_AVAILABLE,
10
+ MSG_UPDATE_INSTRUCTION,
11
+ MSG_VERSION_DISPLAY,
12
+ UI_COLORS,
13
+ UI_THINKING_MESSAGE,
14
+ )
10
15
  from tunacode.core.state import StateManager
11
16
  from tunacode.utils.file_utils import DotDict
12
17