feat: add custom HeaderPanel widget and switchable agent modes
Replace built-in Header with a custom HeaderPanel showing model name, mode badge, and live token usage. Add AgentMode enum (normal/plan/auto) with mode-aware permission gating — Plan mode restricts to read-only tools, Auto mode auto-approves everything. Includes /mode slash command and Ctrl+P keybinding to cycle modes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,82 @@ from textual.widgets import Button, Input, Static
|
||||
|
||||
from rich.text import Text
|
||||
|
||||
from app.models.config import AgentMode
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Header Panel
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class HeaderPanel(Static):
|
||||
"""Single-line header showing model name, agent mode, and token usage."""
|
||||
|
||||
DEFAULT_CSS = """
|
||||
HeaderPanel {
|
||||
dock: top;
|
||||
height: 1;
|
||||
background: $accent;
|
||||
color: $text;
|
||||
padding: 0 2;
|
||||
}
|
||||
"""
|
||||
|
||||
def __init__(self, model_name: str) -> None:
|
||||
super().__init__("")
|
||||
self._model_name = model_name
|
||||
self._mode: AgentMode = AgentMode.NORMAL
|
||||
self._tokens: int = 0
|
||||
self._budget: int = 0
|
||||
|
||||
def on_resize(self) -> None:
|
||||
self._refresh_display()
|
||||
|
||||
def update_model(self, name: str) -> None:
|
||||
"""Update the displayed model name."""
|
||||
self._model_name = name
|
||||
self._refresh_display()
|
||||
|
||||
def update_mode(self, mode: AgentMode) -> None:
|
||||
"""Update the displayed agent mode."""
|
||||
self._mode = mode
|
||||
self._refresh_display()
|
||||
|
||||
def update_tokens(self, tokens: int, budget: int) -> None:
|
||||
"""Update the token usage display."""
|
||||
self._tokens = tokens
|
||||
self._budget = budget
|
||||
self._refresh_display()
|
||||
|
||||
def _refresh_display(self) -> None:
|
||||
"""Rebuild the header text."""
|
||||
left = Text.assemble(
|
||||
("⚡ SneakyCode", "bold"),
|
||||
" │ ",
|
||||
(self._model_name, "bold"),
|
||||
)
|
||||
|
||||
mode_styles = {
|
||||
AgentMode.NORMAL: ("NORMAL", "bold black on white"),
|
||||
AgentMode.PLAN: ("PLAN", "bold black on yellow"),
|
||||
AgentMode.AUTO: ("AUTO", "bold white on red"),
|
||||
}
|
||||
mode_label, mode_style = mode_styles[self._mode]
|
||||
mode_text = Text.assemble((" ", mode_style), (mode_label, mode_style), (" ", mode_style))
|
||||
|
||||
right = Text(f"~{self._tokens:,} / {self._budget:,} tokens")
|
||||
|
||||
# Pad between sections
|
||||
total_content = left.plain + " " + mode_text.plain + " " + right.plain
|
||||
available = self.size.width if self.size.width > 0 else 80
|
||||
gap_left = max(1, (available - len(total_content)) // 2)
|
||||
gap_right = max(1, available - len(total_content) - gap_left)
|
||||
|
||||
full = Text.assemble(
|
||||
left, " " * gap_left, mode_text, " " * gap_right, right,
|
||||
)
|
||||
self.update(full)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Modal Dialogs
|
||||
@@ -139,8 +215,6 @@ class StatusBar(Static):
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__("")
|
||||
self._tokens: int = 0
|
||||
self._budget: int = 0
|
||||
self._iteration: int = 0
|
||||
self._max_iterations: int = 0
|
||||
self._streaming: bool = False
|
||||
@@ -149,12 +223,6 @@ class StatusBar(Static):
|
||||
self._stream_tokens: int = 0
|
||||
self._active_skill: str | None = None
|
||||
|
||||
def update_tokens(self, tokens: int, budget: int) -> None:
|
||||
"""Update the token usage display."""
|
||||
self._tokens = tokens
|
||||
self._budget = budget
|
||||
self._refresh_display()
|
||||
|
||||
def update_iteration(self, iteration: int, max_iterations: int) -> None:
|
||||
"""Update the iteration count display."""
|
||||
self._iteration = iteration
|
||||
@@ -200,8 +268,6 @@ class StatusBar(Static):
|
||||
parts.append(f"{spinner} Thinking")
|
||||
if self._stream_tokens > 0:
|
||||
parts.append(f"~{self._stream_tokens:,} tokens")
|
||||
if self._budget > 0:
|
||||
parts.append(f"Tokens: ~{self._tokens:,} / {self._budget:,}")
|
||||
if self._max_iterations > 0:
|
||||
parts.append(f"Iteration {self._iteration}/{self._max_iterations}")
|
||||
self.update(Text(" \u2502 ".join(parts), style="dim"))
|
||||
|
||||
Reference in New Issue
Block a user