refactored config plugin defintions
This commit is contained in:
+156
-315
@@ -7,7 +7,7 @@ from textual import on, work
|
||||
from textual.app import ComposeResult
|
||||
from textual.containers import Container, Horizontal, Vertical, ScrollableContainer
|
||||
from textual.screen import ModalScreen
|
||||
from textual.widgets import Static, Button, Input, Label, ListView, ListItem, Rule, Select, Checkbox
|
||||
from textual.widgets import Static, Button, Input, Label, ListView, ListItem, Rule, Select, Checkbox, TextArea
|
||||
from pathlib import Path
|
||||
|
||||
from SYS.config import (
|
||||
@@ -15,15 +15,24 @@ from SYS.config import (
|
||||
save_config,
|
||||
save_config_and_verify,
|
||||
reload_config,
|
||||
global_config,
|
||||
count_changed_entries,
|
||||
ConfigSaveConflict,
|
||||
coerce_config_value,
|
||||
)
|
||||
from SYS.database import db
|
||||
from SYS.logger import log, debug
|
||||
from Store.registry import _discover_store_classes, _required_keys_for
|
||||
from ProviderCore.registry import get_plugin, list_plugins
|
||||
from SYS.plugin_config import (
|
||||
build_default_provider_config,
|
||||
build_default_store_config,
|
||||
build_default_tool_config,
|
||||
get_configurable_provider_types,
|
||||
get_configurable_store_types,
|
||||
get_configurable_tool_types,
|
||||
get_global_schema,
|
||||
get_item_schema_map,
|
||||
get_required_config_keys,
|
||||
)
|
||||
from ProviderCore.registry import get_plugin
|
||||
from TUI.modalscreen.matrix_room_picker import MatrixRoomPicker
|
||||
from TUI.modalscreen.selection_modal import SelectionModal
|
||||
import logging
|
||||
@@ -93,6 +102,18 @@ class ConfigModal(ModalScreen):
|
||||
width: 1fr;
|
||||
}
|
||||
|
||||
.config-textarea {
|
||||
height: 8;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
.config-group {
|
||||
color: $accent;
|
||||
text-style: bold;
|
||||
margin-top: 1;
|
||||
margin-bottom: 1;
|
||||
}
|
||||
|
||||
#config-actions {
|
||||
height: 3;
|
||||
align: right middle;
|
||||
@@ -277,18 +298,19 @@ class ConfigModal(ModalScreen):
|
||||
|
||||
def render_globals(self, container: ScrollableContainer) -> None:
|
||||
container.mount(Label("General Configuration", classes="config-label"))
|
||||
|
||||
# Get global schema
|
||||
schema_map = {f["key"].lower(): f for f in global_config()}
|
||||
|
||||
schema = get_global_schema()
|
||||
schema_map = {f["key"].lower(): f for f in schema}
|
||||
existing_keys_lower = set()
|
||||
|
||||
render_state = {"group": None, "mounted_any": False}
|
||||
|
||||
idx = 0
|
||||
# Show fields defined in schema first
|
||||
for key_lower, field_def in schema_map.items():
|
||||
for field_def in schema:
|
||||
key_lower = field_def["key"].lower()
|
||||
existing_keys_lower.add(key_lower)
|
||||
label_text = field_def.get("label") or field_def["key"]
|
||||
choices = field_def.get("choices")
|
||||
|
||||
self._mount_schema_group(container, field_def, render_state)
|
||||
|
||||
# Find current value (case-insensitive)
|
||||
current_val = None
|
||||
found_key = field_def["key"]
|
||||
@@ -301,33 +323,10 @@ class ConfigModal(ModalScreen):
|
||||
if current_val is None:
|
||||
current_val = str(field_def.get("default") or "")
|
||||
|
||||
container.mount(Label(label_text))
|
||||
inp_id = f"global-{idx}"
|
||||
self._input_id_map[inp_id] = found_key
|
||||
|
||||
if choices:
|
||||
# Normalize boolean-like choices to lowercase ('true'/'false') to avoid duplicate choices
|
||||
normalized_choices = []
|
||||
for c in choices:
|
||||
s = str(c)
|
||||
if s.lower() in ("true", "false"):
|
||||
normalized_choices.append(s.lower())
|
||||
else:
|
||||
normalized_choices.append(s)
|
||||
|
||||
select_options = [(str(c), str(c)) for c in normalized_choices]
|
||||
# Normalize current value as well
|
||||
cur_val = str(current_val) if current_val is not None else ""
|
||||
if cur_val.lower() in ("true", "false"):
|
||||
cur_val = cur_val.lower()
|
||||
if cur_val not in normalized_choices:
|
||||
select_options.insert(0, (cur_val, cur_val))
|
||||
sel = Select(select_options, value=cur_val, id=inp_id)
|
||||
container.mount(sel)
|
||||
else:
|
||||
row = Horizontal(classes="field-row")
|
||||
container.mount(row)
|
||||
row.mount(Input(value=current_val, id=inp_id, classes="config-input"))
|
||||
self._mount_schema_field(container, field_def, inp_id, current_val, allow_paste=True)
|
||||
render_state["mounted_any"] = True
|
||||
idx += 1
|
||||
|
||||
# Show any other top-level keys not in schema
|
||||
@@ -345,6 +344,85 @@ class ConfigModal(ModalScreen):
|
||||
row.mount(Button("Paste", id=f"paste-{inp_id}", classes="paste-btn"))
|
||||
idx += 1
|
||||
|
||||
def _mount_schema_group(self, container: ScrollableContainer, field_def: Dict[str, Any], state: Dict[str, Any]) -> None:
|
||||
group_name = str(field_def.get("group") or "").strip()
|
||||
if not group_name:
|
||||
return
|
||||
if state.get("group") == group_name:
|
||||
return
|
||||
if state.get("mounted_any"):
|
||||
container.mount(Rule())
|
||||
container.mount(Label(group_name, classes="config-group"))
|
||||
state["group"] = group_name
|
||||
|
||||
def _normalized_select_options(self, choices: Any, current_val: Any) -> tuple[list[tuple[str, str]], str]:
|
||||
select_options: list[tuple[str, str]] = []
|
||||
choice_values: list[str] = []
|
||||
for choice in list(choices or []):
|
||||
if isinstance(choice, tuple) and len(choice) == 2:
|
||||
label = str(choice[0])
|
||||
value = str(choice[1])
|
||||
else:
|
||||
label = str(choice)
|
||||
value = str(choice)
|
||||
if value.lower() in ("true", "false"):
|
||||
value = value.lower()
|
||||
label = value
|
||||
select_options.append((label, value))
|
||||
choice_values.append(value)
|
||||
|
||||
normalized_current = str(current_val) if current_val is not None else ""
|
||||
if normalized_current.lower() in ("true", "false"):
|
||||
normalized_current = normalized_current.lower()
|
||||
if normalized_current not in choice_values:
|
||||
select_options.insert(0, (normalized_current, normalized_current))
|
||||
return select_options, normalized_current
|
||||
|
||||
def _mount_schema_field(
|
||||
self,
|
||||
container: ScrollableContainer,
|
||||
field_def: Dict[str, Any],
|
||||
widget_id: str,
|
||||
current_value: Any,
|
||||
*,
|
||||
allow_paste: bool,
|
||||
) -> None:
|
||||
label_text = str(field_def.get("label") or field_def.get("key") or widget_id)
|
||||
if field_def.get("required"):
|
||||
label_text += " *"
|
||||
container.mount(Label(label_text))
|
||||
|
||||
choices = field_def.get("choices")
|
||||
field_type = str(field_def.get("type") or "").strip().lower()
|
||||
if choices:
|
||||
select_options, normalized_current = self._normalized_select_options(choices, current_value)
|
||||
container.mount(Select(select_options, value=normalized_current, id=widget_id))
|
||||
return
|
||||
|
||||
if field_type in {"multiline", "textarea"}:
|
||||
text_area = TextArea(str(current_value or ""), id=widget_id, classes="config-textarea")
|
||||
placeholder = field_def.get("placeholder")
|
||||
if placeholder:
|
||||
try:
|
||||
text_area.tooltip = str(placeholder)
|
||||
except Exception:
|
||||
pass
|
||||
container.mount(text_area)
|
||||
return
|
||||
|
||||
row = Horizontal(classes="field-row")
|
||||
container.mount(row)
|
||||
input_widget = Input(value=str(current_value or ""), id=widget_id, classes="config-input")
|
||||
if field_def.get("secret") or field_type == "secret":
|
||||
input_widget.password = True
|
||||
if field_def.get("placeholder"):
|
||||
input_widget.placeholder = str(field_def.get("placeholder") or "")
|
||||
elif field_type == "path":
|
||||
input_widget.placeholder = "Path"
|
||||
row.mount(input_widget)
|
||||
if allow_paste:
|
||||
row.mount(Button("Paste", id=f"paste-{widget_id}", classes="paste-btn"))
|
||||
|
||||
def render_stores(self, container: ScrollableContainer) -> None:
|
||||
container.mount(Label("Configured Stores", classes="config-label"))
|
||||
stores = self.config_data.get("store", {})
|
||||
@@ -423,54 +501,18 @@ class ConfigModal(ModalScreen):
|
||||
def render_item_editor(self, container: ScrollableContainer) -> None:
|
||||
item_type = str(self.editing_item_type or "")
|
||||
item_name = str(self.editing_item_name or "")
|
||||
|
||||
provider_schema_map = {}
|
||||
item_schema_map = get_item_schema_map(item_type, item_name)
|
||||
render_state = {"group": None, "mounted_any": False}
|
||||
|
||||
# Parse item_type for store-{stype} or just provider
|
||||
if item_type.startswith("store-"):
|
||||
stype = item_type.replace("store-", "")
|
||||
container.mount(Label(f"Editing Store: {item_name} ({stype})", classes="config-label"))
|
||||
section = self.config_data.get("store", {}).get(stype, {}).get(item_name, {})
|
||||
|
||||
# Fetch Store schema
|
||||
classes = _discover_store_classes()
|
||||
if stype in classes:
|
||||
cls = classes[stype]
|
||||
if hasattr(cls, "config_schema") and callable(cls.config_schema):
|
||||
for field_def in cls.config_schema():
|
||||
k = field_def.get("key")
|
||||
if k:
|
||||
provider_schema_map[k.upper()] = field_def
|
||||
|
||||
else:
|
||||
container.mount(Label(f"Editing {item_type.capitalize()}: {item_name}", classes="config-label"))
|
||||
section = self.config_data.get(item_type, {}).get(item_name, {})
|
||||
|
||||
# Fetch Provider schema
|
||||
if item_type == "provider":
|
||||
from ProviderCore.registry import get_plugin_class
|
||||
try:
|
||||
pcls = get_plugin_class(item_name)
|
||||
if pcls and hasattr(pcls, "config_schema") and callable(pcls.config_schema):
|
||||
for field_def in pcls.config_schema():
|
||||
k = field_def.get("key")
|
||||
if k:
|
||||
provider_schema_map[k.upper()] = field_def
|
||||
except Exception:
|
||||
logger.exception("Failed to retrieve provider config_schema")
|
||||
# Fetch Tool schema
|
||||
if item_type == "tool":
|
||||
try:
|
||||
import importlib
|
||||
mod = importlib.import_module(f"tool.{item_name}")
|
||||
if hasattr(mod, "config_schema") and callable(mod.config_schema):
|
||||
for field_def in mod.config_schema():
|
||||
k = field_def.get("key")
|
||||
if k:
|
||||
provider_schema_map[k.upper()] = field_def
|
||||
except Exception:
|
||||
logger.exception("Failed to retrieve tool config_schema")
|
||||
|
||||
|
||||
# Use columns for better layout of inputs with paste buttons
|
||||
container.mount(Label("Edit Settings"))
|
||||
# render_item_editor will handle the inputs for us if we set these
|
||||
@@ -504,144 +546,53 @@ class ConfigModal(ModalScreen):
|
||||
label_text = k
|
||||
is_secret = False
|
||||
choices = None
|
||||
schema = provider_schema_map.get(k_upper)
|
||||
schema = item_schema_map.get(k_upper)
|
||||
if schema:
|
||||
label_text = schema.get("label") or k
|
||||
if schema.get("required"):
|
||||
label_text += " *"
|
||||
self._mount_schema_group(container, schema, render_state)
|
||||
if schema.get("secret"):
|
||||
is_secret = True
|
||||
choices = schema.get("choices")
|
||||
|
||||
container.mount(Label(label_text))
|
||||
inp_id = f"item-{idx}"
|
||||
self._input_id_map[inp_id] = k
|
||||
|
||||
if choices:
|
||||
# Select takes a list of (label, value) tuples; normalize boolean-like values
|
||||
select_options = []
|
||||
choice_values = []
|
||||
for c in choices:
|
||||
if isinstance(c, tuple) and len(c) == 2:
|
||||
label = str(c[0])
|
||||
val = str(c[1])
|
||||
else:
|
||||
label = str(c)
|
||||
val = str(c)
|
||||
if val.lower() in ("true", "false"):
|
||||
val = val.lower()
|
||||
label = val
|
||||
select_options.append((label, val))
|
||||
choice_values.append(val)
|
||||
|
||||
# If current value not in choices, add it or stay blank
|
||||
current_val = str(v)
|
||||
if current_val.lower() in ("true", "false"):
|
||||
current_val = current_val.lower()
|
||||
if current_val not in choice_values:
|
||||
select_options.insert(0, (current_val, current_val))
|
||||
|
||||
sel = Select(select_options, value=current_val, id=inp_id)
|
||||
container.mount(sel)
|
||||
if schema:
|
||||
self._mount_schema_field(container, schema, inp_id, v, allow_paste=True)
|
||||
else:
|
||||
container.mount(Label(label_text))
|
||||
row = Horizontal(classes="field-row")
|
||||
container.mount(row)
|
||||
inp = Input(value=str(v), id=inp_id, classes="config-input")
|
||||
if is_secret:
|
||||
inp.password = True
|
||||
row.mount(inp)
|
||||
render_state["mounted_any"] = True
|
||||
idx += 1
|
||||
|
||||
# Add required/optional fields from schema that are missing
|
||||
for k_upper, field_def in provider_schema_map.items():
|
||||
for k_upper, field_def in item_schema_map.items():
|
||||
if k_upper not in existing_keys_upper:
|
||||
existing_keys_upper.add(k_upper)
|
||||
key = field_def["key"]
|
||||
label_text = field_def.get("label") or key
|
||||
if field_def.get("required"):
|
||||
label_text += " *"
|
||||
|
||||
self._mount_schema_group(container, field_def, render_state)
|
||||
default_val = str(field_def.get("default") or "")
|
||||
choices = field_def.get("choices")
|
||||
|
||||
container.mount(Label(label_text))
|
||||
inp_id = f"item-{idx}"
|
||||
self._input_id_map[inp_id] = key
|
||||
|
||||
if choices:
|
||||
select_options = []
|
||||
choice_values = []
|
||||
for c in choices:
|
||||
if isinstance(c, tuple) and len(c) == 2:
|
||||
label = str(c[0])
|
||||
val = str(c[1])
|
||||
else:
|
||||
label = str(c)
|
||||
val = str(c)
|
||||
if val.lower() in ("true", "false"):
|
||||
val = val.lower()
|
||||
label = val
|
||||
select_options.append((label, val))
|
||||
choice_values.append(val)
|
||||
|
||||
# Normalize default/current value
|
||||
current_val = str(default_val) if default_val is not None else ""
|
||||
if current_val.lower() in ("true", "false"):
|
||||
current_val = current_val.lower()
|
||||
if current_val not in choice_values:
|
||||
select_options.insert(0, (current_val, current_val))
|
||||
|
||||
sel = Select(select_options, value=current_val, id=inp_id)
|
||||
container.mount(sel)
|
||||
else:
|
||||
row = Horizontal(classes="field-row")
|
||||
container.mount(row)
|
||||
inp = Input(value=default_val, id=inp_id, classes="config-input")
|
||||
if field_def.get("secret"):
|
||||
inp.password = True
|
||||
if field_def.get("placeholder"):
|
||||
inp.placeholder = field_def.get("placeholder")
|
||||
row.mount(inp)
|
||||
row.mount(Button("Paste", id=f"paste-{inp_id}", classes="paste-btn"))
|
||||
self._mount_schema_field(container, field_def, inp_id, default_val, allow_paste=True)
|
||||
render_state["mounted_any"] = True
|
||||
idx += 1
|
||||
|
||||
# If it's a store, we might have required keys (legacy check fallback)
|
||||
if item_type.startswith("store-"):
|
||||
stype = item_type.replace("store-", "")
|
||||
classes = _discover_store_classes()
|
||||
if stype in classes:
|
||||
required_keys = _required_keys_for(classes[stype])
|
||||
for rk in required_keys:
|
||||
# Case-insensitive deduplication (fix path vs PATH)
|
||||
if rk.upper() not in existing_keys_upper:
|
||||
container.mount(Label(rk))
|
||||
inp_id = f"item-{idx}"
|
||||
self._input_id_map[inp_id] = rk
|
||||
row = Horizontal(classes="field-row")
|
||||
container.mount(row)
|
||||
row.mount(Input(value="", id=inp_id, classes="config-input"))
|
||||
idx += 1
|
||||
|
||||
# If it's a provider, we might have required keys (legacy check fallback)
|
||||
if item_type == "provider":
|
||||
# 2. Legacy required_config_keys
|
||||
from ProviderCore.registry import get_plugin_class
|
||||
try:
|
||||
pcls = get_plugin_class(item_name)
|
||||
if pcls:
|
||||
required_keys = pcls.required_config_keys()
|
||||
for rk in required_keys:
|
||||
if rk.upper() not in existing_keys_upper:
|
||||
container.mount(Label(rk))
|
||||
inp_id = f"item-{idx}"
|
||||
self._input_id_map[inp_id] = rk
|
||||
row = Horizontal(classes="field-row")
|
||||
container.mount(row)
|
||||
row.mount(Input(value="", id=inp_id, classes="config-input"))
|
||||
row.mount(Button("Paste", id=f"paste-{inp_id}", classes="paste-btn"))
|
||||
idx += 1
|
||||
except Exception:
|
||||
logger.exception("Failed to build required config inputs for provider/tool")
|
||||
|
||||
for required_key in get_required_config_keys(item_type, item_name):
|
||||
if required_key.upper() in existing_keys_upper:
|
||||
continue
|
||||
existing_keys_upper.add(required_key.upper())
|
||||
container.mount(Label(required_key))
|
||||
inp_id = f"item-{idx}"
|
||||
self._input_id_map[inp_id] = required_key
|
||||
row = Horizontal(classes="field-row")
|
||||
container.mount(row)
|
||||
row.mount(Input(value="", id=inp_id, classes="config-input"))
|
||||
row.mount(Button("Paste", id=f"paste-{inp_id}", classes="paste-btn"))
|
||||
idx += 1
|
||||
|
||||
if (
|
||||
item_type == "provider"
|
||||
@@ -875,48 +826,13 @@ class ConfigModal(ModalScreen):
|
||||
self.notify(f"Save failed: {exc}", severity="error", timeout=10)
|
||||
self.refresh_view()
|
||||
elif bid == "add-store-btn":
|
||||
all_classes = _discover_store_classes()
|
||||
options = []
|
||||
for stype, cls in all_classes.items():
|
||||
if hasattr(cls, "config_schema") and callable(cls.config_schema):
|
||||
try:
|
||||
if cls.config_schema():
|
||||
options.append(stype)
|
||||
except Exception:
|
||||
logger.exception("Failed to inspect store class config_schema for '%s'", stype)
|
||||
options = get_configurable_store_types()
|
||||
self.app.push_screen(SelectionModal("Select Store Type", options), callback=self.on_store_type_selected)
|
||||
elif bid == "add-provider-btn":
|
||||
provider_names = list(list_plugins().keys())
|
||||
options = []
|
||||
from ProviderCore.registry import get_plugin_class
|
||||
for ptype in provider_names:
|
||||
pcls = get_plugin_class(ptype)
|
||||
if pcls and hasattr(pcls, "config_schema") and callable(pcls.config_schema):
|
||||
try:
|
||||
if pcls.config_schema():
|
||||
options.append(ptype)
|
||||
except Exception:
|
||||
logger.exception("Failed to inspect provider class config_schema for '%s'", ptype)
|
||||
options = get_configurable_provider_types()
|
||||
self.app.push_screen(SelectionModal("Select Plugin Type", options), callback=self.on_provider_type_selected)
|
||||
elif bid == "add-tool-btn":
|
||||
# Discover tool modules that advertise a config_schema()
|
||||
options = []
|
||||
try:
|
||||
import pkgutil
|
||||
import importlib
|
||||
import tool as _tool_pkg
|
||||
|
||||
for _mod in pkgutil.iter_modules(_tool_pkg.__path__):
|
||||
try:
|
||||
mod = importlib.import_module(f"tool.{_mod.name}")
|
||||
if hasattr(mod, "config_schema") and callable(mod.config_schema):
|
||||
options.append(_mod.name)
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
# Fallback to known entry
|
||||
options = ["ytdlp"]
|
||||
|
||||
options = get_configurable_tool_types() or ["ytdlp"]
|
||||
if options:
|
||||
options.sort()
|
||||
self.app.push_screen(SelectionModal("Select Tool", options), callback=self.on_tool_type_selected)
|
||||
@@ -1033,26 +949,7 @@ class ConfigModal(ModalScreen):
|
||||
self.config_data["store"][stype] = {}
|
||||
|
||||
# Default config for the new store
|
||||
new_config = {"NAME": new_name}
|
||||
classes = _discover_store_classes()
|
||||
if stype in classes:
|
||||
cls = classes[stype]
|
||||
# Use schema for defaults if present
|
||||
if hasattr(cls, "config_schema") and callable(cls.config_schema):
|
||||
for field_def in cls.config_schema():
|
||||
key = field_def.get("key")
|
||||
if key:
|
||||
val = field_def.get("default", "")
|
||||
# Don't override NAME if we already set it to new_stype
|
||||
if key.upper() == "NAME":
|
||||
continue
|
||||
new_config[key] = val
|
||||
else:
|
||||
# Fallback to required keys list
|
||||
required = _required_keys_for(cls)
|
||||
for rk in required:
|
||||
if rk.upper() != "NAME":
|
||||
new_config[rk] = ""
|
||||
new_config = build_default_store_config(stype, new_name)
|
||||
|
||||
self.config_data["store"][stype][new_name] = new_config
|
||||
self.editing_item_type = f"store-{stype}"
|
||||
@@ -1067,25 +964,7 @@ class ConfigModal(ModalScreen):
|
||||
|
||||
# For providers, they are usually top-level entries in 'provider' dict
|
||||
if ptype not in self.config_data["provider"]:
|
||||
from ProviderCore.registry import get_plugin_class
|
||||
try:
|
||||
pcls = get_plugin_class(ptype)
|
||||
new_config = {}
|
||||
if pcls:
|
||||
# Use schema for defaults
|
||||
if hasattr(pcls, "config_schema") and callable(pcls.config_schema):
|
||||
for field_def in pcls.config_schema():
|
||||
key = field_def.get("key")
|
||||
if key:
|
||||
new_config[key] = field_def.get("default", "")
|
||||
else:
|
||||
# Fallback to legacy required keys
|
||||
required = pcls.required_config_keys()
|
||||
for rk in required:
|
||||
new_config[rk] = ""
|
||||
self.config_data["provider"][ptype] = new_config
|
||||
except Exception:
|
||||
self.config_data["provider"][ptype] = {}
|
||||
self.config_data["provider"][ptype] = build_default_provider_config(ptype)
|
||||
|
||||
self.editing_item_type = "provider"
|
||||
self.editing_item_name = ptype
|
||||
@@ -1099,18 +978,7 @@ class ConfigModal(ModalScreen):
|
||||
self.config_data["tool"] = {}
|
||||
|
||||
if tname not in self.config_data["tool"]:
|
||||
new_config = {}
|
||||
try:
|
||||
import importlib
|
||||
mod = importlib.import_module(f"tool.{tname}")
|
||||
if hasattr(mod, "config_schema") and callable(mod.config_schema):
|
||||
for field_def in mod.config_schema():
|
||||
key = field_def.get("key")
|
||||
if key:
|
||||
new_config[key] = field_def.get("default", "")
|
||||
except Exception:
|
||||
logger.exception("Failed to load config_schema for tool '%s'", tname)
|
||||
self.config_data["tool"][tname] = new_config
|
||||
self.config_data["tool"][tname] = build_default_tool_config(tname)
|
||||
|
||||
self.editing_item_type = "tool"
|
||||
self.editing_item_name = tname
|
||||
@@ -1182,7 +1050,7 @@ class ConfigModal(ModalScreen):
|
||||
|
||||
def _synchronize_inputs_to_config(self) -> None:
|
||||
"""Capture current input/select values before saving."""
|
||||
widgets = list(self.query(Input)) + list(self.query(Select))
|
||||
widgets = list(self.query(Input)) + list(self.query(Select)) + list(self.query(TextArea))
|
||||
for widget in widgets:
|
||||
widget_id = widget.id
|
||||
if not widget_id or widget_id not in self._input_id_map:
|
||||
@@ -1192,6 +1060,8 @@ class ConfigModal(ModalScreen):
|
||||
if widget.value == Select.BLANK:
|
||||
continue
|
||||
value = widget.value
|
||||
elif isinstance(widget, TextArea):
|
||||
value = widget.text
|
||||
else:
|
||||
value = widget.value
|
||||
|
||||
@@ -1696,6 +1566,11 @@ class ConfigModal(ModalScreen):
|
||||
if event.input.id:
|
||||
self._update_config_value(event.input.id, event.value)
|
||||
|
||||
@on(TextArea.Changed)
|
||||
def on_text_area_changed(self, event: TextArea.Changed) -> None:
|
||||
if event.text_area.id:
|
||||
self._update_config_value(event.text_area.id, event.text_area.text)
|
||||
|
||||
@on(Select.Changed)
|
||||
def on_select_changed(self, event: Select.Changed) -> None:
|
||||
if event.select.id:
|
||||
@@ -1860,52 +1735,18 @@ class ConfigModal(ModalScreen):
|
||||
|
||||
item_type = str(self.editing_item_type or "")
|
||||
item_name = str(self.editing_item_name or "")
|
||||
|
||||
required_keys = []
|
||||
section = {}
|
||||
|
||||
if item_type.startswith("store-"):
|
||||
stype = item_type.replace("store-", "")
|
||||
classes = _discover_store_classes()
|
||||
if stype in classes:
|
||||
required_keys = list(_required_keys_for(classes[stype]))
|
||||
section = self.config_data.get("store", {}).get(stype, {}).get(item_name, {})
|
||||
elif item_type == "provider":
|
||||
from ProviderCore.registry import get_plugin_class
|
||||
try:
|
||||
pcls = get_plugin_class(item_name)
|
||||
if pcls:
|
||||
# Collect required keys from schema
|
||||
if hasattr(pcls, "config_schema") and callable(pcls.config_schema):
|
||||
for field_def in pcls.config_schema():
|
||||
if field_def.get("required"):
|
||||
k = field_def.get("key")
|
||||
if k and k not in required_keys:
|
||||
required_keys.append(k)
|
||||
|
||||
# Merge with legacy required keys
|
||||
for rk in pcls.required_config_keys():
|
||||
if rk not in required_keys:
|
||||
required_keys.append(rk)
|
||||
except Exception:
|
||||
logger.exception("Failed to inspect provider class '%s' for required keys", item_name)
|
||||
section = self.config_data.get("provider", {}).get(item_name, {})
|
||||
elif item_type == "tool":
|
||||
try:
|
||||
import importlib
|
||||
mod = importlib.import_module(f"tool.{item_name}")
|
||||
if hasattr(mod, "config_schema") and callable(mod.config_schema):
|
||||
for field_def in mod.config_schema():
|
||||
if field_def.get("required"):
|
||||
k = field_def.get("key")
|
||||
if k and k not in required_keys:
|
||||
required_keys.append(k)
|
||||
except Exception:
|
||||
logger.exception("Failed to inspect tool module 'tool.%s' for required keys", item_name)
|
||||
section = self.config_data.get("tool", {}).get(item_name, {})
|
||||
|
||||
# Check required keys
|
||||
for rk in required_keys:
|
||||
for rk in get_required_config_keys(item_type, item_name):
|
||||
# Case-insensitive lookup for the required key in the current section
|
||||
val = None
|
||||
rk_upper = rk.upper()
|
||||
|
||||
Reference in New Issue
Block a user