changes to config screen
This commit is contained in:
parent
c05a459fc4
commit
192c2b7363
1 changed files with 155 additions and 91 deletions
|
|
@ -1,17 +1,21 @@
|
||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
from cognee.cli.reference import SupportsCliCommand
|
from cognee.cli.reference import SupportsCliCommand
|
||||||
from cognee.cli import DEFAULT_DOCS_URL
|
from cognee.cli import DEFAULT_DOCS_URL
|
||||||
from cognee.cli.exceptions import CliCommandException
|
from cognee.cli.exceptions import CliCommandException
|
||||||
|
|
||||||
from textual.app import App, ComposeResult
|
try:
|
||||||
from textual.screen import Screen
|
from textual.app import App, ComposeResult
|
||||||
from textual.widgets import DataTable, Input, Label, Button, Static
|
from textual.screen import Screen
|
||||||
from textual.containers import Container, Horizontal
|
from textual.widgets import DataTable, Input, Label, Button, Static
|
||||||
from textual.binding import Binding
|
from textual.containers import Container, Horizontal
|
||||||
|
from textual.binding import Binding
|
||||||
from cognee.cli.tui.base_screen import BaseTUIScreen
|
from cognee.cli.tui.base_screen import BaseTUIScreen
|
||||||
|
except ImportError:
|
||||||
|
# Handle case where textual is not installed to prevent import errors at module level
|
||||||
|
BaseTUIScreen = object
|
||||||
|
|
||||||
|
|
||||||
class ConfirmModal(Screen):
|
class ConfirmModal(Screen):
|
||||||
|
|
@ -27,17 +31,32 @@ class ConfirmModal(Screen):
|
||||||
}
|
}
|
||||||
|
|
||||||
#confirm-dialog {
|
#confirm-dialog {
|
||||||
width: 50;
|
width: 60;
|
||||||
height: 11;
|
height: auto;
|
||||||
border: thick $warning;
|
border: thick $warning;
|
||||||
background: $surface;
|
background: $surface;
|
||||||
padding: 1 2;
|
padding: 1 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#confirm-title {
|
||||||
|
text-align: center;
|
||||||
|
text-style: bold;
|
||||||
|
margin-bottom: 1;
|
||||||
|
}
|
||||||
|
|
||||||
#confirm-message {
|
#confirm-message {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 2;
|
margin-bottom: 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.tui-dialog-buttons {
|
||||||
|
align: center middle;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
margin: 0 1;
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, key: str, default_value: str):
|
def __init__(self, key: str, default_value: str):
|
||||||
|
|
@ -47,9 +66,10 @@ class ConfirmModal(Screen):
|
||||||
|
|
||||||
def compose(self) -> ComposeResult:
|
def compose(self) -> ComposeResult:
|
||||||
with Container(id="confirm-dialog"):
|
with Container(id="confirm-dialog"):
|
||||||
yield Label("⚠ Reset Configuration", classes="tui-dialog-title")
|
yield Label("⚠ Reset Configuration", id="confirm-title")
|
||||||
yield Label(f"Reset '{self.key}' to default?", id="confirm-message")
|
yield Label(f"Are you sure you want to reset '{self.key}'?", id="confirm-message")
|
||||||
yield Label(f"Default value: {self.default_value}", id="confirm-message")
|
yield Label(f"It will revert to: {self.default_value}", classes="dim-text")
|
||||||
|
|
||||||
with Horizontal(classes="tui-dialog-buttons"):
|
with Horizontal(classes="tui-dialog-buttons"):
|
||||||
yield Button("Reset", variant="error", id="confirm-btn")
|
yield Button("Reset", variant="error", id="confirm-btn")
|
||||||
yield Button("Cancel", variant="default", id="cancel-btn")
|
yield Button("Cancel", variant="default", id="cancel-btn")
|
||||||
|
|
@ -65,7 +85,7 @@ class ConfirmModal(Screen):
|
||||||
|
|
||||||
|
|
||||||
class ConfigTUIScreen(BaseTUIScreen):
|
class ConfigTUIScreen(BaseTUIScreen):
|
||||||
"""Main config TUI screen with inline editing."""
|
"""Main config TUI screen with inline editing and live data fetching."""
|
||||||
|
|
||||||
BINDINGS = [
|
BINDINGS = [
|
||||||
Binding("q", "quit_app", "Quit"),
|
Binding("q", "quit_app", "Quit"),
|
||||||
|
|
@ -96,16 +116,22 @@ class ConfigTUIScreen(BaseTUIScreen):
|
||||||
|
|
||||||
#edit-label {
|
#edit-label {
|
||||||
color: $text-muted;
|
color: $text-muted;
|
||||||
margin-bottom: 0;
|
margin-bottom: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#inline-input {
|
#inline-input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dim-text {
|
||||||
|
color: $text-muted;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 1;
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Config key mappings with defaults (from existing config.py)
|
# Config key mappings: Key -> (Reset Method Name, Default Value)
|
||||||
CONFIG_KEYS = {
|
CONFIG_MAP = {
|
||||||
"llm_provider": ("set_llm_provider", "openai"),
|
"llm_provider": ("set_llm_provider", "openai"),
|
||||||
"llm_model": ("set_llm_model", "gpt-5-mini"),
|
"llm_model": ("set_llm_model", "gpt-5-mini"),
|
||||||
"llm_api_key": ("set_llm_api_key", ""),
|
"llm_api_key": ("set_llm_api_key", ""),
|
||||||
|
|
@ -114,8 +140,8 @@ class ConfigTUIScreen(BaseTUIScreen):
|
||||||
"vector_db_provider": ("set_vector_db_provider", "lancedb"),
|
"vector_db_provider": ("set_vector_db_provider", "lancedb"),
|
||||||
"vector_db_url": ("set_vector_db_url", ""),
|
"vector_db_url": ("set_vector_db_url", ""),
|
||||||
"vector_db_key": ("set_vector_db_key", ""),
|
"vector_db_key": ("set_vector_db_key", ""),
|
||||||
"chunk_size": ("set_chunk_size", "1500"),
|
"chunk_size": ("set_chunk_size", 1500),
|
||||||
"chunk_overlap": ("set_chunk_overlap", "10"),
|
"chunk_overlap": ("set_chunk_overlap", 10),
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
@ -125,12 +151,14 @@ class ConfigTUIScreen(BaseTUIScreen):
|
||||||
def compose_content(self) -> ComposeResult:
|
def compose_content(self) -> ComposeResult:
|
||||||
with Container(classes="tui-main-container"):
|
with Container(classes="tui-main-container"):
|
||||||
with Container(classes="tui-title-wrapper"):
|
with Container(classes="tui-title-wrapper"):
|
||||||
yield Static("⚙️ Change Config", classes="tui-title-bordered")
|
yield Static("⚙️ Configuration Manager", classes="tui-title-bordered")
|
||||||
|
|
||||||
with Container(classes="tui-bordered-wrapper"):
|
with Container(classes="tui-bordered-wrapper"):
|
||||||
table = DataTable()
|
table = DataTable(id="config-table")
|
||||||
table.cursor_type = "row"
|
table.cursor_type = "row"
|
||||||
table.zebra_stripes = True
|
table.zebra_stripes = True
|
||||||
yield table
|
yield table
|
||||||
|
|
||||||
with Container(id="inline-edit-container"):
|
with Container(id="inline-edit-container"):
|
||||||
yield Label("", id="edit-label")
|
yield Label("", id="edit-label")
|
||||||
yield Input(placeholder="Enter new value", id="inline-input")
|
yield Input(placeholder="Enter new value", id="inline-input")
|
||||||
|
|
@ -142,121 +170,127 @@ class ConfigTUIScreen(BaseTUIScreen):
|
||||||
)
|
)
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
|
"""Initialize the table with columns and current data."""
|
||||||
table = self.query_one(DataTable)
|
table = self.query_one(DataTable)
|
||||||
key_col, value_col = table.add_columns("KEY", "VALUE")
|
table.add_columns("Configuration Key", "Current Value")
|
||||||
# Add all config keys
|
|
||||||
for key, (method, default) in self.CONFIG_KEYS.items():
|
|
||||||
display_default = "(empty)" if default == "" else str(default)
|
|
||||||
table.add_row(key, display_default)
|
|
||||||
|
|
||||||
|
self._load_table_data()
|
||||||
table.focus()
|
table.focus()
|
||||||
|
|
||||||
def action_cursor_up(self) -> None:
|
def _load_table_data(self) -> None:
|
||||||
"""Move cursor up in the table."""
|
"""Fetch real config values and populate the table."""
|
||||||
if self.editing_key:
|
|
||||||
return # Don't navigate while editing
|
|
||||||
table = self.query_one(DataTable)
|
table = self.query_one(DataTable)
|
||||||
table.action_cursor_up()
|
table.clear()
|
||||||
|
|
||||||
|
try:
|
||||||
|
import cognee
|
||||||
|
# Check if get method exists, otherwise warn
|
||||||
|
has_get = hasattr(cognee.config, "get")
|
||||||
|
except ImportError:
|
||||||
|
has_get = False
|
||||||
|
self.notify("Could not import cognee config", severity="error")
|
||||||
|
|
||||||
|
for key, (_, default_val) in self.CONFIG_MAP.items():
|
||||||
|
value_display = "N/A"
|
||||||
|
|
||||||
|
if has_get:
|
||||||
|
try:
|
||||||
|
raw_val = cognee.config.get(key)
|
||||||
|
if raw_val is None:
|
||||||
|
raw_val = default_val
|
||||||
|
value_display = str(raw_val) if raw_val is not None else "(empty)"
|
||||||
|
except Exception:
|
||||||
|
value_display = "Error fetching value"
|
||||||
|
|
||||||
|
table.add_row(key, value_display, key=key)
|
||||||
|
|
||||||
|
def action_cursor_up(self) -> None:
|
||||||
|
if self.editing_key: return
|
||||||
|
self.query_one(DataTable).action_cursor_up()
|
||||||
|
|
||||||
def action_cursor_down(self) -> None:
|
def action_cursor_down(self) -> None:
|
||||||
"""Move cursor down in the table."""
|
if self.editing_key: return
|
||||||
if self.editing_key:
|
self.query_one(DataTable).action_cursor_down()
|
||||||
return # Don't navigate while editing
|
|
||||||
table = self.query_one(DataTable)
|
|
||||||
table.action_cursor_down()
|
|
||||||
|
|
||||||
def action_cancel_or_back(self) -> None:
|
def action_cancel_or_back(self) -> None:
|
||||||
"""Cancel editing or go back to main menu."""
|
|
||||||
if self.editing_key:
|
if self.editing_key:
|
||||||
self._cancel_edit()
|
self._cancel_edit()
|
||||||
else:
|
else:
|
||||||
self.app.pop_screen()
|
self.app.pop_screen()
|
||||||
|
|
||||||
def action_quit_app(self) -> None:
|
def action_quit_app(self) -> None:
|
||||||
"""Quit the entire application."""
|
|
||||||
self.app.exit()
|
self.app.exit()
|
||||||
|
|
||||||
def action_edit(self) -> None:
|
def action_edit(self) -> None:
|
||||||
"""Start inline editing for the selected config value."""
|
"""Start inline editing for the selected config value."""
|
||||||
if self.editing_key:
|
if self.editing_key: return
|
||||||
return # Already editing
|
|
||||||
|
|
||||||
table = self.query_one(DataTable)
|
table = self.query_one(DataTable)
|
||||||
if table.cursor_coordinate.row < 0:
|
if table.cursor_row < 0: return
|
||||||
return
|
|
||||||
|
|
||||||
row_data = table.get_row_at(table.cursor_coordinate.row)
|
# Get row data using the cursor
|
||||||
key = str(row_data[0])
|
row_key = table.coordinate_to_cell_key(table.cursor_coordinate).row_key
|
||||||
default_value = str(row_data[1])
|
current_val = table.get_cell(row_key, list(table.columns.keys())[1]) # Get value column
|
||||||
|
|
||||||
self.editing_key = key
|
self.editing_key = str(row_key.value)
|
||||||
|
|
||||||
# Show the inline edit container
|
# Show edit container
|
||||||
edit_container = self.query_one("#inline-edit-container")
|
edit_container = self.query_one("#inline-edit-container")
|
||||||
edit_container.add_class("visible")
|
edit_container.add_class("visible")
|
||||||
|
|
||||||
# Update label and input
|
# Update UI
|
||||||
label = self.query_one("#edit-label", Label)
|
label = self.query_one("#edit-label", Label)
|
||||||
label.update(f"Editing: {key} (default: {default_value})")
|
label.update(f"Editing: [bold]{self.editing_key}[/bold]")
|
||||||
|
|
||||||
input_widget = self.query_one("#inline-input", Input)
|
input_widget = self.query_one("#inline-input", Input)
|
||||||
input_widget.value = ""
|
input_widget.value = ""
|
||||||
input_widget.placeholder = f"Enter new value for {key}"
|
# Don't put "empty" or "N/A" into the input box to save user deleting it
|
||||||
|
if current_val not in ["(empty)", "N/A", "Error fetching value"]:
|
||||||
|
input_widget.value = str(current_val)
|
||||||
|
|
||||||
|
input_widget.placeholder = f"Enter new value for {self.editing_key}"
|
||||||
input_widget.focus()
|
input_widget.focus()
|
||||||
|
|
||||||
def action_confirm_edit(self) -> None:
|
def action_confirm_edit(self) -> None:
|
||||||
"""Confirm the inline edit and save the value."""
|
"""Confirm the inline edit and save the value."""
|
||||||
if not self.editing_key:
|
if not self.editing_key: return
|
||||||
return
|
|
||||||
|
|
||||||
input_widget = self.query_one("#inline-input", Input)
|
input_widget = self.query_one("#inline-input", Input)
|
||||||
value = input_widget.value.strip()
|
value = input_widget.value.strip()
|
||||||
|
|
||||||
if value:
|
# Allow saving even if empty (might mean unset/empty string)
|
||||||
self._save_config(self.editing_key, value)
|
self._save_config(self.editing_key, value)
|
||||||
|
|
||||||
self._cancel_edit()
|
self._cancel_edit()
|
||||||
|
|
||||||
def _cancel_edit(self) -> None:
|
def _cancel_edit(self) -> None:
|
||||||
"""Cancel the current edit and hide the input."""
|
|
||||||
self.editing_key = None
|
self.editing_key = None
|
||||||
|
|
||||||
# Hide the inline edit container
|
|
||||||
edit_container = self.query_one("#inline-edit-container")
|
edit_container = self.query_one("#inline-edit-container")
|
||||||
edit_container.remove_class("visible")
|
edit_container.remove_class("visible")
|
||||||
|
self.query_one("#inline-input", Input).value = ""
|
||||||
# Clear input
|
self.query_one(DataTable).focus()
|
||||||
input_widget = self.query_one("#inline-input", Input)
|
|
||||||
input_widget.value = ""
|
|
||||||
|
|
||||||
# Return focus to table
|
|
||||||
table = self.query_one(DataTable)
|
|
||||||
table.focus()
|
|
||||||
|
|
||||||
def on_input_submitted(self, event: Input.Submitted) -> None:
|
def on_input_submitted(self, event: Input.Submitted) -> None:
|
||||||
"""Handle Enter key in the input field."""
|
|
||||||
if event.input.id == "inline-input" and self.editing_key:
|
if event.input.id == "inline-input" and self.editing_key:
|
||||||
self.action_confirm_edit()
|
self.action_confirm_edit()
|
||||||
|
|
||||||
def action_reset(self) -> None:
|
def action_reset(self) -> None:
|
||||||
"""Reset the selected config to default."""
|
"""Reset the selected config to default."""
|
||||||
table = self.query_one(DataTable)
|
table = self.query_one(DataTable)
|
||||||
|
if table.cursor_row < 0: return
|
||||||
|
|
||||||
if table.cursor_coordinate.row < 0:
|
row_key_obj = table.coordinate_to_cell_key(table.cursor_coordinate).row_key
|
||||||
|
key = str(row_key_obj.value)
|
||||||
|
|
||||||
|
if key not in self.CONFIG_MAP:
|
||||||
|
self.notify(f"Cannot reset '{key}'", severity="warning")
|
||||||
return
|
return
|
||||||
|
|
||||||
row_key = table.get_row_at(table.cursor_coordinate.row)
|
_, default_value = self.CONFIG_MAP[key]
|
||||||
key = str(row_key[0])
|
|
||||||
|
|
||||||
if key not in self.CONFIG_KEYS:
|
|
||||||
return
|
|
||||||
|
|
||||||
method_name, default_value = self.CONFIG_KEYS[key]
|
|
||||||
display_default = "(empty)" if default_value == "" else str(default_value)
|
display_default = "(empty)" if default_value == "" else str(default_value)
|
||||||
|
|
||||||
def handle_confirm_result(confirmed: bool) -> None:
|
def handle_confirm_result(confirmed: bool) -> None:
|
||||||
if confirmed:
|
if confirmed:
|
||||||
self._reset_config(key, method_name, default_value)
|
self._reset_config(key)
|
||||||
|
|
||||||
self.app.push_screen(
|
self.app.push_screen(
|
||||||
ConfirmModal(key, display_default),
|
ConfirmModal(key, display_default),
|
||||||
|
|
@ -264,39 +298,63 @@ class ConfigTUIScreen(BaseTUIScreen):
|
||||||
)
|
)
|
||||||
|
|
||||||
def _save_config(self, key: str, value: str) -> None:
|
def _save_config(self, key: str, value: str) -> None:
|
||||||
"""Save config value using cognee.config.set()."""
|
"""Save config value and update UI."""
|
||||||
try:
|
try:
|
||||||
import cognee
|
import cognee
|
||||||
|
|
||||||
# Try to parse as JSON (numbers, booleans, etc)
|
# Parse value types (restore JSON behavior)
|
||||||
try:
|
try:
|
||||||
parsed_value = json.loads(value)
|
parsed_value = json.loads(value)
|
||||||
except json.JSONDecodeError:
|
except (json.JSONDecodeError, TypeError):
|
||||||
parsed_value = value
|
# If it looks like a boolean but json didn't catch it
|
||||||
|
if value.lower() == "true":
|
||||||
|
parsed_value = True
|
||||||
|
elif value.lower() == "false":
|
||||||
|
parsed_value = False
|
||||||
|
else:
|
||||||
|
parsed_value = value
|
||||||
|
|
||||||
cognee.config.set(key, parsed_value)
|
cognee.config.set(key, parsed_value)
|
||||||
self.notify(f"✓ Set {key} = {parsed_value}", severity="information")
|
self._update_table_row(key, parsed_value)
|
||||||
|
self.notify(f"✓ Set {key}", severity="information")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.notify(f"✗ Failed to set {key}: {str(e)}", severity="error")
|
self.notify(f"✗ Error setting {key}: {str(e)}", severity="error")
|
||||||
|
|
||||||
def _reset_config(self, key: str, method_name: str, default_value: any) -> None:
|
def _reset_config(self, key: str) -> None:
|
||||||
"""Reset config to default using the mapped method."""
|
"""Reset config to default using mapped method and update UI."""
|
||||||
try:
|
try:
|
||||||
import cognee
|
import cognee
|
||||||
|
|
||||||
method = getattr(cognee.config, method_name)
|
method_name, default_value = self.CONFIG_MAP[key]
|
||||||
method(default_value)
|
|
||||||
|
|
||||||
display_default = "(empty)" if default_value == "" else str(default_value)
|
if hasattr(cognee.config, method_name):
|
||||||
self.notify(
|
method = getattr(cognee.config, method_name)
|
||||||
f"✓ Reset {key} to default: {display_default}",
|
method(default_value)
|
||||||
severity="information"
|
|
||||||
)
|
# IMPROVEMENT: Update table immediately
|
||||||
|
self._update_table_row(key, default_value)
|
||||||
|
self.notify(f"✓ Reset {key}", severity="information")
|
||||||
|
else:
|
||||||
|
self.notify(f"✗ Reset method '{method_name}' not found", severity="error")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.notify(f"✗ Failed to reset {key}: {str(e)}", severity="error")
|
self.notify(f"✗ Failed to reset {key}: {str(e)}", severity="error")
|
||||||
|
|
||||||
|
def _update_table_row(self, key: str, value: Any) -> None:
|
||||||
|
"""Helper to update a specific row's value column visually."""
|
||||||
|
table = self.query_one(DataTable)
|
||||||
|
display_val = str(value) if value != "" else "(empty)"
|
||||||
|
|
||||||
|
# 'key' was used as the row_key in add_row, so we can address it directly
|
||||||
|
# The value column is at index 1 (0 is key, 1 is value)
|
||||||
|
try:
|
||||||
|
col_key = list(table.columns.keys())[1]
|
||||||
|
table.update_cell(key, col_key, display_val)
|
||||||
|
except Exception:
|
||||||
|
# Fallback if key update fails, reload all
|
||||||
|
self._load_table_data()
|
||||||
|
|
||||||
|
|
||||||
class ConfigTUICommand(SupportsCliCommand):
|
class ConfigTUICommand(SupportsCliCommand):
|
||||||
"""TUI command for config management."""
|
"""TUI command for config management."""
|
||||||
|
|
@ -310,8 +368,14 @@ class ConfigTUICommand(SupportsCliCommand):
|
||||||
|
|
||||||
def execute(self, args: argparse.Namespace) -> None:
|
def execute(self, args: argparse.Namespace) -> None:
|
||||||
try:
|
try:
|
||||||
|
# Import here to check if Textual is actually installed
|
||||||
|
from textual.app import App
|
||||||
|
|
||||||
class ConfigTUIApp(App):
|
class ConfigTUIApp(App):
|
||||||
"""Simple app wrapper for config TUI."""
|
"""Simple app wrapper for config TUI."""
|
||||||
|
CSS = """
|
||||||
|
Screen { background: $surface; }
|
||||||
|
"""
|
||||||
|
|
||||||
def on_mount(self) -> None:
|
def on_mount(self) -> None:
|
||||||
self.push_screen(ConfigTUIScreen())
|
self.push_screen(ConfigTUIScreen())
|
||||||
|
|
@ -329,4 +393,4 @@ class ConfigTUICommand(SupportsCliCommand):
|
||||||
f"Failed to launch config TUI: {str(ex)}",
|
f"Failed to launch config TUI: {str(ex)}",
|
||||||
docs_url=self.docs_url,
|
docs_url=self.docs_url,
|
||||||
raiseable_exception=ex,
|
raiseable_exception=ex,
|
||||||
)
|
)
|
||||||
Loading…
Add table
Reference in a new issue