Added OTP Activity Review WIP

This commit is contained in:
2025-11-13 11:45:13 -05:00
parent 5cb079dad7
commit f0e77db414
11 changed files with 787 additions and 74 deletions
+7 -3
View File
@@ -1,5 +1,5 @@
import logging
from typing import List
from typing import List, Optional
from textual.containers import Horizontal, Vertical
from textual.css.query import NoMatches
@@ -31,7 +31,11 @@ class OTPGenerator(Widget):
class OTPInfo(Message):
def __init__(
self, devices: List[Agent], requestor: str, reasoning: str, duration: int
self,
devices: Optional[List[Agent]],
requestor: str,
reasoning: str,
duration: int,
):
super().__init__()
self.devices = devices
@@ -243,7 +247,7 @@ class OTPGenerator(Widget):
self.otp_generated = True
# Access API from the app - this is the key change!
api = self.app.api
api = self.app.api # type: ignore
output_lines = [
"Requested OTP Codes:",
-35
View File
@@ -1,35 +0,0 @@
from textual.color import Color
from textual.theme import Theme
def get_amber_terminal_theme():
"""Amber CRT theme with compensated brightness for blending."""
return Theme(
name="amber-terminal",
background=Color.parse("#000000"), # pure black
primary=Color.parse("#ffb733"), # bright amber
secondary=Color.parse("#e69500"), # strong amber
success=Color.parse("#ffb733"),
warning=Color.parse("#ffff66"),
error=Color.parse("#ff3300"),
surface=Color.parse("#3a1f00"), # brighter brown for blending
)
AMBER_TERMINAL_CSS = """
Screen {
align: center middle;
background: #000000; /* force black */
color: #ffb733; /* force amber text */
}
.widget {
border: tall #ffb733; /* force amber border */
background: #3a1f00; /* compensated surface */
width: 80%;
}
* {
font-family: "Courier New", monospace;
}
"""
+2 -2
View File
@@ -1,6 +1,6 @@
import difflib
import re
from typing import List
from typing import List, Optional
from textual.containers import Horizontal, Vertical
from textual.css.query import NoMatches
@@ -25,7 +25,7 @@ class MultiAgentSelector(Widget):
super().__init__()
self.selected_agents = selected_agents
def __init__(self, all_agents: List[Agent]):
def __init__(self, all_agents: Optional[List[Agent]]):
super().__init__()
self.all_agents = all_agents
self._match_type = "exact"
+55 -12
View File
@@ -4,7 +4,7 @@ import logging
from rich.text import Text
from textual.containers import Horizontal, Vertical
from textual.widget import Widget
from textual.widgets import Input, OptionList, Static, Tree
from textual.widgets import Input, OptionList, Static, Switch, Tree
from textual.widgets.option_list import Option
logger = logging.getLogger(__name__)
@@ -19,29 +19,49 @@ class PolicyTreeWidget(Widget):
self.devices = devices
self.last_highlighted_node = None
self.leaf_counts = defaultdict(int)
self.match_type = "Count" # Default to sorting by count
def compose(self):
# Create the switch and its label
switch = Switch(value=False, id="match_switch")
switch.styles.margin = (0, 0, 0, 0) # top, right, bottom, left
switch.styles.padding = (0, 0, 0, 0)
switch_label = Static("Sort: Count", id="match_switch_label")
switch_label.styles.margin = (1, 0, 0, 0)
switch_label.styles.padding = (0, 0, 0, 0)
# Create the tree
policy_tree = Tree("", id="policy_tree") # Label set in on_mount
policy_tree.styles.width = "2fr"
policy_tree.styles.height = "100%"
# Create the search box and details pane
label = Static("Device Search:")
search_box = Input(
placeholder="Search policies or devices...", id="tree_search"
)
details_pane = Static("", id="details_pane")
# Layout the UI
with Horizontal():
yield policy_tree
with Vertical() as right_pane:
right_pane.styles.width = "3fr"
# Use a Horizontal container for the switch and label
with Horizontal() as switch_container:
switch_container.styles.height = 3
switch_container.styles.margin = (0, 0, 0, 1)
switch_container.styles.padding = (0, 0, 0, 0)
yield switch
yield switch_label
# Add the search box and details pane
yield label
yield search_box
yield details_pane
def on_mount(self) -> None:
self._precompute_leaf_counts()
# Update root label with total leaf count
total_leaves = sum(
self.leaf_counts.get(policy.groupid, 0)
@@ -50,8 +70,9 @@ class PolicyTreeWidget(Widget):
)
policy_tree = self.query_one("#policy_tree", Tree)
policy_tree.root.set_label(f"Agents in Policies: ({total_leaves})")
self._build_tree()
# Expand the root node
policy_tree.root.expand()
def _precompute_leaf_counts(self):
"""Precompute leaf counts for each policy group."""
@@ -76,15 +97,21 @@ class PolicyTreeWidget(Widget):
def _build_tree(self):
policy_tree = self.query_one("#policy_tree", Tree)
policy_tree.clear() # Clear existing nodes
node_map = {}
# Sort top-level policies
top_policies = [
p for p in self.policies if p.parent == "global-policy-settings"
]
top_policies.sort(
key=lambda p: self.leaf_counts.get(p.groupid, 0), reverse=True
)
# Sort by count (default) or alphabetically
if getattr(self, "match_type", "Count") == "Count":
top_policies.sort(
key=lambda p: self.leaf_counts.get(p.groupid, 0), reverse=True
)
else: # Alphabetical
top_policies.sort(key=lambda p: p.name.lower())
for policy in top_policies:
label = f"{policy.name} ({self.leaf_counts.get(policy.groupid, 0)})"
@@ -98,9 +125,13 @@ class PolicyTreeWidget(Widget):
children_by_parent[policy.parent].append(policy)
for parent_id, children in children_by_parent.items():
children.sort(
key=lambda p: self.leaf_counts.get(p.groupid, 0), reverse=True
)
if getattr(self, "match_type", "Count") == "Count":
children.sort(
key=lambda p: self.leaf_counts.get(p.groupid, 0), reverse=True
)
else: # Alphabetical
children.sort(key=lambda p: p.name.lower())
parent_node = node_map.get(parent_id)
if parent_node:
for policy in children:
@@ -108,12 +139,17 @@ class PolicyTreeWidget(Widget):
node = parent_node.add(label=label, data=policy)
node_map[policy.groupid] = node
# Add devices (leaf nodes)
# Add devices (leaf nodes) - always sort alphabetically
devices_by_group = defaultdict(list)
for device in self.devices:
group_id = device.groupid
devices_by_group[device.groupid].append(device)
for group_id, devices in devices_by_group.items():
devices.sort(key=lambda d: d.hostname.lower()) # Always sort alphabetically
parent_node = node_map.get(group_id)
if parent_node:
parent_node.add(label=device.hostname, data=device)
for device in devices:
parent_node.add(label=device.hostname, data=device)
def _collect_tree_nodes(self, node, all_nodes):
all_nodes.append(node)
@@ -157,6 +193,13 @@ class PolicyTreeWidget(Widget):
message.stop()
def on_switch_changed(self, event: Switch.Changed):
self.match_type = "Alpha" if event.value else "Count"
self.query_one("#match_switch_label", Static).update(
f"Sort: {self.match_type.capitalize()}"
)
self._build_tree()
def on_input_submitted(self, message: Input.Submitted) -> None:
self._remove_match_selector()
-38
View File
@@ -1,38 +0,0 @@
from textual.color import Color
def get_retro_terminal_theme():
from textual.theme import Theme
return Theme(
name="retro-terminal",
background=Color.parse("#000000"),
primary=Color.parse("#00ff00"),
secondary=Color.parse("#00aa00"),
success=Color.parse("#00ff00"),
warning=Color.parse("#ffff00"),
error=Color.parse("#ff0000"),
surface=Color.parse("#071802"),
)
RETRO_TERMINAL_CSS = """
/* Retro terminal CRT effect */
Screen {
align: center middle;
background: $background;
color: $text;
}
/* Blocky, pixelated widgets */
.widget {
border: tall $primary;
background: $surface;
width: 80%;
}
/* Monospaced font */
* {
font-family: "Courier New", monospace;
}
"""