fixing docs
This commit is contained in:
parent
0fb35da5dd
commit
3ffab8a3dd
5 changed files with 725 additions and 10 deletions
|
|
@ -3,6 +3,7 @@ Configuration management for xml-pipeline.
|
||||||
|
|
||||||
Handles:
|
Handles:
|
||||||
- Agent configs (~/.xml-pipeline/agents/*.yaml)
|
- Agent configs (~/.xml-pipeline/agents/*.yaml)
|
||||||
|
- Listener configs (~/.xml-pipeline/listeners/*.yaml)
|
||||||
- Organism config (organism.yaml)
|
- Organism config (organism.yaml)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -13,11 +14,21 @@ from .agents import (
|
||||||
CONFIG_DIR,
|
CONFIG_DIR,
|
||||||
AGENTS_DIR,
|
AGENTS_DIR,
|
||||||
)
|
)
|
||||||
|
from .listeners import (
|
||||||
|
ListenerConfigStore,
|
||||||
|
get_listener_config_store,
|
||||||
|
LISTENERS_DIR,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
# Agent config
|
||||||
"AgentConfig",
|
"AgentConfig",
|
||||||
"AgentConfigStore",
|
"AgentConfigStore",
|
||||||
"get_agent_config_store",
|
"get_agent_config_store",
|
||||||
"CONFIG_DIR",
|
"CONFIG_DIR",
|
||||||
"AGENTS_DIR",
|
"AGENTS_DIR",
|
||||||
|
# Listener config
|
||||||
|
"ListenerConfigStore",
|
||||||
|
"get_listener_config_store",
|
||||||
|
"LISTENERS_DIR",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ FEATURES: dict[str, tuple[Callable[[], bool], str]] = {
|
||||||
"search": (lambda: _check_import("duckduckgo_search"), "DuckDuckGo search"),
|
"search": (lambda: _check_import("duckduckgo_search"), "DuckDuckGo search"),
|
||||||
"auth": (lambda: _check_import("pyotp") and _check_import("argon2"), "TOTP auth"),
|
"auth": (lambda: _check_import("pyotp") and _check_import("argon2"), "TOTP auth"),
|
||||||
"server": (lambda: _check_import("websockets"), "WebSocket server"),
|
"server": (lambda: _check_import("websockets"), "WebSocket server"),
|
||||||
|
"lsp": (lambda: _check_import("lsp_client"), "LSP client for config editor"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,31 +2,100 @@
|
||||||
Full-screen text editor using prompt_toolkit.
|
Full-screen text editor using prompt_toolkit.
|
||||||
|
|
||||||
Provides a vim-like editing experience for configuration files.
|
Provides a vim-like editing experience for configuration files.
|
||||||
|
Supports optional LSP integration for YAML files.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Optional, Tuple
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional, Tuple, TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from agentserver.console.lsp import YAMLLSPClient, ASLSClient
|
||||||
|
from typing import Union
|
||||||
|
LSPClientType = Union[YAMLLSPClient, ASLSClient]
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from prompt_toolkit import Application
|
from prompt_toolkit import Application
|
||||||
from prompt_toolkit.buffer import Buffer
|
from prompt_toolkit.buffer import Buffer
|
||||||
from prompt_toolkit.layout import Layout, HSplit, VSplit
|
from prompt_toolkit.layout import Layout, HSplit, VSplit
|
||||||
from prompt_toolkit.layout.containers import Window, ConditionalContainer
|
from prompt_toolkit.layout.containers import Window, ConditionalContainer, Float, FloatContainer
|
||||||
from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl
|
from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl
|
||||||
|
from prompt_toolkit.layout.menus import CompletionsMenu
|
||||||
from prompt_toolkit.key_binding import KeyBindings
|
from prompt_toolkit.key_binding import KeyBindings
|
||||||
from prompt_toolkit.filters import Condition
|
from prompt_toolkit.filters import Condition
|
||||||
from prompt_toolkit.styles import Style
|
from prompt_toolkit.styles import Style
|
||||||
from prompt_toolkit.lexers import PygmentsLexer
|
from prompt_toolkit.lexers import PygmentsLexer
|
||||||
|
from prompt_toolkit.completion import Completer, Completion
|
||||||
|
from prompt_toolkit.document import Document
|
||||||
PROMPT_TOOLKIT_AVAILABLE = True
|
PROMPT_TOOLKIT_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
PROMPT_TOOLKIT_AVAILABLE = False
|
PROMPT_TOOLKIT_AVAILABLE = False
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from pygments.lexers.data import YamlLexer
|
from pygments.lexers.data import YamlLexer
|
||||||
|
from pygments.lexers.javascript import TypeScriptLexer
|
||||||
PYGMENTS_AVAILABLE = True
|
PYGMENTS_AVAILABLE = True
|
||||||
except ImportError:
|
except ImportError:
|
||||||
PYGMENTS_AVAILABLE = False
|
PYGMENTS_AVAILABLE = False
|
||||||
|
|
||||||
|
|
||||||
|
# Supported syntax types and their lexers
|
||||||
|
SYNTAX_LEXERS = {
|
||||||
|
"yaml": "YamlLexer",
|
||||||
|
"typescript": "TypeScriptLexer",
|
||||||
|
"assemblyscript": "TypeScriptLexer", # AS uses TS syntax
|
||||||
|
"ts": "TypeScriptLexer",
|
||||||
|
"as": "TypeScriptLexer",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_lexer_for_syntax(syntax: str) -> Optional[object]:
|
||||||
|
"""
|
||||||
|
Get a Pygments lexer for the given syntax type.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
syntax: Syntax name ("yaml", "typescript", "ts", "as", "assemblyscript")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
PygmentsLexer instance or None
|
||||||
|
"""
|
||||||
|
if not PYGMENTS_AVAILABLE:
|
||||||
|
return None
|
||||||
|
|
||||||
|
syntax_lower = syntax.lower()
|
||||||
|
|
||||||
|
if syntax_lower in ("yaml", "yml"):
|
||||||
|
return PygmentsLexer(YamlLexer)
|
||||||
|
elif syntax_lower in ("typescript", "ts", "assemblyscript", "as"):
|
||||||
|
return PygmentsLexer(TypeScriptLexer)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def detect_syntax_from_path(path: str | Path) -> str:
|
||||||
|
"""
|
||||||
|
Detect syntax type from file extension.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Syntax name for use with get_lexer_for_syntax()
|
||||||
|
"""
|
||||||
|
ext = Path(path).suffix.lower()
|
||||||
|
|
||||||
|
extension_map = {
|
||||||
|
".yaml": "yaml",
|
||||||
|
".yml": "yaml",
|
||||||
|
".ts": "typescript",
|
||||||
|
".as": "assemblyscript",
|
||||||
|
}
|
||||||
|
|
||||||
|
return extension_map.get(ext, "text")
|
||||||
|
|
||||||
|
|
||||||
def edit_text(
|
def edit_text(
|
||||||
initial_text: str,
|
initial_text: str,
|
||||||
title: str = "Editor",
|
title: str = "Editor",
|
||||||
|
|
@ -38,7 +107,7 @@ def edit_text(
|
||||||
Args:
|
Args:
|
||||||
initial_text: Text to edit
|
initial_text: Text to edit
|
||||||
title: Title shown in header
|
title: Title shown in header
|
||||||
syntax: Syntax highlighting ("yaml", "text")
|
syntax: Syntax highlighting ("yaml", "typescript", "ts", "as", "text")
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
(edited_text, saved) - edited_text is None if cancelled
|
(edited_text, saved) - edited_text is None if cancelled
|
||||||
|
|
@ -79,9 +148,7 @@ def edit_text(
|
||||||
event.app.exit()
|
event.app.exit()
|
||||||
|
|
||||||
# Syntax highlighting
|
# Syntax highlighting
|
||||||
lexer = None
|
lexer = get_lexer_for_syntax(syntax)
|
||||||
if PYGMENTS_AVAILABLE and syntax == "yaml":
|
|
||||||
lexer = PygmentsLexer(YamlLexer)
|
|
||||||
|
|
||||||
# Layout
|
# Layout
|
||||||
header = Window(
|
header = Window(
|
||||||
|
|
@ -225,3 +292,461 @@ def edit_with_system_editor(filepath: str) -> bool:
|
||||||
return mtime_before is None or mtime_after > mtime_before
|
return mtime_before is None or mtime_after > mtime_before
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# LSP-Enhanced Editor
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class LSPEditor:
|
||||||
|
"""
|
||||||
|
Full-screen editor with optional LSP integration.
|
||||||
|
|
||||||
|
Provides:
|
||||||
|
- Syntax highlighting (YAML, TypeScript/AssemblyScript via Pygments)
|
||||||
|
- Autocompletion (LSP when available)
|
||||||
|
- Inline diagnostics (LSP when available)
|
||||||
|
- Hover documentation on F1 (LSP when available)
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
# YAML config editing
|
||||||
|
editor = LSPEditor(schema_type="listener")
|
||||||
|
edited_text, saved = await editor.edit(initial_text, title="greeter.yaml")
|
||||||
|
|
||||||
|
# AssemblyScript listener editing
|
||||||
|
editor = LSPEditor(syntax="assemblyscript")
|
||||||
|
edited_text, saved = await editor.edit(source_code, title="handler.ts")
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
schema_type: Optional[str] = None,
|
||||||
|
schema_uri: Optional[str] = None,
|
||||||
|
syntax: str = "yaml",
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Initialize the LSP editor.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
schema_type: Schema type ("organism" or "listener") for YAML modeline
|
||||||
|
schema_uri: Explicit schema URI to use
|
||||||
|
syntax: Syntax highlighting ("yaml", "typescript", "assemblyscript", "ts", "as")
|
||||||
|
"""
|
||||||
|
self.schema_type = schema_type
|
||||||
|
self.schema_uri = schema_uri
|
||||||
|
self.syntax = syntax
|
||||||
|
self._lsp_client: Optional["LSPClientType"] = None
|
||||||
|
self._lsp_type: Optional[str] = None # "yaml" or "assemblyscript"
|
||||||
|
self._diagnostics_text = ""
|
||||||
|
self._hover_text = ""
|
||||||
|
self._show_hover = False
|
||||||
|
self._document_version = 0
|
||||||
|
|
||||||
|
async def edit(
|
||||||
|
self,
|
||||||
|
initial_text: str,
|
||||||
|
title: str = "Editor",
|
||||||
|
document_uri: Optional[str] = None,
|
||||||
|
) -> Tuple[Optional[str], bool]:
|
||||||
|
"""
|
||||||
|
Open the editor with LSP support.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
initial_text: Initial content to edit
|
||||||
|
title: Title shown in header
|
||||||
|
document_uri: URI for LSP (auto-generated if not provided)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(edited_text, saved) - edited_text is None if cancelled
|
||||||
|
"""
|
||||||
|
if not PROMPT_TOOLKIT_AVAILABLE:
|
||||||
|
print("Error: prompt_toolkit not installed")
|
||||||
|
return None, False
|
||||||
|
|
||||||
|
# Determine LSP type based on syntax
|
||||||
|
syntax_lower = self.syntax.lower()
|
||||||
|
if syntax_lower in ("yaml", "yml"):
|
||||||
|
self._lsp_type = "yaml"
|
||||||
|
elif syntax_lower in ("typescript", "ts", "assemblyscript", "as"):
|
||||||
|
self._lsp_type = "assemblyscript"
|
||||||
|
else:
|
||||||
|
self._lsp_type = None
|
||||||
|
|
||||||
|
# Try to get appropriate LSP client
|
||||||
|
try:
|
||||||
|
from agentserver.console.lsp import get_lsp_manager, LSPServerType
|
||||||
|
manager = get_lsp_manager()
|
||||||
|
|
||||||
|
if self._lsp_type == "yaml":
|
||||||
|
self._lsp_client = await manager.get_yaml_client()
|
||||||
|
elif self._lsp_type == "assemblyscript":
|
||||||
|
self._lsp_client = await manager.get_asls_client()
|
||||||
|
else:
|
||||||
|
self._lsp_client = None
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"LSP not available: {e}")
|
||||||
|
self._lsp_client = None
|
||||||
|
|
||||||
|
# Generate document URI with appropriate extension
|
||||||
|
if document_uri is None:
|
||||||
|
ext = ".yaml" if self._lsp_type == "yaml" else ".ts"
|
||||||
|
document_uri = f"file:///temp/{title.replace(' ', '_')}{ext}"
|
||||||
|
|
||||||
|
# Ensure schema modeline is present (YAML only)
|
||||||
|
if self._lsp_type == "yaml":
|
||||||
|
initial_text = self._ensure_modeline(initial_text)
|
||||||
|
|
||||||
|
# Open document in LSP
|
||||||
|
if self._lsp_client:
|
||||||
|
await self._lsp_client.did_open(document_uri, initial_text)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await self._run_editor(initial_text, title, document_uri)
|
||||||
|
finally:
|
||||||
|
# Close document in LSP
|
||||||
|
if self._lsp_client:
|
||||||
|
await self._lsp_client.did_close(document_uri)
|
||||||
|
try:
|
||||||
|
from agentserver.console.lsp import get_lsp_manager, LSPServerType
|
||||||
|
manager = get_lsp_manager()
|
||||||
|
if self._lsp_type == "yaml":
|
||||||
|
await manager.release_client(LSPServerType.YAML)
|
||||||
|
elif self._lsp_type == "assemblyscript":
|
||||||
|
await manager.release_client(LSPServerType.ASSEMBLYSCRIPT)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def _ensure_modeline(self, text: str) -> str:
|
||||||
|
"""Ensure YAML has schema modeline if schema_type is set."""
|
||||||
|
if self.schema_type is None:
|
||||||
|
return text
|
||||||
|
|
||||||
|
modeline = f"# yaml-language-server: $schema=~/.xml-pipeline/schemas/{self.schema_type}.schema.json"
|
||||||
|
|
||||||
|
# Check if modeline already exists
|
||||||
|
lines = text.split("\n")
|
||||||
|
for line in lines[:3]: # Check first 3 lines
|
||||||
|
if "yaml-language-server" in line and "$schema" in line:
|
||||||
|
return text
|
||||||
|
|
||||||
|
# Add modeline at the top
|
||||||
|
return modeline + "\n" + text
|
||||||
|
|
||||||
|
async def _run_editor(
|
||||||
|
self,
|
||||||
|
initial_text: str,
|
||||||
|
title: str,
|
||||||
|
uri: str,
|
||||||
|
) -> Tuple[Optional[str], bool]:
|
||||||
|
"""Run the editor application."""
|
||||||
|
result = {"text": None, "saved": False}
|
||||||
|
|
||||||
|
# Create buffer
|
||||||
|
buffer = Buffer(multiline=True, name="editor")
|
||||||
|
buffer.text = initial_text
|
||||||
|
|
||||||
|
# Key bindings
|
||||||
|
kb = KeyBindings()
|
||||||
|
|
||||||
|
@kb.add("c-s") # Ctrl+S to save
|
||||||
|
def save(event):
|
||||||
|
result["text"] = buffer.text
|
||||||
|
result["saved"] = True
|
||||||
|
event.app.exit()
|
||||||
|
|
||||||
|
@kb.add("c-q") # Ctrl+Q to quit without saving
|
||||||
|
def quit_nosave(event):
|
||||||
|
result["text"] = None
|
||||||
|
result["saved"] = False
|
||||||
|
event.app.exit()
|
||||||
|
|
||||||
|
@kb.add("escape") # Escape to quit
|
||||||
|
def escape(event):
|
||||||
|
result["text"] = None
|
||||||
|
result["saved"] = False
|
||||||
|
event.app.exit()
|
||||||
|
|
||||||
|
@kb.add("c-space") # Ctrl+Space for completion
|
||||||
|
async def trigger_completion(event):
|
||||||
|
if self._lsp_client:
|
||||||
|
doc = buffer.document
|
||||||
|
line = doc.cursor_position_row
|
||||||
|
col = doc.cursor_position_col
|
||||||
|
completions = await self._lsp_client.completion(uri, line, col)
|
||||||
|
if completions:
|
||||||
|
# Show first completion as hint
|
||||||
|
self._diagnostics_text = f"Completions: {', '.join(c.label for c in completions[:5])}"
|
||||||
|
event.app.invalidate()
|
||||||
|
|
||||||
|
@kb.add("f1") # F1 for hover
|
||||||
|
async def show_hover(event):
|
||||||
|
if self._lsp_client:
|
||||||
|
doc = buffer.document
|
||||||
|
line = doc.cursor_position_row
|
||||||
|
col = doc.cursor_position_col
|
||||||
|
hover = await self._lsp_client.hover(uri, line, col)
|
||||||
|
if hover:
|
||||||
|
self._hover_text = hover.contents[:200] # Truncate
|
||||||
|
self._show_hover = True
|
||||||
|
else:
|
||||||
|
self._hover_text = ""
|
||||||
|
self._show_hover = False
|
||||||
|
event.app.invalidate()
|
||||||
|
|
||||||
|
@kb.add("escape", filter=Condition(lambda: self._show_hover))
|
||||||
|
def hide_hover(event):
|
||||||
|
self._show_hover = False
|
||||||
|
event.app.invalidate()
|
||||||
|
|
||||||
|
@kb.add("c-p") # Ctrl+P for signature help (ASLS only)
|
||||||
|
async def show_signature_help(event):
|
||||||
|
# Only available for ASLS
|
||||||
|
if self._lsp_client and self._lsp_type == "assemblyscript":
|
||||||
|
doc = buffer.document
|
||||||
|
line = doc.cursor_position_row
|
||||||
|
col = doc.cursor_position_col
|
||||||
|
try:
|
||||||
|
sig_help = await self._lsp_client.signature_help(uri, line, col)
|
||||||
|
if sig_help and sig_help.get("signatures"):
|
||||||
|
sig = sig_help["signatures"][0]
|
||||||
|
label = sig.get("label", "")
|
||||||
|
self._hover_text = f"Signature: {label}"
|
||||||
|
self._show_hover = True
|
||||||
|
else:
|
||||||
|
self._hover_text = ""
|
||||||
|
self._show_hover = False
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
event.app.invalidate()
|
||||||
|
|
||||||
|
# Syntax highlighting
|
||||||
|
lexer = get_lexer_for_syntax(self.syntax)
|
||||||
|
|
||||||
|
# Header
|
||||||
|
def get_header():
|
||||||
|
if self._lsp_client:
|
||||||
|
if self._lsp_type == "yaml":
|
||||||
|
lsp_status = " [YAML LSP]"
|
||||||
|
elif self._lsp_type == "assemblyscript":
|
||||||
|
lsp_status = " [ASLS]"
|
||||||
|
else:
|
||||||
|
lsp_status = " [LSP]"
|
||||||
|
else:
|
||||||
|
lsp_status = ""
|
||||||
|
|
||||||
|
parts = [
|
||||||
|
("class:header", f" {title}{lsp_status} "),
|
||||||
|
("class:header.key", " Ctrl+S"),
|
||||||
|
("class:header", "=Save "),
|
||||||
|
("class:header.key", " Ctrl+Q"),
|
||||||
|
("class:header", "=Quit "),
|
||||||
|
("class:header.key", " F1"),
|
||||||
|
("class:header", "=Hover "),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add Ctrl+P hint for AssemblyScript
|
||||||
|
if self._lsp_type == "assemblyscript" and self._lsp_client:
|
||||||
|
parts.extend([
|
||||||
|
("class:header.key", " Ctrl+P"),
|
||||||
|
("class:header", "=Sig "),
|
||||||
|
])
|
||||||
|
|
||||||
|
return parts
|
||||||
|
|
||||||
|
header = Window(
|
||||||
|
height=1,
|
||||||
|
content=FormattedTextControl(get_header),
|
||||||
|
style="class:header",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Editor window
|
||||||
|
editor_window = Window(
|
||||||
|
content=BufferControl(
|
||||||
|
buffer=buffer,
|
||||||
|
lexer=lexer,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Status bar
|
||||||
|
def get_status():
|
||||||
|
row = buffer.document.cursor_position_row + 1
|
||||||
|
col = buffer.document.cursor_position_col + 1
|
||||||
|
lines = len(buffer.text.split("\n"))
|
||||||
|
|
||||||
|
parts = [("class:status", f" Line {row}/{lines}, Col {col} ")]
|
||||||
|
|
||||||
|
if self._diagnostics_text:
|
||||||
|
parts.append(("class:status.diag", f" | {self._diagnostics_text}"))
|
||||||
|
|
||||||
|
return parts
|
||||||
|
|
||||||
|
status_bar = Window(
|
||||||
|
height=1,
|
||||||
|
content=FormattedTextControl(get_status),
|
||||||
|
style="class:status",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Hover popup (shown conditionally)
|
||||||
|
def get_hover_text():
|
||||||
|
if self._show_hover and self._hover_text:
|
||||||
|
return [("class:hover", self._hover_text)]
|
||||||
|
return []
|
||||||
|
|
||||||
|
hover_window = ConditionalContainer(
|
||||||
|
Window(
|
||||||
|
height=3,
|
||||||
|
content=FormattedTextControl(get_hover_text),
|
||||||
|
style="class:hover",
|
||||||
|
),
|
||||||
|
filter=Condition(lambda: self._show_hover and bool(self._hover_text)),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Layout
|
||||||
|
layout = Layout(
|
||||||
|
HSplit([
|
||||||
|
header,
|
||||||
|
editor_window,
|
||||||
|
hover_window,
|
||||||
|
status_bar,
|
||||||
|
])
|
||||||
|
)
|
||||||
|
|
||||||
|
# Styles
|
||||||
|
style = Style.from_dict({
|
||||||
|
"header": "bg:#005f87 #ffffff",
|
||||||
|
"header.key": "bg:#005f87 #ffff00 bold",
|
||||||
|
"status": "bg:#444444 #ffffff",
|
||||||
|
"status.diag": "bg:#444444 #ff8800",
|
||||||
|
"hover": "bg:#333333 #ffffff italic",
|
||||||
|
"diagnostic.error": "bg:#5f0000 #ffffff",
|
||||||
|
"diagnostic.warning": "bg:#5f5f00 #ffffff",
|
||||||
|
})
|
||||||
|
|
||||||
|
# Set up diagnostics callback
|
||||||
|
async def on_text_changed(buff):
|
||||||
|
if self._lsp_client:
|
||||||
|
self._document_version += 1
|
||||||
|
diagnostics = await self._lsp_client.did_change(
|
||||||
|
uri, buff.text, self._document_version
|
||||||
|
)
|
||||||
|
if diagnostics:
|
||||||
|
errors = sum(1 for d in diagnostics if d.severity == "error")
|
||||||
|
warnings = sum(1 for d in diagnostics if d.severity == "warning")
|
||||||
|
parts = []
|
||||||
|
if errors:
|
||||||
|
parts.append(f"{errors} error{'s' if errors > 1 else ''}")
|
||||||
|
if warnings:
|
||||||
|
parts.append(f"{warnings} warning{'s' if warnings > 1 else ''}")
|
||||||
|
self._diagnostics_text = " | ".join(parts) if parts else ""
|
||||||
|
else:
|
||||||
|
self._diagnostics_text = ""
|
||||||
|
|
||||||
|
buffer.on_text_changed += lambda buff: asyncio.create_task(on_text_changed(buff))
|
||||||
|
|
||||||
|
# Create and run application
|
||||||
|
app: Application = Application(
|
||||||
|
layout=layout,
|
||||||
|
key_bindings=kb,
|
||||||
|
style=style,
|
||||||
|
full_screen=True,
|
||||||
|
mouse_support=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
await app.run_async()
|
||||||
|
|
||||||
|
return result["text"], result["saved"]
|
||||||
|
|
||||||
|
|
||||||
|
async def edit_text_async(
|
||||||
|
initial_text: str,
|
||||||
|
title: str = "Editor",
|
||||||
|
schema_type: Optional[str] = None,
|
||||||
|
syntax: str = "yaml",
|
||||||
|
) -> Tuple[Optional[str], bool]:
|
||||||
|
"""
|
||||||
|
Async wrapper for LSP-enabled text editing.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
initial_text: Text to edit
|
||||||
|
title: Title shown in header
|
||||||
|
schema_type: "organism" or "listener" for YAML schema modeline
|
||||||
|
syntax: Syntax highlighting ("yaml", "typescript", "assemblyscript", "ts", "as")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(edited_text, saved) - edited_text is None if cancelled
|
||||||
|
"""
|
||||||
|
editor = LSPEditor(schema_type=schema_type, syntax=syntax)
|
||||||
|
return await editor.edit(initial_text, title=title)
|
||||||
|
|
||||||
|
|
||||||
|
async def edit_file_async(
|
||||||
|
filepath: str,
|
||||||
|
title: Optional[str] = None,
|
||||||
|
schema_type: Optional[str] = None,
|
||||||
|
syntax: Optional[str] = None,
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Edit a file with LSP support.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filepath: Path to file
|
||||||
|
title: Optional title (defaults to filename)
|
||||||
|
schema_type: "organism" or "listener" for YAML schema modeline
|
||||||
|
syntax: Syntax highlighting (auto-detected from extension if not specified)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if saved, False if cancelled
|
||||||
|
"""
|
||||||
|
path = Path(filepath)
|
||||||
|
title = title or path.name
|
||||||
|
|
||||||
|
# Auto-detect syntax from extension if not specified
|
||||||
|
if syntax is None:
|
||||||
|
syntax = detect_syntax_from_path(path)
|
||||||
|
|
||||||
|
# Load existing content or empty
|
||||||
|
if path.exists():
|
||||||
|
initial_text = path.read_text()
|
||||||
|
else:
|
||||||
|
initial_text = ""
|
||||||
|
|
||||||
|
# Edit
|
||||||
|
edited_text, saved = await edit_text_async(
|
||||||
|
initial_text,
|
||||||
|
title=title,
|
||||||
|
schema_type=schema_type,
|
||||||
|
syntax=syntax,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Save if requested
|
||||||
|
if saved and edited_text is not None:
|
||||||
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
path.write_text(edited_text)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def edit_assemblyscript_source(
|
||||||
|
filepath: str,
|
||||||
|
title: Optional[str] = None,
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Edit an AssemblyScript listener source file with ASLS support.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
filepath: Path to .ts or .as file
|
||||||
|
title: Optional title (defaults to filename)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
True if saved, False if cancelled
|
||||||
|
"""
|
||||||
|
return await edit_file_async(
|
||||||
|
filepath,
|
||||||
|
title=title,
|
||||||
|
syntax="assemblyscript",
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -494,7 +494,12 @@ class SecureConsole:
|
||||||
cprint(" /buffer <thread> Inspect thread's context buffer", Colors.DIM)
|
cprint(" /buffer <thread> Inspect thread's context buffer", Colors.DIM)
|
||||||
cprint(" /monitor <thread> Show recent messages from thread", Colors.DIM)
|
cprint(" /monitor <thread> Show recent messages from thread", Colors.DIM)
|
||||||
cprint(" /monitor * Show recent messages from all threads", Colors.DIM)
|
cprint(" /monitor * Show recent messages from all threads", Colors.DIM)
|
||||||
cprint(" /config View current configuration", Colors.DIM)
|
cprint("")
|
||||||
|
cprint("Configuration:", Colors.CYAN)
|
||||||
|
cprint(" /config Show current config", Colors.DIM)
|
||||||
|
cprint(" /config -e Edit organism.yaml", Colors.DIM)
|
||||||
|
cprint(" /config @name Edit listener config", Colors.DIM)
|
||||||
|
cprint(" /config --list List listener configs", Colors.DIM)
|
||||||
cprint("")
|
cprint("")
|
||||||
cprint("Protected (require password):", Colors.YELLOW)
|
cprint("Protected (require password):", Colors.YELLOW)
|
||||||
cprint(" /restart Restart the pipeline", Colors.DIM)
|
cprint(" /restart Restart the pipeline", Colors.DIM)
|
||||||
|
|
@ -670,16 +675,186 @@ class SecureConsole:
|
||||||
cprint(f" {payload_str}", Colors.DIM)
|
cprint(f" {payload_str}", Colors.DIM)
|
||||||
|
|
||||||
async def _cmd_config(self, args: str) -> None:
|
async def _cmd_config(self, args: str) -> None:
|
||||||
"""View current configuration (read-only)."""
|
"""
|
||||||
|
Edit configuration files.
|
||||||
|
|
||||||
|
/config - Edit organism.yaml
|
||||||
|
/config @name - Edit listener config (e.g., /config @greeter)
|
||||||
|
/config --list - List available listener configs
|
||||||
|
/config --show - Show current config (read-only)
|
||||||
|
"""
|
||||||
|
args = args.strip() if args else ""
|
||||||
|
|
||||||
|
if args == "--list":
|
||||||
|
await self._config_list()
|
||||||
|
elif args == "--show" or args == "":
|
||||||
|
await self._config_show()
|
||||||
|
elif args.startswith("@"):
|
||||||
|
listener_name = args[1:].strip()
|
||||||
|
if listener_name:
|
||||||
|
await self._config_edit_listener(listener_name)
|
||||||
|
else:
|
||||||
|
cprint("Usage: /config @listener_name", Colors.RED)
|
||||||
|
elif args == "--edit" or args == "-e":
|
||||||
|
await self._config_edit_organism()
|
||||||
|
else:
|
||||||
|
cprint(f"Unknown option: {args}", Colors.RED)
|
||||||
|
cprint("Usage:", Colors.DIM)
|
||||||
|
cprint(" /config Show current config", Colors.DIM)
|
||||||
|
cprint(" /config -e Edit organism.yaml", Colors.DIM)
|
||||||
|
cprint(" /config @name Edit listener config", Colors.DIM)
|
||||||
|
cprint(" /config --list List listener configs", Colors.DIM)
|
||||||
|
|
||||||
|
async def _config_show(self) -> None:
|
||||||
|
"""Show current configuration (read-only)."""
|
||||||
cprint(f"\nOrganism: {self.pump.config.name}", Colors.CYAN)
|
cprint(f"\nOrganism: {self.pump.config.name}", Colors.CYAN)
|
||||||
cprint(f"Port: {self.pump.config.port}", Colors.DIM)
|
cprint(f"Port: {self.pump.config.port}", Colors.DIM)
|
||||||
cprint(f"Thread scheduling: {self.pump.config.thread_scheduling}", Colors.DIM)
|
cprint(f"Thread scheduling: {self.pump.config.thread_scheduling}", Colors.DIM)
|
||||||
cprint(f"Max concurrent pipelines: {self.pump.config.max_concurrent_pipelines}", Colors.DIM)
|
cprint(f"Max concurrent pipelines: {self.pump.config.max_concurrent_pipelines}", Colors.DIM)
|
||||||
cprint(f"Max concurrent handlers: {self.pump.config.max_concurrent_handlers}", Colors.DIM)
|
cprint(f"Max concurrent handlers: {self.pump.config.max_concurrent_handlers}", Colors.DIM)
|
||||||
cprint(f"Max concurrent per agent: {self.pump.config.max_concurrent_per_agent}", Colors.DIM)
|
cprint(f"Max concurrent per agent: {self.pump.config.max_concurrent_per_agent}", Colors.DIM)
|
||||||
cprint("\nTo modify: /quit -> edit organism.yaml -> restart", Colors.DIM)
|
cprint("\nUse /config -e to edit organism.yaml", Colors.DIM)
|
||||||
|
cprint("Use /config @listener to edit a listener config", Colors.DIM)
|
||||||
cprint("")
|
cprint("")
|
||||||
|
|
||||||
|
async def _config_list(self) -> None:
|
||||||
|
"""List available listener configs."""
|
||||||
|
from agentserver.config import get_listener_config_store
|
||||||
|
|
||||||
|
store = get_listener_config_store()
|
||||||
|
listeners = store.list_listeners()
|
||||||
|
|
||||||
|
cprint("\nListener configurations:", Colors.CYAN)
|
||||||
|
cprint(f"Directory: {store.listeners_dir}", Colors.DIM)
|
||||||
|
cprint("")
|
||||||
|
|
||||||
|
if not listeners:
|
||||||
|
cprint(" No listener configs found.", Colors.DIM)
|
||||||
|
cprint(" Use /config @name to create one.", Colors.DIM)
|
||||||
|
else:
|
||||||
|
for name in sorted(listeners):
|
||||||
|
config = store.get(name)
|
||||||
|
agent_tag = "[agent]" if config.agent else "[tool]" if config.tool else ""
|
||||||
|
cprint(f" @{name:20} {agent_tag} {config.description or ''}", Colors.DIM)
|
||||||
|
|
||||||
|
# Also show registered listeners without config files
|
||||||
|
unconfigured = [
|
||||||
|
name for name in self.pump.listeners.keys()
|
||||||
|
if name not in listeners
|
||||||
|
]
|
||||||
|
if unconfigured:
|
||||||
|
cprint("\nRegistered listeners without config files:", Colors.YELLOW)
|
||||||
|
for name in sorted(unconfigured):
|
||||||
|
listener = self.pump.listeners[name]
|
||||||
|
agent_tag = "[agent]" if listener.is_agent else ""
|
||||||
|
cprint(f" @{name:20} {agent_tag} {listener.description}", Colors.DIM)
|
||||||
|
|
||||||
|
cprint("")
|
||||||
|
|
||||||
|
async def _config_edit_organism(self) -> None:
|
||||||
|
"""Edit organism.yaml in the full-screen editor."""
|
||||||
|
from agentserver.console.editor import edit_text_async
|
||||||
|
from agentserver.config.schema import ensure_schemas
|
||||||
|
from agentserver.config.split_loader import (
|
||||||
|
get_organism_yaml_path,
|
||||||
|
load_organism_yaml_content,
|
||||||
|
save_organism_yaml_content,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure schemas are written for LSP
|
||||||
|
try:
|
||||||
|
ensure_schemas()
|
||||||
|
except Exception as e:
|
||||||
|
cprint(f"Warning: Could not write schemas: {e}", Colors.YELLOW)
|
||||||
|
|
||||||
|
# Find organism.yaml
|
||||||
|
config_path = get_organism_yaml_path()
|
||||||
|
if config_path is None:
|
||||||
|
cprint("No organism.yaml found.", Colors.RED)
|
||||||
|
cprint("Searched in:", Colors.DIM)
|
||||||
|
cprint(" ~/.xml-pipeline/organism.yaml", Colors.DIM)
|
||||||
|
cprint(" ./organism.yaml", Colors.DIM)
|
||||||
|
cprint(" ./config/organism.yaml", Colors.DIM)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Load content
|
||||||
|
try:
|
||||||
|
content = load_organism_yaml_content(config_path)
|
||||||
|
except Exception as e:
|
||||||
|
cprint(f"Failed to load config: {e}", Colors.RED)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Edit
|
||||||
|
cprint(f"Editing: {config_path}", Colors.CYAN)
|
||||||
|
cprint("Press Ctrl+S to save, Ctrl+Q to cancel", Colors.DIM)
|
||||||
|
cprint("")
|
||||||
|
|
||||||
|
edited_text, saved = await edit_text_async(
|
||||||
|
content,
|
||||||
|
title=f"organism.yaml ({config_path.name})",
|
||||||
|
schema_type="organism",
|
||||||
|
)
|
||||||
|
|
||||||
|
if saved and edited_text is not None:
|
||||||
|
try:
|
||||||
|
save_organism_yaml_content(config_path, edited_text)
|
||||||
|
cprint("Configuration saved.", Colors.GREEN)
|
||||||
|
cprint("Note: Restart required for changes to take effect.", Colors.YELLOW)
|
||||||
|
except yaml.YAMLError as e:
|
||||||
|
cprint(f"Invalid YAML: {e}", Colors.RED)
|
||||||
|
except Exception as e:
|
||||||
|
cprint(f"Failed to save: {e}", Colors.RED)
|
||||||
|
else:
|
||||||
|
cprint("Edit cancelled.", Colors.DIM)
|
||||||
|
|
||||||
|
async def _config_edit_listener(self, name: str) -> None:
|
||||||
|
"""Edit a listener config in the full-screen editor."""
|
||||||
|
from agentserver.config import get_listener_config_store
|
||||||
|
from agentserver.console.editor import edit_text_async
|
||||||
|
from agentserver.config.schema import ensure_schemas
|
||||||
|
|
||||||
|
# Ensure schemas are written for LSP
|
||||||
|
try:
|
||||||
|
ensure_schemas()
|
||||||
|
except Exception as e:
|
||||||
|
cprint(f"Warning: Could not write schemas: {e}", Colors.YELLOW)
|
||||||
|
|
||||||
|
store = get_listener_config_store()
|
||||||
|
|
||||||
|
# Load or create content
|
||||||
|
if store.exists(name):
|
||||||
|
content = store.load_yaml(name)
|
||||||
|
cprint(f"Editing: {store.path_for(name)}", Colors.CYAN)
|
||||||
|
else:
|
||||||
|
# Check if it's a registered listener
|
||||||
|
if name in self.pump.listeners:
|
||||||
|
cprint(f"Creating new config for registered listener: {name}", Colors.CYAN)
|
||||||
|
else:
|
||||||
|
cprint(f"Creating new config for: {name}", Colors.CYAN)
|
||||||
|
content = store._default_template(name)
|
||||||
|
|
||||||
|
cprint("Press Ctrl+S to save, Ctrl+Q to cancel", Colors.DIM)
|
||||||
|
cprint("")
|
||||||
|
|
||||||
|
# Edit
|
||||||
|
edited_text, saved = await edit_text_async(
|
||||||
|
content,
|
||||||
|
title=f"{name}.yaml",
|
||||||
|
schema_type="listener",
|
||||||
|
)
|
||||||
|
|
||||||
|
if saved and edited_text is not None:
|
||||||
|
try:
|
||||||
|
path = store.save_yaml(name, edited_text)
|
||||||
|
cprint(f"Saved: {path}", Colors.GREEN)
|
||||||
|
cprint("Note: Restart required for changes to take effect.", Colors.YELLOW)
|
||||||
|
except yaml.YAMLError as e:
|
||||||
|
cprint(f"Invalid YAML: {e}", Colors.RED)
|
||||||
|
except Exception as e:
|
||||||
|
cprint(f"Failed to save: {e}", Colors.RED)
|
||||||
|
else:
|
||||||
|
cprint("Edit cancelled.", Colors.DIM)
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# Commands: Protected
|
# Commands: Protected
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -63,9 +63,12 @@ auth = [
|
||||||
# WebSocket server (for remote connections)
|
# WebSocket server (for remote connections)
|
||||||
server = ["websockets"]
|
server = ["websockets"]
|
||||||
|
|
||||||
|
# LSP support for config editor (requires yaml-language-server: npm install -g yaml-language-server)
|
||||||
|
lsp = ["lsp-client>=0.3.0"]
|
||||||
|
|
||||||
# All optional features
|
# All optional features
|
||||||
all = [
|
all = [
|
||||||
"xml-pipeline[anthropic,openai,redis,search,auth,server]",
|
"xml-pipeline[anthropic,openai,redis,search,auth,server,lsp]",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Development
|
# Development
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue