Skip to content

task_detail

task_detail

Task detail rendering widget and helper.

TaskDetails(**kwargs)

Bases: Static

Panel showing details for the currently selected task.

Initialize the task details panel.

Source code in src/terok/tui/widgets/task_detail.py
def __init__(self, **kwargs: Any) -> None:
    """Initialize the task details panel."""
    super().__init__(**kwargs)
    self.current_project_name: str | None = None
    self._current_task: TaskMeta | None = None
    self._current_empty_message: str | None = None
    self._current_image_old: bool | None = None
    self._last_render_width = -1

current_project_name = None instance-attribute

compose()

Yield the inner Static widget used for rendered task content.

Source code in src/terok/tui/widgets/task_detail.py
def compose(self) -> ComposeResult:
    """Yield the inner Static widget used for rendered task content."""
    yield Static(id="task-details-content")

set_task(task, empty_message=None, image_old=None)

Render and display details for the given task (or clear if None).

Source code in src/terok/tui/widgets/task_detail.py
def set_task(
    self,
    task: TaskMeta | None,
    empty_message: str | None = None,
    image_old: bool | None = None,
) -> None:
    """Render and display details for the given task (or clear if None)."""
    if task is None:
        self.current_project_name = None
    else:
        # current_project_name lives on TerokTUI, not the base App[Any] type.
        self.current_project_name = (
            getattr(self.app, "current_project_name", None) if self.app else None
        )

    self._current_task = task
    self._current_empty_message = empty_message
    self._current_image_old = image_old
    self._last_render_width = -1  # task changed — force re-render
    self._redraw_content()

on_resize(event)

Re-render only when the panel's content width changes.

Source code in src/terok/tui/widgets/task_detail.py
def on_resize(self, event: events.Resize) -> None:
    """Re-render only when the panel's content width changes."""
    width = self.query_one("#task-details-content", Static).scrollable_content_region.size.width
    if width == self._last_render_width:
        return
    self._redraw_content()

Open href via the app's URL driver — no xterm.js confirm dialog.

Source code in src/terok/tui/widgets/task_detail.py
def action_open_link(self, href: str) -> None:
    """Open *href* via the app's URL driver — no xterm.js confirm dialog."""
    self.app.open_url(href)

render_task_details(task, project_name=None, image_old=None, empty_message=None, css_variables=None, show_workspace=True, shield_hooks_ok=None, is_web=False, width=0)

Render task details as a Rich Text object.

is_web is the caller's app.is_web — when False (the TUI is running in a real terminal) we emit OSC 8 hyperlinks so wrapped URL lines stay one clickable link. In web mode we skip OSC 8 because xterm.js's link handler shows a "could be dangerous" confirmation dialog for every click; the @click meta below keeps that path working without the dialog.

Source code in src/terok/tui/widgets/task_detail.py
def render_task_details(
    task: TaskMeta | None,
    project_name: str | None = None,
    image_old: bool | None = None,
    empty_message: str | None = None,
    css_variables: dict[str, str] | None = None,
    show_workspace: bool = True,
    shield_hooks_ok: bool | None = None,
    is_web: bool = False,
    width: int = 0,
) -> Text:
    """Render task details as a Rich Text object.

    *is_web* is the caller's ``app.is_web`` — when False (the TUI is
    running in a real terminal) we emit OSC 8 hyperlinks so wrapped
    URL lines stay one clickable link.  In web mode we skip OSC 8
    because xterm.js's link handler shows a "could be dangerous"
    confirmation dialog for every click; the ``@click`` meta below
    keeps that path working without the dialog.
    """
    if task is None:
        return Text(empty_message or "")

    variables = css_variables or {}
    accent_style = Style(color=variables.get("primary", "cyan"))
    warning_style = Style(color=variables.get("warning", "yellow"))

    m_info = mode_info(task.mode)
    m_emoji = render_emoji(m_info)
    # Empty label for an unset mode lets the cricket emoji speak for
    # itself — modes are picked via the Start CLI/Toad/Unattended menu,
    # not from this panel.
    mode_display = m_info.label

    s_info = STATUS_DISPLAY.get(task.status, STATUS_DISPLAY["created"])

    lines = [
        Text(f"Task ID:   {task.task_id}"),
    ]
    lines.append(Text(wrap_with_hanging_indent("Name:      ", task.name, "", width)))
    type_line = f"Type:      {m_emoji} {mode_display}".rstrip()
    lines += [
        Text(f"Status:    {render_emoji(s_info)} {s_info.label}"),
        Text(type_line),
    ]
    if task.work_status:
        work_text = task.work_status
        if task.work_message:
            work_text += f' \u2014 "{task.work_message}"'
        lines.append(Text(f"Work:      {work_text}"))
    if show_workspace:
        lines.append(Text(f"Workspace: {task.workspace}"))
    if task.status == "running" and image_old:
        lines.append(Text.assemble("Image:     ", Text("old", style=warning_style)))
    if task.web_port:
        base_url = f"http://{url_host(get_config().public_host)}:{task.web_port}/"
        # Token in the query is what Caddy trades for its auth cookie.
        # Render the full URL verbatim so users without OSC-8 support
        # (and copy-paste) still get the tokenised URL.
        link_url = f"{base_url}?token={task.web_token}" if task.web_token else base_url
        # ``@click`` meta is the in-Textual click dispatcher — it routes
        # through ``open_url`` and avoids xterm.js's OSC 8 confirm
        # dialog when the TUI is served via textual-serve.  Outside web
        # mode (real terminal underneath) we *also* attach Rich's
        # ``link=`` so the host terminal sees the OSC 8 hyperlink: with
        # the shared ``id=`` Rich emits, wrapped URL segments stitch
        # back into one clickable link instead of breaking at the panel
        # edge.
        click_style = accent_style + Style.from_meta({"@click": f"open_link({link_url!r})"})
        if not is_web:
            click_style = click_style + Style(link=link_url)
        lines.append(Text.assemble("Web URL:   ", Text(link_url, style=click_style)))
    if task.mode == "cli" and project_name:
        lines.append(
            Text.assemble(
                "Log in:    ",
                Text(f"terok login {project_name} {task.task_id}", style=accent_style),
            )
        )
    if task.unrestricted is not None:
        perm_label = "unrestricted" if task.unrestricted else "restricted"
        lines.append(Text(f"Perms:     {perm_label}"))
    if task.shield_state:
        primary_color = variables.get("primary", "blue")
        success_color = variables.get("success", "green")
        error_color = variables.get("error", "red")
        warning_color = variables.get("warning", "yellow")

        container_live = task.container_state == "running"
        # OFFLINE + container not running + hooks healthy → "ready" (no alarm)
        hooks_ok = shield_hooks_ok is not False  # True or unknown (None)
        if task.shield_state == "OFFLINE" and not container_live and hooks_ok:
            lines.append(
                Text.assemble(
                    "Shield:    ",
                    Text("ready", style=Style(dim=True)),
                )
            )
        else:
            # UP (enforcing): primary/blue — the normal, trusted state.
            # DOWN (relaxed): success/green — operator-chosen, safe by
            # virtue of private-range protection still in place.
            # DISENGAGED (full bypass): warning/yellow — operator
            # consciously stepped further out of the safe zone.
            # QUARANTINE/OFFLINE/DISABLED/ERROR: error/red — emergency
            # mode, not-present, or invalid.
            shield_colors = {
                "UP": primary_color,
                "DOWN": success_color,
                "DISENGAGED": warning_color,
                "QUARANTINE": error_color,
                "OFFLINE": error_color,
                "DISABLED": error_color,
            }
            shield_color = shield_colors.get(task.shield_state, error_color)
            lines.append(
                Text.assemble(
                    "Shield:    ",
                    Text(task.shield_state.lower(), style=Style(color=shield_color)),
                )
            )
            if task.shield_state in {"DISABLED", "OFFLINE"}:
                lines.append(
                    Text(
                        f"           {get_config().shield_security_hint}",
                        style=Style(color=error_color),
                    )
                )
    if task.mode == "run":
        if task.exit_code is not None:
            lines.append(Text(f"Exit code: {task.exit_code}"))
        if project_name:
            lines.append(
                Text.assemble(
                    "Logs:      ",
                    Text(
                        f"terok task logs {project_name} {task.task_id} -f",
                        style=accent_style,
                    ),
                )
            )

    return Text("\n").join(lines)