up-cli 0.1.1__py3-none-any.whl → 0.5.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.
- up/__init__.py +1 -1
- up/ai_cli.py +229 -0
- up/cli.py +75 -4
- up/commands/agent.py +521 -0
- up/commands/bisect.py +343 -0
- up/commands/branch.py +350 -0
- up/commands/dashboard.py +248 -0
- up/commands/init.py +195 -6
- up/commands/learn.py +1741 -0
- up/commands/memory.py +545 -0
- up/commands/new.py +108 -10
- up/commands/provenance.py +267 -0
- up/commands/review.py +239 -0
- up/commands/start.py +1124 -0
- up/commands/status.py +360 -0
- up/commands/summarize.py +122 -0
- up/commands/sync.py +317 -0
- up/commands/vibe.py +304 -0
- up/context.py +421 -0
- up/core/__init__.py +69 -0
- up/core/checkpoint.py +479 -0
- up/core/provenance.py +364 -0
- up/core/state.py +678 -0
- up/events.py +512 -0
- up/git/__init__.py +37 -0
- up/git/utils.py +270 -0
- up/git/worktree.py +331 -0
- up/learn/__init__.py +155 -0
- up/learn/analyzer.py +227 -0
- up/learn/plan.py +374 -0
- up/learn/research.py +511 -0
- up/learn/utils.py +117 -0
- up/memory.py +1096 -0
- up/parallel.py +551 -0
- up/summarizer.py +407 -0
- up/templates/__init__.py +70 -2
- up/templates/config/__init__.py +502 -20
- up/templates/docs/SKILL.md +28 -0
- up/templates/docs/__init__.py +341 -0
- up/templates/docs/standards/HEADERS.md +24 -0
- up/templates/docs/standards/STRUCTURE.md +18 -0
- up/templates/docs/standards/TEMPLATES.md +19 -0
- up/templates/learn/__init__.py +567 -14
- up/templates/loop/__init__.py +546 -27
- up/templates/mcp/__init__.py +474 -0
- up/templates/projects/__init__.py +786 -0
- up/ui/__init__.py +14 -0
- up/ui/loop_display.py +650 -0
- up/ui/theme.py +137 -0
- up_cli-0.5.0.dist-info/METADATA +519 -0
- up_cli-0.5.0.dist-info/RECORD +55 -0
- up_cli-0.1.1.dist-info/METADATA +0 -186
- up_cli-0.1.1.dist-info/RECORD +0 -14
- {up_cli-0.1.1.dist-info → up_cli-0.5.0.dist-info}/WHEEL +0 -0
- {up_cli-0.1.1.dist-info → up_cli-0.5.0.dist-info}/entry_points.txt +0 -0
up/ui/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""UP-CLI UI Components.
|
|
2
|
+
|
|
3
|
+
Cybersecurity/AI themed terminal UI for the product loop.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from up.ui.theme import CyberTheme, THEME
|
|
7
|
+
from up.ui.loop_display import ProductLoopDisplay, TaskStatus
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"CyberTheme",
|
|
11
|
+
"THEME",
|
|
12
|
+
"ProductLoopDisplay",
|
|
13
|
+
"TaskStatus",
|
|
14
|
+
]
|
up/ui/loop_display.py
ADDED
|
@@ -0,0 +1,650 @@
|
|
|
1
|
+
"""Product Loop Display - Cybersecurity/AI themed dashboard.
|
|
2
|
+
|
|
3
|
+
A real-time, refreshing terminal UI for the UP product loop.
|
|
4
|
+
Auto-detects terminal size and adapts layout accordingly.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import shutil
|
|
10
|
+
import time
|
|
11
|
+
from collections import deque
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from datetime import datetime
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import Optional, Callable
|
|
16
|
+
|
|
17
|
+
from rich.align import Align
|
|
18
|
+
from rich.console import Console, Group, RenderableType
|
|
19
|
+
from rich.layout import Layout
|
|
20
|
+
from rich.live import Live
|
|
21
|
+
from rich.panel import Panel
|
|
22
|
+
from rich.progress import (
|
|
23
|
+
Progress,
|
|
24
|
+
BarColumn,
|
|
25
|
+
TextColumn,
|
|
26
|
+
TaskProgressColumn,
|
|
27
|
+
TimeElapsedColumn,
|
|
28
|
+
SpinnerColumn,
|
|
29
|
+
)
|
|
30
|
+
from rich.style import Style
|
|
31
|
+
from rich.table import Table
|
|
32
|
+
from rich.text import Text
|
|
33
|
+
|
|
34
|
+
from up.ui.theme import CyberTheme, THEME, Symbols
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TaskStatus(Enum):
|
|
38
|
+
"""Status of a task in the queue."""
|
|
39
|
+
PENDING = "pending"
|
|
40
|
+
IN_PROGRESS = "in_progress"
|
|
41
|
+
COMPLETE = "complete"
|
|
42
|
+
FAILED = "failed"
|
|
43
|
+
SKIPPED = "skipped"
|
|
44
|
+
ROLLED_BACK = "rolled_back"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class LoopStatus(Enum):
|
|
48
|
+
"""Overall loop status."""
|
|
49
|
+
IDLE = "idle"
|
|
50
|
+
RUNNING = "running"
|
|
51
|
+
VERIFYING = "verifying"
|
|
52
|
+
PAUSED = "paused"
|
|
53
|
+
FAILED = "failed"
|
|
54
|
+
COMPLETE = "complete"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class TaskInfo:
|
|
59
|
+
"""Information about a task."""
|
|
60
|
+
id: str
|
|
61
|
+
title: str
|
|
62
|
+
status: TaskStatus = TaskStatus.PENDING
|
|
63
|
+
priority: str = "medium"
|
|
64
|
+
effort: str = "medium"
|
|
65
|
+
phase: str = ""
|
|
66
|
+
description: str = ""
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@dataclass
|
|
70
|
+
class LoopStats:
|
|
71
|
+
"""Statistics for the loop."""
|
|
72
|
+
elapsed_seconds: float = 0.0
|
|
73
|
+
failures: int = 0
|
|
74
|
+
rollbacks: int = 0
|
|
75
|
+
completed: int = 0
|
|
76
|
+
total: int = 0
|
|
77
|
+
current_iteration: int = 0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class DisplayState:
|
|
82
|
+
"""Current state of the display."""
|
|
83
|
+
status: LoopStatus = LoopStatus.IDLE
|
|
84
|
+
current_task: Optional[TaskInfo] = None
|
|
85
|
+
current_phase: str = "INIT"
|
|
86
|
+
tasks: list[TaskInfo] = field(default_factory=list)
|
|
87
|
+
stats: LoopStats = field(default_factory=LoopStats)
|
|
88
|
+
log_entries: deque = field(default_factory=lambda: deque(maxlen=6))
|
|
89
|
+
start_time: Optional[datetime] = None
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class ProductLoopDisplay:
|
|
93
|
+
"""Real-time product loop dashboard with cybersecurity/AI theme.
|
|
94
|
+
|
|
95
|
+
Features:
|
|
96
|
+
- Auto-detects terminal size (compact vs full layout)
|
|
97
|
+
- Real-time refresh using Rich Live
|
|
98
|
+
- Animated progress bars
|
|
99
|
+
- Status indicators with color coding
|
|
100
|
+
- Scrolling activity log
|
|
101
|
+
|
|
102
|
+
Usage:
|
|
103
|
+
display = ProductLoopDisplay()
|
|
104
|
+
display.start()
|
|
105
|
+
|
|
106
|
+
display.set_tasks(tasks)
|
|
107
|
+
display.update_task_status("C-001", TaskStatus.IN_PROGRESS)
|
|
108
|
+
display.log("Starting implementation...")
|
|
109
|
+
|
|
110
|
+
display.stop()
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
# Layout thresholds
|
|
114
|
+
COMPACT_WIDTH = 80
|
|
115
|
+
COMPACT_HEIGHT = 24
|
|
116
|
+
|
|
117
|
+
def __init__(self, console: Optional[Console] = None):
|
|
118
|
+
"""Initialize the display.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
console: Rich console instance (created if not provided)
|
|
122
|
+
"""
|
|
123
|
+
self.console = console or Console(theme=THEME)
|
|
124
|
+
self.state = DisplayState()
|
|
125
|
+
self.live: Optional[Live] = None
|
|
126
|
+
self._running = False
|
|
127
|
+
self._spinner_frame = 0
|
|
128
|
+
self._last_update = time.time()
|
|
129
|
+
|
|
130
|
+
def start(self) -> None:
|
|
131
|
+
"""Start the live display."""
|
|
132
|
+
if self._running:
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
self.state.start_time = datetime.now()
|
|
136
|
+
self.state.status = LoopStatus.RUNNING
|
|
137
|
+
self._running = True
|
|
138
|
+
|
|
139
|
+
# Create live display with appropriate refresh rate
|
|
140
|
+
self.live = Live(
|
|
141
|
+
self._render(),
|
|
142
|
+
console=self.console,
|
|
143
|
+
refresh_per_second=4,
|
|
144
|
+
transient=False,
|
|
145
|
+
)
|
|
146
|
+
self.live.start()
|
|
147
|
+
|
|
148
|
+
def stop(self) -> None:
|
|
149
|
+
"""Stop the live display."""
|
|
150
|
+
if not self._running:
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
self._running = False
|
|
154
|
+
if self.live:
|
|
155
|
+
self.live.stop()
|
|
156
|
+
self.live = None
|
|
157
|
+
|
|
158
|
+
def update(self) -> None:
|
|
159
|
+
"""Force update the display."""
|
|
160
|
+
if self.live and self._running:
|
|
161
|
+
self._update_elapsed()
|
|
162
|
+
self._spinner_frame = (self._spinner_frame + 1) % len(Symbols.SPINNER)
|
|
163
|
+
self.live.update(self._render())
|
|
164
|
+
|
|
165
|
+
def _update_elapsed(self) -> None:
|
|
166
|
+
"""Update elapsed time."""
|
|
167
|
+
if self.state.start_time:
|
|
168
|
+
delta = datetime.now() - self.state.start_time
|
|
169
|
+
self.state.stats.elapsed_seconds = delta.total_seconds()
|
|
170
|
+
|
|
171
|
+
# ─── State Setters ───────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
def set_status(self, status: LoopStatus) -> None:
|
|
174
|
+
"""Set the overall loop status."""
|
|
175
|
+
self.state.status = status
|
|
176
|
+
self.update()
|
|
177
|
+
|
|
178
|
+
def set_tasks(self, tasks: list[dict]) -> None:
|
|
179
|
+
"""Set the task queue from PRD task dicts."""
|
|
180
|
+
self.state.tasks = []
|
|
181
|
+
for t in tasks:
|
|
182
|
+
task_info = TaskInfo(
|
|
183
|
+
id=t.get("id", ""),
|
|
184
|
+
title=t.get("title", ""),
|
|
185
|
+
priority=t.get("priority", "medium"),
|
|
186
|
+
effort=t.get("effort", "medium"),
|
|
187
|
+
phase=t.get("phase", ""),
|
|
188
|
+
description=t.get("description", ""),
|
|
189
|
+
status=TaskStatus.COMPLETE if t.get("passes") else TaskStatus.PENDING,
|
|
190
|
+
)
|
|
191
|
+
self.state.tasks.append(task_info)
|
|
192
|
+
|
|
193
|
+
self.state.stats.total = len(tasks)
|
|
194
|
+
self.state.stats.completed = sum(1 for t in self.state.tasks if t.status == TaskStatus.COMPLETE)
|
|
195
|
+
self.update()
|
|
196
|
+
|
|
197
|
+
def set_current_task(self, task_id: str, phase: str = "EXECUTE") -> None:
|
|
198
|
+
"""Set the current task being processed."""
|
|
199
|
+
self.state.current_phase = phase
|
|
200
|
+
|
|
201
|
+
for task in self.state.tasks:
|
|
202
|
+
if task.id == task_id:
|
|
203
|
+
task.status = TaskStatus.IN_PROGRESS
|
|
204
|
+
self.state.current_task = task
|
|
205
|
+
break
|
|
206
|
+
|
|
207
|
+
self.update()
|
|
208
|
+
|
|
209
|
+
def update_task_status(self, task_id: str, status: TaskStatus) -> None:
|
|
210
|
+
"""Update a task's status."""
|
|
211
|
+
for task in self.state.tasks:
|
|
212
|
+
if task.id == task_id:
|
|
213
|
+
task.status = status
|
|
214
|
+
|
|
215
|
+
if status == TaskStatus.COMPLETE:
|
|
216
|
+
self.state.stats.completed += 1
|
|
217
|
+
elif status == TaskStatus.FAILED:
|
|
218
|
+
self.state.stats.failures += 1
|
|
219
|
+
elif status == TaskStatus.ROLLED_BACK:
|
|
220
|
+
self.state.stats.rollbacks += 1
|
|
221
|
+
break
|
|
222
|
+
|
|
223
|
+
# Clear current task if it's the one being updated
|
|
224
|
+
if self.state.current_task and self.state.current_task.id == task_id:
|
|
225
|
+
if status in (TaskStatus.COMPLETE, TaskStatus.FAILED, TaskStatus.ROLLED_BACK):
|
|
226
|
+
self.state.current_task = None
|
|
227
|
+
|
|
228
|
+
self.update()
|
|
229
|
+
|
|
230
|
+
def set_phase(self, phase: str) -> None:
|
|
231
|
+
"""Set the current phase."""
|
|
232
|
+
self.state.current_phase = phase
|
|
233
|
+
if phase == "VERIFY":
|
|
234
|
+
self.state.status = LoopStatus.VERIFYING
|
|
235
|
+
elif phase == "EXECUTE":
|
|
236
|
+
self.state.status = LoopStatus.RUNNING
|
|
237
|
+
self.update()
|
|
238
|
+
|
|
239
|
+
def increment_iteration(self) -> None:
|
|
240
|
+
"""Increment the iteration counter."""
|
|
241
|
+
self.state.stats.current_iteration += 1
|
|
242
|
+
self.update()
|
|
243
|
+
|
|
244
|
+
def log(self, message: str, style: str = "") -> None:
|
|
245
|
+
"""Add a log entry."""
|
|
246
|
+
timestamp = datetime.now().strftime("%H:%M:%S")
|
|
247
|
+
entry = (timestamp, message, style)
|
|
248
|
+
self.state.log_entries.append(entry)
|
|
249
|
+
self.update()
|
|
250
|
+
|
|
251
|
+
def log_success(self, message: str) -> None:
|
|
252
|
+
"""Add a success log entry."""
|
|
253
|
+
self.log(f"{Symbols.COMPLETE} {message}", "task.complete")
|
|
254
|
+
|
|
255
|
+
def log_error(self, message: str) -> None:
|
|
256
|
+
"""Add an error log entry."""
|
|
257
|
+
self.log(f"{Symbols.FAILED} {message}", "task.failed")
|
|
258
|
+
|
|
259
|
+
def log_warning(self, message: str) -> None:
|
|
260
|
+
"""Add a warning log entry."""
|
|
261
|
+
self.log(f"⚠ {message}", "task.skipped")
|
|
262
|
+
|
|
263
|
+
# ─── Rendering ───────────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
def _get_terminal_size(self) -> tuple[int, int]:
|
|
266
|
+
"""Get terminal dimensions."""
|
|
267
|
+
size = shutil.get_terminal_size((80, 24))
|
|
268
|
+
return size.columns, size.lines
|
|
269
|
+
|
|
270
|
+
def _is_compact(self) -> bool:
|
|
271
|
+
"""Check if we should use compact layout."""
|
|
272
|
+
width, height = self._get_terminal_size()
|
|
273
|
+
return width < self.COMPACT_WIDTH or height < self.COMPACT_HEIGHT
|
|
274
|
+
|
|
275
|
+
def _render(self) -> RenderableType:
|
|
276
|
+
"""Render the dashboard."""
|
|
277
|
+
if self._is_compact():
|
|
278
|
+
return self._render_compact()
|
|
279
|
+
return self._render_full()
|
|
280
|
+
|
|
281
|
+
def _render_full(self) -> Panel:
|
|
282
|
+
"""Render full layout dashboard."""
|
|
283
|
+
layout = Layout()
|
|
284
|
+
|
|
285
|
+
# Main structure
|
|
286
|
+
layout.split_column(
|
|
287
|
+
Layout(name="header", size=3),
|
|
288
|
+
Layout(name="progress", size=3),
|
|
289
|
+
Layout(name="main", ratio=1),
|
|
290
|
+
Layout(name="log", size=10),
|
|
291
|
+
Layout(name="stats", size=3),
|
|
292
|
+
Layout(name="footer", size=1),
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# Split main into current task and queue
|
|
296
|
+
layout["main"].split_row(
|
|
297
|
+
Layout(name="current", ratio=1),
|
|
298
|
+
Layout(name="queue", ratio=1),
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# Render components
|
|
302
|
+
layout["header"].update(self._render_header())
|
|
303
|
+
layout["progress"].update(self._render_progress_bar())
|
|
304
|
+
layout["current"].update(self._render_current_task())
|
|
305
|
+
layout["queue"].update(self._render_task_queue())
|
|
306
|
+
layout["log"].update(self._render_log())
|
|
307
|
+
layout["stats"].update(self._render_stats())
|
|
308
|
+
layout["footer"].update(self._render_footer())
|
|
309
|
+
|
|
310
|
+
return Panel(
|
|
311
|
+
layout,
|
|
312
|
+
border_style=Style(color=CyberTheme.BORDER),
|
|
313
|
+
padding=0,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
def _render_compact(self) -> Panel:
|
|
317
|
+
"""Render compact layout for small terminals."""
|
|
318
|
+
parts = []
|
|
319
|
+
|
|
320
|
+
# Header with status
|
|
321
|
+
parts.append(self._render_compact_header())
|
|
322
|
+
parts.append("")
|
|
323
|
+
|
|
324
|
+
# Progress
|
|
325
|
+
parts.append(self._render_compact_progress())
|
|
326
|
+
parts.append("")
|
|
327
|
+
|
|
328
|
+
# Current task (one line)
|
|
329
|
+
if self.state.current_task:
|
|
330
|
+
task = self.state.current_task
|
|
331
|
+
parts.append(Text(f" Current: {task.id} {task.title[:30]}...", style="task.progress"))
|
|
332
|
+
parts.append(Text(f" Phase: {self.state.current_phase}", style="text.dim"))
|
|
333
|
+
else:
|
|
334
|
+
parts.append(Text(" Current: None", style="text.dim"))
|
|
335
|
+
|
|
336
|
+
parts.append("")
|
|
337
|
+
|
|
338
|
+
# Compact task list
|
|
339
|
+
parts.append(self._render_compact_tasks())
|
|
340
|
+
parts.append("")
|
|
341
|
+
|
|
342
|
+
# Stats line
|
|
343
|
+
stats = self.state.stats
|
|
344
|
+
elapsed = self._format_duration(stats.elapsed_seconds)
|
|
345
|
+
parts.append(Text(
|
|
346
|
+
f" ⏱ {elapsed} {Symbols.FAILED} {stats.failures} fails {Symbols.ROLLBACK} {stats.rollbacks} rollbacks",
|
|
347
|
+
style="text.dim"
|
|
348
|
+
))
|
|
349
|
+
|
|
350
|
+
return Panel(
|
|
351
|
+
Group(*parts),
|
|
352
|
+
title=f"[title]UP LOOP[/]",
|
|
353
|
+
subtitle="[text.dim]Ctrl+C to pause[/]",
|
|
354
|
+
border_style=Style(color=CyberTheme.BORDER),
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
def _render_header(self) -> Panel:
|
|
358
|
+
"""Render the header with status badge."""
|
|
359
|
+
status = self.state.status
|
|
360
|
+
spinner = Symbols.SPINNER[self._spinner_frame] if status == LoopStatus.RUNNING else ""
|
|
361
|
+
|
|
362
|
+
status_styles = {
|
|
363
|
+
LoopStatus.RUNNING: ("status.running", f"{spinner} RUNNING"),
|
|
364
|
+
LoopStatus.VERIFYING: ("status.verifying", "◉ VERIFYING"),
|
|
365
|
+
LoopStatus.PAUSED: ("status.paused", "◉ PAUSED"),
|
|
366
|
+
LoopStatus.FAILED: ("status.failed", "◉ FAILED"),
|
|
367
|
+
LoopStatus.COMPLETE: ("status.complete", "◉ COMPLETE"),
|
|
368
|
+
LoopStatus.IDLE: ("text.dim", "◉ IDLE"),
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
style, label = status_styles.get(status, ("text.dim", "◉ UNKNOWN"))
|
|
372
|
+
|
|
373
|
+
title_text = Text()
|
|
374
|
+
title_text.append(" UP ", style="title")
|
|
375
|
+
title_text.append("PRODUCT LOOP", style="secondary")
|
|
376
|
+
title_text.append(" " * 30)
|
|
377
|
+
title_text.append(label, style=style)
|
|
378
|
+
title_text.append(" ")
|
|
379
|
+
|
|
380
|
+
return Panel(
|
|
381
|
+
Align.center(title_text),
|
|
382
|
+
border_style=Style(color=CyberTheme.BORDER_DIM),
|
|
383
|
+
padding=0,
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
def _render_compact_header(self) -> Text:
|
|
387
|
+
"""Render compact header."""
|
|
388
|
+
status = self.state.status
|
|
389
|
+
spinner = Symbols.SPINNER[self._spinner_frame] if status == LoopStatus.RUNNING else "●"
|
|
390
|
+
|
|
391
|
+
status_colors = {
|
|
392
|
+
LoopStatus.RUNNING: CyberTheme.STATUS_RUNNING,
|
|
393
|
+
LoopStatus.VERIFYING: CyberTheme.STATUS_VERIFYING,
|
|
394
|
+
LoopStatus.PAUSED: CyberTheme.STATUS_PAUSED,
|
|
395
|
+
LoopStatus.FAILED: CyberTheme.STATUS_FAILED,
|
|
396
|
+
LoopStatus.COMPLETE: CyberTheme.STATUS_COMPLETE,
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
color = status_colors.get(status, CyberTheme.TEXT_DIM)
|
|
400
|
+
|
|
401
|
+
header = Text()
|
|
402
|
+
header.append(f" {spinner} ", style=Style(color=color))
|
|
403
|
+
header.append("UP LOOP", style="title")
|
|
404
|
+
header.append(f" │ {status.value.upper()}", style=Style(color=color))
|
|
405
|
+
|
|
406
|
+
return header
|
|
407
|
+
|
|
408
|
+
def _render_progress_bar(self) -> Panel:
|
|
409
|
+
"""Render the animated progress bar."""
|
|
410
|
+
stats = self.state.stats
|
|
411
|
+
total = stats.total or 1
|
|
412
|
+
completed = stats.completed
|
|
413
|
+
percentage = (completed / total) * 100
|
|
414
|
+
|
|
415
|
+
# Create progress bar
|
|
416
|
+
width = 40
|
|
417
|
+
filled = int(width * completed / total)
|
|
418
|
+
|
|
419
|
+
bar = Text()
|
|
420
|
+
bar.append(" Progress ", style="text.dim")
|
|
421
|
+
bar.append(Symbols.BAR_FULL * filled, style="progress.complete")
|
|
422
|
+
bar.append(Symbols.BAR_EMPTY * (width - filled), style="progress.remaining")
|
|
423
|
+
bar.append(f" {percentage:5.1f}%", style="primary")
|
|
424
|
+
bar.append(f" ({completed}/{total} tasks)", style="text.dim")
|
|
425
|
+
|
|
426
|
+
return Panel(
|
|
427
|
+
Align.center(bar),
|
|
428
|
+
border_style=Style(color=CyberTheme.BORDER_DIM),
|
|
429
|
+
padding=0,
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
def _render_compact_progress(self) -> Text:
|
|
433
|
+
"""Render compact progress bar."""
|
|
434
|
+
stats = self.state.stats
|
|
435
|
+
total = stats.total or 1
|
|
436
|
+
completed = stats.completed
|
|
437
|
+
percentage = (completed / total) * 100
|
|
438
|
+
|
|
439
|
+
width = 25
|
|
440
|
+
filled = int(width * completed / total)
|
|
441
|
+
|
|
442
|
+
bar = Text()
|
|
443
|
+
bar.append(" ", style="text")
|
|
444
|
+
bar.append(Symbols.BAR_FULL * filled, style="progress.complete")
|
|
445
|
+
bar.append(Symbols.BAR_EMPTY * (width - filled), style="progress.remaining")
|
|
446
|
+
bar.append(f" {percentage:5.1f}% ({completed}/{total})", style="primary")
|
|
447
|
+
|
|
448
|
+
return bar
|
|
449
|
+
|
|
450
|
+
def _render_current_task(self) -> Panel:
|
|
451
|
+
"""Render current task panel."""
|
|
452
|
+
task = self.state.current_task
|
|
453
|
+
|
|
454
|
+
if not task:
|
|
455
|
+
content = Text("\n No task in progress\n", style="text.dim")
|
|
456
|
+
return Panel(
|
|
457
|
+
content,
|
|
458
|
+
title="[title]Current Task[/]",
|
|
459
|
+
border_style=Style(color=CyberTheme.BORDER_DIM),
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
lines = []
|
|
463
|
+
lines.append("")
|
|
464
|
+
lines.append(Text(f" {task.id}: {task.title}", style="task.progress"))
|
|
465
|
+
lines.append("")
|
|
466
|
+
lines.append(Text(f" Priority: {task.priority} │ Effort: {task.effort} │ Phase: {task.phase}", style="text.dim"))
|
|
467
|
+
lines.append("")
|
|
468
|
+
|
|
469
|
+
# Current phase indicator
|
|
470
|
+
phase = self.state.current_phase
|
|
471
|
+
phase_icon = {
|
|
472
|
+
"INIT": "○",
|
|
473
|
+
"CHECKPOINT": "◐",
|
|
474
|
+
"EXECUTE": Symbols.SPINNER[self._spinner_frame],
|
|
475
|
+
"VERIFY": "◑",
|
|
476
|
+
"COMMIT": "◒",
|
|
477
|
+
}.get(phase, "○")
|
|
478
|
+
|
|
479
|
+
lines.append(Text(f" Status: {phase_icon} {phase}", style="status.running"))
|
|
480
|
+
lines.append("")
|
|
481
|
+
|
|
482
|
+
if task.description:
|
|
483
|
+
desc = task.description[:60] + "..." if len(task.description) > 60 else task.description
|
|
484
|
+
lines.append(Text(f" {desc}", style="text.dim"))
|
|
485
|
+
|
|
486
|
+
return Panel(
|
|
487
|
+
Group(*lines),
|
|
488
|
+
title="[title]Current Task[/]",
|
|
489
|
+
border_style=Style(color=CyberTheme.PRIMARY),
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
def _render_task_queue(self) -> Panel:
|
|
493
|
+
"""Render task queue panel."""
|
|
494
|
+
table = Table(
|
|
495
|
+
show_header=False,
|
|
496
|
+
box=None,
|
|
497
|
+
padding=(0, 1),
|
|
498
|
+
expand=True,
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
table.add_column("Status", width=3)
|
|
502
|
+
table.add_column("ID", width=8)
|
|
503
|
+
table.add_column("Title", ratio=1)
|
|
504
|
+
table.add_column("State", width=12, justify="right")
|
|
505
|
+
|
|
506
|
+
status_symbols = {
|
|
507
|
+
TaskStatus.COMPLETE: (Symbols.COMPLETE, "task.complete"),
|
|
508
|
+
TaskStatus.IN_PROGRESS: (Symbols.IN_PROGRESS, "task.progress"),
|
|
509
|
+
TaskStatus.PENDING: (Symbols.PENDING, "task.pending"),
|
|
510
|
+
TaskStatus.FAILED: (Symbols.FAILED, "task.failed"),
|
|
511
|
+
TaskStatus.SKIPPED: (Symbols.SKIPPED, "task.skipped"),
|
|
512
|
+
TaskStatus.ROLLED_BACK: (Symbols.ROLLBACK, "task.skipped"),
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
for task in self.state.tasks[:8]: # Show max 8 tasks
|
|
516
|
+
symbol, style = status_symbols.get(task.status, (Symbols.PENDING, "task.pending"))
|
|
517
|
+
|
|
518
|
+
title = task.title[:30] + "..." if len(task.title) > 30 else task.title
|
|
519
|
+
state_label = task.status.value.replace("_", " ")
|
|
520
|
+
|
|
521
|
+
table.add_row(
|
|
522
|
+
Text(symbol, style=style),
|
|
523
|
+
Text(task.id, style=style),
|
|
524
|
+
Text(title, style=style if task.status == TaskStatus.IN_PROGRESS else "text"),
|
|
525
|
+
Text(state_label, style=style),
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
# Show count if more tasks
|
|
529
|
+
remaining = len(self.state.tasks) - 8
|
|
530
|
+
if remaining > 0:
|
|
531
|
+
table.add_row(
|
|
532
|
+
Text("", style="text.dim"),
|
|
533
|
+
Text("", style="text.dim"),
|
|
534
|
+
Text(f"... +{remaining} more tasks", style="text.dim"),
|
|
535
|
+
Text("", style="text.dim"),
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
return Panel(
|
|
539
|
+
table,
|
|
540
|
+
title="[title]Task Queue[/]",
|
|
541
|
+
border_style=Style(color=CyberTheme.BORDER_DIM),
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
def _render_compact_tasks(self) -> Text:
|
|
545
|
+
"""Render compact task indicators."""
|
|
546
|
+
status_symbols = {
|
|
547
|
+
TaskStatus.COMPLETE: (Symbols.COMPLETE, CyberTheme.TASK_COMPLETE),
|
|
548
|
+
TaskStatus.IN_PROGRESS: (Symbols.IN_PROGRESS, CyberTheme.TASK_IN_PROGRESS),
|
|
549
|
+
TaskStatus.PENDING: (Symbols.PENDING, CyberTheme.TASK_PENDING),
|
|
550
|
+
TaskStatus.FAILED: (Symbols.FAILED, CyberTheme.TASK_FAILED),
|
|
551
|
+
TaskStatus.SKIPPED: (Symbols.SKIPPED, CyberTheme.TASK_SKIPPED),
|
|
552
|
+
TaskStatus.ROLLED_BACK: (Symbols.ROLLBACK, CyberTheme.TASK_SKIPPED),
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
text = Text(" ")
|
|
556
|
+
for task in self.state.tasks[:12]:
|
|
557
|
+
symbol, color = status_symbols.get(task.status, (Symbols.PENDING, CyberTheme.TASK_PENDING))
|
|
558
|
+
text.append(f"{symbol} {task.id} ", style=Style(color=color))
|
|
559
|
+
|
|
560
|
+
return text
|
|
561
|
+
|
|
562
|
+
def _render_log(self) -> Panel:
|
|
563
|
+
"""Render activity log panel."""
|
|
564
|
+
lines = []
|
|
565
|
+
|
|
566
|
+
for timestamp, message, style in self.state.log_entries:
|
|
567
|
+
line = Text()
|
|
568
|
+
line.append(f" {timestamp} ", style="text.dim")
|
|
569
|
+
line.append(message[:60], style=style or "text")
|
|
570
|
+
lines.append(line)
|
|
571
|
+
|
|
572
|
+
# Pad with empty lines if needed
|
|
573
|
+
while len(lines) < 6:
|
|
574
|
+
lines.append(Text(""))
|
|
575
|
+
|
|
576
|
+
return Panel(
|
|
577
|
+
Group(*lines),
|
|
578
|
+
title="[title]Activity Log[/]",
|
|
579
|
+
border_style=Style(color=CyberTheme.BORDER_DIM),
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
def _render_stats(self) -> Panel:
|
|
583
|
+
"""Render stats panel."""
|
|
584
|
+
stats = self.state.stats
|
|
585
|
+
elapsed = self._format_duration(stats.elapsed_seconds)
|
|
586
|
+
|
|
587
|
+
text = Text()
|
|
588
|
+
text.append(" ⏱ ", style="text.dim")
|
|
589
|
+
text.append(f"Elapsed: {elapsed}", style="primary")
|
|
590
|
+
text.append(" │ ", style="text.dim")
|
|
591
|
+
text.append(f"{Symbols.FAILED} ", style="task.failed")
|
|
592
|
+
text.append(f"Failures: {stats.failures}", style="text")
|
|
593
|
+
text.append(" │ ", style="text.dim")
|
|
594
|
+
text.append(f"{Symbols.ROLLBACK} ", style="task.skipped")
|
|
595
|
+
text.append(f"Rollbacks: {stats.rollbacks}", style="text")
|
|
596
|
+
text.append(" │ ", style="text.dim")
|
|
597
|
+
text.append(f"Iteration: {stats.current_iteration}", style="secondary")
|
|
598
|
+
|
|
599
|
+
return Panel(
|
|
600
|
+
Align.center(text),
|
|
601
|
+
border_style=Style(color=CyberTheme.BORDER_DIM),
|
|
602
|
+
padding=0,
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
def _render_footer(self) -> Text:
|
|
606
|
+
"""Render footer."""
|
|
607
|
+
return Text(" Press Ctrl+C to pause │ q to quit", style="text.dim", justify="center")
|
|
608
|
+
|
|
609
|
+
def _format_duration(self, seconds: float) -> str:
|
|
610
|
+
"""Format duration as human readable string."""
|
|
611
|
+
if seconds < 60:
|
|
612
|
+
return f"{int(seconds)}s"
|
|
613
|
+
elif seconds < 3600:
|
|
614
|
+
minutes = int(seconds // 60)
|
|
615
|
+
secs = int(seconds % 60)
|
|
616
|
+
return f"{minutes}m {secs}s"
|
|
617
|
+
else:
|
|
618
|
+
hours = int(seconds // 3600)
|
|
619
|
+
minutes = int((seconds % 3600) // 60)
|
|
620
|
+
return f"{hours}h {minutes}m"
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
# ─── Context Manager Support ─────────────────────────────────────────────
|
|
624
|
+
|
|
625
|
+
class ProductLoopDisplayContext:
|
|
626
|
+
"""Context manager for the display."""
|
|
627
|
+
|
|
628
|
+
def __init__(self, display: ProductLoopDisplay):
|
|
629
|
+
self.display = display
|
|
630
|
+
|
|
631
|
+
def __enter__(self) -> ProductLoopDisplay:
|
|
632
|
+
self.display.start()
|
|
633
|
+
return self.display
|
|
634
|
+
|
|
635
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
636
|
+
self.display.stop()
|
|
637
|
+
return False
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def create_display(console: Optional[Console] = None) -> ProductLoopDisplayContext:
|
|
641
|
+
"""Create a display context manager.
|
|
642
|
+
|
|
643
|
+
Usage:
|
|
644
|
+
with create_display() as display:
|
|
645
|
+
display.set_tasks(tasks)
|
|
646
|
+
display.log("Starting...")
|
|
647
|
+
# ... do work ...
|
|
648
|
+
"""
|
|
649
|
+
display = ProductLoopDisplay(console)
|
|
650
|
+
return ProductLoopDisplayContext(display)
|