@anastops/cli 1.0.0 → 1.2.0
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.
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +8 -6
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts +17 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +315 -17
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/ranger.d.ts +7 -2
- package/dist/commands/ranger.d.ts.map +1 -1
- package/dist/commands/ranger.js +99 -21
- package/dist/commands/ranger.js.map +1 -1
- package/dist/commands/uninstall.d.ts +16 -0
- package/dist/commands/uninstall.d.ts.map +1 -0
- package/dist/commands/uninstall.js +206 -0
- package/dist/commands/uninstall.js.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -1
- package/package.json +8 -6
- package/ranger-tui/pyproject.toml +51 -0
- package/ranger-tui/ranger_tui/__init__.py +5 -0
- package/ranger-tui/ranger_tui/__main__.py +16 -0
- package/ranger-tui/ranger_tui/__pycache__/__init__.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/__pycache__/__main__.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/__pycache__/accessibility.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/__pycache__/app.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/__pycache__/config.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/__pycache__/theme.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/accessibility.py +499 -0
- package/ranger-tui/ranger_tui/actions/__init__.py +13 -0
- package/ranger-tui/ranger_tui/actions/agent_actions.py +74 -0
- package/ranger-tui/ranger_tui/actions/session_actions.py +110 -0
- package/ranger-tui/ranger_tui/actions/task_actions.py +107 -0
- package/ranger-tui/ranger_tui/app.py +93 -0
- package/ranger-tui/ranger_tui/assets/ranger_head.png +0 -0
- package/ranger-tui/ranger_tui/config.py +100 -0
- package/ranger-tui/ranger_tui/data/__init__.py +16 -0
- package/ranger-tui/ranger_tui/data/__pycache__/__init__.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/data/__pycache__/client.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/data/__pycache__/models.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/data/client.py +858 -0
- package/ranger-tui/ranger_tui/data/models.py +151 -0
- package/ranger-tui/ranger_tui/screens/__init__.py +16 -0
- package/ranger-tui/ranger_tui/screens/__pycache__/__init__.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/screens/__pycache__/dashboard.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/screens/__pycache__/modals.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/screens/__pycache__/session.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/screens/__pycache__/task.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/screens/command_palette.py +357 -0
- package/ranger-tui/ranger_tui/screens/dashboard.py +232 -0
- package/ranger-tui/ranger_tui/screens/help.py +103 -0
- package/ranger-tui/ranger_tui/screens/modals.py +95 -0
- package/ranger-tui/ranger_tui/screens/session.py +289 -0
- package/ranger-tui/ranger_tui/screens/task.py +187 -0
- package/ranger-tui/ranger_tui/styles/ranger.tcss +254 -0
- package/ranger-tui/ranger_tui/theme.py +93 -0
- package/ranger-tui/ranger_tui/widgets/__init__.py +23 -0
- package/ranger-tui/ranger_tui/widgets/__pycache__/__init__.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/widgets/__pycache__/accessible.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/widgets/__pycache__/logo.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/widgets/__pycache__/logo_assets.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/widgets/__pycache__/ranger_image.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/widgets/__pycache__/sidebar.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/widgets/__pycache__/topbar.cpython-314.pyc +0 -0
- package/ranger-tui/ranger_tui/widgets/accessible.py +176 -0
- package/ranger-tui/ranger_tui/widgets/agents_table.py +151 -0
- package/ranger-tui/ranger_tui/widgets/header.py +141 -0
- package/ranger-tui/ranger_tui/widgets/logo.py +258 -0
- package/ranger-tui/ranger_tui/widgets/logo_assets.py +62 -0
- package/ranger-tui/ranger_tui/widgets/metrics_panel.py +121 -0
- package/ranger-tui/ranger_tui/widgets/ranger_image.py +91 -0
- package/ranger-tui/ranger_tui/widgets/sessions_table.py +191 -0
- package/ranger-tui/ranger_tui/widgets/sidebar.py +91 -0
- package/ranger-tui/ranger_tui/widgets/tasks_table.py +189 -0
- package/ranger-tui/ranger_tui/widgets/topbar.py +168 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Session detail screen - View tasks and agents in a session.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from textual.app import ComposeResult
|
|
7
|
+
from textual.screen import Screen
|
|
8
|
+
from textual.binding import Binding
|
|
9
|
+
from textual.containers import Horizontal
|
|
10
|
+
from textual.widgets import DataTable, Footer, TabbedContent, TabPane, RichLog
|
|
11
|
+
|
|
12
|
+
from ranger_tui.data import SessionReport
|
|
13
|
+
from ranger_tui.widgets import TopBar, Sidebar
|
|
14
|
+
from ranger_tui.theme import format_status, format_model, STATUS_SYMBOLS
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SessionScreen(Screen):
|
|
18
|
+
"""Session detail screen with tasks and agents."""
|
|
19
|
+
|
|
20
|
+
BINDINGS = [
|
|
21
|
+
Binding("enter", "open_task", "Open", show=True),
|
|
22
|
+
Binding("t", "new_task", "New Task", show=True),
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
session_id: str = ""
|
|
26
|
+
report: SessionReport | None = None
|
|
27
|
+
|
|
28
|
+
def __init__(self, session_id: str):
|
|
29
|
+
super().__init__()
|
|
30
|
+
self.session_id = session_id
|
|
31
|
+
|
|
32
|
+
def compose(self) -> ComposeResult:
|
|
33
|
+
"""Compose the session detail layout."""
|
|
34
|
+
yield TopBar(title="Loading...")
|
|
35
|
+
with Horizontal(id="main"):
|
|
36
|
+
with TabbedContent(id="tabs"):
|
|
37
|
+
with TabPane("Tasks", id="tasks-tab"):
|
|
38
|
+
yield DataTable(id="tasks-table", cursor_type="row")
|
|
39
|
+
with TabPane("Agents", id="agents-tab"):
|
|
40
|
+
yield DataTable(id="agents-table", cursor_type="row")
|
|
41
|
+
with TabPane("Logs", id="logs-tab"):
|
|
42
|
+
yield RichLog(id="activity-log", highlight=True, markup=True)
|
|
43
|
+
yield Sidebar(title="Details")
|
|
44
|
+
yield Footer()
|
|
45
|
+
|
|
46
|
+
async def on_mount(self) -> None:
|
|
47
|
+
"""Called when screen is mounted."""
|
|
48
|
+
# Setup tables after layout is complete
|
|
49
|
+
self.call_later(self._setup_tables)
|
|
50
|
+
|
|
51
|
+
# Auto-refresh
|
|
52
|
+
self.set_interval(2.0, self._load_session)
|
|
53
|
+
|
|
54
|
+
# Focus on tasks table after a brief delay (after data loads)
|
|
55
|
+
self.call_later(self._focus_tasks_table)
|
|
56
|
+
|
|
57
|
+
def _focus_tasks_table(self) -> None:
|
|
58
|
+
"""Focus on the tasks table."""
|
|
59
|
+
try:
|
|
60
|
+
tasks_table = self.query_one("#tasks-table", DataTable)
|
|
61
|
+
tasks_table.focus()
|
|
62
|
+
# Set cursor to first row if there are rows
|
|
63
|
+
if tasks_table.row_count > 0:
|
|
64
|
+
tasks_table.cursor_coordinate = (0, 0)
|
|
65
|
+
except Exception:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
async def _setup_tables(self) -> None:
|
|
69
|
+
"""Setup tables with proportional column widths after layout is complete."""
|
|
70
|
+
# Calculate available width: terminal width - sidebar(28) - padding/borders(~6)
|
|
71
|
+
try:
|
|
72
|
+
terminal_width = self.app.size.width
|
|
73
|
+
table_width = terminal_width - 28 - 6
|
|
74
|
+
except Exception:
|
|
75
|
+
table_width = 100
|
|
76
|
+
|
|
77
|
+
table_width = max(table_width, 60)
|
|
78
|
+
usable_width = table_width - 5 # Space for column separators
|
|
79
|
+
|
|
80
|
+
# Setup tasks table with proportional widths
|
|
81
|
+
# Status: 10%, Type: 8%, Description: 28%, Provider: 12%, Model: 16%, Cost: 10%, Tokens: 14%
|
|
82
|
+
tasks_table = self.query_one("#tasks-table", DataTable)
|
|
83
|
+
if not tasks_table.columns:
|
|
84
|
+
tasks_table.zebra_stripes = True
|
|
85
|
+
status_w = max(8, int(usable_width * 0.10))
|
|
86
|
+
type_w = max(6, int(usable_width * 0.08))
|
|
87
|
+
provider_w = max(8, int(usable_width * 0.12))
|
|
88
|
+
model_w = max(10, int(usable_width * 0.16))
|
|
89
|
+
cost_w = max(6, int(usable_width * 0.10))
|
|
90
|
+
tokens_w = max(8, int(usable_width * 0.14))
|
|
91
|
+
desc_w = usable_width - status_w - type_w - provider_w - model_w - cost_w - tokens_w
|
|
92
|
+
desc_w = max(15, desc_w)
|
|
93
|
+
|
|
94
|
+
tasks_table.add_column("Status", key="status", width=status_w)
|
|
95
|
+
tasks_table.add_column("Type", key="type", width=type_w)
|
|
96
|
+
tasks_table.add_column("Description", key="description", width=desc_w)
|
|
97
|
+
tasks_table.add_column("Provider", key="provider", width=provider_w)
|
|
98
|
+
tasks_table.add_column("Model", key="model", width=model_w)
|
|
99
|
+
tasks_table.add_column("Cost", key="cost", width=cost_w)
|
|
100
|
+
tasks_table.add_column("Tokens", key="tokens", width=tokens_w)
|
|
101
|
+
|
|
102
|
+
# Setup agents table with proportional widths
|
|
103
|
+
# Status: 12%, Name: 30%, Role: 28%, Provider: 15%, Tasks: 15%
|
|
104
|
+
agents_table = self.query_one("#agents-table", DataTable)
|
|
105
|
+
if not agents_table.columns:
|
|
106
|
+
agents_table.zebra_stripes = True
|
|
107
|
+
status_w = max(8, int(usable_width * 0.12))
|
|
108
|
+
name_w = max(12, int(usable_width * 0.30))
|
|
109
|
+
role_w = max(10, int(usable_width * 0.28))
|
|
110
|
+
provider_w = max(8, int(usable_width * 0.15))
|
|
111
|
+
tasks_w = max(6, int(usable_width * 0.15))
|
|
112
|
+
|
|
113
|
+
agents_table.add_column("Status", key="status", width=status_w)
|
|
114
|
+
agents_table.add_column("Name", key="name", width=name_w)
|
|
115
|
+
agents_table.add_column("Role", key="role", width=role_w)
|
|
116
|
+
agents_table.add_column("Provider", key="provider", width=provider_w)
|
|
117
|
+
agents_table.add_column("Tasks", key="tasks", width=tasks_w)
|
|
118
|
+
|
|
119
|
+
# Load data
|
|
120
|
+
await self._load_session()
|
|
121
|
+
|
|
122
|
+
async def _load_session(self) -> None:
|
|
123
|
+
"""Load session data."""
|
|
124
|
+
self.report = await self.app.client.get_session_report(self.session_id)
|
|
125
|
+
|
|
126
|
+
if self.report:
|
|
127
|
+
self._update_header()
|
|
128
|
+
self._update_tasks()
|
|
129
|
+
self._update_agents()
|
|
130
|
+
self._update_details()
|
|
131
|
+
self._update_logs()
|
|
132
|
+
|
|
133
|
+
def _update_header(self) -> None:
|
|
134
|
+
"""Update session header in topbar."""
|
|
135
|
+
if not self.report:
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
session = self.report.session
|
|
139
|
+
topbar = self.query_one(TopBar)
|
|
140
|
+
|
|
141
|
+
symbol = STATUS_SYMBOLS.get(session.status, "?")
|
|
142
|
+
topbar.title = f"{symbol} {session.objective}"
|
|
143
|
+
topbar.set_connection_status(self.app.mongo_connected, self.app.redis_connected)
|
|
144
|
+
|
|
145
|
+
def _update_tasks(self) -> None:
|
|
146
|
+
"""Update tasks table."""
|
|
147
|
+
if not self.report:
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
table = self.query_one("#tasks-table", DataTable)
|
|
151
|
+
selected_row = table.cursor_row
|
|
152
|
+
|
|
153
|
+
table.clear()
|
|
154
|
+
|
|
155
|
+
for task in self.report.tasks:
|
|
156
|
+
status = format_status(task.status)
|
|
157
|
+
tokens = f"{task.token_usage.total_tokens:,}"
|
|
158
|
+
cost = self._format_cost(task.token_usage.cost)
|
|
159
|
+
model_display = format_model(task.model)
|
|
160
|
+
|
|
161
|
+
table.add_row(status, task.type, task.description, task.provider, model_display, cost, tokens, key=task.id)
|
|
162
|
+
|
|
163
|
+
if selected_row is not None and selected_row < table.row_count:
|
|
164
|
+
table.cursor_coordinate = (selected_row, 0)
|
|
165
|
+
|
|
166
|
+
def _format_cost(self, cost: float) -> str:
|
|
167
|
+
"""Format cost in dollars."""
|
|
168
|
+
if cost == 0:
|
|
169
|
+
return "[dim]$0.00[/]"
|
|
170
|
+
elif cost < 0.01:
|
|
171
|
+
return f"${cost:.4f}"
|
|
172
|
+
else:
|
|
173
|
+
return f"${cost:.2f}"
|
|
174
|
+
|
|
175
|
+
def _update_agents(self) -> None:
|
|
176
|
+
"""Update agents table."""
|
|
177
|
+
if not self.report:
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
table = self.query_one("#agents-table", DataTable)
|
|
181
|
+
selected_row = table.cursor_row
|
|
182
|
+
|
|
183
|
+
table.clear()
|
|
184
|
+
|
|
185
|
+
for agent in self.report.agents:
|
|
186
|
+
status = format_status(agent.status)
|
|
187
|
+
tasks = f"{agent.tasks_completed}/{agent.tasks_completed + agent.tasks_failed}"
|
|
188
|
+
|
|
189
|
+
table.add_row(status, agent.name, agent.role, agent.provider, tasks, key=agent.id)
|
|
190
|
+
|
|
191
|
+
if selected_row is not None and selected_row < table.row_count:
|
|
192
|
+
table.cursor_coordinate = (selected_row, 0)
|
|
193
|
+
|
|
194
|
+
def _update_details(self) -> None:
|
|
195
|
+
"""Update details sidebar."""
|
|
196
|
+
if not self.report:
|
|
197
|
+
return
|
|
198
|
+
|
|
199
|
+
session = self.report.session
|
|
200
|
+
stats = self.report.statistics
|
|
201
|
+
|
|
202
|
+
sidebar = self.query_one(Sidebar)
|
|
203
|
+
sidebar.update_content(
|
|
204
|
+
f"ID: {session.id[:12]}...\n"
|
|
205
|
+
f"Status: {session.status}\n"
|
|
206
|
+
f"Created: {session.created_at.strftime('%m-%d %H:%M')}\n"
|
|
207
|
+
f"\n"
|
|
208
|
+
f"[bold]Tasks[/]\n"
|
|
209
|
+
f" [green]✓[/] {stats.tasks.completed} completed\n"
|
|
210
|
+
f" [red]✗[/] {stats.tasks.failed} failed\n"
|
|
211
|
+
f" [yellow]●[/] {stats.tasks.running} running\n"
|
|
212
|
+
f" [dim]○[/] {stats.tasks.pending} pending\n"
|
|
213
|
+
f"\n"
|
|
214
|
+
f"Tokens: {stats.total_tokens:,}\n"
|
|
215
|
+
f"Cost: ${stats.total_cost_usd:.4f}"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def _update_logs(self) -> None:
|
|
219
|
+
"""Update activity log."""
|
|
220
|
+
if not self.report:
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
log = self.query_one("#activity-log", RichLog)
|
|
224
|
+
log.clear()
|
|
225
|
+
|
|
226
|
+
for task in sorted(self.report.tasks, key=lambda t: t.created_at):
|
|
227
|
+
time_str = task.created_at.strftime("%H:%M:%S")
|
|
228
|
+
|
|
229
|
+
# Status with color
|
|
230
|
+
status_colors = {
|
|
231
|
+
"completed": "green",
|
|
232
|
+
"failed": "red",
|
|
233
|
+
"running": "yellow",
|
|
234
|
+
"pending": "dim",
|
|
235
|
+
}
|
|
236
|
+
color = status_colors.get(task.status, "white")
|
|
237
|
+
symbol = STATUS_SYMBOLS.get(task.status, "?")
|
|
238
|
+
|
|
239
|
+
log.write(f"[dim]{time_str}[/] [{color}]{symbol}[/] [bold]{task.type}[/]: {task.description[:50]}")
|
|
240
|
+
|
|
241
|
+
if task.error:
|
|
242
|
+
log.write(f" [red]ERROR: {task.error[:60]}[/]")
|
|
243
|
+
|
|
244
|
+
if task.output and task.output.content:
|
|
245
|
+
preview = task.output.content[:80].replace('\n', ' ')
|
|
246
|
+
log.write(f" [dim]→ {preview}...[/]")
|
|
247
|
+
|
|
248
|
+
if not self.report.tasks:
|
|
249
|
+
log.write("[dim]No activity yet[/]")
|
|
250
|
+
|
|
251
|
+
async def action_open_task(self) -> None:
|
|
252
|
+
"""Open selected task."""
|
|
253
|
+
tabs = self.query_one("#tabs", TabbedContent)
|
|
254
|
+
|
|
255
|
+
if tabs.active == "tasks-tab":
|
|
256
|
+
table = self.query_one("#tasks-table", DataTable)
|
|
257
|
+
if table.cursor_row is not None and table.row_count > 0:
|
|
258
|
+
row_key = table.coordinate_to_cell_key((table.cursor_row, 0)).row_key
|
|
259
|
+
task_id = str(row_key.value) if row_key else None
|
|
260
|
+
|
|
261
|
+
if task_id:
|
|
262
|
+
from ranger_tui.screens.task import TaskScreen
|
|
263
|
+
await self.app.push_screen(TaskScreen(task_id))
|
|
264
|
+
|
|
265
|
+
async def action_new_task(self) -> None:
|
|
266
|
+
"""Create a new task."""
|
|
267
|
+
from ranger_tui.screens.modals import NewTaskModal
|
|
268
|
+
|
|
269
|
+
async def on_submit(data: dict) -> None:
|
|
270
|
+
if data:
|
|
271
|
+
task = await self.app.client.create_task(
|
|
272
|
+
session_id=self.session_id,
|
|
273
|
+
task_type=data.get("type", "other"),
|
|
274
|
+
description=data.get("description", ""),
|
|
275
|
+
prompt=data.get("prompt", ""),
|
|
276
|
+
provider=data.get("provider", "claude"),
|
|
277
|
+
)
|
|
278
|
+
self.app.notify(f"Created task: {task.id[:8]}")
|
|
279
|
+
await self._load_session()
|
|
280
|
+
|
|
281
|
+
await self.app.push_screen(NewTaskModal(), on_submit)
|
|
282
|
+
|
|
283
|
+
def on_data_table_row_selected(self, event: DataTable.RowSelected) -> None:
|
|
284
|
+
"""Handle enter on row."""
|
|
285
|
+
if event.data_table.id == "tasks-table":
|
|
286
|
+
task_id = str(event.row_key.value) if event.row_key else None
|
|
287
|
+
if task_id:
|
|
288
|
+
from ranger_tui.screens.task import TaskScreen
|
|
289
|
+
self.app.push_screen(TaskScreen(task_id))
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Task detail screen - View task details, input, output, and errors.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from textual.app import ComposeResult
|
|
6
|
+
from textual.screen import Screen
|
|
7
|
+
from textual.containers import Horizontal
|
|
8
|
+
from textual.widgets import Footer, TabbedContent, TabPane, RichLog, Markdown
|
|
9
|
+
|
|
10
|
+
from ranger_tui.data import Task
|
|
11
|
+
from ranger_tui.widgets import TopBar, Sidebar
|
|
12
|
+
from ranger_tui.theme import STATUS_SYMBOLS
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TaskScreen(Screen):
|
|
16
|
+
"""Task detail screen."""
|
|
17
|
+
|
|
18
|
+
task_id: str = ""
|
|
19
|
+
task: Task | None = None
|
|
20
|
+
|
|
21
|
+
def __init__(self, task_id: str):
|
|
22
|
+
super().__init__()
|
|
23
|
+
self.task_id = task_id
|
|
24
|
+
|
|
25
|
+
def compose(self) -> ComposeResult:
|
|
26
|
+
"""Compose the task detail layout."""
|
|
27
|
+
yield TopBar(title="Loading...")
|
|
28
|
+
with Horizontal(id="main"):
|
|
29
|
+
with TabbedContent(id="tabs"):
|
|
30
|
+
with TabPane("Output", id="output-tab"):
|
|
31
|
+
yield RichLog(id="output-log", highlight=True, markup=True)
|
|
32
|
+
with TabPane("Input", id="input-tab"):
|
|
33
|
+
yield RichLog(id="input-log", highlight=True, markup=True)
|
|
34
|
+
with TabPane("Error", id="error-tab"):
|
|
35
|
+
yield RichLog(id="error-log", highlight=True, markup=True)
|
|
36
|
+
yield Sidebar(title="Info")
|
|
37
|
+
yield Footer()
|
|
38
|
+
|
|
39
|
+
async def on_mount(self) -> None:
|
|
40
|
+
"""Called when screen is mounted."""
|
|
41
|
+
await self._load_task()
|
|
42
|
+
self.set_interval(2.0, self._load_task)
|
|
43
|
+
|
|
44
|
+
# Focus on output log
|
|
45
|
+
self.call_later(self._focus_output)
|
|
46
|
+
|
|
47
|
+
def _focus_output(self) -> None:
|
|
48
|
+
"""Focus on the output log."""
|
|
49
|
+
try:
|
|
50
|
+
output_log = self.query_one("#output-log", RichLog)
|
|
51
|
+
output_log.focus()
|
|
52
|
+
except Exception:
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
async def _load_task(self) -> None:
|
|
56
|
+
"""Load task data."""
|
|
57
|
+
self.task = await self.app.client.get_task(self.task_id)
|
|
58
|
+
|
|
59
|
+
if self.task:
|
|
60
|
+
self._update_header()
|
|
61
|
+
self._update_output()
|
|
62
|
+
self._update_input()
|
|
63
|
+
self._update_error()
|
|
64
|
+
self._update_info()
|
|
65
|
+
|
|
66
|
+
def _update_header(self) -> None:
|
|
67
|
+
"""Update task header in topbar."""
|
|
68
|
+
if not self.task:
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
topbar = self.query_one(TopBar)
|
|
72
|
+
symbol = STATUS_SYMBOLS.get(self.task.status, "?")
|
|
73
|
+
topbar.title = f"{symbol} {self.task.description}"
|
|
74
|
+
topbar.set_connection_status(self.app.mongo_connected, self.app.redis_connected)
|
|
75
|
+
|
|
76
|
+
def _update_output(self) -> None:
|
|
77
|
+
"""Update output tab."""
|
|
78
|
+
if not self.task:
|
|
79
|
+
return
|
|
80
|
+
|
|
81
|
+
log = self.query_one("#output-log", RichLog)
|
|
82
|
+
log.clear()
|
|
83
|
+
|
|
84
|
+
if self.task.output and self.task.output.content:
|
|
85
|
+
# Task completed - show final output
|
|
86
|
+
log.write(self.task.output.content)
|
|
87
|
+
|
|
88
|
+
if self.task.output.files_modified:
|
|
89
|
+
log.write("\n[bold]Files Modified[/]")
|
|
90
|
+
for f in self.task.output.files_modified:
|
|
91
|
+
log.write(f" [green]•[/] {f}")
|
|
92
|
+
|
|
93
|
+
if self.task.output.artifacts:
|
|
94
|
+
log.write("\n[bold]Artifacts[/]")
|
|
95
|
+
for a in self.task.output.artifacts:
|
|
96
|
+
log.write(f" [blue]•[/] {a}")
|
|
97
|
+
elif self.task.logs:
|
|
98
|
+
# Task running - show streaming logs
|
|
99
|
+
if self.task.status == "running":
|
|
100
|
+
log.write("[cyan]● LIVE[/] [dim]Streaming output...[/]\n")
|
|
101
|
+
log.write(self.task.logs)
|
|
102
|
+
else:
|
|
103
|
+
# No output yet
|
|
104
|
+
status_msgs = {
|
|
105
|
+
"pending": "[dim]Task is pending...[/]",
|
|
106
|
+
"queued": "[yellow]Task is queued...[/]",
|
|
107
|
+
"running": "[yellow]Task is running... (waiting for output)[/]",
|
|
108
|
+
"cancelled": "[red]Task was cancelled.[/]",
|
|
109
|
+
}
|
|
110
|
+
log.write(status_msgs.get(self.task.status, "[dim]No output.[/]"))
|
|
111
|
+
|
|
112
|
+
def _update_input(self) -> None:
|
|
113
|
+
"""Update input tab."""
|
|
114
|
+
if not self.task:
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
log = self.query_one("#input-log", RichLog)
|
|
118
|
+
log.clear()
|
|
119
|
+
|
|
120
|
+
log.write("[bold]Prompt[/]")
|
|
121
|
+
log.write(self.task.input.prompt)
|
|
122
|
+
|
|
123
|
+
if self.task.input.context_files:
|
|
124
|
+
log.write("\n[bold]Context Files[/]")
|
|
125
|
+
for f in self.task.input.context_files:
|
|
126
|
+
log.write(f" [dim]•[/] {f}")
|
|
127
|
+
|
|
128
|
+
if self.task.input.agent:
|
|
129
|
+
log.write(f"\n[bold]Agent[/]\n{self.task.input.agent}")
|
|
130
|
+
|
|
131
|
+
if self.task.input.skills:
|
|
132
|
+
log.write("\n[bold]Skills[/]")
|
|
133
|
+
for s in self.task.input.skills:
|
|
134
|
+
log.write(f" [blue]•[/] {s}")
|
|
135
|
+
|
|
136
|
+
def _update_error(self) -> None:
|
|
137
|
+
"""Update error tab."""
|
|
138
|
+
if not self.task:
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
log = self.query_one("#error-log", RichLog)
|
|
142
|
+
log.clear()
|
|
143
|
+
|
|
144
|
+
if self.task.error:
|
|
145
|
+
log.write("[bold red]Error[/]")
|
|
146
|
+
log.write(f"[red]{self.task.error}[/]")
|
|
147
|
+
else:
|
|
148
|
+
log.write("[green]No errors.[/]")
|
|
149
|
+
|
|
150
|
+
def _update_info(self) -> None:
|
|
151
|
+
"""Update info sidebar."""
|
|
152
|
+
if not self.task:
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
sidebar = self.query_one(Sidebar)
|
|
156
|
+
|
|
157
|
+
duration = ""
|
|
158
|
+
if self.task.started_at and self.task.completed_at:
|
|
159
|
+
delta = self.task.completed_at - self.task.started_at
|
|
160
|
+
duration = f"{delta.total_seconds():.1f}s"
|
|
161
|
+
elif self.task.started_at:
|
|
162
|
+
duration = "[yellow]running...[/]"
|
|
163
|
+
|
|
164
|
+
# Status color
|
|
165
|
+
status_colors = {
|
|
166
|
+
"completed": "green",
|
|
167
|
+
"failed": "red",
|
|
168
|
+
"running": "yellow",
|
|
169
|
+
"pending": "dim",
|
|
170
|
+
}
|
|
171
|
+
color = status_colors.get(self.task.status, "white")
|
|
172
|
+
|
|
173
|
+
sidebar.update_content(
|
|
174
|
+
f"ID: {self.task.id[:12]}...\n"
|
|
175
|
+
f"Type: {self.task.type}\n"
|
|
176
|
+
f"Status: [{color}]{self.task.status}[/]\n"
|
|
177
|
+
f"Provider: {self.task.provider}\n"
|
|
178
|
+
f"Model: {self.task.model}\n"
|
|
179
|
+
f"\n"
|
|
180
|
+
f"Created: {self.task.created_at.strftime('%H:%M:%S')}\n"
|
|
181
|
+
f"Duration: {duration or 'N/A'}\n"
|
|
182
|
+
f"\n"
|
|
183
|
+
f"Tokens: {self.task.token_usage.total_tokens:,}\n"
|
|
184
|
+
f"Cost: ${self.task.token_usage.cost:.6f}\n"
|
|
185
|
+
f"\n"
|
|
186
|
+
f"Retries: {self.task.retry_count}/{self.task.max_retries}"
|
|
187
|
+
)
|