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.
Files changed (58) hide show
  1. {todo_agent-0.2.1 → todo_agent-0.2.4}/PKG-INFO +1 -1
  2. {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_core/test_conversation_manager.py +1 -1
  3. {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_interface/test_cli.py +2 -2
  4. {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/_version.py +3 -3
  5. {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/core/conversation_manager.py +1 -1
  6. todo_agent-0.2.4/todo_agent/interface/__init__.py +25 -0
  7. {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/interface/cli.py +119 -51
  8. todo_agent-0.2.4/todo_agent/interface/formatters.py +457 -0
  9. {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/main.py +10 -2
  10. {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent.egg-info/PKG-INFO +1 -1
  11. {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent.egg-info/SOURCES.txt +1 -0
  12. todo_agent-0.2.1/todo_agent/interface/__init__.py +0 -10
  13. {todo_agent-0.2.1 → todo_agent-0.2.4}/.gitignore +0 -0
  14. {todo_agent-0.2.1 → todo_agent-0.2.4}/LICENSE +0 -0
  15. {todo_agent-0.2.1 → todo_agent-0.2.4}/MANIFEST.in +0 -0
  16. {todo_agent-0.2.1 → todo_agent-0.2.4}/Makefile +0 -0
  17. {todo_agent-0.2.1 → todo_agent-0.2.4}/README.md +0 -0
  18. {todo_agent-0.2.1 → todo_agent-0.2.4}/docs/publishing.md +0 -0
  19. {todo_agent-0.2.1 → todo_agent-0.2.4}/pyproject.toml +0 -0
  20. {todo_agent-0.2.1 → todo_agent-0.2.4}/requirements-dev.txt +0 -0
  21. {todo_agent-0.2.1 → todo_agent-0.2.4}/requirements.txt +0 -0
  22. {todo_agent-0.2.1 → todo_agent-0.2.4}/setup.cfg +0 -0
  23. {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/__init__.py +0 -0
  24. {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_core/__init__.py +0 -0
  25. {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_core/test_todo_manager.py +0 -0
  26. {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_infrastructure/__init__.py +0 -0
  27. {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_infrastructure/test_config.py +0 -0
  28. {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_infrastructure/test_inference.py +0 -0
  29. {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_infrastructure/test_llm_client_factory.py +0 -0
  30. {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_infrastructure/test_ollama_client.py +0 -0
  31. {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_infrastructure/test_openrouter_client.py +0 -0
  32. {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_infrastructure/test_todo_shell.py +0 -0
  33. {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_infrastructure/test_token_counter.py +0 -0
  34. {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_interface/__init__.py +0 -0
  35. {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_interface/test_tools.py +0 -0
  36. {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_linting.py +0 -0
  37. {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_logger.py +0 -0
  38. {todo_agent-0.2.1 → todo_agent-0.2.4}/tests/test_main.py +0 -0
  39. {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/__init__.py +0 -0
  40. {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/core/__init__.py +0 -0
  41. {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/core/exceptions.py +0 -0
  42. {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/core/todo_manager.py +0 -0
  43. {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/infrastructure/__init__.py +0 -0
  44. {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/infrastructure/config.py +0 -0
  45. {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/infrastructure/inference.py +0 -0
  46. {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/infrastructure/llm_client.py +0 -0
  47. {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/infrastructure/llm_client_factory.py +0 -0
  48. {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/infrastructure/logger.py +0 -0
  49. {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/infrastructure/ollama_client.py +0 -0
  50. {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/infrastructure/openrouter_client.py +0 -0
  51. {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/infrastructure/prompts/system_prompt.txt +0 -0
  52. {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/infrastructure/todo_shell.py +0 -0
  53. {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/infrastructure/token_counter.py +0 -0
  54. {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent/interface/tools.py +0 -0
  55. {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent.egg-info/dependency_links.txt +0 -0
  56. {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent.egg-info/entry_points.txt +0 -0
  57. {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent.egg-info/requires.txt +0 -0
  58. {todo_agent-0.2.1 → todo_agent-0.2.4}/todo_agent.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: todo-agent
3
- Version: 0.2.1
3
+ Version: 0.2.4
4
4
  Summary: A natural language interface for todo.sh task management
5
5
  Author: codeprimate
6
6
  Maintainer: codeprimate
@@ -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 == 4000
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"Error: {error_message}"
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.1'
32
- __version_tuple__ = version_tuple = (0, 2, 1)
31
+ __version__ = version = '0.2.4'
32
+ __version_tuple__ = version_tuple = (0, 2, 4)
33
33
 
34
- __commit_id__ = commit_id = 'g753e8c9be'
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 = 4000, max_messages: int = 50, model: str = "gpt-4"
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
- print("Todo.sh LLM Agent - Type 'quit' to exit")
89
- print("Commands: 'clear' (clear conversation), 'history' (show stats), 'help'")
90
- print("=" * 50)
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
- user_input = input("\n> ").strip()
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("Conversation history cleared.")
183
+ self.console.print(
184
+ ResponseFormatter.format_success(
185
+ "Conversation history cleared."
186
+ )
187
+ )
109
188
  continue
110
189
 
111
- if user_input.lower() == "history":
112
- self.logger.debug("User requested conversation history")
190
+ if user_input.lower() == "stats":
191
+ self.logger.debug("User requested conversation stats")
113
192
  summary = self.inference.get_conversation_summary()
114
- print(f"Conversation Stats:")
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
- print("Available commands:")
147
- print(" clear - Clear conversation history")
148
- print(" history - Show conversation statistics")
149
- print(" help - Show this help message")
150
- print(" list - List all tasks (no LLM interaction)")
151
- print(" quit - Exit the application")
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
- print(output)
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
- print(f"Error: Failed to list tasks: {e!s}")
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
- print(response)
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("\nGoodbye!")
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
- print(f"Error: {e!s}")
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 f"Error: {e!s}"
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
- response = cli.run_single_request(args.command)
40
- print(response)
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
  Metadata-Version: 2.4
2
2
  Name: todo-agent
3
- Version: 0.2.1
3
+ Version: 0.2.4
4
4
  Summary: A natural language interface for todo.sh task management
5
5
  Author: codeprimate
6
6
  Maintainer: codeprimate
@@ -51,4 +51,5 @@ todo_agent/infrastructure/token_counter.py
51
51
  todo_agent/infrastructure/prompts/system_prompt.txt
52
52
  todo_agent/interface/__init__.py
53
53
  todo_agent/interface/cli.py
54
+ todo_agent/interface/formatters.py
54
55
  todo_agent/interface/tools.py
@@ -1,10 +0,0 @@
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 .tools import ToolCallHandler
9
-
10
- __all__ = ["CLI", "ToolCallHandler"]
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