feat: show scheduled prompts in session rows

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 09:04:26 -04:00
parent 7ebc3bf560
commit dfea5a5630
2 changed files with 64 additions and 1 deletions
+19 -1
View File
@@ -1,3 +1,6 @@
from datetime import datetime
from math import ceil
from rich.markup import escape from rich.markup import escape
from textual.app import ComposeResult from textual.app import ComposeResult
from textual.message import Message from textual.message import Message
@@ -28,12 +31,23 @@ _STATUS_COLORS: dict[str, str] = {
} }
def format_scheduled_prompt(due_at: datetime, now: datetime | None = None) -> str:
if now is None:
now = datetime.now()
seconds = max(0, (due_at - now).total_seconds())
if seconds < 3600:
return f"@ {max(1, ceil(seconds / 60))}m"
return f"@ {due_at.strftime('%H:%M')}"
def format_session_text( def format_session_text(
nickname: str, nickname: str,
harness_name: str, harness_name: str,
status_text: str, status_text: str,
symbol: str, symbol: str,
worktree_branch: str | None = None, worktree_branch: str | None = None,
scheduled_prompt=None,
now: datetime | None = None,
) -> str: ) -> str:
color = _STATUS_COLORS.get(status_text, "#c6d0f5") # default: Text color = _STATUS_COLORS.get(status_text, "#c6d0f5") # default: Text
name = nickname name = nickname
@@ -47,7 +61,10 @@ def format_session_text(
else: else:
name = f"{nickname}{worktree_branch}" name = f"{nickname}{worktree_branch}"
label = escape(f"{name} [{harness_name}]") label = escape(f"{name} [{harness_name}]")
return f"[{color}]{symbol}[/] {label} {status_text}" schedule = ""
if scheduled_prompt is not None:
schedule = f" {format_scheduled_prompt(scheduled_prompt.due_at, now=now)}"
return f"[{color}]{symbol}[/] {label} {status_text}{schedule}"
class SessionList(Widget, can_focus=False): class SessionList(Widget, can_focus=False):
@@ -71,6 +88,7 @@ class SessionList(Widget, can_focus=False):
status_text, status_text,
symbol, symbol,
getattr(s, "worktree_branch", None), getattr(s, "worktree_branch", None),
scheduled_prompt=getattr(info, "scheduled_prompt", None),
) )
new_items.append((f"sess-{s.id}", text, s.id)) new_items.append((f"sess-{s.id}", text, s.id))
+45
View File
@@ -1032,6 +1032,51 @@ def test_format_session_text_status_colors():
) # Overlay0 ) # Overlay0
def _make_scheduled_prompt(due_at):
prompt = MagicMock()
prompt.due_at = due_at
prompt.status = "pending"
return prompt
def test_format_session_text_shows_scheduled_prompt_countdown():
from datetime import datetime, timedelta
from hqt.tui.widgets.session_list import format_session_text
now = datetime(2026, 6, 11, 14, 0, 0)
scheduled = _make_scheduled_prompt(now + timedelta(minutes=12, seconds=1))
text = format_session_text(
"mywork",
"claude",
"idle",
"",
scheduled_prompt=scheduled,
now=now,
)
assert "@ 13m" in text
def test_format_session_text_shows_scheduled_prompt_clock_time():
from datetime import datetime, timedelta
from hqt.tui.widgets.session_list import format_session_text
now = datetime(2026, 6, 11, 14, 0, 0)
scheduled = _make_scheduled_prompt(now + timedelta(hours=2, minutes=30))
text = format_session_text(
"mywork",
"claude",
"idle",
"",
scheduled_prompt=scheduled,
now=now,
)
assert "@ 16:30" in text
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_session_list_renders_harness_name_brackets(): async def test_session_list_renders_harness_name_brackets():
"""Regression: '[claude]' must be visible, not swallowed as a markup tag.""" """Regression: '[claude]' must be visible, not swallowed as a markup tag."""