feat: show scheduled prompts in session rows
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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))
|
||||||
|
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
Reference in New Issue
Block a user