tunacode-cli 0.0.56__py3-none-any.whl → 0.0.57__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/commands/implementations/plan.py +8 -8
- tunacode/cli/commands/registry.py +2 -2
- tunacode/cli/repl.py +214 -407
- tunacode/cli/repl_components/command_parser.py +37 -4
- tunacode/cli/repl_components/error_recovery.py +79 -1
- tunacode/cli/repl_components/output_display.py +14 -11
- tunacode/cli/repl_components/tool_executor.py +7 -4
- tunacode/configuration/defaults.py +8 -0
- tunacode/constants.py +8 -2
- tunacode/core/agents/agent_components/agent_config.py +128 -65
- tunacode/core/agents/agent_components/node_processor.py +6 -2
- tunacode/core/code_index.py +83 -29
- tunacode/core/state.py +1 -1
- tunacode/core/token_usage/usage_tracker.py +2 -2
- tunacode/core/tool_handler.py +3 -3
- tunacode/prompts/system.md +117 -490
- tunacode/services/mcp.py +29 -7
- tunacode/tools/base.py +110 -0
- tunacode/tools/bash.py +96 -1
- tunacode/tools/exit_plan_mode.py +114 -32
- tunacode/tools/glob.py +366 -33
- tunacode/tools/grep.py +226 -77
- tunacode/tools/grep_components/result_formatter.py +98 -4
- tunacode/tools/list_dir.py +132 -2
- tunacode/tools/present_plan.py +111 -31
- tunacode/tools/read_file.py +91 -0
- tunacode/tools/run_command.py +99 -0
- tunacode/tools/schema_assembler.py +167 -0
- tunacode/tools/todo.py +108 -1
- tunacode/tools/update_file.py +94 -0
- tunacode/tools/write_file.py +86 -0
- tunacode/types.py +10 -9
- tunacode/ui/input.py +1 -0
- tunacode/ui/keybindings.py +1 -0
- tunacode/ui/panels.py +49 -27
- tunacode/ui/prompt_manager.py +13 -7
- tunacode/utils/json_utils.py +206 -0
- tunacode/utils/ripgrep.py +332 -9
- {tunacode_cli-0.0.56.dist-info → tunacode_cli-0.0.57.dist-info}/METADATA +5 -1
- {tunacode_cli-0.0.56.dist-info → tunacode_cli-0.0.57.dist-info}/RECORD +44 -43
- tunacode/tools/read_file_async_poc.py +0 -196
- {tunacode_cli-0.0.56.dist-info → tunacode_cli-0.0.57.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.56.dist-info → tunacode_cli-0.0.57.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.56.dist-info → tunacode_cli-0.0.57.dist-info}/licenses/LICENSE +0 -0
- {tunacode_cli-0.0.56.dist-info → tunacode_cli-0.0.57.dist-info}/top_level.txt +0 -0
tunacode/tools/todo.py
CHANGED
|
@@ -4,10 +4,13 @@ This tool allows the AI agent to manage todo items during task execution.
|
|
|
4
4
|
It provides functionality for creating, updating, and tracking tasks.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
+
import logging
|
|
7
8
|
import uuid
|
|
8
9
|
from datetime import datetime
|
|
9
|
-
from
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, List, Literal, Optional, Union
|
|
10
12
|
|
|
13
|
+
import defusedxml.ElementTree as ET
|
|
11
14
|
from pydantic_ai.exceptions import ModelRetry
|
|
12
15
|
|
|
13
16
|
from tunacode.constants import (
|
|
@@ -21,10 +24,114 @@ from tunacode.types import TodoItem, ToolResult, UILogger
|
|
|
21
24
|
|
|
22
25
|
from .base import BaseTool
|
|
23
26
|
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
24
29
|
|
|
25
30
|
class TodoTool(BaseTool):
|
|
26
31
|
"""Tool for managing todo items from the AI agent."""
|
|
27
32
|
|
|
33
|
+
def _get_base_prompt(self) -> str:
|
|
34
|
+
"""Load and return the base prompt from XML file.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
str: The loaded prompt from XML or a default prompt
|
|
38
|
+
"""
|
|
39
|
+
try:
|
|
40
|
+
# Load prompt from XML file
|
|
41
|
+
prompt_file = Path(__file__).parent / "prompts" / "todo_prompt.xml"
|
|
42
|
+
if prompt_file.exists():
|
|
43
|
+
tree = ET.parse(prompt_file)
|
|
44
|
+
root = tree.getroot()
|
|
45
|
+
description = root.find("description")
|
|
46
|
+
if description is not None:
|
|
47
|
+
return description.text.strip()
|
|
48
|
+
except Exception as e:
|
|
49
|
+
logger.warning(f"Failed to load XML prompt for todo: {e}")
|
|
50
|
+
|
|
51
|
+
# Fallback to default prompt
|
|
52
|
+
return """Use this tool to create and manage a structured task list"""
|
|
53
|
+
|
|
54
|
+
def _get_parameters_schema(self) -> Dict[str, Any]:
|
|
55
|
+
"""Get the parameters schema for todo tool.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Dict containing the JSON schema for tool parameters
|
|
59
|
+
"""
|
|
60
|
+
# Try to load from XML first
|
|
61
|
+
try:
|
|
62
|
+
prompt_file = Path(__file__).parent / "prompts" / "todo_prompt.xml"
|
|
63
|
+
if prompt_file.exists():
|
|
64
|
+
tree = ET.parse(prompt_file)
|
|
65
|
+
root = tree.getroot()
|
|
66
|
+
parameters = root.find("parameters")
|
|
67
|
+
if parameters is not None:
|
|
68
|
+
schema: Dict[str, Any] = {"type": "object", "properties": {}, "required": []}
|
|
69
|
+
required_fields: List[str] = []
|
|
70
|
+
|
|
71
|
+
for param in parameters.findall("parameter"):
|
|
72
|
+
name = param.get("name")
|
|
73
|
+
required = param.get("required", "false").lower() == "true"
|
|
74
|
+
param_type = param.find("type")
|
|
75
|
+
description = param.find("description")
|
|
76
|
+
|
|
77
|
+
if name and param_type is not None:
|
|
78
|
+
prop = {
|
|
79
|
+
"type": param_type.text.strip(),
|
|
80
|
+
"description": description.text.strip()
|
|
81
|
+
if description is not None
|
|
82
|
+
else "",
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Handle array types and nested objects
|
|
86
|
+
if param_type.text.strip() == "array":
|
|
87
|
+
items = param.find("items")
|
|
88
|
+
if items is not None:
|
|
89
|
+
item_type = items.find("type")
|
|
90
|
+
if item_type is not None and item_type.text.strip() == "object":
|
|
91
|
+
# Handle object items
|
|
92
|
+
item_props = {}
|
|
93
|
+
item_properties = items.find("properties")
|
|
94
|
+
if item_properties is not None:
|
|
95
|
+
for item_prop in item_properties.findall("property"):
|
|
96
|
+
prop_name = item_prop.get("name")
|
|
97
|
+
prop_type_elem = item_prop.find("type")
|
|
98
|
+
if prop_name and prop_type_elem is not None:
|
|
99
|
+
item_props[prop_name] = {
|
|
100
|
+
"type": prop_type_elem.text.strip()
|
|
101
|
+
}
|
|
102
|
+
prop["items"] = {"type": "object", "properties": item_props}
|
|
103
|
+
else:
|
|
104
|
+
prop["items"] = {"type": items.text.strip()}
|
|
105
|
+
|
|
106
|
+
schema["properties"][name] = prop
|
|
107
|
+
if required:
|
|
108
|
+
required_fields.append(name)
|
|
109
|
+
|
|
110
|
+
schema["required"] = required_fields
|
|
111
|
+
return schema
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.warning(f"Failed to load parameters from XML for todo: {e}")
|
|
114
|
+
|
|
115
|
+
# Fallback to hardcoded schema
|
|
116
|
+
return {
|
|
117
|
+
"type": "object",
|
|
118
|
+
"properties": {
|
|
119
|
+
"todos": {
|
|
120
|
+
"type": "array",
|
|
121
|
+
"description": "The updated todo list",
|
|
122
|
+
"items": {
|
|
123
|
+
"type": "object",
|
|
124
|
+
"properties": {
|
|
125
|
+
"id": {"type": "string"},
|
|
126
|
+
"content": {"type": "string"},
|
|
127
|
+
"status": {"type": "string"},
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
"required": ["todos"],
|
|
133
|
+
}
|
|
134
|
+
|
|
28
135
|
def __init__(self, state_manager, ui_logger: UILogger | None = None):
|
|
29
136
|
"""Initialize the todo tool.
|
|
30
137
|
|
tunacode/tools/update_file.py
CHANGED
|
@@ -5,14 +5,21 @@ File update tool for agent operations in the TunaCode application.
|
|
|
5
5
|
Provides targeted file content modification with diff-based updates.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import logging
|
|
8
9
|
import os
|
|
10
|
+
from functools import lru_cache
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, List
|
|
9
13
|
|
|
14
|
+
import defusedxml.ElementTree as ET
|
|
10
15
|
from pydantic_ai.exceptions import ModelRetry
|
|
11
16
|
|
|
12
17
|
from tunacode.exceptions import ToolExecutionError
|
|
13
18
|
from tunacode.tools.base import FileBasedTool
|
|
14
19
|
from tunacode.types import ToolResult
|
|
15
20
|
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
16
23
|
|
|
17
24
|
class UpdateFileTool(FileBasedTool):
|
|
18
25
|
"""Tool for updating existing files by replacing text blocks."""
|
|
@@ -21,6 +28,93 @@ class UpdateFileTool(FileBasedTool):
|
|
|
21
28
|
def tool_name(self) -> str:
|
|
22
29
|
return "Update"
|
|
23
30
|
|
|
31
|
+
@lru_cache(maxsize=1)
|
|
32
|
+
def _get_base_prompt(self) -> str:
|
|
33
|
+
"""Load and return the base prompt from XML file.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
str: The loaded prompt from XML or a default prompt
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
# Load prompt from XML file
|
|
40
|
+
prompt_file = Path(__file__).parent / "prompts" / "update_file_prompt.xml"
|
|
41
|
+
if prompt_file.exists():
|
|
42
|
+
tree = ET.parse(prompt_file)
|
|
43
|
+
root = tree.getroot()
|
|
44
|
+
description = root.find("description")
|
|
45
|
+
if description is not None:
|
|
46
|
+
return description.text.strip()
|
|
47
|
+
except Exception as e:
|
|
48
|
+
logger.warning(f"Failed to load XML prompt for update_file: {e}")
|
|
49
|
+
|
|
50
|
+
# Fallback to default prompt
|
|
51
|
+
return """Performs exact string replacements in files"""
|
|
52
|
+
|
|
53
|
+
@lru_cache(maxsize=1)
|
|
54
|
+
def _get_parameters_schema(self) -> Dict[str, Any]:
|
|
55
|
+
"""Get the parameters schema for update_file tool.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Dict containing the JSON schema for tool parameters
|
|
59
|
+
"""
|
|
60
|
+
# Try to load from XML first
|
|
61
|
+
try:
|
|
62
|
+
prompt_file = Path(__file__).parent / "prompts" / "update_file_prompt.xml"
|
|
63
|
+
if prompt_file.exists():
|
|
64
|
+
tree = ET.parse(prompt_file)
|
|
65
|
+
root = tree.getroot()
|
|
66
|
+
parameters = root.find("parameters")
|
|
67
|
+
if parameters is not None:
|
|
68
|
+
schema: Dict[str, Any] = {"type": "object", "properties": {}, "required": []}
|
|
69
|
+
required_fields: List[str] = []
|
|
70
|
+
|
|
71
|
+
for param in parameters.findall("parameter"):
|
|
72
|
+
name = param.get("name")
|
|
73
|
+
required = param.get("required", "false").lower() == "true"
|
|
74
|
+
param_type = param.find("type")
|
|
75
|
+
description = param.find("description")
|
|
76
|
+
|
|
77
|
+
if name and param_type is not None:
|
|
78
|
+
prop = {
|
|
79
|
+
"type": param_type.text.strip(),
|
|
80
|
+
"description": description.text.strip()
|
|
81
|
+
if description is not None
|
|
82
|
+
else "",
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
schema["properties"][name] = prop
|
|
86
|
+
if required:
|
|
87
|
+
required_fields.append(name)
|
|
88
|
+
|
|
89
|
+
schema["required"] = required_fields
|
|
90
|
+
return schema
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.warning(f"Failed to load parameters from XML for update_file: {e}")
|
|
93
|
+
|
|
94
|
+
# Fallback to hardcoded schema
|
|
95
|
+
return {
|
|
96
|
+
"type": "object",
|
|
97
|
+
"properties": {
|
|
98
|
+
"file_path": {
|
|
99
|
+
"type": "string",
|
|
100
|
+
"description": "The absolute path to the file to modify",
|
|
101
|
+
},
|
|
102
|
+
"old_string": {
|
|
103
|
+
"type": "string",
|
|
104
|
+
"description": "The text to replace",
|
|
105
|
+
},
|
|
106
|
+
"new_string": {
|
|
107
|
+
"type": "string",
|
|
108
|
+
"description": "The text to replace it with",
|
|
109
|
+
},
|
|
110
|
+
"replace_all": {
|
|
111
|
+
"type": "boolean",
|
|
112
|
+
"description": "Replace all occurences of old_string",
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
"required": ["file_path", "old_string", "new_string"],
|
|
116
|
+
}
|
|
117
|
+
|
|
24
118
|
async def _execute(self, filepath: str, target: str, patch: str) -> ToolResult:
|
|
25
119
|
"""Update an existing file by replacing a target text block with a patch.
|
|
26
120
|
|
tunacode/tools/write_file.py
CHANGED
|
@@ -5,14 +5,21 @@ File writing tool for agent operations in the TunaCode application.
|
|
|
5
5
|
Provides safe file creation with conflict detection and encoding handling.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import logging
|
|
8
9
|
import os
|
|
10
|
+
from functools import lru_cache
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Dict, List
|
|
9
13
|
|
|
14
|
+
import defusedxml.ElementTree as ET
|
|
10
15
|
from pydantic_ai.exceptions import ModelRetry
|
|
11
16
|
|
|
12
17
|
from tunacode.exceptions import ToolExecutionError
|
|
13
18
|
from tunacode.tools.base import FileBasedTool
|
|
14
19
|
from tunacode.types import ToolResult
|
|
15
20
|
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
16
23
|
|
|
17
24
|
class WriteFileTool(FileBasedTool):
|
|
18
25
|
"""Tool for writing content to new files."""
|
|
@@ -21,6 +28,85 @@ class WriteFileTool(FileBasedTool):
|
|
|
21
28
|
def tool_name(self) -> str:
|
|
22
29
|
return "Write"
|
|
23
30
|
|
|
31
|
+
@lru_cache(maxsize=1)
|
|
32
|
+
def _get_base_prompt(self) -> str:
|
|
33
|
+
"""Load and return the base prompt from XML file.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
str: The loaded prompt from XML or a default prompt
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
# Load prompt from XML file
|
|
40
|
+
prompt_file = Path(__file__).parent / "prompts" / "write_file_prompt.xml"
|
|
41
|
+
if prompt_file.exists():
|
|
42
|
+
tree = ET.parse(prompt_file)
|
|
43
|
+
root = tree.getroot()
|
|
44
|
+
description = root.find("description")
|
|
45
|
+
if description is not None:
|
|
46
|
+
return description.text.strip()
|
|
47
|
+
except Exception as e:
|
|
48
|
+
logger.warning(f"Failed to load XML prompt for write_file: {e}")
|
|
49
|
+
|
|
50
|
+
# Fallback to default prompt
|
|
51
|
+
return """Writes a file to the local filesystem"""
|
|
52
|
+
|
|
53
|
+
@lru_cache(maxsize=1)
|
|
54
|
+
def _get_parameters_schema(self) -> Dict[str, Any]:
|
|
55
|
+
"""Get the parameters schema for write_file tool.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Dict containing the JSON schema for tool parameters
|
|
59
|
+
"""
|
|
60
|
+
# Try to load from XML first
|
|
61
|
+
try:
|
|
62
|
+
prompt_file = Path(__file__).parent / "prompts" / "write_file_prompt.xml"
|
|
63
|
+
if prompt_file.exists():
|
|
64
|
+
tree = ET.parse(prompt_file)
|
|
65
|
+
root = tree.getroot()
|
|
66
|
+
parameters = root.find("parameters")
|
|
67
|
+
if parameters is not None:
|
|
68
|
+
schema: Dict[str, Any] = {"type": "object", "properties": {}, "required": []}
|
|
69
|
+
required_fields: List[str] = []
|
|
70
|
+
|
|
71
|
+
for param in parameters.findall("parameter"):
|
|
72
|
+
name = param.get("name")
|
|
73
|
+
required = param.get("required", "false").lower() == "true"
|
|
74
|
+
param_type = param.find("type")
|
|
75
|
+
description = param.find("description")
|
|
76
|
+
|
|
77
|
+
if name and param_type is not None:
|
|
78
|
+
prop = {
|
|
79
|
+
"type": param_type.text.strip(),
|
|
80
|
+
"description": description.text.strip()
|
|
81
|
+
if description is not None
|
|
82
|
+
else "",
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
schema["properties"][name] = prop
|
|
86
|
+
if required:
|
|
87
|
+
required_fields.append(name)
|
|
88
|
+
|
|
89
|
+
schema["required"] = required_fields
|
|
90
|
+
return schema
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.warning(f"Failed to load parameters from XML for write_file: {e}")
|
|
93
|
+
|
|
94
|
+
# Fallback to hardcoded schema
|
|
95
|
+
return {
|
|
96
|
+
"type": "object",
|
|
97
|
+
"properties": {
|
|
98
|
+
"file_path": {
|
|
99
|
+
"type": "string",
|
|
100
|
+
"description": "The absolute path to the file to write",
|
|
101
|
+
},
|
|
102
|
+
"content": {
|
|
103
|
+
"type": "string",
|
|
104
|
+
"description": "The content to write to the file",
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
"required": ["file_path", "content"],
|
|
108
|
+
}
|
|
109
|
+
|
|
24
110
|
async def _execute(self, filepath: str, content: str) -> ToolResult:
|
|
25
111
|
"""Write content to a new file. Fails if the file already exists.
|
|
26
112
|
|
tunacode/types.py
CHANGED
|
@@ -7,6 +7,9 @@ used throughout the TunaCode codebase.
|
|
|
7
7
|
|
|
8
8
|
from dataclasses import dataclass, field
|
|
9
9
|
from datetime import datetime
|
|
10
|
+
|
|
11
|
+
# Plan types will be defined below
|
|
12
|
+
from enum import Enum
|
|
10
13
|
from pathlib import Path
|
|
11
14
|
from typing import (
|
|
12
15
|
Any,
|
|
@@ -21,9 +24,6 @@ from typing import (
|
|
|
21
24
|
Union,
|
|
22
25
|
)
|
|
23
26
|
|
|
24
|
-
# Plan types will be defined below
|
|
25
|
-
from enum import Enum
|
|
26
|
-
|
|
27
27
|
# Try to import pydantic-ai types if available
|
|
28
28
|
try:
|
|
29
29
|
from pydantic_ai import Agent
|
|
@@ -202,6 +202,7 @@ class SimpleResult:
|
|
|
202
202
|
|
|
203
203
|
class PlanPhase(Enum):
|
|
204
204
|
"""Plan Mode phases."""
|
|
205
|
+
|
|
205
206
|
PLANNING_RESEARCH = "research"
|
|
206
207
|
PLANNING_DRAFT = "draft"
|
|
207
208
|
PLAN_READY = "ready"
|
|
@@ -211,14 +212,14 @@ class PlanPhase(Enum):
|
|
|
211
212
|
@dataclass
|
|
212
213
|
class PlanDoc:
|
|
213
214
|
"""Structured plan document with all required sections."""
|
|
214
|
-
|
|
215
|
+
|
|
215
216
|
# Required sections
|
|
216
217
|
title: str
|
|
217
218
|
overview: str
|
|
218
219
|
steps: List[str]
|
|
219
220
|
files_to_modify: List[str]
|
|
220
221
|
files_to_create: List[str]
|
|
221
|
-
|
|
222
|
+
|
|
222
223
|
# Optional but recommended sections
|
|
223
224
|
risks: List[str] = field(default_factory=list)
|
|
224
225
|
tests: List[str] = field(default_factory=list)
|
|
@@ -226,16 +227,16 @@ class PlanDoc:
|
|
|
226
227
|
open_questions: List[str] = field(default_factory=list)
|
|
227
228
|
success_criteria: List[str] = field(default_factory=list)
|
|
228
229
|
references: List[str] = field(default_factory=list)
|
|
229
|
-
|
|
230
|
+
|
|
230
231
|
def validate(self) -> Tuple[bool, List[str]]:
|
|
231
232
|
"""
|
|
232
233
|
Validate the plan document.
|
|
233
|
-
|
|
234
|
+
|
|
234
235
|
Returns:
|
|
235
236
|
tuple: (is_valid, list_of_missing_sections)
|
|
236
237
|
"""
|
|
237
238
|
missing = []
|
|
238
|
-
|
|
239
|
+
|
|
239
240
|
# Check required fields
|
|
240
241
|
if not self.title or not self.title.strip():
|
|
241
242
|
missing.append("title")
|
|
@@ -245,7 +246,7 @@ class PlanDoc:
|
|
|
245
246
|
missing.append("steps")
|
|
246
247
|
if not self.files_to_modify and not self.files_to_create:
|
|
247
248
|
missing.append("files_to_modify or files_to_create")
|
|
248
|
-
|
|
249
|
+
|
|
249
250
|
return len(missing) == 0, missing
|
|
250
251
|
|
|
251
252
|
|
tunacode/ui/input.py
CHANGED
tunacode/ui/keybindings.py
CHANGED
tunacode/ui/panels.py
CHANGED
|
@@ -100,7 +100,7 @@ class StreamingAgentPanel:
|
|
|
100
100
|
self._last_update_time = 0.0
|
|
101
101
|
self._dots_task = None
|
|
102
102
|
self._dots_count = 0
|
|
103
|
-
self._show_dots =
|
|
103
|
+
self._show_dots = True # Start with dots enabled for "Thinking..."
|
|
104
104
|
|
|
105
105
|
def _create_panel(self) -> Padding:
|
|
106
106
|
"""Create a Rich panel with current content."""
|
|
@@ -111,7 +111,15 @@ class StreamingAgentPanel:
|
|
|
111
111
|
|
|
112
112
|
# Show "Thinking..." only when no content has arrived yet
|
|
113
113
|
if not self.content:
|
|
114
|
-
|
|
114
|
+
# Apply dots animation to "Thinking..." message too
|
|
115
|
+
thinking_msg = UI_THINKING_MESSAGE
|
|
116
|
+
if self._show_dots:
|
|
117
|
+
# Remove the existing ... from the message and add animated dots
|
|
118
|
+
base_msg = thinking_msg.replace("...", "")
|
|
119
|
+
dots_patterns = ["", ".", "..", "..."]
|
|
120
|
+
dots = dots_patterns[self._dots_count % len(dots_patterns)]
|
|
121
|
+
thinking_msg = base_msg + dots
|
|
122
|
+
content_renderable: Union[Text, Markdown] = Text.from_markup(thinking_msg)
|
|
115
123
|
else:
|
|
116
124
|
# Once we have content, show it with optional dots animation
|
|
117
125
|
display_content = self.content
|
|
@@ -143,17 +151,21 @@ class StreamingAgentPanel:
|
|
|
143
151
|
async def _animate_dots(self):
|
|
144
152
|
"""Animate dots after a pause in streaming."""
|
|
145
153
|
while True:
|
|
146
|
-
await asyncio.sleep(0.
|
|
154
|
+
await asyncio.sleep(0.2) # Faster animation cycle
|
|
147
155
|
current_time = time.time()
|
|
148
|
-
#
|
|
149
|
-
if
|
|
156
|
+
# Use shorter delay for initial "Thinking..." phase
|
|
157
|
+
delay_threshold = 0.3 if not self.content else 1.0
|
|
158
|
+
# Show dots after the delay threshold
|
|
159
|
+
if current_time - self._last_update_time > delay_threshold:
|
|
150
160
|
self._show_dots = True
|
|
151
161
|
self._dots_count += 1
|
|
152
162
|
if self.live:
|
|
153
163
|
self.live.update(self._create_panel())
|
|
154
164
|
else:
|
|
155
|
-
|
|
156
|
-
self.
|
|
165
|
+
# Only reset if we have content (keep dots for initial "Thinking...")
|
|
166
|
+
if self.content:
|
|
167
|
+
self._show_dots = False
|
|
168
|
+
self._dots_count = 0
|
|
157
169
|
|
|
158
170
|
async def start(self):
|
|
159
171
|
"""Start the live streaming display."""
|
|
@@ -161,7 +173,11 @@ class StreamingAgentPanel:
|
|
|
161
173
|
|
|
162
174
|
self.live = Live(self._create_panel(), console=console, refresh_per_second=4)
|
|
163
175
|
self.live.start()
|
|
164
|
-
|
|
176
|
+
# For "Thinking...", set time in past to trigger dots immediately
|
|
177
|
+
if not self.content:
|
|
178
|
+
self._last_update_time = time.time() - 0.4 # Triggers dots on first cycle
|
|
179
|
+
else:
|
|
180
|
+
self._last_update_time = time.time()
|
|
165
181
|
# Start the dots animation task
|
|
166
182
|
self._dots_task = asyncio.create_task(self._animate_dots())
|
|
167
183
|
|
|
@@ -170,18 +186,21 @@ class StreamingAgentPanel:
|
|
|
170
186
|
# Defensive: some providers may yield None chunks intermittently
|
|
171
187
|
if content_chunk is None:
|
|
172
188
|
content_chunk = ""
|
|
173
|
-
|
|
189
|
+
|
|
174
190
|
# Filter out plan mode system prompts and tool definitions from streaming
|
|
175
|
-
if any(
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
191
|
+
if any(
|
|
192
|
+
phrase in str(content_chunk)
|
|
193
|
+
for phrase in [
|
|
194
|
+
"🔧 PLAN MODE",
|
|
195
|
+
"TOOL EXECUTION ONLY",
|
|
196
|
+
"planning assistant that ONLY communicates",
|
|
197
|
+
"namespace functions {",
|
|
198
|
+
"namespace multi_tool_use {",
|
|
199
|
+
"You are trained on data up to",
|
|
200
|
+
]
|
|
201
|
+
):
|
|
183
202
|
return
|
|
184
|
-
|
|
203
|
+
|
|
185
204
|
# Ensure type safety for concatenation
|
|
186
205
|
self.content = (self.content or "") + str(content_chunk)
|
|
187
206
|
|
|
@@ -195,16 +214,19 @@ class StreamingAgentPanel:
|
|
|
195
214
|
async def set_content(self, content: str):
|
|
196
215
|
"""Set the complete content (overwrites previous)."""
|
|
197
216
|
# Filter out plan mode system prompts and tool definitions
|
|
198
|
-
if any(
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
217
|
+
if any(
|
|
218
|
+
phrase in str(content)
|
|
219
|
+
for phrase in [
|
|
220
|
+
"🔧 PLAN MODE",
|
|
221
|
+
"TOOL EXECUTION ONLY",
|
|
222
|
+
"planning assistant that ONLY communicates",
|
|
223
|
+
"namespace functions {",
|
|
224
|
+
"namespace multi_tool_use {",
|
|
225
|
+
"You are trained on data up to",
|
|
226
|
+
]
|
|
227
|
+
):
|
|
206
228
|
return
|
|
207
|
-
|
|
229
|
+
|
|
208
230
|
self.content = content
|
|
209
231
|
if self.live:
|
|
210
232
|
self.live.update(self._create_panel())
|
tunacode/ui/prompt_manager.py
CHANGED
|
@@ -106,13 +106,19 @@ class PromptManager:
|
|
|
106
106
|
base_prompt = prompt
|
|
107
107
|
|
|
108
108
|
# Add Plan Mode indicator if active
|
|
109
|
-
if (
|
|
110
|
-
self.state_manager
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
109
|
+
if (
|
|
110
|
+
self.state_manager
|
|
111
|
+
and self.state_manager.is_plan_mode()
|
|
112
|
+
and "PLAN MODE ON" not in base_prompt
|
|
113
|
+
):
|
|
114
|
+
base_prompt = (
|
|
115
|
+
'<style fg="#40E0D0"><bold>⏸ PLAN MODE ON</bold></style>\n' + base_prompt
|
|
116
|
+
)
|
|
117
|
+
elif (
|
|
118
|
+
self.state_manager
|
|
119
|
+
and not self.state_manager.is_plan_mode()
|
|
120
|
+
and ("⏸" in base_prompt or "PLAN MODE ON" in base_prompt)
|
|
121
|
+
):
|
|
116
122
|
# Remove plan mode indicator if no longer in plan mode
|
|
117
123
|
lines = base_prompt.split("\n")
|
|
118
124
|
if len(lines) > 1 and ("⏸" in lines[0] or "PLAN MODE ON" in lines[0]):
|