todo-agent 0.2.1__tar.gz → 0.2.3__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.3}/PKG-INFO +1 -1
  2. {todo_agent-0.2.1 → todo_agent-0.2.3}/tests/test_interface/test_cli.py +2 -2
  3. {todo_agent-0.2.1 → todo_agent-0.2.3}/todo_agent/_version.py +3 -3
  4. todo_agent-0.2.3/todo_agent/interface/__init__.py +25 -0
  5. {todo_agent-0.2.1 → todo_agent-0.2.3}/todo_agent/interface/cli.py +91 -51
  6. todo_agent-0.2.3/todo_agent/interface/formatters.py +399 -0
  7. {todo_agent-0.2.1 → todo_agent-0.2.3}/todo_agent/main.py +10 -2
  8. {todo_agent-0.2.1 → todo_agent-0.2.3}/todo_agent.egg-info/PKG-INFO +1 -1
  9. {todo_agent-0.2.1 → todo_agent-0.2.3}/todo_agent.egg-info/SOURCES.txt +1 -0
  10. todo_agent-0.2.1/todo_agent/interface/__init__.py +0 -10
  11. {todo_agent-0.2.1 → todo_agent-0.2.3}/.gitignore +0 -0
  12. {todo_agent-0.2.1 → todo_agent-0.2.3}/LICENSE +0 -0
  13. {todo_agent-0.2.1 → todo_agent-0.2.3}/MANIFEST.in +0 -0
  14. {todo_agent-0.2.1 → todo_agent-0.2.3}/Makefile +0 -0
  15. {todo_agent-0.2.1 → todo_agent-0.2.3}/README.md +0 -0
  16. {todo_agent-0.2.1 → todo_agent-0.2.3}/docs/publishing.md +0 -0
  17. {todo_agent-0.2.1 → todo_agent-0.2.3}/pyproject.toml +0 -0
  18. {todo_agent-0.2.1 → todo_agent-0.2.3}/requirements-dev.txt +0 -0
  19. {todo_agent-0.2.1 → todo_agent-0.2.3}/requirements.txt +0 -0
  20. {todo_agent-0.2.1 → todo_agent-0.2.3}/setup.cfg +0 -0
  21. {todo_agent-0.2.1 → todo_agent-0.2.3}/tests/__init__.py +0 -0
  22. {todo_agent-0.2.1 → todo_agent-0.2.3}/tests/test_core/__init__.py +0 -0
  23. {todo_agent-0.2.1 → todo_agent-0.2.3}/tests/test_core/test_conversation_manager.py +0 -0
  24. {todo_agent-0.2.1 → todo_agent-0.2.3}/tests/test_core/test_todo_manager.py +0 -0
  25. {todo_agent-0.2.1 → todo_agent-0.2.3}/tests/test_infrastructure/__init__.py +0 -0
  26. {todo_agent-0.2.1 → todo_agent-0.2.3}/tests/test_infrastructure/test_config.py +0 -0
  27. {todo_agent-0.2.1 → todo_agent-0.2.3}/tests/test_infrastructure/test_inference.py +0 -0
  28. {todo_agent-0.2.1 → todo_agent-0.2.3}/tests/test_infrastructure/test_llm_client_factory.py +0 -0
  29. {todo_agent-0.2.1 → todo_agent-0.2.3}/tests/test_infrastructure/test_ollama_client.py +0 -0
  30. {todo_agent-0.2.1 → todo_agent-0.2.3}/tests/test_infrastructure/test_openrouter_client.py +0 -0
  31. {todo_agent-0.2.1 → todo_agent-0.2.3}/tests/test_infrastructure/test_todo_shell.py +0 -0
  32. {todo_agent-0.2.1 → todo_agent-0.2.3}/tests/test_infrastructure/test_token_counter.py +0 -0
  33. {todo_agent-0.2.1 → todo_agent-0.2.3}/tests/test_interface/__init__.py +0 -0
  34. {todo_agent-0.2.1 → todo_agent-0.2.3}/tests/test_interface/test_tools.py +0 -0
  35. {todo_agent-0.2.1 → todo_agent-0.2.3}/tests/test_linting.py +0 -0
  36. {todo_agent-0.2.1 → todo_agent-0.2.3}/tests/test_logger.py +0 -0
  37. {todo_agent-0.2.1 → todo_agent-0.2.3}/tests/test_main.py +0 -0
  38. {todo_agent-0.2.1 → todo_agent-0.2.3}/todo_agent/__init__.py +0 -0
  39. {todo_agent-0.2.1 → todo_agent-0.2.3}/todo_agent/core/__init__.py +0 -0
  40. {todo_agent-0.2.1 → todo_agent-0.2.3}/todo_agent/core/conversation_manager.py +0 -0
  41. {todo_agent-0.2.1 → todo_agent-0.2.3}/todo_agent/core/exceptions.py +0 -0
  42. {todo_agent-0.2.1 → todo_agent-0.2.3}/todo_agent/core/todo_manager.py +0 -0
  43. {todo_agent-0.2.1 → todo_agent-0.2.3}/todo_agent/infrastructure/__init__.py +0 -0
  44. {todo_agent-0.2.1 → todo_agent-0.2.3}/todo_agent/infrastructure/config.py +0 -0
  45. {todo_agent-0.2.1 → todo_agent-0.2.3}/todo_agent/infrastructure/inference.py +0 -0
  46. {todo_agent-0.2.1 → todo_agent-0.2.3}/todo_agent/infrastructure/llm_client.py +0 -0
  47. {todo_agent-0.2.1 → todo_agent-0.2.3}/todo_agent/infrastructure/llm_client_factory.py +0 -0
  48. {todo_agent-0.2.1 → todo_agent-0.2.3}/todo_agent/infrastructure/logger.py +0 -0
  49. {todo_agent-0.2.1 → todo_agent-0.2.3}/todo_agent/infrastructure/ollama_client.py +0 -0
  50. {todo_agent-0.2.1 → todo_agent-0.2.3}/todo_agent/infrastructure/openrouter_client.py +0 -0
  51. {todo_agent-0.2.1 → todo_agent-0.2.3}/todo_agent/infrastructure/prompts/system_prompt.txt +0 -0
  52. {todo_agent-0.2.1 → todo_agent-0.2.3}/todo_agent/infrastructure/todo_shell.py +0 -0
  53. {todo_agent-0.2.1 → todo_agent-0.2.3}/todo_agent/infrastructure/token_counter.py +0 -0
  54. {todo_agent-0.2.1 → todo_agent-0.2.3}/todo_agent/interface/tools.py +0 -0
  55. {todo_agent-0.2.1 → todo_agent-0.2.3}/todo_agent.egg-info/dependency_links.txt +0 -0
  56. {todo_agent-0.2.1 → todo_agent-0.2.3}/todo_agent.egg-info/entry_points.txt +0 -0
  57. {todo_agent-0.2.1 → todo_agent-0.2.3}/todo_agent.egg-info/requires.txt +0 -0
  58. {todo_agent-0.2.1 → todo_agent-0.2.3}/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.3
4
4
  Summary: A natural language interface for todo.sh task management
5
5
  Author: codeprimate
6
6
  Maintainer: codeprimate
@@ -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.3'
32
+ __version_tuple__ = version_tuple = (0, 2, 3)
33
33
 
34
- __commit_id__ = commit_id = 'g753e8c9be'
34
+ __commit_id__ = commit_id = 'ge1a08d166'
@@ -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
+ ]
@@ -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
- print("Todo.sh LLM Agent - Type 'quit' to exit")
89
- print("Commands: 'clear' (clear conversation), 'history' (show stats), 'help'")
90
- print("=" * 50)
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
- user_input = input("\n> ").strip()
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("Conversation history cleared.")
160
+ self.console.print(
161
+ ResponseFormatter.format_success(
162
+ "Conversation history cleared."
163
+ )
164
+ )
109
165
  continue
110
166
 
111
- if user_input.lower() == "history":
112
- self.logger.debug("User requested conversation history")
167
+ if user_input.lower() == "stats":
168
+ self.logger.debug("User requested conversation stats")
113
169
  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
- )
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
- 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!")
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
- print(output)
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
- print(f"Error: Failed to list tasks: {e!s}")
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
- print(response)
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("\nGoodbye!")
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
- print(f"Error: {e!s}")
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 f"Error: {e!s}"
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
+ )
@@ -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.3
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