aloop 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of aloop might be problematic. Click here for more details.
- agent/__init__.py +0 -0
- agent/agent.py +182 -0
- agent/base.py +406 -0
- agent/context.py +126 -0
- agent/todo.py +149 -0
- agent/tool_executor.py +54 -0
- agent/verification.py +135 -0
- aloop-0.1.0.dist-info/METADATA +246 -0
- aloop-0.1.0.dist-info/RECORD +62 -0
- aloop-0.1.0.dist-info/WHEEL +5 -0
- aloop-0.1.0.dist-info/entry_points.txt +2 -0
- aloop-0.1.0.dist-info/licenses/LICENSE +21 -0
- aloop-0.1.0.dist-info/top_level.txt +9 -0
- cli.py +19 -0
- config.py +146 -0
- interactive.py +865 -0
- llm/__init__.py +51 -0
- llm/base.py +26 -0
- llm/compat.py +226 -0
- llm/content_utils.py +309 -0
- llm/litellm_adapter.py +450 -0
- llm/message_types.py +245 -0
- llm/model_manager.py +265 -0
- llm/retry.py +95 -0
- main.py +246 -0
- memory/__init__.py +20 -0
- memory/compressor.py +554 -0
- memory/manager.py +538 -0
- memory/serialization.py +82 -0
- memory/short_term.py +88 -0
- memory/token_tracker.py +203 -0
- memory/types.py +51 -0
- tools/__init__.py +6 -0
- tools/advanced_file_ops.py +557 -0
- tools/base.py +51 -0
- tools/calculator.py +50 -0
- tools/code_navigator.py +975 -0
- tools/explore.py +254 -0
- tools/file_ops.py +150 -0
- tools/git_tools.py +791 -0
- tools/notify.py +69 -0
- tools/parallel_execute.py +420 -0
- tools/session_manager.py +205 -0
- tools/shell.py +147 -0
- tools/shell_background.py +470 -0
- tools/smart_edit.py +491 -0
- tools/todo.py +130 -0
- tools/web_fetch.py +673 -0
- tools/web_search.py +61 -0
- utils/__init__.py +15 -0
- utils/logger.py +105 -0
- utils/model_pricing.py +49 -0
- utils/runtime.py +75 -0
- utils/terminal_ui.py +422 -0
- utils/tui/__init__.py +39 -0
- utils/tui/command_registry.py +49 -0
- utils/tui/components.py +306 -0
- utils/tui/input_handler.py +393 -0
- utils/tui/model_ui.py +204 -0
- utils/tui/progress.py +292 -0
- utils/tui/status_bar.py +178 -0
- utils/tui/theme.py +165 -0
utils/terminal_ui.py
ADDED
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
"""Terminal UI utilities using Rich library for beautiful output.
|
|
2
|
+
|
|
3
|
+
This module provides a unified interface for terminal output, integrating
|
|
4
|
+
with the TUI theme system for consistent styling.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, Optional
|
|
8
|
+
|
|
9
|
+
from rich import box
|
|
10
|
+
from rich.console import Console
|
|
11
|
+
from rich.markdown import Markdown
|
|
12
|
+
from rich.panel import Panel
|
|
13
|
+
from rich.syntax import Syntax
|
|
14
|
+
from rich.table import Table
|
|
15
|
+
from rich.text import Text
|
|
16
|
+
|
|
17
|
+
from config import Config
|
|
18
|
+
from utils.tui.theme import Theme, set_theme
|
|
19
|
+
|
|
20
|
+
# Initialize theme from config
|
|
21
|
+
set_theme(Config.TUI_THEME)
|
|
22
|
+
|
|
23
|
+
# Global console instance with theme support
|
|
24
|
+
console = Console(theme=Theme.get_rich_theme())
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _get_colors():
|
|
28
|
+
"""Get current theme colors."""
|
|
29
|
+
return Theme.get_colors()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
ALOOP_LOGO = r"""
|
|
33
|
+
█████╗ ██╗ ██████╗ ██████╗ ██████╗
|
|
34
|
+
██╔══██╗██║ ██╔═══██╗██╔═══██╗██╔══██╗
|
|
35
|
+
███████║██║ ██║ ██║██║ ██║██████╔╝
|
|
36
|
+
██╔══██║██║ ██║ ██║██║ ██║██╔═══╝
|
|
37
|
+
██║ ██║███████╗╚██████╔╝╚██████╔╝██║
|
|
38
|
+
╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
TAGLINES = [
|
|
42
|
+
"Think. Act. Observe. Repeat.",
|
|
43
|
+
"Your terminal, now with agency.",
|
|
44
|
+
"Reasoning in loops, so you don't have to.",
|
|
45
|
+
"One loop to rule them all.",
|
|
46
|
+
"Where thoughts become tool calls.",
|
|
47
|
+
"The agent that thinks before it acts. Usually.",
|
|
48
|
+
"Ctrl+C is your safe word.",
|
|
49
|
+
"Turning vibes into function calls since 2025.",
|
|
50
|
+
"I think, therefore I tool_call.",
|
|
51
|
+
"sudo make me a sandwich. Actually, I can do that now.",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def print_banner(subtitle: Optional[str] = None) -> None:
|
|
56
|
+
"""Print the ASCII art banner with a random tagline."""
|
|
57
|
+
import random
|
|
58
|
+
|
|
59
|
+
colors = _get_colors()
|
|
60
|
+
content = f"[bold {colors.primary}]{ALOOP_LOGO.lstrip(chr(10))}[/bold {colors.primary}]"
|
|
61
|
+
tagline = subtitle or random.choice(TAGLINES)
|
|
62
|
+
content += f"\n\n[italic {colors.secondary}]{tagline}[/italic {colors.secondary}]"
|
|
63
|
+
|
|
64
|
+
console.print(Panel(content, border_style=colors.primary, box=box.DOUBLE, padding=(1, 2)))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def print_header(title: str, subtitle: Optional[str] = None) -> None:
|
|
68
|
+
"""Print a formatted header panel.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
title: Main title text
|
|
72
|
+
subtitle: Optional subtitle text
|
|
73
|
+
"""
|
|
74
|
+
colors = _get_colors()
|
|
75
|
+
content = f"[bold {colors.primary}]{title}[/bold {colors.primary}]"
|
|
76
|
+
if subtitle:
|
|
77
|
+
content += f"\n[{colors.text_secondary}]{subtitle}[/{colors.text_secondary}]"
|
|
78
|
+
|
|
79
|
+
console.print(Panel(content, border_style=colors.primary, box=box.DOUBLE, padding=(1, 2)))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def print_config(config: Dict[str, Any]) -> None:
|
|
83
|
+
"""Print configuration in a formatted table.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
config: Dictionary of configuration key-value pairs
|
|
87
|
+
"""
|
|
88
|
+
colors = _get_colors()
|
|
89
|
+
table = Table(show_header=False, box=box.SIMPLE, border_style=colors.text_muted, padding=(0, 2))
|
|
90
|
+
table.add_column("Key", style=f"{colors.primary} bold")
|
|
91
|
+
table.add_column("Value", style=colors.success)
|
|
92
|
+
|
|
93
|
+
for key, value in config.items():
|
|
94
|
+
table.add_row(key, str(value))
|
|
95
|
+
|
|
96
|
+
console.print(table)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def print_thinking(thinking: str, max_length: Optional[int] = None) -> None:
|
|
100
|
+
"""Print AI thinking/reasoning content.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
thinking: Thinking content string
|
|
104
|
+
max_length: Maximum length to display (uses config default if None)
|
|
105
|
+
"""
|
|
106
|
+
if not thinking:
|
|
107
|
+
return
|
|
108
|
+
|
|
109
|
+
if not Config.TUI_SHOW_THINKING:
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
colors = _get_colors()
|
|
113
|
+
max_len = max_length if max_length is not None else Config.TUI_THINKING_MAX_PREVIEW
|
|
114
|
+
|
|
115
|
+
# Truncate if too long
|
|
116
|
+
if len(thinking) > max_len:
|
|
117
|
+
display_text = (
|
|
118
|
+
thinking[:max_len]
|
|
119
|
+
+ f"... [{colors.text_muted}]({len(thinking) - max_len} more chars)[/{colors.text_muted}]"
|
|
120
|
+
)
|
|
121
|
+
else:
|
|
122
|
+
display_text = thinking
|
|
123
|
+
|
|
124
|
+
console.print(
|
|
125
|
+
Panel(
|
|
126
|
+
display_text,
|
|
127
|
+
title=f"[bold {colors.thinking_accent}]Thinking[/bold {colors.thinking_accent}]",
|
|
128
|
+
border_style=colors.text_muted,
|
|
129
|
+
box=box.ROUNDED,
|
|
130
|
+
padding=(0, 1),
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def print_tool_call(tool_name: str, arguments: Dict[str, Any]) -> None:
|
|
136
|
+
"""Print tool call information.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
tool_name: Name of the tool being called
|
|
140
|
+
arguments: Tool arguments
|
|
141
|
+
"""
|
|
142
|
+
colors = _get_colors()
|
|
143
|
+
|
|
144
|
+
# Format arguments nicely
|
|
145
|
+
args_lines = []
|
|
146
|
+
for key, value in arguments.items():
|
|
147
|
+
value_str = str(value)
|
|
148
|
+
if len(value_str) > 100:
|
|
149
|
+
value_str = value_str[:97] + "..."
|
|
150
|
+
args_lines.append(
|
|
151
|
+
f" [{colors.text_secondary}]{key}:[/{colors.text_secondary}] {value_str}"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
content = "\n".join(args_lines) if args_lines else ""
|
|
155
|
+
|
|
156
|
+
console.print(
|
|
157
|
+
Panel(
|
|
158
|
+
content,
|
|
159
|
+
title=f"[{colors.tool_accent}]Tool: {tool_name}[/{colors.tool_accent}]",
|
|
160
|
+
title_align="left",
|
|
161
|
+
border_style=colors.text_muted,
|
|
162
|
+
box=box.ROUNDED,
|
|
163
|
+
padding=(0, 1),
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def print_tool_result(
|
|
169
|
+
result: str,
|
|
170
|
+
truncated: bool = False,
|
|
171
|
+
success: bool = True,
|
|
172
|
+
duration: Optional[float] = None,
|
|
173
|
+
) -> None:
|
|
174
|
+
"""Print tool result.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
result: Tool result string
|
|
178
|
+
truncated: Whether the result was truncated
|
|
179
|
+
success: Whether the tool call succeeded
|
|
180
|
+
duration: Optional duration in seconds
|
|
181
|
+
"""
|
|
182
|
+
colors = _get_colors()
|
|
183
|
+
|
|
184
|
+
if truncated:
|
|
185
|
+
console.print(f"[{colors.warning}]Result truncated[/{colors.warning}]")
|
|
186
|
+
|
|
187
|
+
# Show status line
|
|
188
|
+
status_icon = "✓" if success else "✗"
|
|
189
|
+
status_color = colors.success if success else colors.error
|
|
190
|
+
status_parts = [f"[{status_color}]{status_icon}[/{status_color}]"]
|
|
191
|
+
|
|
192
|
+
if duration:
|
|
193
|
+
status_parts.append(f"({duration:.1f}s)")
|
|
194
|
+
|
|
195
|
+
if status_parts:
|
|
196
|
+
console.print(" ".join(status_parts))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def print_final_answer(answer: str) -> None:
|
|
200
|
+
"""Print final answer in a formatted panel with Markdown rendering.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
answer: Final answer text (supports Markdown)
|
|
204
|
+
"""
|
|
205
|
+
colors = _get_colors()
|
|
206
|
+
console.print()
|
|
207
|
+
# Render markdown content
|
|
208
|
+
md = Markdown(answer)
|
|
209
|
+
console.print(
|
|
210
|
+
Panel(
|
|
211
|
+
md,
|
|
212
|
+
title=f"[bold {colors.success}]Final Answer[/bold {colors.success}]",
|
|
213
|
+
border_style=colors.success,
|
|
214
|
+
box=box.DOUBLE,
|
|
215
|
+
padding=(1, 2),
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def print_unfinished_answer(answer: str) -> None:
|
|
221
|
+
"""Print an intermediate answer that did not pass verification.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
answer: Answer text (supports Markdown)
|
|
225
|
+
"""
|
|
226
|
+
colors = _get_colors()
|
|
227
|
+
console.print()
|
|
228
|
+
md = Markdown(answer)
|
|
229
|
+
console.print(
|
|
230
|
+
Panel(
|
|
231
|
+
md,
|
|
232
|
+
title=f"[bold {colors.warning}]Unfinished Answer[/bold {colors.warning}]",
|
|
233
|
+
border_style=colors.warning,
|
|
234
|
+
box=box.ROUNDED,
|
|
235
|
+
padding=(1, 2),
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def print_memory_stats(stats: Dict[str, Any]) -> None:
|
|
241
|
+
"""Print memory statistics in a formatted table.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
stats: Dictionary of memory statistics
|
|
245
|
+
"""
|
|
246
|
+
colors = _get_colors()
|
|
247
|
+
console.print()
|
|
248
|
+
console.print(
|
|
249
|
+
f"[bold {colors.primary}]Memory Statistics[/bold {colors.primary}]", justify="left"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
table = Table(
|
|
253
|
+
show_header=True,
|
|
254
|
+
header_style=f"bold {colors.primary}",
|
|
255
|
+
box=box.ROUNDED,
|
|
256
|
+
border_style=colors.text_muted,
|
|
257
|
+
padding=(0, 1),
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
table.add_column("Metric", style=colors.primary)
|
|
261
|
+
table.add_column("Value", justify="right", style=colors.success)
|
|
262
|
+
|
|
263
|
+
# Calculate total tokens
|
|
264
|
+
total_used = stats["total_input_tokens"] + stats["total_output_tokens"]
|
|
265
|
+
|
|
266
|
+
# Add rows
|
|
267
|
+
table.add_row("Total Tokens", f"{total_used:,}")
|
|
268
|
+
table.add_row("├─ Input", f"{stats['total_input_tokens']:,}")
|
|
269
|
+
table.add_row("└─ Output", f"{stats['total_output_tokens']:,}")
|
|
270
|
+
table.add_row("Current Context", f"{stats['current_tokens']:,}")
|
|
271
|
+
table.add_row("Compressions", str(stats["compression_count"]))
|
|
272
|
+
|
|
273
|
+
# Net savings with color
|
|
274
|
+
savings = stats["net_savings"]
|
|
275
|
+
savings_str = (
|
|
276
|
+
f"{savings:,}" if savings >= 0 else f"[{colors.error}]{savings:,}[/{colors.error}]"
|
|
277
|
+
)
|
|
278
|
+
table.add_row("Net Savings", savings_str)
|
|
279
|
+
|
|
280
|
+
table.add_row("Total Cost", f"${stats['total_cost']:.4f}")
|
|
281
|
+
table.add_row("Messages", f"{stats['short_term_count']} in memory")
|
|
282
|
+
|
|
283
|
+
console.print(table)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def print_error(message: str, title: str = "Error") -> None:
|
|
287
|
+
"""Print an error message.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
message: Error message
|
|
291
|
+
title: Error title (default: "Error")
|
|
292
|
+
"""
|
|
293
|
+
colors = _get_colors()
|
|
294
|
+
console.print(
|
|
295
|
+
Panel(
|
|
296
|
+
f"[{colors.error}]{message}[/{colors.error}]",
|
|
297
|
+
title=f"[bold {colors.error}]{title}[/bold {colors.error}]",
|
|
298
|
+
border_style=colors.error,
|
|
299
|
+
box=box.ROUNDED,
|
|
300
|
+
)
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def print_warning(message: str) -> None:
|
|
305
|
+
"""Print a warning message.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
message: Warning message
|
|
309
|
+
"""
|
|
310
|
+
colors = _get_colors()
|
|
311
|
+
console.print(f"[{colors.warning}]{message}[/{colors.warning}]")
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def print_success(message: str) -> None:
|
|
315
|
+
"""Print a success message.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
message: Success message
|
|
319
|
+
"""
|
|
320
|
+
colors = _get_colors()
|
|
321
|
+
console.print(f"[{colors.success}]✓ {message}[/{colors.success}]")
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def print_info(message: str) -> None:
|
|
325
|
+
"""Print an info message.
|
|
326
|
+
|
|
327
|
+
Args:
|
|
328
|
+
message: Info message
|
|
329
|
+
"""
|
|
330
|
+
colors = _get_colors()
|
|
331
|
+
console.print(f"[{colors.primary}]ℹ {message}[/{colors.primary}]")
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def print_log_location(log_file: str) -> None:
|
|
335
|
+
"""Print log file location.
|
|
336
|
+
|
|
337
|
+
Args:
|
|
338
|
+
log_file: Path to log file
|
|
339
|
+
"""
|
|
340
|
+
colors = _get_colors()
|
|
341
|
+
console.print()
|
|
342
|
+
console.print(f"[{colors.text_muted}]Detailed logs: {log_file}[/{colors.text_muted}]")
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def print_code(code: str, language: str = "python") -> None:
|
|
346
|
+
"""Print syntax-highlighted code.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
code: Code string
|
|
350
|
+
language: Programming language (default: python)
|
|
351
|
+
"""
|
|
352
|
+
syntax = Syntax(code, language, theme="monokai", line_numbers=True)
|
|
353
|
+
console.print(syntax)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def print_markdown(markdown_text: str) -> None:
|
|
357
|
+
"""Print formatted markdown.
|
|
358
|
+
|
|
359
|
+
Args:
|
|
360
|
+
markdown_text: Markdown text to render
|
|
361
|
+
"""
|
|
362
|
+
md = Markdown(markdown_text)
|
|
363
|
+
console.print(md)
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def print_divider(width: int = 60) -> None:
|
|
367
|
+
"""Print a horizontal divider.
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
width: Width of the divider in characters
|
|
371
|
+
"""
|
|
372
|
+
colors = _get_colors()
|
|
373
|
+
console.print(Text("─" * width, style=colors.text_muted))
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def print_user_message(message: str) -> None:
|
|
377
|
+
"""Print a user message in Claude Code style.
|
|
378
|
+
|
|
379
|
+
Args:
|
|
380
|
+
message: User message text
|
|
381
|
+
"""
|
|
382
|
+
colors = _get_colors()
|
|
383
|
+
prefix = Text("> ", style=f"bold {colors.user_input}")
|
|
384
|
+
content = Text(message, style=colors.user_input)
|
|
385
|
+
console.print(Text.assemble(prefix, content))
|
|
386
|
+
if not Config.TUI_COMPACT_MODE:
|
|
387
|
+
console.print()
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
def print_assistant_message(message: str, use_markdown: bool = True) -> None:
|
|
391
|
+
"""Print an assistant message.
|
|
392
|
+
|
|
393
|
+
Args:
|
|
394
|
+
message: Assistant message text
|
|
395
|
+
use_markdown: Whether to render as markdown
|
|
396
|
+
"""
|
|
397
|
+
colors = _get_colors()
|
|
398
|
+
if use_markdown:
|
|
399
|
+
md = Markdown(message)
|
|
400
|
+
console.print(md)
|
|
401
|
+
else:
|
|
402
|
+
console.print(Text(message, style=colors.assistant_output))
|
|
403
|
+
if not Config.TUI_COMPACT_MODE:
|
|
404
|
+
console.print()
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
def print_turn_divider(turn_number: Optional[int] = None) -> None:
|
|
408
|
+
"""Print a divider between conversation turns.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
turn_number: Optional turn number to display
|
|
412
|
+
"""
|
|
413
|
+
colors = _get_colors()
|
|
414
|
+
if turn_number is not None:
|
|
415
|
+
left_line = "─" * 25
|
|
416
|
+
right_line = "─" * 25
|
|
417
|
+
turn_text = f" Turn {turn_number} "
|
|
418
|
+
console.print(Text(f"{left_line}{turn_text}{right_line}", style=colors.text_muted))
|
|
419
|
+
else:
|
|
420
|
+
console.print(Text("─" * 60, style=colors.text_muted))
|
|
421
|
+
if not Config.TUI_COMPACT_MODE:
|
|
422
|
+
console.print()
|
utils/tui/__init__.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""TUI (Terminal User Interface) package for aloop.
|
|
2
|
+
|
|
3
|
+
This package provides a modern, professional terminal UI with:
|
|
4
|
+
- Theme support (dark/light modes)
|
|
5
|
+
- Clean message display (Claude Code style)
|
|
6
|
+
- Persistent status bar
|
|
7
|
+
- Progress spinners and animations
|
|
8
|
+
- Enhanced input handling with auto-completion
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from utils.tui.components import (
|
|
12
|
+
Divider,
|
|
13
|
+
MessageDisplay,
|
|
14
|
+
ThinkingDisplay,
|
|
15
|
+
ToolCallDisplay,
|
|
16
|
+
)
|
|
17
|
+
from utils.tui.input_handler import InputHandler
|
|
18
|
+
from utils.tui.progress import ProgressContext, Spinner
|
|
19
|
+
from utils.tui.status_bar import StatusBar
|
|
20
|
+
from utils.tui.theme import Theme, get_theme, set_theme
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
# Theme
|
|
24
|
+
"Theme",
|
|
25
|
+
"get_theme",
|
|
26
|
+
"set_theme",
|
|
27
|
+
# Components
|
|
28
|
+
"MessageDisplay",
|
|
29
|
+
"ToolCallDisplay",
|
|
30
|
+
"ThinkingDisplay",
|
|
31
|
+
"Divider",
|
|
32
|
+
# Status Bar
|
|
33
|
+
"StatusBar",
|
|
34
|
+
# Progress
|
|
35
|
+
"Spinner",
|
|
36
|
+
"ProgressContext",
|
|
37
|
+
# Input
|
|
38
|
+
"InputHandler",
|
|
39
|
+
]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass(frozen=True, slots=True)
|
|
7
|
+
class CommandSpec:
|
|
8
|
+
name: str
|
|
9
|
+
description: str = ""
|
|
10
|
+
args_hint: str = ""
|
|
11
|
+
group: str = ""
|
|
12
|
+
subcommands: dict[str, CommandSpec] = field(default_factory=dict)
|
|
13
|
+
|
|
14
|
+
@property
|
|
15
|
+
def display(self) -> str:
|
|
16
|
+
if self.args_hint:
|
|
17
|
+
return f"/{self.name} {self.args_hint}"
|
|
18
|
+
return f"/{self.name}"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class CommandRegistry:
|
|
23
|
+
commands: list[CommandSpec]
|
|
24
|
+
|
|
25
|
+
def to_help_map(self) -> dict[str, str]:
|
|
26
|
+
result: dict[str, str] = {}
|
|
27
|
+
for cmd in self.commands:
|
|
28
|
+
result[cmd.name] = cmd.description
|
|
29
|
+
for sub_name, sub in cmd.subcommands.items():
|
|
30
|
+
key = f"{cmd.name} {sub_name}".strip()
|
|
31
|
+
result[key] = sub.description
|
|
32
|
+
return result
|
|
33
|
+
|
|
34
|
+
def to_subcommand_map(self) -> dict[str, dict[str, str]]:
|
|
35
|
+
result: dict[str, dict[str, str]] = {}
|
|
36
|
+
for cmd in self.commands:
|
|
37
|
+
if cmd.subcommands:
|
|
38
|
+
result[cmd.name] = {k: v.description for k, v in cmd.subcommands.items()}
|
|
39
|
+
return result
|
|
40
|
+
|
|
41
|
+
def to_display_map(self) -> dict[str, str]:
|
|
42
|
+
result: dict[str, str] = {}
|
|
43
|
+
for cmd in self.commands:
|
|
44
|
+
result[cmd.name] = cmd.display
|
|
45
|
+
for sub_name, sub in cmd.subcommands.items():
|
|
46
|
+
key = f"{cmd.name} {sub_name}".strip()
|
|
47
|
+
extra = f" {sub.args_hint}" if sub.args_hint else ""
|
|
48
|
+
result[key] = f"/{cmd.name} {sub_name}{extra}"
|
|
49
|
+
return result
|