Tailor is a multi-window AI workspace framework built with Rust/Tauri (backend), Python (sidecar processes), and WebSocket communication. It allows users to open multiple independent "vaults" (project directories), each running isolated Python processes with custom plugins and AI capabilities.
The Tauri application manages windows, processes, and event routing.
-
main.rs: Entry point that initializes three core managers:WindowManager: Tracks vault windowsSidecarManager: Spawns/terminates Python processesEventBus: Routes events between sidecars and UI
-
sidecar_manager.rs:- Spawns isolated Python processes for each vault
- Allocates unique WebSocket ports (9000+)
- Captures stdout/stderr for debugging
- Terminates processes on window close
-
event_bus.rs:- Routes events based on scope:
window(single window),vault(all windows with same vault),global(all windows) - Maps windows to vault IDs for proper routing
- Routes events based on scope:
-
ipc_router.rs:open_vault: Creates window → spawns sidecar → returns WebSocket portsend_to_sidecar: Communicates with Python via WebSocket (placeholder)close_vault: Terminates sidecar → removes window
Each vault gets its own isolated Python process with professional-grade code quality.
sidecar/
├── main.py # CLI entry point with argparse
├── websocket_server.py # JSON-RPC 2.0 WebSocket server
├── vault_brain.py # Plugin orchestrator
├── event_emitter.py # Event emission API
├── constants.py # Centralized constants & enums
├── exceptions.py # Custom exception hierarchy
├── api/
│ ├── plugin_base.py # Abstract PluginBase class
│ └── __init__.py
└── utils/
├── logging_config.py # Professional logging system
├── json_rpc.py # JSON-RPC utilities
├── path_utils.py # Safe path operations
└── __init__.py
CLI Arguments:
python main.py --vault <path> --ws-port <port> [--log-level DEBUG] [--verbose]Flow:
- Parses CLI arguments with
argparse - Configures professional logging system (rotating file handler)
- Validates vault path with security checks
- Adds
vault/lib/toPYTHONPATHfor isolated dependencies - Initializes
WebSocketServeron allocated port - Initializes
VaultBrain(orchestrator) - Runs WebSocket server + tick loop concurrently
- Handles errors with custom exceptions
Type Safety: All functions have type hints for IDE support and mypy validation.
JSON-RPC 2.0 Server for bi-directional communication:
Features:
- Full JSON-RPC 2.0 compliance with validation
- Type-safe command handlers with
CommandHandlertype alias - Structured error responses with error codes
- Professional logging (no print statements)
- Automatic async handler wrapping
Key Methods:
register_handler(method: str, handler: CommandHandler) -> Nonesend_to_rust(data: Dict[str, Any]) -> Nonehandle_message(message: str) -> Noneget_registered_methods() -> List[str]
Uses: utils.json_rpc for message building/validation, constants for error codes.
Plugin API for emitting events to the UI:
Features:
- Type-safe with enums (
EventType,EventScope,Severity) - Input validation for scope and severity
- Logging all event emissions
- JSON-RPC message building
from constants import EventType, EventScope, Severity
emitter.notify("Task complete!", severity=Severity.SUCCESS) # Type-safe
emitter.progress(75, "Processing...") # Progress bar
emitter.update_state("key", value) # UI state
emitter.emit(EventType.CUSTOM, {...}, scope=EventScope.GLOBAL)Plugin Orchestrator that manages vault-specific operations:
Features:
- Type-safe plugin loading with validation
- Command registry with async handler verification
- Per-plugin logger setup
- Safe path operations (prevents directory traversal)
- Structured error handling
Initialization:
- Validates vault path with
utils.path_utils - Loads and validates
.vault.tomlconfig - Creates
.memory/directory safely - Discovers plugins using
discover_plugins() - Validates plugin structure (main.py exists)
- Loads plugins with isolated error handling
- Builds LangGraph instance (placeholder)
- Registers command handlers with WebSocket server
Plugin Loading:
- Scans
vault/plugins/*/main.py(directory-based plugins) - Validates plugin structure before loading
- Dynamically imports each module
- Finds
Pluginclass → validates it's async-compatible - Instantiates with
EventEmitter,VaultBrain,plugin_dir,vault_path - Uses
get_plugin_logger()for plugin-specific logging
Command Registry:
- Validates handlers are async functions
- Provides
execute_command()with error handling - Returns available commands via
get_commands()
Tick Loop:
- Runs every 5 seconds (configurable via
DEFAULT_TICK_INTERVAL) - Calls
on_tick()for plugins that implement it - Isolates errors (one plugin failure doesn't crash others)
- Calls
plugin.on_tick(emitter)for each plugin - Allows plugins to emit periodic events
Command Handlers:
chat.send_message: Processes chat (placeholder echoes back)execute_command: Calls plugin methods dynamically (e.g.,example_plugin.custom_action)
Plugins use a command registry pattern similar to VSCode and Obsidian, where they register commands that can be executed by anyone (UI, other plugins, keyboard shortcuts, etc.).
All plugins now inherit from PluginBase, which provides:
Automatic Setup:
- Plugin name from directory
- Logger configuration (
self.logger) - State tracking (
self.is_loaded)
Lifecycle Hooks:
__init__()- Plugin instantiationregister_commands()- Required: Register plugin commandsasync on_load()- Optional: Called after all plugins loadedasync on_tick(emitter)- Optional: Called every 5 secondsasync on_unload()- Optional: Cleanup before shutdown
Helper Methods:
load_settings(filename="settings.json")- Load plugin configsave_settings(settings, filename="settings.json")- Save plugin configget_config_path(filename)- Get path to config file
my_plugin/
\u251c\u2500\u2500 main.py # Required: Plugin class inheriting from PluginBase
\u251c\u2500\u2500 settings.json # Optional: Plugin configuration
\u2514\u2500\u2500 README.md # Optional: Documentation
import sys
from pathlib import Path
from typing import Dict, Any
# Add sidecar to path
sidecar_path = Path(__file__).parent.parent.parent.parent / "sidecar"
sys.path.insert(0, str(sidecar_path))
from api.plugin_base import PluginBase
class Plugin(PluginBase):
"""Demo plugin showing PluginBase usage."""
def __init__(self, emitter, brain, plugin_dir, vault_path):
"""Initialize plugin - automatic logging setup."""
super().__init__(emitter, brain, plugin_dir, vault_path)
# Load settings
settings = self.load_settings() # Helper method
self.my_setting = settings.get("key", "default")
# Register commands
self.register_commands()
self.logger.info("Plugin initialized") # Automatic logger
def register_commands(self) -> None:
"""Register plugin commands - REQUIRED."""
self.brain.register_command(
"demo.hello",
self.handle_hello,
self.name # Automatic plugin name
)
async def handle_hello(self, name: str = "World", **kwargs) -> Dict[str, Any]:
"""Command handler with type hints."""
message = f"Hello, {name}!"
self.emitter.notify(message, severity="success")
return {"status": "ok", "message": message}
async def on_load(self) -> None:
"""Called after all plugins loaded."""
await super().on_load()
self.emitter.notify("Demo plugin loaded!", severity="success")
async def on_tick(self, emitter) -> None:
"""Called every 5 seconds - optional."""
self.logger.debug("Tick from demo plugin")See PLUGIN_GUIDE.md for complete documentation.
- Always inherit from PluginBase
- Use type hints for all functions
- Use
self.loggerinstead ofprint() - Validate input in command handlers
- Handle errors gracefully with try-except
- Use
load_settings()for configuration
Register Command:
brain.register_command(command_id, handler, plugin_name)
# Example: brain.register_command("database.query", self.query, "database")Execute Command (from any plugin):
result = await self.brain.execute_command("database.query", table="users")List Commands:
commands = brain.get_commands()
# Returns: {"example.customAction": {"plugin": "example_plugin"}, ...}Plugin Lifecycle:
- Load:
VaultBrain._load_plugins()imports module - Init:
Plugin(emitter, brain)constructor called, commands registered - Tick:
on_tick()called every 5s by tick loop - Commands: Executed via
brain.execute_command(command_id, **args)
Simple launcher UI:
// User clicks "Open Vault" → selects directory
const result = await invoke('open_vault', { vaultPath: selected });
// Returns: { window_label: "vault_1", ws_port: 9001 }New Window Created:
- Separate Tauri window for each vault
- Connects to WebSocket at
ws://localhost:{ws_port} - Listens for events via
sidecar-eventchannel
sequenceDiagram
actor User
participant UI as Frontend (HTML)
participant Rust as Tauri/Rust
participant SM as SidecarManager
participant Python as Python Sidecar
participant Plugin as Example Plugin
User->>UI: Clicks "Open Vault"
UI->>UI: Select directory (example-vault/)
UI->>Rust: invoke('open_vault', {vaultPath})
Rust->>Rust: DependencyChecker.check_and_install()
Rust->>Rust: WindowManager.create_vault_window()
Rust->>SM: spawn_sidecar(window_label, vault_path)
SM->>SM: Allocate port 9001
SM->>Python: Spawn process: python main.py --vault ... --ws-port 9001
Python->>Python: Load .vault.toml config
Python->>Python: Create .memory/ directory
Python->>Python: Initialize WebSocketServer(9001)
Python->>Plugin: Load plugins/example_plugin.py
Plugin->>Plugin: __init__(emitter)
Plugin->>Python: emitter.notify("Plugin loaded!")
Python->>Rust: WebSocket: trigger_event(NOTIFY)
Rust->>Rust: EventBus.route_from_sidecar()
Rust->>UI: Emit "sidecar-event" to window
UI->>User: Show notification: "Plugin loaded!"
Python->>Python: Start tick loop (every 5s)
SM->>Rust: Return ws_port: 9001
Rust->>UI: Return {window_label, ws_port}
UI->>User: Display status: "Opened: vault_1 (Port: 9001)"
Step-by-Step:
- User clicks "Open Vault" → selects
example-vault/ - Rust:
ipc_router::open_vault()called - Dependency Check: Installs
plugins/requirements.txttolib/ - Window Creation:
WindowManagercreates new windowvault_1 - Sidecar Spawn:
SidecarManagerallocates port9001- Spawns:
python -m sidecar --vault example-vault --ws-port 9001
- Python Sidecar Starts:
- Loads config from
.vault.toml - Creates
.memory/directory - Loads
plugins/example_plugin.py - Starts WebSocket server on port
9001 - Starts tick loop
- Loads config from
- Plugin Initialization:
example_plugin.Plugin(emitter)constructor runs- Emits:
emitter.notify("Plugin loaded!", severity="success")
- WebSocket → Rust EventBus → Window UI shows notification
sequenceDiagram
participant Loop as Tick Loop
participant Plugin as Example Plugin
participant Emitter as EventEmitter
participant WS as WebSocket Server
participant Rust as Tauri/Rust
participant UI as Window UI
Loop->>Loop: Sleep 5 seconds
Loop->>Plugin: on_tick(emitter)
Plugin->>Plugin: Check tick_count % 3 == 0
Plugin->>Emitter: notify("Heartbeat #5", severity="info")
Emitter->>Emitter: Build JSON-RPC message
Emitter->>WS: send_to_rust({method: "trigger_event", ...})
WS->>Rust: WebSocket send JSON
Rust->>Rust: EventBus.route_from_sidecar()
Rust->>Rust: Match scope: "window"
Rust->>UI: window.emit("sidecar-event", event)
UI->>UI: Show notification toast
Step-by-Step:
- Tick Loop (every 5s): Calls
example_plugin.on_tick(emitter) - Every 3rd tick (15s):
emitter.notify("Heartbeat #5 from example_plugin", severity="info")
- EventEmitter sends JSON-RPC:
{ "jsonrpc": "2.0", "method": "trigger_event", "params": { "event_type": "NOTIFY", "scope": "window", "data": {"message": "Heartbeat #5", "severity": "info"} } } - WebSocket → Rust: Receives event
- EventBus: Routes to
vault_1window (scope:window) - UI: Shows notification toast
sequenceDiagram
actor User
participant UI as Window UI
participant Rust as Tauri/Rust
participant WS as WebSocket Server
participant Brain as VaultBrain
participant LLM as LangGraph
User->>UI: Types "Hello" in chat
UI->>Rust: invoke('send_to_sidecar', {command: {...}})
Rust->>WS: WebSocket connect ws://localhost:9001
Rust->>WS: Send JSON-RPC: chat.send_message
WS->>WS: Parse JSON, lookup handler
WS->>Brain: handle_chat({message: "Hello"})
Brain->>LLM: Process with LangGraph
LLM->>Brain: Return response
Brain->>WS: Return {response: "...", status: "success"}
WS->>Rust: WebSocket send response
Rust->>UI: Return result
UI->>User: Display AI response
Step-by-Step:
- UI: User types "Hello" → calls
invoke('send_to_sidecar', {...}) - WebSocket Server: Receives
chat.sendcommand - VaultBrain:
- Starts
DefaultPipeline(or GraphPipeline) - Calls
LLMServiceto generate response
- Starts
- Streaming:
- Emits
chat:stream_start - Emits
chat:tokenfor each token - Emits
chat:stream_endwhen done
- Emits
- UI: Updates chat window in real-time
- Memory:
Memoryplugin listens for output and saves to disk
sequenceDiagram
actor User
participant UI as Window
participant Rust as Tauri/Rust
participant SM as SidecarManager
participant Python as Python Sidecar
User->>UI: Closes window
UI->>Rust: invoke('close_vault', {window_label})
Rust->>SM: terminate_sidecar(window_label)
SM->>Python: process.child.kill()
Python->>Python: Process exits
SM->>Rust: Return OK
Rust->>Rust: WindowManager.remove_window()
Rust->>Rust: EventBus.unregister_window()
Rust->>UI: Return success
UI->>UI: Window closes
Step-by-Step:
- Window Close Event →
ipc_router::close_vault(vault_1) - SidecarManager:
process.child.kill()terminates Python - WindowManager: Removes window tracking
- EventBus: Unregisters window from vault mapping
sequenceDiagram
participant PluginA as Plugin A (API)
participant Brain as VaultBrain
participant PluginB as Plugin B (Database)
participant PluginC as Plugin C (Cache)
Note over PluginA: User requests data
PluginA->>Brain: execute_command("cache.get", key="users")
Brain->>PluginC: handler(**kwargs)
PluginC->>Brain: return None (cache miss)
Brain->>PluginA: return None
PluginA->>Brain: execute_command("database.query", table="users")
Brain->>PluginB: handler(**kwargs)
PluginB->>PluginB: Query database
PluginB->>Brain: return {rows: [...]}
Brain->>PluginA: return {rows: [...]}
PluginA->>Brain: execute_command("cache.set", key="users", value=data)
Brain->>PluginC: handler(**kwargs)
PluginC->>PluginC: Store in cache
PluginC->>Brain: return {cached: true}
Brain->>PluginA: return {cached: true}
Step-by-Step:
- Plugin A (API plugin) receives request for user data
- Check cache: Calls
brain.execute_command("cache.get", key="users") - Cache miss: Plugin C returns
None - Query database: Calls
brain.execute_command("database.query", table="users") - Plugin B queries database and returns data
- Store in cache: Calls
brain.execute_command("cache.set", key="users", value=data) - Plugin C caches the data
- Plugin A returns data to user
Benefits:
- Plugins don't need direct references to each other
- Commands can be called even if plugin isn't loaded yet
- Easy to test (mock the command registry)
- Discoverable via
brain.get_commands()
The EventBus supports three routing scopes:
emitter.notify("Task complete!") # scope="window"- Routes to only the window that spawned this sidecar
- Most common for plugin notifications
emitter.vault_event("STATE_CHANGED", {...})- Routes to all windows that have the same vault open
- Useful for synchronized state across multiple windows
emitter.global_event("SYSTEM_ALERT", {...})- Routes to every window in the application
- Useful for system-wide notifications
{
"jsonrpc": "2.0",
"method": "chat.send",
"params": {
"message": "Hello, world!",
"stream": true,
"chat_id": "chat_123"
},
"id": "cmd_123"
}{
"jsonrpc": "2.0",
"method": "trigger_event",
"params": {
"event_type": "chat:token",
"scope": "window",
"data": {
"stream_id": "stream_abc",
"token": "Hello",
"accumulated": "Hello"
}
}
}- Process Isolation: Each vault = separate Python process (no shared state)
- Portable Vaults:
vault/folder is self-contained (plugins + deps + memory) - Bi-Directional Events: Plugins can push events to UI (not just request/response)
- Scoped Routing:
window: Single window onlyvault: All windows with same vaultglobal: Every window in the app
- Dynamic Plugin Loading: Drop
.pyfiles inplugins/→ auto-loaded - Async Tick System: Plugins run periodic tasks without blocking WebSocket
tailor/
├── src-tauri/ # Rust backend
│ ├── src/
│ │ ├── main.rs # Entry point (AppState init)
│ │ ├── window_manager.rs # Window lifecycle management
│ │ ├── sidecar_manager.rs # Python process orchestration
│ │ ├── event_bus.rs # Event routing (window/vault/global)
│ │ ├── ipc_router.rs # IPC commands (open/close/send)
│ │ └── dependency_checker.rs # Auto install dependencies
│ └── Cargo.toml
├── sidecar/ # Python sidecar
│ ├── main.py # Entry point (CLI args parser)
│ ├── websocket_server.py # JSON-RPC WebSocket server
│ ├── event_emitter.py # Plugin API for events
│ ├── vault_brain.py # LangGraph orchestrator
│ └── requirements.txt
├── example-vault/ # Example vault
│ ├── .vault.toml # Vault metadata
│ ├── plugins/
│ │ ├── example_plugin.py # Demo plugin
│ │ └── requirements.txt # Plugin dependencies
│ ├── lib/ # Auto-managed dependencies (gitignored)
│ └── .memory/ # Conversation history (gitignored)
├── index.html # Frontend launcher
└── README.md
- Multi-window vault management
- Isolated Python sidecars per vault
- WebSocket JSON-RPC communication
- LLM Integration:
LLMServicesupporting local (Ollama) and cloud models - Streaming Pipeline: Real-time token streaming to frontend
- Memory System:
Memoryplugin with file-based persistence - Plugin System: Directory-based plugins with full lifecycle
- Command Registry: VSCode-style command pattern
- LangGraph Enhancements: Moving from dict-based graph to full LangGraph
- Advanced UI: React/Vue migration for complex interfaces
- Plugin Marketplace: Discovery system
- Hot Reload: Reload plugins without restarting sidecar
pixi run devThis will:
- Start Vite dev server for frontend
- Compile Rust backend
- Launch Tauri application
- Create
my-vault/.vault.toml - Add
my-vault/plugins/my_plugin.py - Open vault in Tailor
- Watch console for sidecar logs
- Observe
on_ticknotifications every 5 seconds
# my-vault/plugins/my_plugin.py
class Plugin:
def __init__(self, emitter):
self.emitter = emitter
self.name = "my_plugin"
async def on_tick(self, emitter):
"""Called every 5 seconds"""
pass
async def custom_method(self, **kwargs):
"""Call via: execute_command("my_plugin.custom_method", {...})"""
return {"status": "ok"}- Frontend: HTML + Vanilla JavaScript + Tauri API
- Backend: Rust + Tauri Framework
- Sidecar: Python 3.10+ (asyncio)
- Communication: WebSocket (JSON-RPC 2.0)
- Process Management: Rust
std::process::Command - Event System: Tauri event emitter + custom EventBus
- Future: LangGraph for AI orchestration
- Process Isolation: Each vault runs in separate Python process
- Port Allocation: Unique WebSocket port per vault (9000+)
- Dependency Isolation: Each vault has its own
lib/directory - No Shared State: Vaults cannot interfere with each other
- Local-Only: WebSocket listens on
localhostonly
- Check Python is in PATH:
python --version - Check sidecar logs in Rust console
- Verify
sidecar/requirements.txtinstalled
- Ensure plugin has
class Pluginwith__init__(self, emitter) - Check sidecar console for import errors
- Verify plugin file doesn't start with
_
- Verify WebSocket connection established (check port)
- Check EventBus routing scope matches expectation
- Inspect browser console for
sidecar-eventlistener
- Implement LangGraph: Replace placeholder with actual LangGraph orchestration
- Memory System: Implement conversation history persistence
- Rust WebSocket Client: Complete
send_to_sidecarfunctionality - Vault Window UI: Build React/Vue UI for each vault window (chat, tools, etc.)
- Plugin Marketplace: Sharing and discovering community plugins
- Hot Reload: Reload plugins without restarting sidecar