todo-agent 0.2.1__py3-none-any.whl → 0.2.3__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.
- todo_agent/_version.py +2 -2
- todo_agent/interface/__init__.py +16 -1
- todo_agent/interface/cli.py +91 -51
- todo_agent/interface/formatters.py +399 -0
- todo_agent/main.py +10 -2
- {todo_agent-0.2.1.dist-info → todo_agent-0.2.3.dist-info}/METADATA +1 -1
- {todo_agent-0.2.1.dist-info → todo_agent-0.2.3.dist-info}/RECORD +11 -10
- {todo_agent-0.2.1.dist-info → todo_agent-0.2.3.dist-info}/WHEEL +0 -0
- {todo_agent-0.2.1.dist-info → todo_agent-0.2.3.dist-info}/entry_points.txt +0 -0
- {todo_agent-0.2.1.dist-info → todo_agent-0.2.3.dist-info}/licenses/LICENSE +0 -0
- {todo_agent-0.2.1.dist-info → todo_agent-0.2.3.dist-info}/top_level.txt +0 -0
todo_agent/_version.py
CHANGED
@@ -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.3'
|
32
|
+
__version_tuple__ = version_tuple = (0, 2, 3)
|
33
33
|
|
34
34
|
__commit_id__ = commit_id = None
|
todo_agent/interface/__init__.py
CHANGED
@@ -5,6 +5,21 @@ This module contains user interfaces and presentation logic.
|
|
5
5
|
"""
|
6
6
|
|
7
7
|
from .cli import CLI
|
8
|
+
from .formatters import (
|
9
|
+
PanelFormatter,
|
10
|
+
ResponseFormatter,
|
11
|
+
StatsFormatter,
|
12
|
+
TableFormatter,
|
13
|
+
TaskFormatter,
|
14
|
+
)
|
8
15
|
from .tools import ToolCallHandler
|
9
16
|
|
10
|
-
__all__ = [
|
17
|
+
__all__ = [
|
18
|
+
"CLI",
|
19
|
+
"PanelFormatter",
|
20
|
+
"ResponseFormatter",
|
21
|
+
"StatsFormatter",
|
22
|
+
"TableFormatter",
|
23
|
+
"TaskFormatter",
|
24
|
+
"ToolCallHandler",
|
25
|
+
]
|
todo_agent/interface/cli.py
CHANGED
@@ -3,6 +3,9 @@ Command-line interface for todo.sh LLM agent.
|
|
3
3
|
"""
|
4
4
|
|
5
5
|
try:
|
6
|
+
import readline
|
7
|
+
|
8
|
+
from rich.align import Align
|
6
9
|
from rich.console import Console
|
7
10
|
from rich.live import Live
|
8
11
|
from rich.spinner import Spinner
|
@@ -13,6 +16,13 @@ try:
|
|
13
16
|
from todo_agent.infrastructure.inference import Inference
|
14
17
|
from todo_agent.infrastructure.logger import Logger
|
15
18
|
from todo_agent.infrastructure.todo_shell import TodoShell
|
19
|
+
from todo_agent.interface.formatters import (
|
20
|
+
CLI_WIDTH,
|
21
|
+
PanelFormatter,
|
22
|
+
ResponseFormatter,
|
23
|
+
TableFormatter,
|
24
|
+
TaskFormatter,
|
25
|
+
)
|
16
26
|
from todo_agent.interface.tools import ToolCallHandler
|
17
27
|
except ImportError:
|
18
28
|
from core.todo_manager import TodoManager # type: ignore[no-redef]
|
@@ -20,7 +30,15 @@ except ImportError:
|
|
20
30
|
from infrastructure.inference import Inference # type: ignore[no-redef]
|
21
31
|
from infrastructure.logger import Logger # type: ignore[no-redef]
|
22
32
|
from infrastructure.todo_shell import TodoShell # type: ignore[no-redef]
|
33
|
+
from interface.formatters import ( # type: ignore[no-redef]
|
34
|
+
CLI_WIDTH,
|
35
|
+
PanelFormatter,
|
36
|
+
ResponseFormatter,
|
37
|
+
TableFormatter,
|
38
|
+
TaskFormatter,
|
39
|
+
)
|
23
40
|
from interface.tools import ToolCallHandler # type: ignore[no-redef]
|
41
|
+
from rich.align import Align
|
24
42
|
from rich.console import Console
|
25
43
|
from rich.live import Live
|
26
44
|
from rich.spinner import Spinner
|
@@ -31,6 +49,9 @@ class CLI:
|
|
31
49
|
"""User interaction loop and input/output handling."""
|
32
50
|
|
33
51
|
def __init__(self) -> None:
|
52
|
+
# Initialize readline for arrow key navigation
|
53
|
+
readline.set_history_length(50) # Match existing conversation cap
|
54
|
+
|
34
55
|
# Initialize logger first
|
35
56
|
self.logger = Logger("cli")
|
36
57
|
self.logger.info("Initializing CLI")
|
@@ -55,8 +76,8 @@ class CLI:
|
|
55
76
|
self.inference = Inference(self.config, self.tool_handler, self.logger)
|
56
77
|
self.logger.debug("Inference engine initialized")
|
57
78
|
|
58
|
-
# Initialize rich console for animations
|
59
|
-
self.console = Console()
|
79
|
+
# Initialize rich console for animations with consistent width
|
80
|
+
self.console = Console(width=CLI_WIDTH)
|
60
81
|
|
61
82
|
self.logger.info("CLI initialization completed")
|
62
83
|
|
@@ -82,20 +103,51 @@ class CLI:
|
|
82
103
|
initial_spinner = self._create_thinking_spinner("Thinking...")
|
83
104
|
return Live(initial_spinner, console=self.console, refresh_per_second=10)
|
84
105
|
|
106
|
+
def _print_header(self) -> None:
|
107
|
+
"""Print the application header with unicode borders."""
|
108
|
+
header_panel = PanelFormatter.create_header_panel()
|
109
|
+
self.console.print(header_panel)
|
110
|
+
|
111
|
+
subtitle = Text(
|
112
|
+
"Type your request naturally, or enter 'quit' to exit, or 'help' for commands",
|
113
|
+
style="dim",
|
114
|
+
)
|
115
|
+
self.console.print(Align.center(subtitle), style="dim")
|
116
|
+
|
117
|
+
def _print_help(self) -> None:
|
118
|
+
"""Print help information in a formatted table."""
|
119
|
+
table = TableFormatter.create_command_table()
|
120
|
+
self.console.print(table)
|
121
|
+
self.console.print("Or just type your request naturally!", style="italic green")
|
122
|
+
|
123
|
+
def _print_about(self) -> None:
|
124
|
+
"""Print about information in a formatted panel."""
|
125
|
+
about_panel = PanelFormatter.create_about_panel()
|
126
|
+
self.console.print(about_panel)
|
127
|
+
|
128
|
+
def _print_stats(self, summary: dict) -> None:
|
129
|
+
"""Print conversation statistics in a formatted table."""
|
130
|
+
table = TableFormatter.create_stats_table(summary)
|
131
|
+
self.console.print(table)
|
132
|
+
|
85
133
|
def run(self) -> None:
|
86
134
|
"""Main CLI interaction loop."""
|
87
135
|
self.logger.info("Starting CLI interaction loop")
|
88
|
-
|
89
|
-
|
90
|
-
|
136
|
+
|
137
|
+
# Print header
|
138
|
+
self._print_header()
|
139
|
+
|
140
|
+
# Print separator
|
141
|
+
self.console.print("─" * CLI_WIDTH, style="dim")
|
91
142
|
|
92
143
|
while True:
|
93
144
|
try:
|
94
|
-
|
145
|
+
# Print prompt with unicode character
|
146
|
+
user_input = self.console.input("\n[bold cyan]▶[/bold cyan] ").strip()
|
95
147
|
|
96
148
|
if user_input.lower() in ["quit", "exit", "q"]:
|
97
149
|
self.logger.info("User requested exit")
|
98
|
-
print("Goodbye!")
|
150
|
+
self.console.print("\n[bold green]Goodbye! 👋[/bold green]")
|
99
151
|
break
|
100
152
|
|
101
153
|
if not user_input:
|
@@ -105,76 +157,64 @@ class CLI:
|
|
105
157
|
if user_input.lower() == "clear":
|
106
158
|
self.logger.info("User requested conversation clear")
|
107
159
|
self.inference.clear_conversation()
|
108
|
-
print(
|
160
|
+
self.console.print(
|
161
|
+
ResponseFormatter.format_success(
|
162
|
+
"Conversation history cleared."
|
163
|
+
)
|
164
|
+
)
|
109
165
|
continue
|
110
166
|
|
111
|
-
if user_input.lower() == "
|
112
|
-
self.logger.debug("User requested conversation
|
167
|
+
if user_input.lower() == "stats":
|
168
|
+
self.logger.debug("User requested conversation stats")
|
113
169
|
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
|
-
)
|
170
|
+
self._print_stats(summary)
|
142
171
|
continue
|
143
172
|
|
144
173
|
if user_input.lower() == "help":
|
145
174
|
self.logger.debug("User requested help")
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
print(" Or just type your request naturally!")
|
175
|
+
self._print_help()
|
176
|
+
continue
|
177
|
+
|
178
|
+
if user_input.lower() == "about":
|
179
|
+
self.logger.debug("User requested about information")
|
180
|
+
self._print_about()
|
153
181
|
continue
|
154
182
|
|
155
183
|
if user_input.lower() == "list":
|
156
184
|
self.logger.debug("User requested task list")
|
157
185
|
try:
|
158
186
|
output = self.todo_shell.list_tasks()
|
159
|
-
|
187
|
+
formatted_output = TaskFormatter.format_task_list(output)
|
188
|
+
task_panel = PanelFormatter.create_task_panel(formatted_output)
|
189
|
+
self.console.print(task_panel)
|
160
190
|
except Exception as e:
|
161
191
|
self.logger.error(f"Error listing tasks: {e!s}")
|
162
|
-
|
192
|
+
error_msg = ResponseFormatter.format_error(
|
193
|
+
f"Failed to list tasks: {e!s}"
|
194
|
+
)
|
195
|
+
self.console.print(error_msg)
|
163
196
|
continue
|
164
197
|
|
165
198
|
self.logger.info(
|
166
199
|
f"Processing user request: {user_input[:50]}{'...' if len(user_input) > 50 else ''}"
|
167
200
|
)
|
168
201
|
response = self.handle_request(user_input)
|
169
|
-
|
202
|
+
|
203
|
+
# Format the response and create a panel
|
204
|
+
formatted_response = ResponseFormatter.format_response(response)
|
205
|
+
response_panel = PanelFormatter.create_response_panel(
|
206
|
+
formatted_response
|
207
|
+
)
|
208
|
+
self.console.print(response_panel)
|
170
209
|
|
171
210
|
except KeyboardInterrupt:
|
172
211
|
self.logger.info("User interrupted with Ctrl+C")
|
173
|
-
print("\
|
212
|
+
self.console.print("\n[bold green]Goodbye! 👋[/bold green]")
|
174
213
|
break
|
175
214
|
except Exception as e:
|
176
215
|
self.logger.error(f"Error in CLI loop: {e!s}")
|
177
|
-
|
216
|
+
error_msg = ResponseFormatter.format_error(str(e))
|
217
|
+
self.console.print(error_msg)
|
178
218
|
|
179
219
|
def handle_request(self, user_input: str) -> str:
|
180
220
|
"""
|
@@ -206,7 +246,7 @@ class CLI:
|
|
206
246
|
self.logger.error(f"Error in handle_request: {e!s}")
|
207
247
|
|
208
248
|
# Return error message
|
209
|
-
return
|
249
|
+
return ResponseFormatter.format_error(str(e))
|
210
250
|
|
211
251
|
def run_single_request(self, user_input: str) -> str:
|
212
252
|
"""
|
@@ -0,0 +1,399 @@
|
|
1
|
+
"""
|
2
|
+
Formatters for CLI output with unicode characters and consistent styling.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from typing import Any, Dict
|
6
|
+
|
7
|
+
from rich.align import Align
|
8
|
+
from rich.box import ROUNDED
|
9
|
+
from rich.panel import Panel
|
10
|
+
from rich.table import Table
|
11
|
+
from rich.text import Text
|
12
|
+
|
13
|
+
# CLI width configuration
|
14
|
+
CLI_WIDTH = 100
|
15
|
+
PANEL_WIDTH = CLI_WIDTH - 2 # Leave 2 characters for borders
|
16
|
+
|
17
|
+
|
18
|
+
class TaskFormatter:
|
19
|
+
"""Formats task-related output with unicode characters and consistent styling."""
|
20
|
+
|
21
|
+
@staticmethod
|
22
|
+
def format_task_list(raw_tasks: str) -> Text:
|
23
|
+
"""
|
24
|
+
Format a raw task list with unicode characters and numbering.
|
25
|
+
|
26
|
+
Args:
|
27
|
+
raw_tasks: Raw task output from todo.sh
|
28
|
+
title: Title for the task list
|
29
|
+
|
30
|
+
Returns:
|
31
|
+
Formatted task list as Rich Text object
|
32
|
+
"""
|
33
|
+
if not raw_tasks.strip():
|
34
|
+
return Text("No tasks found.")
|
35
|
+
|
36
|
+
lines = raw_tasks.strip().split("\n")
|
37
|
+
formatted_text = Text()
|
38
|
+
task_count = 0
|
39
|
+
|
40
|
+
# Add header
|
41
|
+
formatted_text.append("Tasks", style="bold blue")
|
42
|
+
formatted_text.append("\n\n")
|
43
|
+
|
44
|
+
for line in lines:
|
45
|
+
line = line.strip()
|
46
|
+
# Skip empty lines, separators, and todo.sh's own summary line
|
47
|
+
if line and line != "--" and not line.startswith("TODO:"):
|
48
|
+
task_count += 1
|
49
|
+
# Parse todo.txt format and make it more readable
|
50
|
+
formatted_task = TaskFormatter._format_single_task(line, task_count)
|
51
|
+
# Create a Text object that respects ANSI codes
|
52
|
+
task_text = Text.from_ansi(formatted_task)
|
53
|
+
formatted_text.append(task_text)
|
54
|
+
formatted_text.append("\n")
|
55
|
+
|
56
|
+
# Add task count at the end
|
57
|
+
if task_count > 0:
|
58
|
+
formatted_text.append("\n")
|
59
|
+
formatted_text.append(f"TODO: {task_count} of {task_count} tasks shown")
|
60
|
+
else:
|
61
|
+
formatted_text = Text("No tasks found.")
|
62
|
+
|
63
|
+
return formatted_text
|
64
|
+
|
65
|
+
@staticmethod
|
66
|
+
def _format_single_task(task_line: str, task_number: int) -> str:
|
67
|
+
"""
|
68
|
+
Format a single task line with unicode characters.
|
69
|
+
|
70
|
+
Args:
|
71
|
+
task_line: Raw task line from todo.sh
|
72
|
+
task_number: Task number for display
|
73
|
+
|
74
|
+
Returns:
|
75
|
+
Formatted task string
|
76
|
+
"""
|
77
|
+
# Parse todo.txt format: "1 (A) 2025-08-29 Clean cat box @home +chores due:2025-08-29"
|
78
|
+
parts = task_line.split(
|
79
|
+
" ", 1
|
80
|
+
) # Split on first space to separate number from rest
|
81
|
+
if len(parts) < 2:
|
82
|
+
return f" {task_number:2d} │ │ {task_line}"
|
83
|
+
|
84
|
+
rest = parts[1]
|
85
|
+
|
86
|
+
# Extract priority if present (format: "(A)")
|
87
|
+
priority = ""
|
88
|
+
description = rest
|
89
|
+
|
90
|
+
if rest.startswith("(") and ")" in rest:
|
91
|
+
priority_end = rest.find(")")
|
92
|
+
priority = rest[1:priority_end]
|
93
|
+
description = rest[priority_end + 1 :].strip()
|
94
|
+
|
95
|
+
# Format with unicode characters
|
96
|
+
if priority:
|
97
|
+
formatted_line = f" {task_number:2d} │ {priority} │ {description}"
|
98
|
+
else:
|
99
|
+
formatted_line = f" {task_number:2d} │ │ {description}"
|
100
|
+
|
101
|
+
return formatted_line
|
102
|
+
|
103
|
+
@staticmethod
|
104
|
+
def format_projects(raw_projects: str) -> str:
|
105
|
+
"""
|
106
|
+
Format project list with unicode characters.
|
107
|
+
|
108
|
+
Args:
|
109
|
+
raw_projects: Raw project output from todo.sh
|
110
|
+
|
111
|
+
Returns:
|
112
|
+
Formatted project list string
|
113
|
+
"""
|
114
|
+
if not raw_projects.strip():
|
115
|
+
return "No projects found."
|
116
|
+
|
117
|
+
lines = raw_projects.strip().split("\n")
|
118
|
+
formatted_lines = []
|
119
|
+
|
120
|
+
for i, project in enumerate(lines, 1):
|
121
|
+
if project.strip():
|
122
|
+
# Remove the + prefix and format nicely
|
123
|
+
clean_project = project.strip().lstrip("+")
|
124
|
+
formatted_lines.append(f" {i:2d} │ {clean_project}")
|
125
|
+
|
126
|
+
if formatted_lines:
|
127
|
+
return "\n".join(formatted_lines)
|
128
|
+
else:
|
129
|
+
return "No projects found."
|
130
|
+
|
131
|
+
@staticmethod
|
132
|
+
def format_contexts(raw_contexts: str) -> str:
|
133
|
+
"""
|
134
|
+
Format context list with unicode characters.
|
135
|
+
|
136
|
+
Args:
|
137
|
+
raw_contexts: Raw context output from todo.sh
|
138
|
+
|
139
|
+
Returns:
|
140
|
+
Formatted context list string
|
141
|
+
"""
|
142
|
+
if not raw_contexts.strip():
|
143
|
+
return "No contexts found."
|
144
|
+
|
145
|
+
lines = raw_contexts.strip().split("\n")
|
146
|
+
formatted_lines = []
|
147
|
+
|
148
|
+
for i, context in enumerate(lines, 1):
|
149
|
+
if context.strip():
|
150
|
+
# Remove the @ prefix and format nicely
|
151
|
+
clean_context = context.strip().lstrip("@")
|
152
|
+
formatted_lines.append(f" {i:2d} │ {clean_context}")
|
153
|
+
|
154
|
+
if formatted_lines:
|
155
|
+
return "\n".join(formatted_lines)
|
156
|
+
else:
|
157
|
+
return "No contexts found."
|
158
|
+
|
159
|
+
|
160
|
+
class ResponseFormatter:
|
161
|
+
"""Formats LLM responses and other output with consistent styling."""
|
162
|
+
|
163
|
+
@staticmethod
|
164
|
+
def format_response(response: str) -> str:
|
165
|
+
"""
|
166
|
+
Format an LLM response with consistent styling.
|
167
|
+
|
168
|
+
Args:
|
169
|
+
response: Raw response text
|
170
|
+
title: Title for the response panel
|
171
|
+
|
172
|
+
Returns:
|
173
|
+
Formatted response string
|
174
|
+
"""
|
175
|
+
# If response contains task lists, format them nicely
|
176
|
+
if "No tasks found" in response or "1." in response:
|
177
|
+
# This might be a task list response, try to format it
|
178
|
+
lines = response.split("\n")
|
179
|
+
formatted_lines = []
|
180
|
+
|
181
|
+
for line in lines:
|
182
|
+
if line.strip().startswith(
|
183
|
+
("1.", "2.", "3.", "4.", "5.", "6.", "7.", "8.", "9.")
|
184
|
+
):
|
185
|
+
# This looks like a numbered task list, format it
|
186
|
+
parts = line.split(".", 1)
|
187
|
+
if len(parts) == 2:
|
188
|
+
number = parts[0].strip()
|
189
|
+
content = parts[1].strip()
|
190
|
+
formatted_lines.append(f" {number:>2} │ {content}")
|
191
|
+
else:
|
192
|
+
formatted_lines.append(line)
|
193
|
+
else:
|
194
|
+
formatted_lines.append(line)
|
195
|
+
|
196
|
+
return "\n".join(formatted_lines)
|
197
|
+
|
198
|
+
return response
|
199
|
+
|
200
|
+
@staticmethod
|
201
|
+
def format_error(error_message: str) -> str:
|
202
|
+
"""
|
203
|
+
Format error messages consistently.
|
204
|
+
|
205
|
+
Args:
|
206
|
+
error_message: Error message to format
|
207
|
+
|
208
|
+
Returns:
|
209
|
+
Formatted error string
|
210
|
+
"""
|
211
|
+
return f"❌ {error_message}"
|
212
|
+
|
213
|
+
@staticmethod
|
214
|
+
def format_success(message: str) -> str:
|
215
|
+
"""
|
216
|
+
Format success messages consistently.
|
217
|
+
|
218
|
+
Args:
|
219
|
+
message: Success message to format
|
220
|
+
|
221
|
+
Returns:
|
222
|
+
Formatted success string
|
223
|
+
"""
|
224
|
+
return f"✅ {message}"
|
225
|
+
|
226
|
+
|
227
|
+
class StatsFormatter:
|
228
|
+
"""Formats statistics and overview information."""
|
229
|
+
|
230
|
+
@staticmethod
|
231
|
+
def format_overview(overview: str) -> str:
|
232
|
+
"""
|
233
|
+
Format task overview with unicode characters.
|
234
|
+
|
235
|
+
Args:
|
236
|
+
overview: Raw overview string
|
237
|
+
|
238
|
+
Returns:
|
239
|
+
Formatted overview string
|
240
|
+
"""
|
241
|
+
if "Task Overview:" in overview:
|
242
|
+
lines = overview.split("\n")
|
243
|
+
formatted_lines = []
|
244
|
+
|
245
|
+
for line in lines:
|
246
|
+
if line.startswith("- Active tasks:"):
|
247
|
+
formatted_lines.append(f"📋 {line[2:]}")
|
248
|
+
elif line.startswith("- Completed tasks:"):
|
249
|
+
formatted_lines.append(f"✅ {line[2:]}")
|
250
|
+
else:
|
251
|
+
formatted_lines.append(line)
|
252
|
+
|
253
|
+
return "\n".join(formatted_lines)
|
254
|
+
|
255
|
+
return overview
|
256
|
+
|
257
|
+
|
258
|
+
class TableFormatter:
|
259
|
+
"""Creates rich tables for various data displays."""
|
260
|
+
|
261
|
+
@staticmethod
|
262
|
+
def create_command_table() -> Table:
|
263
|
+
"""Create a table for displaying available commands."""
|
264
|
+
table = Table(
|
265
|
+
title="Available Commands",
|
266
|
+
box=ROUNDED,
|
267
|
+
show_header=True,
|
268
|
+
header_style="bold magenta",
|
269
|
+
width=PANEL_WIDTH,
|
270
|
+
)
|
271
|
+
|
272
|
+
table.add_column("Command", style="cyan", width=12)
|
273
|
+
table.add_column("Description", style="white")
|
274
|
+
|
275
|
+
commands = [
|
276
|
+
("clear", "Clear conversation history"),
|
277
|
+
("stats", "Show conversation statistics"),
|
278
|
+
("help", "Show this help message"),
|
279
|
+
("about", "Show application information"),
|
280
|
+
("list", "List all tasks (no LLM interaction)"),
|
281
|
+
("quit", "Exit the application"),
|
282
|
+
]
|
283
|
+
|
284
|
+
for cmd, desc in commands:
|
285
|
+
table.add_row(cmd, desc)
|
286
|
+
|
287
|
+
return table
|
288
|
+
|
289
|
+
@staticmethod
|
290
|
+
def create_stats_table(summary: Dict[str, Any]) -> Table:
|
291
|
+
"""Create a table for displaying conversation statistics."""
|
292
|
+
table = Table(
|
293
|
+
title="Conversation Statistics",
|
294
|
+
box=ROUNDED,
|
295
|
+
show_header=True,
|
296
|
+
header_style="bold magenta",
|
297
|
+
width=PANEL_WIDTH,
|
298
|
+
)
|
299
|
+
|
300
|
+
table.add_column("Metric", style="cyan", width=20)
|
301
|
+
table.add_column("Value", style="white")
|
302
|
+
|
303
|
+
# Basic stats
|
304
|
+
table.add_row("Total Messages", str(summary["total_messages"]))
|
305
|
+
table.add_row("User Messages", str(summary["user_messages"]))
|
306
|
+
table.add_row("Assistant Messages", str(summary["assistant_messages"]))
|
307
|
+
table.add_row("Tool Messages", str(summary["tool_messages"]))
|
308
|
+
table.add_row("Estimated Tokens", str(summary["estimated_tokens"]))
|
309
|
+
|
310
|
+
# Thinking time stats if available
|
311
|
+
if "thinking_time_count" in summary and summary["thinking_time_count"] > 0:
|
312
|
+
table.add_row("", "") # Empty row for spacing
|
313
|
+
table.add_row(
|
314
|
+
"Total Thinking Time", f"{summary['total_thinking_time']:.2f}s"
|
315
|
+
)
|
316
|
+
table.add_row(
|
317
|
+
"Average Thinking Time", f"{summary['average_thinking_time']:.2f}s"
|
318
|
+
)
|
319
|
+
table.add_row("Min Thinking Time", f"{summary['min_thinking_time']:.2f}s")
|
320
|
+
table.add_row("Max Thinking Time", f"{summary['max_thinking_time']:.2f}s")
|
321
|
+
table.add_row("Requests with Timing", str(summary["thinking_time_count"]))
|
322
|
+
|
323
|
+
return table
|
324
|
+
|
325
|
+
|
326
|
+
class PanelFormatter:
|
327
|
+
"""Creates rich panels for various content displays."""
|
328
|
+
|
329
|
+
@staticmethod
|
330
|
+
def create_header_panel() -> Panel:
|
331
|
+
"""Create the application header panel."""
|
332
|
+
header_text = Text("Todo.sh LLM Agent", style="bold blue")
|
333
|
+
return Panel(
|
334
|
+
Align.center(header_text),
|
335
|
+
title="🤖",
|
336
|
+
border_style="dim",
|
337
|
+
box=ROUNDED,
|
338
|
+
width=PANEL_WIDTH + 2,
|
339
|
+
)
|
340
|
+
|
341
|
+
@staticmethod
|
342
|
+
def create_task_panel(content: str, title: str = "📋 Current Tasks") -> Panel:
|
343
|
+
"""Create a panel for displaying task lists."""
|
344
|
+
return Panel(
|
345
|
+
content, title=title, border_style="dim", box=ROUNDED, width=PANEL_WIDTH
|
346
|
+
)
|
347
|
+
|
348
|
+
@staticmethod
|
349
|
+
def create_response_panel(content: str, title: str = "🤖 Assistant") -> Panel:
|
350
|
+
"""Create a panel for displaying LLM responses."""
|
351
|
+
return Panel(
|
352
|
+
content, title=title, border_style="dim", box=ROUNDED, width=PANEL_WIDTH
|
353
|
+
)
|
354
|
+
|
355
|
+
@staticmethod
|
356
|
+
def create_error_panel(content: str, title: str = "❌ Error") -> Panel:
|
357
|
+
"""Create a panel for displaying errors."""
|
358
|
+
return Panel(
|
359
|
+
content, title=title, border_style="red", box=ROUNDED, width=PANEL_WIDTH
|
360
|
+
)
|
361
|
+
|
362
|
+
@staticmethod
|
363
|
+
def create_about_panel() -> Panel:
|
364
|
+
"""Create a panel for displaying about information."""
|
365
|
+
from todo_agent._version import __commit_id__, __version__
|
366
|
+
|
367
|
+
about_content = Text()
|
368
|
+
about_content.append("Todo.sh LLM Agent\n", style="bold blue")
|
369
|
+
about_content.append("\n")
|
370
|
+
about_content.append(
|
371
|
+
"A natural language interface for todo.sh task management\n", style="white"
|
372
|
+
)
|
373
|
+
about_content.append("powered by LLM function calling.\n", style="white")
|
374
|
+
about_content.append("\n")
|
375
|
+
about_content.append("Version: ", style="cyan")
|
376
|
+
about_content.append(f"{__version__}\n", style="white")
|
377
|
+
if __commit_id__:
|
378
|
+
about_content.append("Commit: ", style="cyan")
|
379
|
+
about_content.append(f"{__commit_id__}\n", style="white")
|
380
|
+
about_content.append("\n")
|
381
|
+
about_content.append(
|
382
|
+
"Transform natural language into todo.sh commands:\n", style="italic"
|
383
|
+
)
|
384
|
+
about_content.append("• 'add buy groceries to shopping list'\n", style="dim")
|
385
|
+
about_content.append("• 'show my work tasks'\n", style="dim")
|
386
|
+
about_content.append("• 'mark task 3 as done'\n", style="dim")
|
387
|
+
about_content.append("\n")
|
388
|
+
about_content.append("GitHub: ", style="cyan")
|
389
|
+
about_content.append(
|
390
|
+
"https://github.com/codeprimate/todo-agent\n", style="blue"
|
391
|
+
)
|
392
|
+
|
393
|
+
return Panel(
|
394
|
+
Align.center(about_content),
|
395
|
+
title="i About",
|
396
|
+
border_style="dim",
|
397
|
+
box=ROUNDED,
|
398
|
+
width=PANEL_WIDTH + 2,
|
399
|
+
)
|
todo_agent/main.py
CHANGED
@@ -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()
|
@@ -1,6 +1,6 @@
|
|
1
1
|
todo_agent/__init__.py,sha256=RUowhd14r3tqB_7rl83unGV8oBjra3UOIl7jix-33fk,254
|
2
|
-
todo_agent/_version.py,sha256=
|
3
|
-
todo_agent/main.py,sha256
|
2
|
+
todo_agent/_version.py,sha256=kBRz0P2plw1eVdIpt70W6m1LMbEIhLY3RyOfVGdubaI,704
|
3
|
+
todo_agent/main.py,sha256=-ryhMm4c4sz4e4anXI8B-CYnpEh5HIkmnYcnGxcWHDk,1628
|
4
4
|
todo_agent/core/__init__.py,sha256=QAZ4it63pXv5-DxtNcuSAmg7ZnCY5ackI5yycvKHr9I,365
|
5
5
|
todo_agent/core/conversation_manager.py,sha256=Mxjxn3BTjSnju6xryQcpOWdE8wWWJqTdDFTC86D9xc8,11639
|
6
6
|
todo_agent/core/exceptions.py,sha256=cPvvkIbKdI7l51wC7cE-ZxUi54P3nf2m7x2lMNMRFYM,399
|
@@ -16,12 +16,13 @@ todo_agent/infrastructure/openrouter_client.py,sha256=GVOJTzPDOKNdHEu-y-HQygMcLv
|
|
16
16
|
todo_agent/infrastructure/todo_shell.py,sha256=z6kqUKDX-i4DfYJKoOLiPLCp8y6m1HdTDLHTvmLpzMc,5801
|
17
17
|
todo_agent/infrastructure/token_counter.py,sha256=PCKheOVJbp1s89yhh_i6iKgURMt9mVoYkwjQJCc2xCE,4958
|
18
18
|
todo_agent/infrastructure/prompts/system_prompt.txt,sha256=uCb6yz3uDQdwcB8HJcF0y1_1b75oRtRnCMMHQLHI3NI,2415
|
19
|
-
todo_agent/interface/__init__.py,sha256=
|
20
|
-
todo_agent/interface/cli.py,sha256=
|
19
|
+
todo_agent/interface/__init__.py,sha256=vDD3rQu4qDkpvVwGVtkDzE1M4IiSHYzTif4GbYSFWaI,457
|
20
|
+
todo_agent/interface/cli.py,sha256=audxpDafLw_o-pitqntUN0PpKUyWZap2wpHw4QUak_8,9926
|
21
|
+
todo_agent/interface/formatters.py,sha256=aJc-a2aUzi3tId46jFK3uwZnsOJiXBTTMT8I_46Jjlg,13181
|
21
22
|
todo_agent/interface/tools.py,sha256=mlPPLVwECYrTtOX8ysIORRfErIOJl43qlTvfdpy2Vbs,28559
|
22
|
-
todo_agent-0.2.
|
23
|
-
todo_agent-0.2.
|
24
|
-
todo_agent-0.2.
|
25
|
-
todo_agent-0.2.
|
26
|
-
todo_agent-0.2.
|
27
|
-
todo_agent-0.2.
|
23
|
+
todo_agent-0.2.3.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
24
|
+
todo_agent-0.2.3.dist-info/METADATA,sha256=VvqQLIc4ExA92Oa-nhukv96wszxQnYSf7nlG09hiZt0,10047
|
25
|
+
todo_agent-0.2.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
26
|
+
todo_agent-0.2.3.dist-info/entry_points.txt,sha256=4W7LrCib6AXP5IZDwWRht8S5gutLu5oNfTJHGbt4oHs,52
|
27
|
+
todo_agent-0.2.3.dist-info/top_level.txt,sha256=a65mlPIhPZHuq2bRIi_sCMAIJsUddvXt171OBF6r6co,11
|
28
|
+
todo_agent-0.2.3.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|