todo-agent 0.2.1__tar.gz → 0.2.4__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.
- {todo_agent-0.2.1 → todo_agent-0.2.4}/PKG-INFO +1 -1
- {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_core/test_conversation_manager.py +1 -1
- {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_interface/test_cli.py +2 -2
- {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/_version.py +3 -3
- {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/core/conversation_manager.py +1 -1
- todo_agent-0.2.4/todo_agent/interface/__init__.py +25 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/interface/cli.py +119 -51
- todo_agent-0.2.4/todo_agent/interface/formatters.py +457 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/main.py +10 -2
- {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent.egg-info/PKG-INFO +1 -1
- {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent.egg-info/SOURCES.txt +1 -0
- todo_agent-0.2.1/todo_agent/interface/__init__.py +0 -10
- {todo_agent-0.2.1 → todo_agent-0.2.4}/.gitignore +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/LICENSE +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/MANIFEST.in +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/Makefile +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/README.md +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/docs/publishing.md +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/pyproject.toml +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/requirements-dev.txt +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/requirements.txt +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/setup.cfg +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/__init__.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_core/__init__.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_core/test_todo_manager.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_infrastructure/__init__.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_infrastructure/test_config.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_infrastructure/test_inference.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_infrastructure/test_llm_client_factory.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_infrastructure/test_ollama_client.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_infrastructure/test_openrouter_client.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_infrastructure/test_todo_shell.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_infrastructure/test_token_counter.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_interface/__init__.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_interface/test_tools.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_linting.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_logger.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_main.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/__init__.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/core/__init__.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/core/exceptions.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/core/todo_manager.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/infrastructure/__init__.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/infrastructure/config.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/infrastructure/inference.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/infrastructure/llm_client.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/infrastructure/llm_client_factory.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/infrastructure/logger.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/infrastructure/ollama_client.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/infrastructure/openrouter_client.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/infrastructure/prompts/system_prompt.txt +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/infrastructure/todo_shell.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/infrastructure/token_counter.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/interface/tools.py +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent.egg-info/dependency_links.txt +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent.egg-info/entry_points.txt +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent.egg-info/requires.txt +0 -0
- {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent.egg-info/top_level.txt +0 -0
@@ -14,7 +14,7 @@ class TestConversationManager:
|
|
14
14
|
"""Test ConversationManager initialization."""
|
15
15
|
manager = ConversationManager()
|
16
16
|
assert len(manager.history) == 0
|
17
|
-
assert manager.max_tokens ==
|
17
|
+
assert manager.max_tokens == 16000
|
18
18
|
assert manager.max_messages == 50
|
19
19
|
assert manager.system_prompt is None
|
20
20
|
|
@@ -151,8 +151,8 @@ class TestCLI:
|
|
151
151
|
|
152
152
|
result = self.cli.handle_request(user_input)
|
153
153
|
|
154
|
-
# Verify error is properly formatted
|
155
|
-
assert result == f"
|
154
|
+
# Verify error is properly formatted with unicode
|
155
|
+
assert result == f"❌ {error_message}"
|
156
156
|
|
157
157
|
# Verify inference engine was called
|
158
158
|
self.cli.inference.process_request.assert_called_once_with(user_input)
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
28
28
|
commit_id: COMMIT_ID
|
29
29
|
__commit_id__: COMMIT_ID
|
30
30
|
|
31
|
-
__version__ = version = '0.2.
|
32
|
-
__version_tuple__ = version_tuple = (0, 2,
|
31
|
+
__version__ = version = '0.2.4'
|
32
|
+
__version_tuple__ = version_tuple = (0, 2, 4)
|
33
33
|
|
34
|
-
__commit_id__ = commit_id = '
|
34
|
+
__commit_id__ = commit_id = 'gc2b1a1313'
|
@@ -35,7 +35,7 @@ class ConversationManager:
|
|
35
35
|
"""Manages conversation state and memory for LLM interactions."""
|
36
36
|
|
37
37
|
def __init__(
|
38
|
-
self, max_tokens: int =
|
38
|
+
self, max_tokens: int = 16000, max_messages: int = 50, model: str = "gpt-4"
|
39
39
|
):
|
40
40
|
self.history: List[ConversationMessage] = []
|
41
41
|
self.max_tokens = max_tokens
|
@@ -0,0 +1,25 @@
|
|
1
|
+
"""
|
2
|
+
Interface layer for todo.sh LLM agent.
|
3
|
+
|
4
|
+
This module contains user interfaces and presentation logic.
|
5
|
+
"""
|
6
|
+
|
7
|
+
from .cli import CLI
|
8
|
+
from .formatters import (
|
9
|
+
PanelFormatter,
|
10
|
+
ResponseFormatter,
|
11
|
+
StatsFormatter,
|
12
|
+
TableFormatter,
|
13
|
+
TaskFormatter,
|
14
|
+
)
|
15
|
+
from .tools import ToolCallHandler
|
16
|
+
|
17
|
+
__all__ = [
|
18
|
+
"CLI",
|
19
|
+
"PanelFormatter",
|
20
|
+
"ResponseFormatter",
|
21
|
+
"StatsFormatter",
|
22
|
+
"TableFormatter",
|
23
|
+
"TaskFormatter",
|
24
|
+
"ToolCallHandler",
|
25
|
+
]
|
@@ -2,7 +2,12 @@
|
|
2
2
|
Command-line interface for todo.sh LLM agent.
|
3
3
|
"""
|
4
4
|
|
5
|
+
from typing import Optional
|
6
|
+
|
5
7
|
try:
|
8
|
+
import readline
|
9
|
+
|
10
|
+
from rich.align import Align
|
6
11
|
from rich.console import Console
|
7
12
|
from rich.live import Live
|
8
13
|
from rich.spinner import Spinner
|
@@ -13,6 +18,13 @@ try:
|
|
13
18
|
from todo_agent.infrastructure.inference import Inference
|
14
19
|
from todo_agent.infrastructure.logger import Logger
|
15
20
|
from todo_agent.infrastructure.todo_shell import TodoShell
|
21
|
+
from todo_agent.interface.formatters import (
|
22
|
+
CLI_WIDTH,
|
23
|
+
PanelFormatter,
|
24
|
+
ResponseFormatter,
|
25
|
+
TableFormatter,
|
26
|
+
TaskFormatter,
|
27
|
+
)
|
16
28
|
from todo_agent.interface.tools import ToolCallHandler
|
17
29
|
except ImportError:
|
18
30
|
from core.todo_manager import TodoManager # type: ignore[no-redef]
|
@@ -20,7 +32,15 @@ except ImportError:
|
|
20
32
|
from infrastructure.inference import Inference # type: ignore[no-redef]
|
21
33
|
from infrastructure.logger import Logger # type: ignore[no-redef]
|
22
34
|
from infrastructure.todo_shell import TodoShell # type: ignore[no-redef]
|
35
|
+
from interface.formatters import ( # type: ignore[no-redef]
|
36
|
+
CLI_WIDTH,
|
37
|
+
PanelFormatter,
|
38
|
+
ResponseFormatter,
|
39
|
+
TableFormatter,
|
40
|
+
TaskFormatter,
|
41
|
+
)
|
23
42
|
from interface.tools import ToolCallHandler # type: ignore[no-redef]
|
43
|
+
from rich.align import Align
|
24
44
|
from rich.console import Console
|
25
45
|
from rich.live import Live
|
26
46
|
from rich.spinner import Spinner
|
@@ -31,6 +51,9 @@ class CLI:
|
|
31
51
|
"""User interaction loop and input/output handling."""
|
32
52
|
|
33
53
|
def __init__(self) -> None:
|
54
|
+
# Initialize readline for arrow key navigation
|
55
|
+
readline.set_history_length(50) # Match existing conversation cap
|
56
|
+
|
34
57
|
# Initialize logger first
|
35
58
|
self.logger = Logger("cli")
|
36
59
|
self.logger.info("Initializing CLI")
|
@@ -55,8 +78,8 @@ class CLI:
|
|
55
78
|
self.inference = Inference(self.config, self.tool_handler, self.logger)
|
56
79
|
self.logger.debug("Inference engine initialized")
|
57
80
|
|
58
|
-
# Initialize rich console for animations
|
59
|
-
self.console = Console()
|
81
|
+
# Initialize rich console for animations with consistent width
|
82
|
+
self.console = Console(width=CLI_WIDTH)
|
60
83
|
|
61
84
|
self.logger.info("CLI initialization completed")
|
62
85
|
|
@@ -82,20 +105,72 @@ class CLI:
|
|
82
105
|
initial_spinner = self._create_thinking_spinner("Thinking...")
|
83
106
|
return Live(initial_spinner, console=self.console, refresh_per_second=10)
|
84
107
|
|
108
|
+
def _print_header(self) -> None:
|
109
|
+
"""Print the application header with unicode borders."""
|
110
|
+
header_panel = PanelFormatter.create_header_panel()
|
111
|
+
self.console.print(header_panel)
|
112
|
+
|
113
|
+
subtitle = Text(
|
114
|
+
"Type your request naturally, or enter 'quit' to exit, or 'help' for commands",
|
115
|
+
style="dim",
|
116
|
+
)
|
117
|
+
self.console.print(Align.center(subtitle), style="dim")
|
118
|
+
|
119
|
+
def _print_help(self) -> None:
|
120
|
+
"""Print help information in a formatted table."""
|
121
|
+
table = TableFormatter.create_command_table()
|
122
|
+
self.console.print(table)
|
123
|
+
self.console.print("Or just type your request naturally!", style="italic green")
|
124
|
+
|
125
|
+
def _print_about(self) -> None:
|
126
|
+
"""Print about information in a formatted panel."""
|
127
|
+
about_panel = PanelFormatter.create_about_panel()
|
128
|
+
self.console.print(about_panel)
|
129
|
+
|
130
|
+
def _print_stats(self, summary: dict) -> None:
|
131
|
+
"""Print conversation statistics in a formatted table."""
|
132
|
+
table = TableFormatter.create_stats_table(summary)
|
133
|
+
self.console.print(table)
|
134
|
+
|
135
|
+
def _get_memory_usage(self) -> Optional[Text]:
|
136
|
+
"""Get session memory usage as a progress bar."""
|
137
|
+
# Get conversation manager to access memory limits and current usage
|
138
|
+
conversation_manager = self.inference.get_conversation_manager()
|
139
|
+
|
140
|
+
# Get current usage from conversation summary
|
141
|
+
summary = conversation_manager.get_conversation_summary()
|
142
|
+
current_tokens = summary.get("estimated_tokens", 0)
|
143
|
+
current_messages = summary.get("total_messages", 0)
|
144
|
+
|
145
|
+
# Get limits from conversation manager
|
146
|
+
max_tokens = conversation_manager.max_tokens
|
147
|
+
max_messages = conversation_manager.max_messages
|
148
|
+
|
149
|
+
# Create memory usage bar
|
150
|
+
memory_bar = PanelFormatter.create_memory_usage_bar(
|
151
|
+
current_tokens, max_tokens, current_messages, max_messages
|
152
|
+
)
|
153
|
+
|
154
|
+
return memory_bar
|
155
|
+
|
85
156
|
def run(self) -> None:
|
86
157
|
"""Main CLI interaction loop."""
|
87
158
|
self.logger.info("Starting CLI interaction loop")
|
88
|
-
|
89
|
-
|
90
|
-
|
159
|
+
|
160
|
+
# Print header
|
161
|
+
self._print_header()
|
162
|
+
|
163
|
+
# Print separator
|
164
|
+
self.console.print("─" * CLI_WIDTH, style="dim")
|
91
165
|
|
92
166
|
while True:
|
93
167
|
try:
|
94
|
-
|
168
|
+
# Print prompt with unicode character
|
169
|
+
user_input = self.console.input("\n[bold cyan]▶[/bold cyan] ").strip()
|
95
170
|
|
96
171
|
if user_input.lower() in ["quit", "exit", "q"]:
|
97
172
|
self.logger.info("User requested exit")
|
98
|
-
print("Goodbye!")
|
173
|
+
self.console.print("\n[bold green]Goodbye! 👋[/bold green]")
|
99
174
|
break
|
100
175
|
|
101
176
|
if not user_input:
|
@@ -105,76 +180,69 @@ class CLI:
|
|
105
180
|
if user_input.lower() == "clear":
|
106
181
|
self.logger.info("User requested conversation clear")
|
107
182
|
self.inference.clear_conversation()
|
108
|
-
print(
|
183
|
+
self.console.print(
|
184
|
+
ResponseFormatter.format_success(
|
185
|
+
"Conversation history cleared."
|
186
|
+
)
|
187
|
+
)
|
109
188
|
continue
|
110
189
|
|
111
|
-
if user_input.lower() == "
|
112
|
-
self.logger.debug("User requested conversation
|
190
|
+
if user_input.lower() == "stats":
|
191
|
+
self.logger.debug("User requested conversation stats")
|
113
192
|
summary = self.inference.get_conversation_summary()
|
114
|
-
|
115
|
-
print(f" Total messages: {summary['total_messages']}")
|
116
|
-
print(f" User messages: {summary['user_messages']}")
|
117
|
-
print(f" Assistant messages: {summary['assistant_messages']}")
|
118
|
-
print(f" Tool messages: {summary['tool_messages']}")
|
119
|
-
print(f" Estimated tokens: {summary['estimated_tokens']}")
|
120
|
-
|
121
|
-
# Display thinking time statistics if available
|
122
|
-
if (
|
123
|
-
"thinking_time_count" in summary
|
124
|
-
and summary["thinking_time_count"] > 0
|
125
|
-
):
|
126
|
-
print(f" Thinking time stats:")
|
127
|
-
print(
|
128
|
-
f" Total thinking time: {summary['total_thinking_time']:.2f}s"
|
129
|
-
)
|
130
|
-
print(
|
131
|
-
f" Average thinking time: {summary['average_thinking_time']:.2f}s"
|
132
|
-
)
|
133
|
-
print(
|
134
|
-
f" Min thinking time: {summary['min_thinking_time']:.2f}s"
|
135
|
-
)
|
136
|
-
print(
|
137
|
-
f" Max thinking time: {summary['max_thinking_time']:.2f}s"
|
138
|
-
)
|
139
|
-
print(
|
140
|
-
f" Requests with timing: {summary['thinking_time_count']}"
|
141
|
-
)
|
193
|
+
self._print_stats(summary)
|
142
194
|
continue
|
143
195
|
|
144
196
|
if user_input.lower() == "help":
|
145
197
|
self.logger.debug("User requested help")
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
print(" Or just type your request naturally!")
|
198
|
+
self._print_help()
|
199
|
+
continue
|
200
|
+
|
201
|
+
if user_input.lower() == "about":
|
202
|
+
self.logger.debug("User requested about information")
|
203
|
+
self._print_about()
|
153
204
|
continue
|
154
205
|
|
155
206
|
if user_input.lower() == "list":
|
156
207
|
self.logger.debug("User requested task list")
|
157
208
|
try:
|
158
209
|
output = self.todo_shell.list_tasks()
|
159
|
-
|
210
|
+
formatted_output = TaskFormatter.format_task_list(output)
|
211
|
+
task_panel = PanelFormatter.create_task_panel(formatted_output)
|
212
|
+
self.console.print(task_panel)
|
160
213
|
except Exception as e:
|
161
214
|
self.logger.error(f"Error listing tasks: {e!s}")
|
162
|
-
|
215
|
+
error_msg = ResponseFormatter.format_error(
|
216
|
+
f"Failed to list tasks: {e!s}"
|
217
|
+
)
|
218
|
+
self.console.print(error_msg)
|
163
219
|
continue
|
164
220
|
|
165
221
|
self.logger.info(
|
166
222
|
f"Processing user request: {user_input[:50]}{'...' if len(user_input) > 50 else ''}"
|
167
223
|
)
|
168
224
|
response = self.handle_request(user_input)
|
169
|
-
|
225
|
+
|
226
|
+
# Format the response and create a panel
|
227
|
+
formatted_response = ResponseFormatter.format_response(response)
|
228
|
+
|
229
|
+
# Get memory usage
|
230
|
+
memory_usage = self._get_memory_usage()
|
231
|
+
|
232
|
+
# Create response panel with memory usage
|
233
|
+
response_panel = PanelFormatter.create_response_panel(
|
234
|
+
formatted_response, memory_usage=memory_usage
|
235
|
+
)
|
236
|
+
self.console.print(response_panel)
|
170
237
|
|
171
238
|
except KeyboardInterrupt:
|
172
239
|
self.logger.info("User interrupted with Ctrl+C")
|
173
|
-
print("\
|
240
|
+
self.console.print("\n[bold green]Goodbye! 👋[/bold green]")
|
174
241
|
break
|
175
242
|
except Exception as e:
|
176
243
|
self.logger.error(f"Error in CLI loop: {e!s}")
|
177
|
-
|
244
|
+
error_msg = ResponseFormatter.format_error(str(e))
|
245
|
+
self.console.print(error_msg)
|
178
246
|
|
179
247
|
def handle_request(self, user_input: str) -> str:
|
180
248
|
"""
|
@@ -206,7 +274,7 @@ class CLI:
|
|
206
274
|
self.logger.error(f"Error in handle_request: {e!s}")
|
207
275
|
|
208
276
|
# Return error message
|
209
|
-
return
|
277
|
+
return ResponseFormatter.format_error(str(e))
|
210
278
|
|
211
279
|
def run_single_request(self, user_input: str) -> str:
|
212
280
|
"""
|
@@ -0,0 +1,457 @@
|
|
1
|
+
"""
|
2
|
+
Formatters for CLI output with unicode characters and consistent styling.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import Any, Dict, Optional
|
6
|
+
|
7
|
+
from rich.align import Align
|
8
|
+
from rich.box import ROUNDED
|
9
|
+
from rich.panel import Panel
|
10
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn
|
11
|
+
from rich.table import Table
|
12
|
+
from rich.text import Text
|
13
|
+
|
14
|
+
# CLI width configuration
|
15
|
+
CLI_WIDTH = 100
|
16
|
+
PANEL_WIDTH = CLI_WIDTH - 2 # Leave 2 characters for borders
|
17
|
+
|
18
|
+
|
19
|
+
class TaskFormatter:
|
20
|
+
"""Formats task-related output with unicode characters and consistent styling."""
|
21
|
+
|
22
|
+
@staticmethod
|
23
|
+
def format_task_list(raw_tasks: str) -> Text:
|
24
|
+
"""
|
25
|
+
Format a raw task list with unicode characters and numbering.
|
26
|
+
|
27
|
+
Args:
|
28
|
+
raw_tasks: Raw task output from todo.sh
|
29
|
+
title: Title for the task list
|
30
|
+
|
31
|
+
Returns:
|
32
|
+
Formatted task list as Rich Text object
|
33
|
+
"""
|
34
|
+
if not raw_tasks.strip():
|
35
|
+
return Text("No tasks found.")
|
36
|
+
|
37
|
+
lines = raw_tasks.strip().split("\n")
|
38
|
+
formatted_text = Text()
|
39
|
+
task_count = 0
|
40
|
+
|
41
|
+
# Add header
|
42
|
+
formatted_text.append("Tasks", style="bold blue")
|
43
|
+
formatted_text.append("\n\n")
|
44
|
+
|
45
|
+
for line in lines:
|
46
|
+
line = line.strip()
|
47
|
+
# Skip empty lines, separators, and todo.sh's own summary line
|
48
|
+
if line and line != "--" and not line.startswith("TODO:"):
|
49
|
+
task_count += 1
|
50
|
+
# Parse todo.txt format and make it more readable
|
51
|
+
formatted_task = TaskFormatter._format_single_task(line, task_count)
|
52
|
+
# Create a Text object that respects ANSI codes
|
53
|
+
task_text = Text.from_ansi(formatted_task)
|
54
|
+
formatted_text.append(task_text)
|
55
|
+
formatted_text.append("\n")
|
56
|
+
|
57
|
+
# Add task count at the end
|
58
|
+
if task_count > 0:
|
59
|
+
formatted_text.append("\n")
|
60
|
+
formatted_text.append(f"TODO: {task_count} of {task_count} tasks shown")
|
61
|
+
else:
|
62
|
+
formatted_text = Text("No tasks found.")
|
63
|
+
|
64
|
+
return formatted_text
|
65
|
+
|
66
|
+
@staticmethod
|
67
|
+
def _format_single_task(task_line: str, task_number: int) -> str:
|
68
|
+
"""
|
69
|
+
Format a single task line with unicode characters.
|
70
|
+
|
71
|
+
Args:
|
72
|
+
task_line: Raw task line from todo.sh
|
73
|
+
task_number: Task number for display
|
74
|
+
|
75
|
+
Returns:
|
76
|
+
Formatted task string
|
77
|
+
"""
|
78
|
+
# Parse todo.txt format: "1 (A) 2025-08-29 Clean cat box @home +chores due:2025-08-29"
|
79
|
+
parts = task_line.split(
|
80
|
+
" ", 1
|
81
|
+
) # Split on first space to separate number from rest
|
82
|
+
if len(parts) < 2:
|
83
|
+
return f" {task_number:2d} │ │ {task_line}"
|
84
|
+
|
85
|
+
rest = parts[1]
|
86
|
+
|
87
|
+
# Extract priority if present (format: "(A)")
|
88
|
+
priority = ""
|
89
|
+
description = rest
|
90
|
+
|
91
|
+
if rest.startswith("(") and ")" in rest:
|
92
|
+
priority_end = rest.find(")")
|
93
|
+
priority = rest[1:priority_end]
|
94
|
+
description = rest[priority_end + 1 :].strip()
|
95
|
+
|
96
|
+
# Format with unicode characters
|
97
|
+
if priority:
|
98
|
+
formatted_line = f" {task_number:2d} │ {priority} │ {description}"
|
99
|
+
else:
|
100
|
+
formatted_line = f" {task_number:2d} │ │ {description}"
|
101
|
+
|
102
|
+
return formatted_line
|
103
|
+
|
104
|
+
@staticmethod
|
105
|
+
def format_projects(raw_projects: str) -> str:
|
106
|
+
"""
|
107
|
+
Format project list with unicode characters.
|
108
|
+
|
109
|
+
Args:
|
110
|
+
raw_projects: Raw project output from todo.sh
|
111
|
+
|
112
|
+
Returns:
|
113
|
+
Formatted project list string
|
114
|
+
"""
|
115
|
+
if not raw_projects.strip():
|
116
|
+
return "No projects found."
|
117
|
+
|
118
|
+
lines = raw_projects.strip().split("\n")
|
119
|
+
formatted_lines = []
|
120
|
+
|
121
|
+
for i, project in enumerate(lines, 1):
|
122
|
+
if project.strip():
|
123
|
+
# Remove the + prefix and format nicely
|
124
|
+
clean_project = project.strip().lstrip("+")
|
125
|
+
formatted_lines.append(f" {i:2d} │ {clean_project}")
|
126
|
+
|
127
|
+
if formatted_lines:
|
128
|
+
return "\n".join(formatted_lines)
|
129
|
+
else:
|
130
|
+
return "No projects found."
|
131
|
+
|
132
|
+
@staticmethod
|
133
|
+
def format_contexts(raw_contexts: str) -> str:
|
134
|
+
"""
|
135
|
+
Format context list with unicode characters.
|
136
|
+
|
137
|
+
Args:
|
138
|
+
raw_contexts: Raw context output from todo.sh
|
139
|
+
|
140
|
+
Returns:
|
141
|
+
Formatted context list string
|
142
|
+
"""
|
143
|
+
if not raw_contexts.strip():
|
144
|
+
return "No contexts found."
|
145
|
+
|
146
|
+
lines = raw_contexts.strip().split("\n")
|
147
|
+
formatted_lines = []
|
148
|
+
|
149
|
+
for i, context in enumerate(lines, 1):
|
150
|
+
if context.strip():
|
151
|
+
# Remove the @ prefix and format nicely
|
152
|
+
clean_context = context.strip().lstrip("@")
|
153
|
+
formatted_lines.append(f" {i:2d} │ {clean_context}")
|
154
|
+
|
155
|
+
if formatted_lines:
|
156
|
+
return "\n".join(formatted_lines)
|
157
|
+
else:
|
158
|
+
return "No contexts found."
|
159
|
+
|
160
|
+
|
161
|
+
class ResponseFormatter:
|
162
|
+
"""Formats LLM responses and other output with consistent styling."""
|
163
|
+
|
164
|
+
@staticmethod
|
165
|
+
def format_response(response: str) -> str:
|
166
|
+
"""
|
167
|
+
Format an LLM response with consistent styling.
|
168
|
+
|
169
|
+
Args:
|
170
|
+
response: Raw response text
|
171
|
+
title: Title for the response panel
|
172
|
+
|
173
|
+
Returns:
|
174
|
+
Formatted response string
|
175
|
+
"""
|
176
|
+
# If response contains task lists, format them nicely
|
177
|
+
if "No tasks found" in response or "1." in response:
|
178
|
+
# This might be a task list response, try to format it
|
179
|
+
lines = response.split("\n")
|
180
|
+
formatted_lines = []
|
181
|
+
|
182
|
+
for line in lines:
|
183
|
+
if line.strip().startswith(
|
184
|
+
("1.", "2.", "3.", "4.", "5.", "6.", "7.", "8.", "9.")
|
185
|
+
):
|
186
|
+
# This looks like a numbered task list, format it
|
187
|
+
parts = line.split(".", 1)
|
188
|
+
if len(parts) == 2:
|
189
|
+
number = parts[0].strip()
|
190
|
+
content = parts[1].strip()
|
191
|
+
formatted_lines.append(f" {number:>2} │ {content}")
|
192
|
+
else:
|
193
|
+
formatted_lines.append(line)
|
194
|
+
else:
|
195
|
+
formatted_lines.append(line)
|
196
|
+
|
197
|
+
return "\n".join(formatted_lines)
|
198
|
+
|
199
|
+
return response
|
200
|
+
|
201
|
+
@staticmethod
|
202
|
+
def format_error(error_message: str) -> str:
|
203
|
+
"""
|
204
|
+
Format error messages consistently.
|
205
|
+
|
206
|
+
Args:
|
207
|
+
error_message: Error message to format
|
208
|
+
|
209
|
+
Returns:
|
210
|
+
Formatted error string
|
211
|
+
"""
|
212
|
+
return f"❌ {error_message}"
|
213
|
+
|
214
|
+
@staticmethod
|
215
|
+
def format_success(message: str) -> str:
|
216
|
+
"""
|
217
|
+
Format success messages consistently.
|
218
|
+
|
219
|
+
Args:
|
220
|
+
message: Success message to format
|
221
|
+
|
222
|
+
Returns:
|
223
|
+
Formatted success string
|
224
|
+
"""
|
225
|
+
return f"✅ {message}"
|
226
|
+
|
227
|
+
|
228
|
+
class StatsFormatter:
|
229
|
+
"""Formats statistics and overview information."""
|
230
|
+
|
231
|
+
@staticmethod
|
232
|
+
def format_overview(overview: str) -> str:
|
233
|
+
"""
|
234
|
+
Format task overview with unicode characters.
|
235
|
+
|
236
|
+
Args:
|
237
|
+
overview: Raw overview string
|
238
|
+
|
239
|
+
Returns:
|
240
|
+
Formatted overview string
|
241
|
+
"""
|
242
|
+
if "Task Overview:" in overview:
|
243
|
+
lines = overview.split("\n")
|
244
|
+
formatted_lines = []
|
245
|
+
|
246
|
+
for line in lines:
|
247
|
+
if line.startswith("- Active tasks:"):
|
248
|
+
formatted_lines.append(f"📋 {line[2:]}")
|
249
|
+
elif line.startswith("- Completed tasks:"):
|
250
|
+
formatted_lines.append(f"✅ {line[2:]}")
|
251
|
+
else:
|
252
|
+
formatted_lines.append(line)
|
253
|
+
|
254
|
+
return "\n".join(formatted_lines)
|
255
|
+
|
256
|
+
return overview
|
257
|
+
|
258
|
+
|
259
|
+
class TableFormatter:
|
260
|
+
"""Creates rich tables for various data displays."""
|
261
|
+
|
262
|
+
@staticmethod
|
263
|
+
def create_command_table() -> Table:
|
264
|
+
"""Create a table for displaying available commands."""
|
265
|
+
table = Table(
|
266
|
+
title="Available Commands",
|
267
|
+
box=ROUNDED,
|
268
|
+
show_header=True,
|
269
|
+
header_style="bold magenta",
|
270
|
+
width=PANEL_WIDTH,
|
271
|
+
)
|
272
|
+
|
273
|
+
table.add_column("Command", style="cyan", width=12)
|
274
|
+
table.add_column("Description", style="white")
|
275
|
+
|
276
|
+
commands = [
|
277
|
+
("clear", "Clear conversation history"),
|
278
|
+
("stats", "Show conversation statistics"),
|
279
|
+
("help", "Show this help message"),
|
280
|
+
("about", "Show application information"),
|
281
|
+
("list", "List all tasks (no LLM interaction)"),
|
282
|
+
("quit", "Exit the application"),
|
283
|
+
]
|
284
|
+
|
285
|
+
for cmd, desc in commands:
|
286
|
+
table.add_row(cmd, desc)
|
287
|
+
|
288
|
+
return table
|
289
|
+
|
290
|
+
@staticmethod
|
291
|
+
def create_stats_table(summary: Dict[str, Any]) -> Table:
|
292
|
+
"""Create a table for displaying conversation statistics."""
|
293
|
+
table = Table(
|
294
|
+
title="Conversation Statistics",
|
295
|
+
box=ROUNDED,
|
296
|
+
show_header=True,
|
297
|
+
header_style="bold magenta",
|
298
|
+
width=PANEL_WIDTH,
|
299
|
+
)
|
300
|
+
|
301
|
+
table.add_column("Metric", style="cyan", width=20)
|
302
|
+
table.add_column("Value", style="white")
|
303
|
+
|
304
|
+
# Basic stats
|
305
|
+
table.add_row("Total Messages", str(summary["total_messages"]))
|
306
|
+
table.add_row("User Messages", str(summary["user_messages"]))
|
307
|
+
table.add_row("Assistant Messages", str(summary["assistant_messages"]))
|
308
|
+
table.add_row("Tool Messages", str(summary["tool_messages"]))
|
309
|
+
table.add_row("Estimated Tokens", str(summary["estimated_tokens"]))
|
310
|
+
|
311
|
+
# Thinking time stats if available
|
312
|
+
if "thinking_time_count" in summary and summary["thinking_time_count"] > 0:
|
313
|
+
table.add_row("", "") # Empty row for spacing
|
314
|
+
table.add_row(
|
315
|
+
"Total Thinking Time", f"{summary['total_thinking_time']:.2f}s"
|
316
|
+
)
|
317
|
+
table.add_row(
|
318
|
+
"Average Thinking Time", f"{summary['average_thinking_time']:.2f}s"
|
319
|
+
)
|
320
|
+
table.add_row("Min Thinking Time", f"{summary['min_thinking_time']:.2f}s")
|
321
|
+
table.add_row("Max Thinking Time", f"{summary['max_thinking_time']:.2f}s")
|
322
|
+
table.add_row("Requests with Timing", str(summary["thinking_time_count"]))
|
323
|
+
|
324
|
+
return table
|
325
|
+
|
326
|
+
|
327
|
+
class PanelFormatter:
|
328
|
+
"""Creates rich panels for various content displays."""
|
329
|
+
|
330
|
+
@staticmethod
|
331
|
+
def create_header_panel() -> Panel:
|
332
|
+
"""Create the application header panel."""
|
333
|
+
header_text = Text("Todo.sh LLM Agent", style="bold blue")
|
334
|
+
return Panel(
|
335
|
+
Align.center(header_text),
|
336
|
+
title="🤖",
|
337
|
+
border_style="dim",
|
338
|
+
box=ROUNDED,
|
339
|
+
width=PANEL_WIDTH + 2,
|
340
|
+
)
|
341
|
+
|
342
|
+
@staticmethod
|
343
|
+
def create_task_panel(content: str, title: str = "📋 Current Tasks") -> Panel:
|
344
|
+
"""Create a panel for displaying task lists."""
|
345
|
+
return Panel(
|
346
|
+
content, title=title, border_style="dim", box=ROUNDED, width=PANEL_WIDTH
|
347
|
+
)
|
348
|
+
|
349
|
+
@staticmethod
|
350
|
+
def create_response_panel(content: str, title: str = "🤖 Assistant", memory_usage: Optional[Text] = None) -> Panel:
|
351
|
+
"""Create a panel for displaying LLM responses."""
|
352
|
+
if memory_usage:
|
353
|
+
# Create the combined content with centered memory usage
|
354
|
+
return Panel(
|
355
|
+
Align.center(
|
356
|
+
Text.assemble(
|
357
|
+
content,
|
358
|
+
"\n\n",
|
359
|
+
"─" * (PANEL_WIDTH - 4), # Separator line
|
360
|
+
"\n",
|
361
|
+
memory_usage
|
362
|
+
)
|
363
|
+
),
|
364
|
+
title=title, border_style="dim", box=ROUNDED, width=PANEL_WIDTH
|
365
|
+
)
|
366
|
+
else:
|
367
|
+
return Panel(
|
368
|
+
content, title=title, border_style="dim", box=ROUNDED, width=PANEL_WIDTH
|
369
|
+
)
|
370
|
+
|
371
|
+
@staticmethod
|
372
|
+
def create_error_panel(content: str, title: str = "❌ Error") -> Panel:
|
373
|
+
"""Create a panel for displaying errors."""
|
374
|
+
return Panel(
|
375
|
+
content, title=title, border_style="red", box=ROUNDED, width=PANEL_WIDTH
|
376
|
+
)
|
377
|
+
|
378
|
+
@staticmethod
|
379
|
+
def create_about_panel() -> Panel:
|
380
|
+
"""Create a panel for displaying about information."""
|
381
|
+
from todo_agent._version import __commit_id__, __version__
|
382
|
+
|
383
|
+
about_content = Text()
|
384
|
+
about_content.append("Todo.sh LLM Agent\n", style="bold blue")
|
385
|
+
about_content.append("\n")
|
386
|
+
about_content.append(
|
387
|
+
"A natural language interface for todo.sh task management\n", style="white"
|
388
|
+
)
|
389
|
+
about_content.append("powered by LLM function calling.\n", style="white")
|
390
|
+
about_content.append("\n")
|
391
|
+
about_content.append("Version: ", style="cyan")
|
392
|
+
about_content.append(f"{__version__}\n", style="white")
|
393
|
+
if __commit_id__:
|
394
|
+
about_content.append("Commit: ", style="cyan")
|
395
|
+
about_content.append(f"{__commit_id__}\n", style="white")
|
396
|
+
about_content.append("\n")
|
397
|
+
about_content.append(
|
398
|
+
"Transform natural language into todo.sh commands:\n", style="italic"
|
399
|
+
)
|
400
|
+
about_content.append("• 'add buy groceries to shopping list'\n", style="dim")
|
401
|
+
about_content.append("• 'show my work tasks'\n", style="dim")
|
402
|
+
about_content.append("• 'mark task 3 as done'\n", style="dim")
|
403
|
+
about_content.append("\n")
|
404
|
+
about_content.append("GitHub: ", style="cyan")
|
405
|
+
about_content.append(
|
406
|
+
"https://github.com/codeprimate/todo-agent\n", style="blue"
|
407
|
+
)
|
408
|
+
|
409
|
+
return Panel(
|
410
|
+
Align.center(about_content),
|
411
|
+
title="i About",
|
412
|
+
border_style="dim",
|
413
|
+
box=ROUNDED,
|
414
|
+
width=PANEL_WIDTH + 2,
|
415
|
+
)
|
416
|
+
|
417
|
+
@staticmethod
|
418
|
+
def create_memory_usage_bar(current_tokens: int, max_tokens: int, current_messages: int, max_messages: int) -> Text:
|
419
|
+
"""
|
420
|
+
Create a rich progress bar showing session memory usage.
|
421
|
+
|
422
|
+
Args:
|
423
|
+
current_tokens: Current number of tokens in conversation
|
424
|
+
max_tokens: Maximum allowed tokens
|
425
|
+
current_messages: Current number of messages in conversation
|
426
|
+
max_messages: Maximum allowed messages
|
427
|
+
|
428
|
+
Returns:
|
429
|
+
Rich Text object with memory usage progress bar
|
430
|
+
"""
|
431
|
+
# Calculate percentage
|
432
|
+
token_percentage = min(100, (current_tokens / max_tokens) * 100)
|
433
|
+
|
434
|
+
# Determine color based on usage
|
435
|
+
if token_percentage >= 90:
|
436
|
+
color = "red"
|
437
|
+
elif token_percentage >= 75:
|
438
|
+
color = "yellow"
|
439
|
+
else:
|
440
|
+
color = "green"
|
441
|
+
|
442
|
+
# Create the progress bar text
|
443
|
+
memory_text = Text()
|
444
|
+
memory_text.append(f"{current_tokens:,}/{max_tokens:,} ", style="dim")
|
445
|
+
|
446
|
+
# Create a simple text-based progress bar
|
447
|
+
bar_length = 25
|
448
|
+
token_filled = int((token_percentage / 100) * bar_length)
|
449
|
+
token_bar = "█" * token_filled + "░" * (bar_length - token_filled)
|
450
|
+
memory_text.append(f"[{token_bar}] ", style="dim")
|
451
|
+
memory_text.append(f"{token_percentage:.1f}%", style="dim")
|
452
|
+
|
453
|
+
# Add message count without progress bar
|
454
|
+
memory_text.append(" | ", style="dim")
|
455
|
+
memory_text.append(f"{current_messages}/{max_messages}", style="dim")
|
456
|
+
|
457
|
+
return memory_text
|
@@ -36,8 +36,16 @@ Examples:
|
|
36
36
|
|
37
37
|
if args.command:
|
38
38
|
# Single command mode
|
39
|
-
|
40
|
-
|
39
|
+
# Handle special commands that don't need LLM processing
|
40
|
+
if args.command.lower() in ["help", "about"]:
|
41
|
+
if args.command.lower() == "help":
|
42
|
+
cli._print_help()
|
43
|
+
elif args.command.lower() == "about":
|
44
|
+
cli._print_about()
|
45
|
+
else:
|
46
|
+
# Process through LLM
|
47
|
+
response = cli.run_single_request(args.command)
|
48
|
+
print(response)
|
41
49
|
else:
|
42
50
|
# Interactive mode
|
43
51
|
cli.run()
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|