Show job log inline below running tasks table
View Log now displays the log in a RichLog panel below the buttons instead of opening a modal screen. The log tails the file in real-time with a 0.3s poll interval while the job is running. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -278,7 +278,8 @@ SelectionList {
|
||||
}
|
||||
|
||||
/* Log viewer */
|
||||
#log-viewer {
|
||||
#log-viewer,
|
||||
#rt-log-viewer {
|
||||
height: 1fr;
|
||||
min-height: 8;
|
||||
border: round $accent;
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from textual.app import ComposeResult
|
||||
from textual.screen import Screen
|
||||
from textual.widgets import Header, Footer, Static, Button, DataTable
|
||||
from textual.widgets import Header, Footer, Static, Button, DataTable, RichLog
|
||||
from textual.containers import Vertical, Horizontal
|
||||
from textual.timer import Timer
|
||||
|
||||
from tui.jobs import job_manager
|
||||
from tui.widgets import ConfirmDialog, OperationLog
|
||||
from tui.widgets import ConfirmDialog
|
||||
|
||||
|
||||
class RunningTasksScreen(Screen):
|
||||
@@ -23,6 +25,7 @@ class RunningTasksScreen(Screen):
|
||||
yield Button("Kill Job", variant="error", id="btn-rt-kill")
|
||||
yield Button("Clear Finished", variant="warning", id="btn-rt-clear")
|
||||
yield Button("Back", id="btn-rt-back")
|
||||
yield RichLog(id="rt-log-viewer", wrap=True, highlight=True)
|
||||
yield Footer()
|
||||
|
||||
def on_mount(self) -> None:
|
||||
@@ -31,6 +34,9 @@ class RunningTasksScreen(Screen):
|
||||
table.cursor_type = "row"
|
||||
self._refresh_table()
|
||||
self._timer = self.set_interval(1, self._refresh_table)
|
||||
self._log_timer: Timer | None = None
|
||||
self._viewing_job_id: str | None = None
|
||||
self._log_file_pos: int = 0
|
||||
|
||||
def _format_duration(self, job) -> str:
|
||||
end = job.finished_at or datetime.now()
|
||||
@@ -47,7 +53,6 @@ class RunningTasksScreen(Screen):
|
||||
def _refresh_table(self) -> None:
|
||||
job_manager.check_reconnected()
|
||||
table = self.query_one("#rt-table", DataTable)
|
||||
# Preserve cursor position
|
||||
old_row = table.cursor_coordinate.row if table.row_count > 0 else 0
|
||||
table.clear()
|
||||
for job in job_manager.list_jobs():
|
||||
@@ -73,20 +78,67 @@ class RunningTasksScreen(Screen):
|
||||
elif event.button.id == "btn-rt-kill":
|
||||
self._kill_selected()
|
||||
elif event.button.id == "btn-rt-view":
|
||||
table = self.query_one("#rt-table", DataTable)
|
||||
if table.row_count == 0:
|
||||
self.notify("No jobs to view", severity="warning")
|
||||
return
|
||||
row_key, _ = table.coordinate_to_cell_key(table.cursor_coordinate)
|
||||
job_id = str(row_key)
|
||||
job = job_manager.get_job(job_id)
|
||||
if job:
|
||||
log_screen = OperationLog(
|
||||
title=job.label,
|
||||
show_spinner=job.status == "running",
|
||||
job_id=job.id,
|
||||
)
|
||||
self.app.push_screen(log_screen)
|
||||
self._view_selected_log()
|
||||
|
||||
def _view_selected_log(self) -> None:
|
||||
table = self.query_one("#rt-table", DataTable)
|
||||
if table.row_count == 0:
|
||||
self.notify("No jobs to view", severity="warning")
|
||||
return
|
||||
row_key, _ = table.coordinate_to_cell_key(table.cursor_coordinate)
|
||||
job_id = str(row_key)
|
||||
job = job_manager.get_job(job_id)
|
||||
if not job:
|
||||
self.notify("Job not found", severity="warning")
|
||||
return
|
||||
log_viewer = self.query_one("#rt-log-viewer", RichLog)
|
||||
log_viewer.clear()
|
||||
self._viewing_job_id = job_id
|
||||
self._log_file_pos = 0
|
||||
# Load existing content from log file
|
||||
if job._log_file and Path(job._log_file).is_file():
|
||||
try:
|
||||
content = Path(job._log_file).read_text()
|
||||
self._log_file_pos = len(content.encode())
|
||||
for line in content.splitlines():
|
||||
log_viewer.write(line)
|
||||
except OSError:
|
||||
pass
|
||||
elif job.output:
|
||||
for line in job.output:
|
||||
log_viewer.write(line)
|
||||
# Start polling for new content if job is running
|
||||
if self._log_timer:
|
||||
self._log_timer.stop()
|
||||
if job.status == "running":
|
||||
self._log_timer = self.set_interval(0.3, self._poll_log)
|
||||
|
||||
def _poll_log(self) -> None:
|
||||
if not self._viewing_job_id:
|
||||
return
|
||||
job = job_manager.get_job(self._viewing_job_id)
|
||||
if not job:
|
||||
if self._log_timer:
|
||||
self._log_timer.stop()
|
||||
return
|
||||
try:
|
||||
log_viewer = self.query_one("#rt-log-viewer", RichLog)
|
||||
except Exception:
|
||||
return
|
||||
if job._log_file and Path(job._log_file).is_file():
|
||||
try:
|
||||
with open(job._log_file, "r") as f:
|
||||
f.seek(self._log_file_pos)
|
||||
new_data = f.read()
|
||||
if new_data:
|
||||
self._log_file_pos += len(new_data.encode())
|
||||
for line in new_data.splitlines():
|
||||
log_viewer.write(line)
|
||||
except OSError:
|
||||
pass
|
||||
if job.status != "running":
|
||||
if self._log_timer:
|
||||
self._log_timer.stop()
|
||||
|
||||
def _kill_selected(self) -> None:
|
||||
table = self.query_one("#rt-table", DataTable)
|
||||
|
||||
Reference in New Issue
Block a user