-
Notifications
You must be signed in to change notification settings - Fork 517
Expand file tree
/
Copy pathui.py
More file actions
232 lines (167 loc) Β· 8.25 KB
/
Copy pathui.py
File metadata and controls
232 lines (167 loc) Β· 8.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
"""Terminal UI rendering β colored output, spinner, tool display."""
from __future__ import annotations
import sys
import threading
import time
from rich.console import Console
console = Console(highlight=False)
# βββ Basic output ββββββββββββββββββββββββββββββββββββββββββ
def print_welcome() -> None:
console.print("\n [bold cyan]Mini Claude Code[/bold cyan][dim] β A minimal coding agent[/dim]\n")
console.print("[dim] Type your request, or 'exit' to quit.[/dim]")
console.print("[dim] Commands: /clear /plan /cost /compact /memory /skills[/dim]\n")
def print_user_prompt() -> None:
console.print("\n[bold green]> [/bold green]", end="")
def print_assistant_text(text: str) -> None:
sys.stdout.write(text)
sys.stdout.flush()
def print_tool_call(name: str, inp: dict) -> None:
icon = _get_tool_icon(name)
summary = _get_tool_summary(name, inp)
console.print(f"\n [yellow]{icon} {name}[/yellow][dim] {summary}[/dim]")
def print_tool_result(name: str, result: str) -> None:
if (name in ("edit_file", "write_file")) and not result.startswith("Error"):
_print_file_change_result(name, result)
return
max_len = 500
truncated = result
if len(result) > max_len:
truncated = result[:max_len] + f"\n ... ({len(result)} chars total)"
lines = "\n".join(" " + l for l in truncated.split("\n"))
console.print(f"[dim]{lines}[/dim]")
def _print_file_change_result(_name: str, result: str) -> None:
lines = result.split("\n")
console.print(f"[dim] {lines[0]}[/dim]")
max_display = 40
content_lines = lines[1:]
display_lines = content_lines[:max_display]
for line in display_lines:
if not line.strip():
continue
if line.startswith("@@"):
console.print(f"[cyan] {line}[/cyan]")
elif line.startswith("- "):
console.print(f"[red] {line}[/red]")
elif line.startswith("+ "):
console.print(f"[green] {line}[/green]")
else:
console.print(f"[dim] {line}[/dim]")
if len(content_lines) > max_display:
console.print(f"[dim] ... ({len(content_lines) - max_display} more lines)[/dim]")
def print_error(msg: str) -> None:
console.print(f"\n [red]Error: {msg}[/red]")
def print_confirmation(command: str) -> None:
console.print(f"\n [yellow]β Dangerous command:[/yellow] [white]{command}[/white]")
def print_divider() -> None:
console.print(f"\n[dim] {'β' * 50}[/dim]")
# βββ Per-model pricing (USD per million tokens) βββββββββββββ
# The default rates are the fixed multipliers used across Claude tiers
# (input $3, output $15, cache read 0.1x = $0.3, cache write 1.25x = $3.75).
# Models with published per-model rates override the default below so budget
# enforcement and the per-turn cost line bill at the correct rate.
DEFAULT_PRICING = {"input": 3.0, "output": 15.0, "cache_read": 0.3, "cache_write": 3.75}
MODEL_PRICING = {
# MiniMax-M3 publishes no separate cache-write rate, so cache writes are
# billed at the input rate.
"MiniMax-M3": {"input": 0.6, "output": 2.4, "cache_read": 0.12, "cache_write": 0.6},
"MiniMax-M2.7": {"input": 0.3, "output": 1.2, "cache_read": 0.06, "cache_write": 0.375},
}
def get_model_pricing(model: str) -> dict:
return MODEL_PRICING.get(model, DEFAULT_PRICING)
def print_cost(input_tokens: int, output_tokens: int, cache_read: int = 0, cache_creation: int = 0, model: str | None = None) -> None:
# Cache read/write and per-token rates are model-specific (see get_model_pricing).
p = get_model_pricing(model) if model else DEFAULT_PRICING
total = (
(input_tokens / 1_000_000) * p["input"]
+ (cache_read / 1_000_000) * p["cache_read"]
+ (cache_creation / 1_000_000) * p["cache_write"]
+ (output_tokens / 1_000_000) * p["output"]
)
cache_str = f", {cache_read} cached" if cache_read else ""
console.print(f"\n[dim] Tokens: {input_tokens} in / {output_tokens} out{cache_str} (~${total:.4f})[/dim]")
def print_retry(attempt: int, max_retries: int, reason: str) -> None:
console.print(f"\n [yellow]β» Retry {attempt}/{max_retries}: {reason}[/yellow]")
def print_info(msg: str) -> None:
console.print(f"\n [cyan]βΉ {msg}[/cyan]")
# βββ Spinner ββββββββββββββββββββββββββββββββββββββββββββββ
SPINNER_FRAMES = ["β ", "β ", "β Ή", "β Έ", "β Ό", "β ΄", "β ¦", "β §", "β ", "β "]
_spinner_thread: threading.Thread | None = None
_spinner_stop = threading.Event()
def start_spinner(label: str = "Thinking") -> None:
global _spinner_thread
if _spinner_thread is not None:
return
_spinner_stop.clear()
def _run() -> None:
frame = 0
sys.stdout.write(f"\n {SPINNER_FRAMES[0]} {label}...")
sys.stdout.flush()
while not _spinner_stop.is_set():
time.sleep(0.08)
frame = (frame + 1) % len(SPINNER_FRAMES)
sys.stdout.write(f"\r {SPINNER_FRAMES[frame]} {label}...")
sys.stdout.flush()
_spinner_thread = threading.Thread(target=_run, daemon=True)
_spinner_thread.start()
def stop_spinner() -> None:
global _spinner_thread
if _spinner_thread is None:
return
_spinner_stop.set()
_spinner_thread.join(timeout=1)
_spinner_thread = None
sys.stdout.write("\r\033[K")
sys.stdout.flush()
# βββ Plan approval display ββββββββββββββββββββββββββββββββββ
def print_plan_for_approval(plan_content: str) -> None:
console.print("\n [cyan]βββ Plan for Approval βββ[/cyan]")
lines = plan_content.split("\n")
max_lines = 60
for line in lines[:max_lines]:
console.print(f" [white]{line}[/white]")
if len(lines) > max_lines:
console.print(f"[dim] ... ({len(lines) - max_lines} more lines)[/dim]")
console.print(" [cyan]ββββββββββββββββββββββββ[/cyan]\n")
def print_plan_approval_options() -> None:
console.print(" [yellow]Choose an option:[/yellow]")
console.print(" [white]1) Yes, clear context and execute[/white][dim] β fresh start with auto-accept edits[/dim]")
console.print(" [white]2) Yes, and execute[/white][dim] β keep context, auto-accept edits[/dim]")
console.print(" [white]3) Yes, manually approve edits[/white][dim] β keep context, confirm each edit[/dim]")
console.print(" [white]4) No, keep planning[/white][dim] β provide feedback to revise[/dim]")
# βββ Sub-agent display ββββββββββββββββββββββββββββββββββββββ
def print_sub_agent_start(agent_type: str, description: str) -> None:
console.print(f"\n [magenta]ββ Sub-agent [{agent_type}]: {description}[/magenta]")
def print_sub_agent_end(agent_type: str, _description: str) -> None:
console.print(f" [magenta]ββ Sub-agent [{agent_type}] completed[/magenta]")
# βββ Tool icons and summaries βββββββββββββββββββββββββββββββ
_TOOL_ICONS = {
"read_file": "π",
"write_file": "βοΈ",
"edit_file": "π§",
"list_files": "π",
"grep_search": "π",
"run_shell": "π»",
"skill": "β‘",
"agent": "π€",
}
def _get_tool_icon(name: str) -> str:
return _TOOL_ICONS.get(name, "π¨")
def _get_tool_summary(name: str, inp: dict) -> str:
if name == "read_file":
return inp.get("file_path", "")
if name == "write_file":
return inp.get("file_path", "")
if name == "edit_file":
return inp.get("file_path", "")
if name == "list_files":
return inp.get("pattern", "")
if name == "grep_search":
return f'"{inp.get("pattern", "")}" in {inp.get("path", ".")}'
if name == "run_shell":
cmd = inp.get("command", "")
return cmd[:60] + "..." if len(cmd) > 60 else cmd
if name == "skill":
return inp.get("skill_name", "")
if name == "agent":
return f'[{inp.get("type", "general")}] {inp.get("description", "")}'
return ""