weco 0.3.7__py3-none-any.whl → 0.3.8__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.
weco/ui.py ADDED
@@ -0,0 +1,315 @@
1
+ """
2
+ Optimization loop UI components.
3
+
4
+ This module contains the UI protocol and implementations for displaying
5
+ optimization progress in the CLI.
6
+ """
7
+
8
+ import time
9
+ from dataclasses import dataclass, field
10
+ from typing import List, Optional, Protocol
11
+
12
+ from rich.console import Console, Group
13
+ from rich.live import Live
14
+ from rich.panel import Panel
15
+ from rich.table import Table
16
+ from rich.text import Text
17
+
18
+
19
+ class OptimizationUI(Protocol):
20
+ """Protocol for optimization UI event handlers."""
21
+
22
+ def on_polling(self, step: int) -> None:
23
+ """Called when polling for execution tasks."""
24
+ ...
25
+
26
+ def on_task_claimed(self, task_id: str, plan: Optional[str]) -> None:
27
+ """Called when a task is successfully claimed."""
28
+ ...
29
+
30
+ def on_executing(self, step: int) -> None:
31
+ """Called when starting to execute code."""
32
+ ...
33
+
34
+ def on_output(self, output: str, max_preview: int = 200) -> None:
35
+ """Called with execution output."""
36
+ ...
37
+
38
+ def on_submitting(self) -> None:
39
+ """Called when submitting result to backend."""
40
+ ...
41
+
42
+ def on_metric(self, step: int, value: float) -> None:
43
+ """Called when a metric value is received."""
44
+ ...
45
+
46
+ def on_complete(self, total_steps: int) -> None:
47
+ """Called when optimization completes successfully."""
48
+ ...
49
+
50
+ def on_stop_requested(self) -> None:
51
+ """Called when a stop request is received from dashboard."""
52
+ ...
53
+
54
+ def on_interrupted(self) -> None:
55
+ """Called when interrupted by user (Ctrl+C)."""
56
+ ...
57
+
58
+ def on_warning(self, message: str) -> None:
59
+ """Called for non-fatal warnings."""
60
+ ...
61
+
62
+ def on_error(self, message: str) -> None:
63
+ """Called for errors."""
64
+ ...
65
+
66
+
67
+ @dataclass
68
+ class UIState:
69
+ """Reactive state for the live optimization UI."""
70
+
71
+ step: int = 0
72
+ total_steps: int = 0
73
+ status: str = "initializing" # polling, executing, submitting, complete, stopped, error
74
+ plan_preview: str = ""
75
+ output_preview: str = ""
76
+ metrics: List[tuple] = field(default_factory=list) # (step, value)
77
+ error: Optional[str] = None
78
+
79
+
80
+ class LiveOptimizationUI:
81
+ """
82
+ Rich Live implementation of OptimizationUI with dynamic single-panel updates.
83
+
84
+ Displays a compact, updating panel showing:
85
+ - Run info (ID, name, dashboard link)
86
+ - Current step and status with visual indicator
87
+ - Plan preview
88
+ - Output preview
89
+ - Metric history as sparkline
90
+ """
91
+
92
+ SPARKLINE_CHARS = "▁▂▃▄▅▆▇█"
93
+ SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]
94
+ # Statuses that show the spinner animation
95
+ ACTIVE_STATUSES = {"initializing", "polling", "executing", "submitting"}
96
+ STATUS_INDICATORS = {
97
+ "initializing": ("⏳", "dim"),
98
+ "polling": ("🔄", "cyan"),
99
+ "executing": ("⚡", "yellow"),
100
+ "submitting": ("🧠", "blue"),
101
+ "complete": ("✅", "green"),
102
+ "stopped": ("⏹", "yellow"),
103
+ "interrupted": ("⚠", "yellow"),
104
+ "error": ("❌", "red"),
105
+ }
106
+
107
+ def __init__(
108
+ self,
109
+ console: Console,
110
+ run_id: str,
111
+ run_name: str,
112
+ total_steps: int,
113
+ dashboard_url: str,
114
+ model: str = "",
115
+ metric_name: str = "",
116
+ ):
117
+ self.console = console
118
+ self.run_id = run_id
119
+ self.run_name = run_name
120
+ self.dashboard_url = dashboard_url
121
+ self.model = model
122
+ self.metric_name = metric_name
123
+ self.state = UIState(total_steps=total_steps)
124
+ self._live: Optional[Live] = None
125
+
126
+ def _sparkline(self, values: List[float], max_width: int) -> str:
127
+ """
128
+ Create a mini sparkline chart from metric values.
129
+
130
+ Automatically slides to show most recent values when they exceed max_width.
131
+ Shows "···" prefix when older values are hidden.
132
+ """
133
+ if not values:
134
+ return ""
135
+
136
+ # Reserve space for "···" prefix if we need to truncate
137
+ if len(values) > max_width:
138
+ prefix = "··"
139
+ available = max_width - len(prefix)
140
+ vals = values[-available:] # Take most recent values that fit
141
+ sparkline_prefix = f"[dim]{prefix}[/]"
142
+ else:
143
+ vals = values
144
+ sparkline_prefix = ""
145
+
146
+ min_v, max_v = min(vals), max(vals)
147
+ if max_v == min_v:
148
+ return sparkline_prefix + self.SPARKLINE_CHARS[4] * len(vals)
149
+
150
+ chars = self.SPARKLINE_CHARS
151
+ sparkline = "".join(chars[int((v - min_v) / (max_v - min_v) * 7)] for v in vals)
152
+ return sparkline_prefix + sparkline
153
+
154
+ def _render(self) -> Group:
155
+ """Render the current UI state as a Rich Panel with top margin."""
156
+ emoji, style = self.STATUS_INDICATORS.get(self.state.status, ("⏳", "dim"))
157
+
158
+ # Build content grid - expands to full terminal width
159
+ grid = Table.grid(padding=(0, 1), expand=True)
160
+ grid.add_column(style="dim", width=10)
161
+ grid.add_column(overflow="ellipsis", no_wrap=True, ratio=1)
162
+
163
+ # Run info (always shown)
164
+ run_display = f"[bold]{self.run_name}[/] [dim]({self.run_id})[/]"
165
+ grid.add_row("Run", run_display)
166
+ grid.add_row("Dashboard", f"[link={self.dashboard_url}]{self.dashboard_url}[/link]")
167
+ if self.model:
168
+ grid.add_row("Model", f"[cyan]{self.model}[/]")
169
+ if self.metric_name:
170
+ grid.add_row("Metric", f"[magenta]{self.metric_name}[/]")
171
+ grid.add_row("", "")
172
+
173
+ # Progress (always shown)
174
+ progress_bar = self._render_progress_bar()
175
+ grid.add_row("Progress", progress_bar)
176
+
177
+ # Status (always shown) - with spinner for active states
178
+ status_text = Text()
179
+ status_text.append(f"{emoji} ", style=style)
180
+ status_text.append(self.state.status.replace("_", " ").title(), style=f"bold {style}")
181
+ if self.state.status in self.ACTIVE_STATUSES:
182
+ # Time-based frame calculation: ~10 fps spinner animation
183
+ frame = int(time.time() * 10) % len(self.SPINNER_FRAMES)
184
+ spinner = self.SPINNER_FRAMES[frame]
185
+ status_text.append(f" {spinner}", style=f"bold {style}")
186
+ grid.add_row("Status", status_text)
187
+
188
+ # Plan (always shown, placeholder when empty)
189
+ if self.state.plan_preview:
190
+ grid.add_row("Plan", f"[dim italic]{self.state.plan_preview}[/]")
191
+ else:
192
+ grid.add_row("Plan", "[dim]—[/]")
193
+
194
+ # Output (always shown, placeholder when empty)
195
+ if self.state.output_preview:
196
+ output_text = self.state.output_preview.replace("\n", " ")
197
+ grid.add_row("Output", f"[dim]{output_text}[/]")
198
+ else:
199
+ grid.add_row("Output", "[dim]—[/]")
200
+
201
+ # Metrics section (always shown, 3 rows: current, best, chart)
202
+ if self.state.metrics:
203
+ values = [m[1] for m in self.state.metrics]
204
+ latest = self.state.metrics[-1][1]
205
+ best = max(values)
206
+
207
+ # Current and best on separate lines
208
+ grid.add_row("Current", f"[bold cyan]{latest:.6g}[/]")
209
+ grid.add_row("Best", f"[bold green]{best:.6g}[/]")
210
+
211
+ # Chart line - calculate available width for sparkline
212
+ # Console width minus: label(10) + padding(4) + panel borders(4) + panel padding(4)
213
+ chart_width = max(self.console.width - 22, 20)
214
+ sparkline = self._sparkline(values, chart_width)
215
+ grid.add_row("History", f"[green]{sparkline}[/]")
216
+ else:
217
+ grid.add_row("Current", "[dim]—[/]")
218
+ grid.add_row("Best", "[dim]—[/]")
219
+ grid.add_row("History", "[dim]—[/]")
220
+
221
+ # Error row (always present, empty when no error)
222
+ if self.state.error:
223
+ grid.add_row("Error", f"[bold red]{self.state.error}[/]")
224
+ else:
225
+ grid.add_row("", "") # Empty row to maintain height
226
+
227
+ panel = Panel(grid, title="[bold blue]⚡ Weco Optimization[/]", border_style="blue", padding=(1, 2), expand=True)
228
+ # Wrap panel with top margin for spacing
229
+ return Group(Text(""), panel)
230
+
231
+ def _render_progress_bar(self) -> Text:
232
+ """Render a simple ASCII progress bar."""
233
+ total = self.state.total_steps
234
+ current = min(self.state.step, total) # Clamp to total to avoid >100%
235
+ width = 40
236
+
237
+ if total <= 0:
238
+ return Text(f"Step {self.state.step}", style="bold")
239
+
240
+ filled = min(int((current / total) * width), width) # Clamp filled bars
241
+ bar = "█" * filled + "░" * (width - filled)
242
+ pct = min((current / total) * 100, 100) # Clamp percentage
243
+ return Text(f"[{bar}] {current}/{total} ({pct:.0f}%)", style="bold")
244
+
245
+ def __rich__(self) -> Group:
246
+ """Called by Rich on each refresh cycle - enables auto-animated spinner."""
247
+ return self._render()
248
+
249
+ def _update(self) -> None:
250
+ """Trigger an immediate live update (for state changes)."""
251
+ if self._live:
252
+ self._live.refresh()
253
+
254
+ # --- Context manager for Live display ---
255
+ def __enter__(self) -> "LiveOptimizationUI":
256
+ # Pass self so Rich calls __rich__() on every auto-refresh (enables spinner animation)
257
+ # Use vertical_overflow="visible" to prevent clipping issues on exit
258
+ self._live = Live(self, console=self.console, refresh_per_second=10, transient=False, vertical_overflow="visible")
259
+ self._live.__enter__()
260
+ return self
261
+
262
+ def __exit__(self, *args) -> None:
263
+ if self._live:
264
+ self._live.__exit__(*args)
265
+ self._live = None
266
+
267
+ # --- OptimizationUI Protocol Implementation ---
268
+ def on_polling(self, step: int) -> None:
269
+ self.state.step = step
270
+ self.state.status = "polling"
271
+ self.state.output_preview = ""
272
+ self._update()
273
+
274
+ def on_task_claimed(self, task_id: str, plan: Optional[str]) -> None:
275
+ self.state.plan_preview = plan or ""
276
+ self._update()
277
+
278
+ def on_executing(self, step: int) -> None:
279
+ self.state.step = step
280
+ self.state.status = "executing"
281
+ self._update()
282
+
283
+ def on_output(self, output: str, max_preview: int = 200) -> None:
284
+ self.state.output_preview = output[:max_preview]
285
+ self._update()
286
+
287
+ def on_submitting(self) -> None:
288
+ self.state.status = "submitting"
289
+ self._update()
290
+
291
+ def on_metric(self, step: int, value: float) -> None:
292
+ self.state.metrics.append((step, value))
293
+ self._update()
294
+
295
+ def on_complete(self, total_steps: int) -> None:
296
+ self.state.step = total_steps
297
+ self.state.status = "complete"
298
+ self._update()
299
+
300
+ def on_stop_requested(self) -> None:
301
+ self.state.status = "stopped"
302
+ self._update()
303
+
304
+ def on_interrupted(self) -> None:
305
+ self.state.status = "interrupted"
306
+ self._update()
307
+
308
+ def on_warning(self, message: str) -> None:
309
+ # Warnings are less critical; we could add a warnings list but keeping it simple
310
+ pass
311
+
312
+ def on_error(self, message: str) -> None:
313
+ self.state.error = message
314
+ self.state.status = "error"
315
+ self._update()
weco/validation.py ADDED
@@ -0,0 +1,112 @@
1
+ """Input validation for the Weco CLI.
2
+
3
+ Provides early validation of user inputs with helpful, actionable error messages.
4
+ Validation happens before expensive operations (auth, API calls) to fail fast.
5
+ """
6
+
7
+ import pathlib
8
+ from difflib import get_close_matches
9
+
10
+ from rich.console import Console
11
+
12
+
13
+ class ValidationError(Exception):
14
+ """Raised when user input validation fails."""
15
+
16
+ def __init__(self, message: str, suggestion: str | None = None):
17
+ self.message = message
18
+ self.suggestion = suggestion
19
+ super().__init__(message)
20
+
21
+
22
+ def validate_source_file(source: str) -> None:
23
+ """
24
+ Validate that the source file exists and is readable.
25
+
26
+ Args:
27
+ source: Path to the source file.
28
+
29
+ Raises:
30
+ ValidationError: If the file doesn't exist, isn't readable, or isn't a valid text file.
31
+ """
32
+ path = pathlib.Path(source)
33
+
34
+ if not path.exists():
35
+ suggestion = _find_similar_files(path)
36
+ raise ValidationError(f"Source file '{source}' not found.", suggestion=suggestion)
37
+
38
+ if path.is_dir():
39
+ raise ValidationError(
40
+ f"'{source}' is a directory, not a file.", suggestion="Please specify a file path, e.g., 'src/model.py'"
41
+ )
42
+
43
+ # Try reading the file to catch permission and encoding issues early
44
+ try:
45
+ path.read_text(encoding="utf-8")
46
+ except PermissionError:
47
+ raise ValidationError(f"Cannot read '{source}' — permission denied.")
48
+ except UnicodeDecodeError:
49
+ raise ValidationError(
50
+ f"'{source}' doesn't appear to be a valid text file.",
51
+ suggestion="Weco optimizes source code files (e.g., .py, .cu, .rs)",
52
+ )
53
+ except OSError as e:
54
+ raise ValidationError(f"Cannot read '{source}': {e}")
55
+
56
+
57
+ def validate_log_directory(log_dir: str) -> None:
58
+ """
59
+ Validate that the log directory is writable.
60
+
61
+ Args:
62
+ log_dir: Path to the log directory.
63
+
64
+ Raises:
65
+ ValidationError: If the directory can't be created or isn't writable.
66
+ """
67
+ path = pathlib.Path(log_dir)
68
+
69
+ try:
70
+ # Attempt to create the directory (no-op if exists)
71
+ path.mkdir(parents=True, exist_ok=True)
72
+ except PermissionError:
73
+ raise ValidationError(f"Cannot create log directory '{log_dir}' — permission denied.")
74
+ except OSError as e:
75
+ raise ValidationError(f"Cannot create log directory '{log_dir}': {e}")
76
+
77
+ # Check if writable by attempting to create a temp file
78
+ test_file = path / ".weco_write_test"
79
+ try:
80
+ test_file.touch()
81
+ test_file.unlink()
82
+ except PermissionError:
83
+ raise ValidationError(f"Log directory '{log_dir}' is not writable.")
84
+ except OSError:
85
+ pass # Directory exists and is likely fine
86
+
87
+
88
+ def _find_similar_files(path: pathlib.Path) -> str | None:
89
+ """Find similar filenames in the same directory to suggest as alternatives."""
90
+ parent = path.parent if path.parent.exists() else pathlib.Path(".")
91
+
92
+ try:
93
+ # Get files with the same extension, or all files if no extension
94
+ if path.suffix:
95
+ candidates = [f.name for f in parent.iterdir() if f.is_file() and f.suffix == path.suffix]
96
+ else:
97
+ candidates = [f.name for f in parent.iterdir() if f.is_file()]
98
+
99
+ matches = get_close_matches(path.name, candidates, n=3, cutoff=0.4)
100
+ if matches:
101
+ return f"Did you mean: {', '.join(matches)}?"
102
+ except OSError:
103
+ pass
104
+
105
+ return None
106
+
107
+
108
+ def print_validation_error(error: ValidationError, console: Console) -> None:
109
+ """Print a validation error in a user-friendly format."""
110
+ console.print(f"[bold red]Error:[/] {error.message}")
111
+ if error.suggestion:
112
+ console.print(f"[dim]{error.suggestion}[/]")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: weco
3
- Version: 0.3.7
3
+ Version: 0.3.8
4
4
  Summary: Documentation for `weco`, a CLI for using Weco AI's code optimizer.
5
5
  Author-email: Weco AI Team <contact@weco.ai>
6
6
  License:
@@ -357,7 +357,7 @@ weco run --model gpt-5 --source optimize.py [other options...]
357
357
  - `claude-opus-4-5`, `claude-opus-4-1`, `claude-opus-4`, `claude-sonnet-4-5`, `claude-sonnet-4`, `claude-haiku-4-5`
358
358
 
359
359
  **Google Gemini:**
360
- - `gemini-3-pro-preview`, `gemini-2.5-pro`, `gemini-2.5-flash`, `gemini-2.5-flash-lite`
360
+ - `gemini-3-pro-preview`, `gemini-3-flash-preview`, `gemini-2.5-pro`, `gemini-2.5-flash`, `gemini-2.5-flash-lite`
361
361
 
362
362
  All models are available through Weco. If no model is specified, Weco automatically selects the best model for your optimization task.
363
363
 
@@ -0,0 +1,18 @@
1
+ weco/__init__.py,sha256=ClO0uT6GKOA0iSptvP0xbtdycf0VpoPTq37jHtvlhtw,303
2
+ weco/api.py,sha256=hy0D01x-AJ26DURtKEywQAS7nQgo38A-wAJfPKHGGqM,17395
3
+ weco/auth.py,sha256=O31Hoj-Loi8DWJJG2LfeWgUMuNqAUeGDpd2ZGjA9Ah0,9997
4
+ weco/browser.py,sha256=nsqQtLqbNOe9Zhu9Zogc8rMmBMyuDxuHzKZQL_w10Ps,923
5
+ weco/cli.py,sha256=u5DSt2YGLOha-ldaW6qg3NO2z3rNfYw37FNtaIs-Kz4,12656
6
+ weco/constants.py,sha256=rxL6yrpIzK8zvPTmPqOYl7LUMZ01vUJ9zUqfZD2n-0U,519
7
+ weco/credits.py,sha256=C08x-TRcLg3ccfKqMGNRY7zBn7t3r7LZ119bxgfztaI,7629
8
+ weco/optimizer.py,sha256=XygxTOTPaPx0dDVI9oWaMQHZ0-YAg1JFcMvGPcnEx-A,23990
9
+ weco/panels.py,sha256=POHt0MdRKDykwUJYXcry92O41lpB9gxna55wFI9abWU,16272
10
+ weco/ui.py,sha256=1shfWxeyfhjTzRZsuODuMx5XzeP9SngqpNpN4vIiDCI,11203
11
+ weco/utils.py,sha256=v_rvgw-ktRoXrpPA2copngI8QDCB8UXmbiN-wAiYvEE,9450
12
+ weco/validation.py,sha256=n5aDuF3BFgwVb4eZ9PuU48nogrseXYNI8S3ePqWZCoc,3736
13
+ weco-0.3.8.dist-info/licenses/LICENSE,sha256=9LUfoGHjLPtak2zps2kL2tm65HAZIICx_FbLaRuS4KU,11337
14
+ weco-0.3.8.dist-info/METADATA,sha256=a9A-tvBbsaojDEXmi1DYsoBAJALFQ6kPeIlWlf2Of0Y,29861
15
+ weco-0.3.8.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
16
+ weco-0.3.8.dist-info/entry_points.txt,sha256=ixJ2uClALbCpBvnIR6BXMNck8SHAab8eVkM9pIUowcs,39
17
+ weco-0.3.8.dist-info/top_level.txt,sha256=F0N7v6e2zBSlsorFv-arAq2yDxQbzX3KVO8GxYhPUeE,5
18
+ weco-0.3.8.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -1,15 +0,0 @@
1
- weco/__init__.py,sha256=ClO0uT6GKOA0iSptvP0xbtdycf0VpoPTq37jHtvlhtw,303
2
- weco/api.py,sha256=xVVRk1pj9jpjTphaInkkAhjqhgFP2-6zHT_V-5Du1Fc,13629
3
- weco/auth.py,sha256=O31Hoj-Loi8DWJJG2LfeWgUMuNqAUeGDpd2ZGjA9Ah0,9997
4
- weco/cli.py,sha256=Mtkv3rE1rQLdeoVydn30EUi1ki3Cyu45Q3cONnQH4QY,11210
5
- weco/constants.py,sha256=rxL6yrpIzK8zvPTmPqOYl7LUMZ01vUJ9zUqfZD2n-0U,519
6
- weco/credits.py,sha256=C08x-TRcLg3ccfKqMGNRY7zBn7t3r7LZ119bxgfztaI,7629
7
- weco/optimizer.py,sha256=2qYweESOAer26gjjhu4dg01XmuhtA2nMrJuijaxizzE,45492
8
- weco/panels.py,sha256=POHt0MdRKDykwUJYXcry92O41lpB9gxna55wFI9abWU,16272
9
- weco/utils.py,sha256=v_rvgw-ktRoXrpPA2copngI8QDCB8UXmbiN-wAiYvEE,9450
10
- weco-0.3.7.dist-info/licenses/LICENSE,sha256=9LUfoGHjLPtak2zps2kL2tm65HAZIICx_FbLaRuS4KU,11337
11
- weco-0.3.7.dist-info/METADATA,sha256=rWJKSr-quLFBWSfmmAuTkaR3nP7G8DPjBZTVib9fvN8,29835
12
- weco-0.3.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
13
- weco-0.3.7.dist-info/entry_points.txt,sha256=ixJ2uClALbCpBvnIR6BXMNck8SHAab8eVkM9pIUowcs,39
14
- weco-0.3.7.dist-info/top_level.txt,sha256=F0N7v6e2zBSlsorFv-arAq2yDxQbzX3KVO8GxYhPUeE,5
15
- weco-0.3.7.dist-info/RECORD,,