134 lines
4.7 KiB
Python
134 lines
4.7 KiB
Python
import difflib
|
|
|
|
from textual.containers import Horizontal
|
|
from textual.css.query import NoMatches
|
|
from textual.message import Message
|
|
from textual.widget import Widget
|
|
from textual.widgets import Button, SelectionList, Static, Switch, TextArea
|
|
|
|
|
|
class MultiAgentSelector(Widget):
|
|
class AgentsSelected(Message):
|
|
def __init__(self, selected_agents):
|
|
super().__init__()
|
|
self.selected_agents = selected_agents
|
|
|
|
def __init__(self, all_agents: list[dict]):
|
|
super().__init__()
|
|
self.all_agents = all_agents
|
|
self._match_type = "fuzzy"
|
|
|
|
@property
|
|
def match_type(self):
|
|
return self._match_type
|
|
|
|
@match_type.setter
|
|
def match_type(self, value):
|
|
self._match_type = value
|
|
|
|
def compose(self):
|
|
yield Static("🔍 Multi-Agent Selector", id="selector_title")
|
|
text_area = TextArea(
|
|
id="device_input",
|
|
placeholder="Paste device names here (one per line)",
|
|
)
|
|
text_area.styles.height = 10
|
|
text_area.styles.overflow_y = "auto"
|
|
yield text_area
|
|
|
|
with Horizontal() as switch_container:
|
|
switch_container.styles.height = "auto"
|
|
switch_container.styles.align = ("left", "middle")
|
|
switch_label = Static("Match Type: Fuzzy", id="match_switch_label")
|
|
switch_label.styles.width = "auto"
|
|
switch_label.styles.padding = (0, 1)
|
|
yield switch_label
|
|
|
|
switch = Switch(value=False, id="match_switch")
|
|
switch.styles.width = "auto"
|
|
yield switch
|
|
|
|
yield Button("Search", id="search_button")
|
|
|
|
yield SelectionList(id="match_results")
|
|
yield Static(id="unmatched_label")
|
|
|
|
yield Horizontal(
|
|
Button("Select All", id="select_all"),
|
|
Button("Select None", id="select_none"),
|
|
)
|
|
|
|
yield Button("Continue with Selected", id="submit_selection", variant="primary")
|
|
|
|
def on_switch_changed(self, event: Switch.Changed):
|
|
self.match_type = "exact" if event.value else "fuzzy"
|
|
self.query_one("#match_switch_label", Static).update(
|
|
f"Match Type: {self.match_type.capitalize()}"
|
|
)
|
|
|
|
def on_button_pressed(self, event: Button.Pressed):
|
|
btn_id = event.button.id # can be None for internal buttons
|
|
|
|
# Only query when needed
|
|
try:
|
|
match_list = self.query_one("#match_results", SelectionList)
|
|
except NoMatches:
|
|
# UI not mounted yet or id changed—just ignore gracefully
|
|
return
|
|
|
|
if btn_id == "select_all":
|
|
match_list.select_all()
|
|
event.stop()
|
|
elif btn_id == "select_none":
|
|
match_list.deselect_all()
|
|
event.stop()
|
|
elif btn_id == "submit_selection":
|
|
selected = list(match_list.selected)
|
|
self.post_message(self.AgentsSelected(selected))
|
|
event.stop()
|
|
elif btn_id == "search_button":
|
|
self.update_matches()
|
|
event.stop()
|
|
|
|
def update_matches(self):
|
|
raw_input = self.query_one("#device_input", TextArea).text.strip()
|
|
device_names = [line.strip() for line in raw_input.split("\n") if line.strip()]
|
|
matched, unmatched = self.match_devices(device_names)
|
|
match_list = self.query_one("#match_results", SelectionList)
|
|
match_list.clear_options()
|
|
for name in matched:
|
|
match_list.add_option((name, name))
|
|
unmatched_label = self.query_one("#unmatched_label", Static)
|
|
if unmatched:
|
|
unmatched_label.update(f"⚠️ No matches for: {', '.join(unmatched)}")
|
|
else:
|
|
unmatched_label.update("")
|
|
|
|
def match_devices(self, device_names: list[str]) -> tuple[list[str], list[str]]:
|
|
if not self.all_agents or not device_names:
|
|
return [], device_names
|
|
agent_names = [agent["hostname"] for agent in self.all_agents]
|
|
matched = set()
|
|
unmatched = []
|
|
for name in device_names:
|
|
if self.match_type == "exact":
|
|
# Case-insensitive exact match
|
|
name_lower = name.lower()
|
|
exact_match = None
|
|
for agent_name in agent_names:
|
|
if agent_name.lower() == name_lower:
|
|
exact_match = agent_name
|
|
break
|
|
if exact_match:
|
|
matched.add(exact_match)
|
|
else:
|
|
unmatched.append(name)
|
|
else:
|
|
# Fuzzy match
|
|
matches = difflib.get_close_matches(name, agent_names, n=5, cutoff=0.5)
|
|
if matches:
|
|
matched.update(matches)
|
|
else:
|
|
unmatched.append(name)
|
|
return sorted(matched), unmatched
|