chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:39:25 +08:00
commit db620d33df
5151 changed files with 925932 additions and 0 deletions
@@ -0,0 +1,23 @@
# Copyright (c) Microsoft. All rights reserved.
"""UI components for the harness console.
This module provides Textual widgets for building the harness console UI,
including status displays, input fields, choice selectors, and scrolling panels.
"""
from .agent_status import AgentStatus
from .list_selection import HarnessListSelection
from .mode_help import AgentModeAndHelp
from .prompt_rule import PromptRule
from .scroll_panel import HarnessScrollPanel
from .text_input import HarnessTextInput
__all__ = [
"AgentStatus",
"AgentModeAndHelp",
"HarnessListSelection",
"PromptRule",
"HarnessScrollPanel",
"HarnessTextInput",
]
@@ -0,0 +1,70 @@
# Copyright (c) Microsoft. All rights reserved.
"""Agent status widget with spinner animation and usage statistics."""
from __future__ import annotations
from textual.reactive import reactive
from textual.widgets import Static
class AgentStatus(Static):
"""Agent status bar with animated spinner and token usage display.
Displays an animated braille pattern spinner when the agent is active,
along with token usage statistics. The component automatically updates
the spinner animation at ~10fps for smooth visual feedback.
Attributes:
show_spinner: Whether to display the animated spinner.
usage_text: Token usage text to display (e.g., "1.2K in / 856 out").
"""
# Braille pattern spinner frames for smooth animation
SPINNER_FRAMES = ["", "", "", "", "", "", "", "", "", ""]
show_spinner: reactive[bool] = reactive(False)
usage_text: reactive[str] = reactive("")
queued_text: reactive[str] = reactive("")
def __init__(self, **kwargs) -> None:
"""Initialize the agent status widget."""
super().__init__(**kwargs)
self._spinner_index = 0
def on_mount(self) -> None:
"""Start the spinner animation timer when the widget is mounted."""
# Update spinner at ~10fps (every 0.1 seconds)
self.set_interval(0.1, self._advance_spinner)
def _advance_spinner(self) -> None:
"""Advance the spinner to the next frame."""
if self.show_spinner:
self._spinner_index = (self._spinner_index + 1) % len(self.SPINNER_FRAMES)
self.refresh()
def render(self) -> str:
"""Render the status bar with spinner and usage text.
Returns:
Formatted string with Rich markup for spinner and usage display.
"""
if not self.show_spinner and not self.usage_text and not self.queued_text:
return ""
parts = []
if self.show_spinner:
frame = self.SPINNER_FRAMES[self._spinner_index]
parts.append(f"[cyan]{frame}[/cyan]")
else:
# Keep consistent spacing when spinner is off
parts.append(" ")
if self.usage_text:
parts.append(f"[dim]{self.usage_text}[/dim]")
if self.queued_text:
parts.append(f"[dim]{self.queued_text}[/dim]")
return " ".join(parts)
@@ -0,0 +1,269 @@
# Copyright (c) Microsoft. All rights reserved.
"""List selection widget with optional custom text input."""
from __future__ import annotations
from textual import on
from textual.app import ComposeResult
from textual.binding import Binding
from textual.containers import Container
from textual.css.query import NoMatches
from textual.events import Key
from textual.message import Message
from textual.reactive import reactive
from textual.widget import Widget
from textual.widgets import Input, Label, OptionList
from textual.widgets.option_list import Option
class HarnessListSelection(Widget):
"""List selection widget with numbered choices and optional custom text input.
Displays a title, a list of numbered choices that can be selected via
keyboard navigation or number keys (1-9), and an optional custom text
input field at the bottom.
All child nodes (title label, option list, custom input) are always
present in the DOM; visibility is toggled via reactive watchers.
Navigation:
- Down arrow on last list item moves focus to the custom text input
- Up arrow on the custom text input moves focus back to the option list
- When custom input has focus, the option list highlight is cleared
Attributes:
title: The title text displayed above the options.
options: List of option strings to display.
allow_custom_text: Whether to show a custom text input field.
"""
DEFAULT_CSS = """
HarnessListSelection {
height: auto;
max-height: 12;
}
HarnessListSelection .list-selection-container {
height: auto;
}
HarnessListSelection #selection-title {
height: auto;
color: $text;
text-style: bold;
padding: 0 0 0 0;
}
HarnessListSelection #option-list {
height: auto;
max-height: 8;
border: none;
padding: 0;
}
HarnessListSelection #custom-input {
height: auto;
min-height: 1;
margin-top: 0;
border: tall transparent;
}
HarnessListSelection #custom-input:focus {
border: tall $accent;
}
"""
BINDINGS = [
Binding("1", "select_option(0)", "Select option 1", show=False),
Binding("2", "select_option(1)", "Select option 2", show=False),
Binding("3", "select_option(2)", "Select option 3", show=False),
Binding("4", "select_option(3)", "Select option 4", show=False),
Binding("5", "select_option(4)", "Select option 5", show=False),
Binding("6", "select_option(5)", "Select option 6", show=False),
Binding("7", "select_option(6)", "Select option 7", show=False),
Binding("8", "select_option(7)", "Select option 8", show=False),
Binding("9", "select_option(8)", "Select option 9", show=False),
]
title: reactive[str] = reactive("")
options: reactive[list[str]] = reactive(list, always_update=True)
allow_custom_text: reactive[bool] = reactive(False)
class Selected(Message):
"""Message sent when an option is selected.
Attributes:
value: The selected option text or custom text.
"""
def __init__(self, value: str) -> None:
"""Initialize the Selected message.
Args:
value: The selected option text or custom text.
"""
self.value = value
super().__init__()
def compose(self) -> ComposeResult:
"""Compose the widget — all nodes are always present.
Yields:
Title label (hidden if empty), option list, custom input (hidden by default).
"""
with Container(classes="list-selection-container"):
yield Label("", id="selection-title")
yield OptionList(id="option-list")
yield Input(
placeholder="Or type a custom response...",
id="custom-input",
)
def on_mount(self) -> None:
"""Configure initial visibility after mount."""
title_label = self.query_one("#selection-title", Label)
title_label.display = bool(self.title)
custom_input = self.query_one("#custom-input", Input)
custom_input.display = self.allow_custom_text
self._update_options()
def on_key(self, event: Key) -> None:
"""Handle key navigation between option list and custom input.
Args:
event: The key event.
"""
if not self.allow_custom_text:
return
option_list = self.query_one("#option-list", OptionList)
custom_input = self.query_one("#custom-input", Input)
# Down arrow on last item → move to custom input
if event.key == "down" and option_list.has_focus:
last_index = option_list.option_count - 1
if last_index >= 0 and option_list.highlighted == last_index:
option_list.highlighted = None # type: ignore[assignment]
custom_input.focus()
event.prevent_default()
event.stop()
# Up arrow on custom input → move back to option list (last item)
elif event.key == "up" and custom_input.has_focus:
last_index = option_list.option_count - 1
if last_index >= 0:
option_list.highlighted = last_index
option_list.focus()
event.prevent_default()
event.stop()
@on(Input.Changed, "#custom-input")
def on_custom_input_focused_or_changed(self, event: Input.Changed) -> None:
"""Clear option list highlight when user is typing in custom input.
Args:
event: The input changed event.
"""
option_list = self.query_one("#option-list", OptionList)
option_list.highlighted = None # type: ignore[assignment]
def watch_title(self, new_title: str) -> None:
"""Update the title label when the title changes.
Args:
new_title: The new title text.
"""
try:
label = self.query_one("#selection-title", Label)
label.update(new_title)
label.display = bool(new_title)
except NoMatches:
pass
def watch_options(self, new_options: list[str]) -> None:
"""Update the option list when options change.
Args:
new_options: The new list of options.
"""
import contextlib
with contextlib.suppress(NoMatches):
self._update_options()
def watch_allow_custom_text(self, allow: bool) -> None:
"""Show/hide the custom input field.
Args:
allow: Whether to show the custom text input.
"""
try:
custom_input = self.query_one("#custom-input", Input)
custom_input.display = allow
except NoMatches:
pass
def _update_options(self) -> None:
"""Update the OptionList with numbered options."""
try:
option_list = self.query_one("#option-list", OptionList)
option_list.clear_options()
for i, option_text in enumerate(self.options):
display_text = f"{i + 1}. {option_text}" if i < 9 else f" {option_text}"
option_list.add_option(Option(display_text, id=str(i)))
except NoMatches:
pass
@on(OptionList.OptionSelected)
def on_option_selected(self, event: OptionList.OptionSelected) -> None:
"""Handle option selection from the list.
Args:
event: The OptionList.OptionSelected event.
"""
option_index = int(event.option.id or "0")
if 0 <= option_index < len(self.options):
selected_value = self.options[option_index]
self.post_message(self.Selected(selected_value))
@on(Input.Submitted)
def on_input_submitted(self, event: Input.Submitted) -> None:
"""Handle custom text input submission.
Args:
event: The Input.Submitted event.
"""
if self.allow_custom_text and event.value:
self.post_message(self.Selected(event.value))
event.input.clear()
def action_select_option(self, index: int) -> None:
"""Select an option by index (0-based).
Args:
index: The option index to select.
"""
if 0 <= index < len(self.options):
selected_value = self.options[index]
self.post_message(self.Selected(selected_value))
def focus_list(self) -> None:
"""Focus the option list."""
try:
option_list = self.query_one("#option-list", OptionList)
option_list.focus()
except NoMatches:
pass
def focus_custom_input(self) -> None:
"""Focus the custom text input field."""
if self.allow_custom_text:
try:
custom_input = self.query_one("#custom-input", Input)
custom_input.focus()
except NoMatches:
pass
@@ -0,0 +1,48 @@
# Copyright (c) Microsoft. All rights reserved.
"""Agent mode and help text display widget."""
from __future__ import annotations
from rich.text import Text
from textual.reactive import reactive
from textual.widgets import Static
class AgentModeAndHelp(Static):
"""Widget displaying the current agent mode and help text.
Shows the current agent mode (e.g., "plan", "execute") in a colored label,
followed by available commands and help text in a dimmed style. Used in
the fixed bottom area of the console.
Attributes:
mode: Current mode name (e.g., "plan", "execute"), or None if no mode.
mode_color: Rich color string for the mode label (e.g., "yellow", "green").
help_text: Help text to display (e.g., "/exit to quit, /mode to switch").
"""
mode: reactive[str | None] = reactive(None)
mode_color: reactive[str] = reactive("yellow")
help_text: reactive[str] = reactive("")
def render(self) -> Text:
"""Render the mode indicator and help text.
Returns:
Rich Text object with styled mode and help display.
"""
result = Text()
if self.mode:
result.append(f"[{self.mode}]", style=self.mode_color)
if self.help_text:
if self.mode:
result.append(" ")
result.append(self.help_text, style="dim")
if not result.plain:
result.append(" ")
return result
@@ -0,0 +1,31 @@
# Copyright (c) Microsoft. All rights reserved.
"""Mode-colored horizontal rule."""
from __future__ import annotations
from textual.reactive import reactive
from textual.widgets import Static
class PromptRule(Static):
"""A full-width horizontal rule colored by the current agent mode.
Renders a line of '' characters across the terminal width,
colored to match the current mode (e.g., cyan for plan, green for execute).
Attributes:
rule_color: Rich color string for the rule (e.g., "cyan", "green").
"""
rule_color: reactive[str] = reactive("cyan")
def render(self) -> str:
"""Render the horizontal rule.
Returns:
Formatted string with Rich markup.
"""
color = self.rule_color
width = self.size.width or 80
return f"[{color}]{'' * width}[/{color}]"
@@ -0,0 +1,127 @@
# Copyright (c) Microsoft. All rights reserved.
"""Scrolling panel for conversation history display."""
from __future__ import annotations
from typing import TYPE_CHECKING
from textual.widgets import RichLog
if TYPE_CHECKING:
from ..app_state import OutputEntry
class HarnessScrollPanel(RichLog):
"""Scrolling panel for displaying conversation history.
Uses Textual's RichLog widget for efficient append-only rendering with
Rich text formatting support. Automatically scrolls to the bottom when
new entries are added.
For streaming text, the panel uses a truncate-and-rewrite strategy: it
tracks where streaming began in the RichLog lines list, and on each update
truncates back to that point and rewrites the full accumulated text as a
single write. This ensures consistent rendering without line-break artifacts
between streamed chunks.
"""
def __init__(self, **kwargs) -> None:
"""Initialize the scroll panel.
Args:
**kwargs: Additional arguments passed to RichLog.
"""
super().__init__(
**kwargs,
auto_scroll=True, # Automatically scroll to bottom
wrap=True, # Wrap long lines instead of horizontal scroll
markup=True, # Enable Rich markup
highlight=True, # Enable syntax highlighting
)
self._entries: list[OutputEntry] = []
self._is_streaming = False
self._streaming_line_start: int = 0
def append_entry(self, entry: OutputEntry) -> None:
"""Append a new output entry to the conversation history.
Args:
entry: The output entry to append.
"""
self._entries.append(entry)
text = self._format_entry(entry)
self.write(text)
def set_streaming_entry(self, entry: OutputEntry) -> None:
"""Set or update the current streaming entry.
On each update, truncates the RichLog back to where streaming
started, then rewrites the full streaming text as a single block.
This ensures no spurious line breaks between chunks while avoiding
a full rewrite of all entries.
Args:
entry: The streaming entry (will be mutated externally).
"""
if not self._is_streaming:
# First streaming chunk — record where streaming lines begin
self._is_streaming = True
self._entries.append(entry)
self._streaming_line_start = len(self.lines)
# Truncate lines back to where streaming started
if len(self.lines) > self._streaming_line_start:
del self.lines[self._streaming_line_start :]
from textual.geometry import Size
self.virtual_size = Size(self._widest_line_width, len(self.lines))
# Write full streaming text as a single renderable
formatted = self._format_text(entry.text, entry.color)
self.write(formatted)
def end_streaming(self) -> None:
"""End the current streaming mode."""
if self._is_streaming:
self._is_streaming = False
self._streaming_line_start = 0
def _rewrite_all(self) -> None:
"""Clear and rewrite all entries from scratch."""
self.clear()
for entry in self._entries:
self.write(self._format_entry(entry))
def _format_entry(self, entry: OutputEntry) -> str:
"""Format an output entry with Rich markup.
Args:
entry: The entry to format.
Returns:
Formatted string with Rich markup for color and styling.
"""
return self._format_text(entry.text, entry.color)
@staticmethod
def _format_text(text: str, color: str | None) -> str:
"""Format text with optional Rich color markup.
Args:
text: The text to format.
color: Optional Rich color name.
Returns:
Formatted string.
"""
if color:
return f"[{color}]{text}[/{color}]"
return text
def clear_history(self) -> None:
"""Clear all conversation history from the panel."""
self._entries.clear()
self._is_streaming = False
self._streaming_line_start = 0
self.clear()
@@ -0,0 +1,102 @@
# Copyright (c) Microsoft. All rights reserved.
"""Text input widget with inline prompt for the harness console."""
from __future__ import annotations
from textual import on
from textual.app import ComposeResult
from textual.containers import Horizontal
from textual.message import Message
from textual.reactive import reactive
from textual.widget import Widget
from textual.widgets import Input, Label
class HarnessTextInput(Widget):
"""Text input widget with a prompt label on the left.
Displays a prompt (e.g., "> ") followed by a borderless input field.
Sits between the two mode-colored horizontal rules.
Attributes:
prompt: The prompt text displayed on the left (e.g., "> ").
placeholder: Placeholder text shown when the input is empty.
"""
prompt: reactive[str] = reactive("> ")
placeholder: reactive[str] = reactive("")
class Submitted(Message):
"""Message sent when the input is submitted.
Attributes:
value: The submitted text value.
"""
def __init__(self, value: str) -> None:
"""Initialize the Submitted message.
Args:
value: The submitted text value.
"""
self.value = value
super().__init__()
def compose(self) -> ComposeResult:
"""Compose the prompt label and input field.
Yields:
A horizontal container with the prompt and input field.
"""
with Horizontal(classes="prompt-container"):
yield Label(self.prompt, classes="prompt-label", id="prompt-label")
yield Input(placeholder=self.placeholder, classes="input-field", id="input-field")
def watch_prompt(self, new_prompt: str) -> None:
"""Update the prompt label when the prompt attribute changes.
Args:
new_prompt: The new prompt text.
"""
try:
label = self.query_one("#prompt-label", Label)
label.update(new_prompt)
except Exception:
pass
def watch_placeholder(self, new_placeholder: str) -> None:
"""Update the input placeholder when the placeholder attribute changes.
Args:
new_placeholder: The new placeholder text.
"""
try:
input_field = self.query_one("#input-field", Input)
input_field.placeholder = new_placeholder
except Exception:
# Input doesn't exist yet (before compose), ignore
pass
@on(Input.Submitted)
def on_input_submitted(self, event: Input.Submitted) -> None:
"""Handle input submission.
Clears the input field and posts a Submitted message with the value.
Args:
event: The Input.Submitted event.
"""
value = event.value
event.input.clear()
self.post_message(self.Submitted(value))
def focus_input(self) -> None:
"""Focus the input field."""
input_field = self.query_one(".input-field", Input)
input_field.focus()
def clear_input(self) -> None:
"""Clear the input field."""
input_field = self.query_one(".input-field", Input)
input_field.clear()