feat(tui): add usage overlay and sidebar for displaying usage statistics and tasks

feat(tui): implement status bar with connection and turn state indicators
feat(tui): create workflow panel for agent status and progress visualization
feat(web): introduce web frontend interface with static file serving
feat(ws): add WebSocket interface for real-time communication and session management
This commit is contained in:
asepharyana
2026-07-20 09:04:57 +07:00
parent bceba665c0
commit da2ed6da25
454 changed files with 13979 additions and 29539 deletions
+788
View File
@@ -0,0 +1,788 @@
//! Daemon state types — `AppStateRest`, `DaemonState`, and all supporting
//! data structures for the background daemon session.
//!
//! `AppStateRest` is the single source-of-truth struct mutated in-place from
//! [`handler::apply_action`](crate::handler::apply_action) and the IPC handler.
//! `DaemonState` wraps it with IPC socket metadata.
//!
//! Also contains [`create_session()`] adapted from the legacy `main.rs`.
use std::collections::VecDeque;
use std::path::PathBuf;
use std::sync::atomic::AtomicBool;
use std::sync::{Arc, Mutex};
use anyhow::Result;
use tokio::sync::RwLock;
use zesdex_domain::cms::EditLog;
use zesdex_domain::Session;
use zesdex_domain::Settings;
use zesdex_domain::AppConfigRepository;
use zesdex_domain::EditLogRepository;
use zesdex_domain::SessionLockRepository;
use zesdex_domain::SessionRepository;
use zesdex_domain::SettingsRepository;
use zesdex_infrastructure::lsp::manager::LspManager;
use zesdex_infrastructure::mcp::manager::McpManager;
use zesdex_infrastructure::persistence::FileSystemSessionLockRepository;
use zesdex_infrastructure::persistence::JsonAppConfigRepository;
use zesdex_infrastructure::persistence::JsonlEditLogRepository;
use zesdex_infrastructure::persistence::JsonSettingsRepository;
use zesdex_infrastructure::AppConfig;
use zesdex_infrastructure::DirCache;
use zesdex_infrastructure::MentionIndex;
use zesdex_infrastructure::SessionRuntime;
use zesdex_infrastructure::Toast;
use zesdex_infrastructure::ToastKind;
use zesdex_infrastructure::TurnEvent;
// ---------------------------------------------------------------------------
// Supporting types
// ---------------------------------------------------------------------------
/// A single transcript entry rendered in the TUI chat pane.
#[derive(Debug, Clone, PartialEq)]
pub struct ChatMessageDisplay {
/// Message author: User, Assistant, System, or Tool.
pub role: RoleWrapper,
/// Rendered text content (plain text, no markdown).
pub content: String,
/// Millisecond timestamp when this display entry was created.
pub timestamp: i64,
}
/// Simple string-backed role wrapper for transcript display (avoids a direct
/// dependency on the domain's `Role` enum which may not round-trip all wire
/// strings).
#[derive(Debug, Clone, PartialEq)]
pub enum RoleWrapper {
User,
Assistant,
System,
Tool,
}
impl ChatMessageDisplay {
/// Build a display entry, stamping it with the current time.
pub fn new(role: RoleWrapper, content: String) -> Self {
tracing::debug!(
"ChatMessageDisplay::new — role={:?}, content_len={}",
role,
content.len()
);
ChatMessageDisplay {
role,
content,
timestamp: chrono::Utc::now().timestamp_millis(),
}
}
}
/// Which modal overlay, if any, is currently shown over the main TUI view.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Overlay {
/// No overlay; the main chat view is shown.
None,
/// Key bindings help screen.
Help,
/// Settings/configuration panel.
Settings,
/// Background bash job viewer.
Bash,
/// "Are you sure you want to quit?" confirmation.
QuitConfirm,
/// Raw key-code input capture (for binding custom keys).
KeyInput,
/// Inline editor (opened via `/edit`).
Editor,
/// Reasoning effort level selector.
Effort,
/// MCP server management panel.
Mcp,
/// TODO list overlay.
Todo,
/// Session rewind / history scrubber.
Rewind,
/// Learning / lesson management panel.
Learning,
/// Token usage statistics panel.
Usage,
/// Generic loading spinner overlay.
Loading,
/// Model selector dropdown.
ModelSelector,
/// "Clear conversation?" confirmation (distinct from QuitConfirm).
ClearConfirm,
}
impl Overlay {
/// Human-readable name for this overlay variant.
pub fn as_str(self) -> &'static str {
match self {
Overlay::None => "none",
Overlay::Help => "help",
Overlay::Settings => "settings",
Overlay::Bash => "bash",
Overlay::QuitConfirm => "quit_confirm",
Overlay::KeyInput => "key_input",
Overlay::Editor => "editor",
Overlay::Effort => "effort",
Overlay::Mcp => "mcp",
Overlay::Todo => "todo",
Overlay::Rewind => "rewind",
Overlay::Learning => "learning",
Overlay::Usage => "usage",
Overlay::Loading => "loading",
Overlay::ModelSelector => "model_selector",
Overlay::ClearConfirm => "clear_confirm",
}
}
/// Whether any overlay (i.e. anything other than `None`) is active.
pub fn is_active(self) -> bool {
!matches!(self, Overlay::None)
}
}
impl std::fmt::Display for Overlay {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
/// Bounded ring of recent chat messages used to render the transcript view.
#[derive(Debug, Clone)]
pub struct TranscriptCache {
/// Ordered display messages (newest appended, oldest evicted when full).
pub messages: Vec<ChatMessageDisplay>,
/// Maximum messages to retain before evicting the oldest.
pub max_lines: usize,
/// Whether the cache has changed since the last render sweep.
pub dirty: bool,
}
impl TranscriptCache {
/// Create an empty transcript cache holding at most `max_lines` messages.
pub fn new(max_lines: usize) -> Self {
TranscriptCache {
messages: Vec::new(),
max_lines,
dirty: true,
}
}
}
/// Which source populated the autocomplete dropdown.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AutocompleteKind {
/// Builtin slash-command (e.g. `/model`, `/help`).
Command,
/// `@file` mention from the workspace file index.
FileMention,
}
/// The user's input buffer, cursor position, history, and autocomplete state.
#[derive(Debug, Clone)]
pub struct InputState {
/// Raw UTF-8 input buffer content.
pub buffer: String,
/// Byte offset of the cursor within `buffer`.
pub cursor: usize,
/// Previously submitted input lines, oldest-first.
pub history: Vec<String>,
/// Index into `history` when browsing (None = at the current input).
pub history_idx: Option<usize>,
/// The prefix string used to filter candidates for autocomplete.
pub autocomplete_prefix: String,
/// Current autocomplete candidate list.
pub autocomplete_candidates: Vec<String>,
/// Focused index within `autocomplete_candidates`.
pub autocomplete_idx: usize,
/// Whether the autocomplete dropdown is visible.
pub autocomplete_visible: bool,
/// Which kind of autocomplete is active.
pub autocomplete_kind: AutocompleteKind,
/// Byte offset of the `@` character that triggered file mention autocomplete.
pub mention_start: usize,
/// Optional path to a persistent history file.
pub history_file: Option<PathBuf>,
}
impl InputState {
/// Create an empty input state with no buffer, no history, and no autocomplete.
pub fn new() -> Self {
InputState {
buffer: String::new(),
cursor: 0,
history: Vec::new(),
history_idx: None,
autocomplete_prefix: String::new(),
autocomplete_candidates: Vec::new(),
autocomplete_idx: 0,
autocomplete_visible: false,
autocomplete_kind: AutocompleteKind::Command,
mention_start: 0,
history_file: None,
}
}
/// Close the autocomplete dropdown.
pub fn close_autocomplete(&mut self) {
self.autocomplete_visible = false;
self.autocomplete_candidates.clear();
self.autocomplete_prefix.clear();
}
/// Open the command-autocomplete dropdown.
pub fn open_autocomplete(&mut self) {
self.autocomplete_kind = AutocompleteKind::Command;
self.autocomplete_visible = true;
}
}
impl Default for InputState {
fn default() -> Self {
Self::new()
}
}
/// Viewport scroll state: current offset and visible-line count.
#[derive(Debug, Clone)]
pub struct ScrollState {
/// Current scroll offset (how many lines have been scrolled past).
pub offset: usize,
/// Maximum number of lines that fit in the visible viewport area.
pub max_visible: usize,
}
impl ScrollState {
/// Create a `ScrollState` with zero offset and 30 rows visible.
pub fn new() -> Self {
ScrollState {
offset: 0,
max_visible: 30,
}
}
/// Scroll the viewport up by `amount` lines (increasing the offset).
pub fn scroll_up(&mut self, amount: usize) {
self.offset = self.offset.saturating_add(amount);
}
/// Scroll the viewport down by `amount` lines (decreasing the offset).
pub fn scroll_down(&mut self, amount: usize) {
self.offset = self.offset.saturating_sub(amount);
}
/// Update the maximum number of visible lines in the viewport.
pub fn set_max_visible(&mut self, max: usize) {
self.max_visible = max;
}
}
impl Default for ScrollState {
fn default() -> Self {
Self::new()
}
}
/// The "miscellaneous" slice of app state: which overlay is showing,
/// toasts, thinking flags, editor state, and tick.
#[derive(Debug, Clone)]
pub struct MiscState {
/// Currently active modal overlay (None = main chat view).
pub overlay: Overlay,
/// Active toast notifications (expired ones removed on each tick).
pub toasts: Vec<Toast>,
/// Whether the agent is currently "thinking".
pub thinking: bool,
/// Current LLM reasoning effort level (1-5).
pub effort_level: usize,
/// Whether the API connection is established.
pub api_connected: bool,
/// Currently focused index in list-type overlays.
pub selected_index: usize,
/// Monotonically increasing tick count, incremented each render frame.
pub tick_count: u64,
/// Cached content of the TODO file, shown in the overlay.
pub todo_content: String,
/// Whether a lesson background task is currently running.
pub lesson_running: bool,
/// Text waiting to be written to the system clipboard.
pub pending_clipboard_copy: Option<String>,
}
impl MiscState {
/// Create a fresh `MiscState` with no overlay, no toasts, and default effort level 1.
pub fn new() -> Self {
MiscState {
overlay: Overlay::None,
toasts: Vec::new(),
thinking: false,
effort_level: 1,
api_connected: false,
selected_index: 0,
tick_count: 0,
todo_content: String::new(),
lesson_running: false,
pending_clipboard_copy: None,
}
}
/// Append a toast notification to the active list.
pub fn push_toast(&mut self, toast: Toast) {
self.toasts.push(toast);
}
/// Remove and return all toasts whose lifetime has expired at `now_ms`.
pub fn drain_expired_toasts(&mut self, now_ms: i64) -> Vec<Toast> {
let expired: Vec<_> = self
.toasts
.iter()
.filter(|t| t.expired(now_ms))
.cloned()
.collect();
self.toasts.retain(|t| !t.expired(now_ms));
expired
}
}
impl Default for MiscState {
fn default() -> Self {
Self::new()
}
}
/// State of a single workflow agent.
#[derive(Debug, Clone, Default)]
pub struct AgentState {
pub id: String,
pub status: String,
pub current_tool: String,
}
/// Minimal workflow-engine placeholder for hive-mind orchestration state.
#[derive(Debug, Clone, Default)]
pub struct WorkflowEngine {
/// List of running workflow agent states.
pub agents: Vec<AgentState>,
}
impl WorkflowEngine {
/// Create an empty workflow engine.
pub fn new() -> Self {
Self {
agents: Vec::new(),
}
}
}
// ---------------------------------------------------------------------------
// AppStateRest — single source-of-truth application state
// ---------------------------------------------------------------------------
/// The single source-of-truth state struct for the daemon.
///
/// Mutated in-place from two locations: `handler::apply_action`
/// and the IPC client handler in `handler::handle_daemon_client`.
/// Read-only from every other module.
#[derive(Clone)]
pub struct AppStateRest {
/// Persistent user settings (loaded from JSON store at startup).
pub settings: Settings,
/// Per-project app configuration (loaded from JSON store at startup).
pub app_config: AppConfig,
/// Absolute paths to each open workspace root directory.
pub workspace_roots: Vec<PathBuf>,
/// Unique session identifier.
pub session_id: String,
/// Path to the session's data directory.
pub session_dir: PathBuf,
/// Path to the session memory directory (lessons, review history).
pub memory_dir: PathBuf,
/// Path to the git worktrees directory (for sandboxed agent experiments).
pub worktrees_dir: PathBuf,
/// Shared async cache of directory listings.
pub dir_cache: Arc<RwLock<DirCache>>,
/// Shared workspace file-path index for `@file` mention autocomplete.
pub mention_index: MentionIndex,
/// Persistent edit history log (appended on every tool write).
pub edit_log: EditLog,
/// Optional per-session runtime state.
pub session_runtime: Option<SessionRuntime>,
/// Active IAM sessions linked to this app instance.
pub sessions: Vec<Session>,
/// Ring buffer of recent chat messages for the TUI transcript pane.
pub transcript_cache: TranscriptCache,
/// Viewport scroll offset tracker.
pub scroll: ScrollState,
/// Chat input buffer, cursor, history, and autocomplete.
pub input: InputState,
/// Miscellaneous state: overlay, toasts, flags, tick.
pub misc: MiscState,
/// Queue of events emitted by the running agent turn.
pub turn_events: Arc<Mutex<VecDeque<TurnEvent>>>,
/// Whether an agent turn is currently in flight.
pub turn_in_flight: Arc<Mutex<bool>>,
/// Atomic flag set when the user aborts the current turn (Ctrl-C / Escape).
pub abort_flag: Arc<AtomicBool>,
/// Workflow engine state for multi-agent hive-mind orchestration.
pub workflow_engine: WorkflowEngine,
/// MCP server manager.
pub mcp_manager: McpManager,
/// LSP server manager, shared with tool context.
pub lsp_manager: Arc<Mutex<LspManager>>,
/// Shared queue for LSP provisioning messages.
pub lsp_provision_msgs: Arc<Mutex<VecDeque<String>>>,
/// Whether the state has been modified since the last render sweep.
pub dirty: bool,
/// Whether the application has been requested to quit.
pub quit: bool,
}
impl AppStateRest {
/// Construct the initial application state for a session.
///
/// Flow: load settings/config → derive download/worktree dirs from
/// `memory_dir`'s parent → derive `session_id` from the session dir's
/// file name → build the sub-state structs.
///
/// Why: falls back to `memory_dir` itself (with a warning) when it has
/// no parent, and to an empty session id when the dir name can't be
/// read, so construction never fails.
pub fn new(
workspace_roots: Vec<PathBuf>,
session_dir: &std::path::Path,
memory_dir: PathBuf,
) -> Self {
let store_base_dir =
zesdex_infrastructure::Store::new().base_dir;
let settings = JsonSettingsRepository::new()
.load(&store_base_dir)
.unwrap_or_default();
let app_config = JsonAppConfigRepository::new()
.load(&store_base_dir)
.unwrap_or_default();
let worktrees_dir = memory_dir
.parent()
.unwrap_or_else(|| {
tracing::warn!(
"[state] memory_dir '{}' has no parent, using it for worktrees",
memory_dir.display()
);
&memory_dir
})
.join("worktrees");
let dir_cache = DirCache::new();
let session_id = session_dir.file_name().map_or_else(
|| {
tracing::warn!(
"[state] session_dir has no file_name component, using empty session_id"
);
String::new()
},
|n| n.to_string_lossy().to_string(),
);
let mut state = AppStateRest {
settings,
app_config,
workspace_roots,
session_id,
session_dir: session_dir.to_path_buf(),
memory_dir: memory_dir.clone(),
worktrees_dir,
turn_events: Arc::new(Mutex::new(VecDeque::new())),
turn_in_flight: Arc::new(Mutex::new(false)),
abort_flag: Arc::new(AtomicBool::new(false)),
dir_cache: Arc::new(RwLock::new(dir_cache)),
mention_index: MentionIndex::new(),
edit_log: JsonlEditLogRepository::new()
.open(session_dir)
.unwrap_or_else(|e| {
tracing::warn!(
"[state] failed to open edit log at '{}': {e}",
session_dir.display()
);
EditLog::new()
}),
session_runtime: Some(SessionRuntime::new(session_dir.to_path_buf())),
workflow_engine: WorkflowEngine::new(),
mcp_manager: McpManager::new(),
lsp_provision_msgs: Arc::new(Mutex::new(VecDeque::new())),
lsp_manager: Arc::new(Mutex::new(LspManager::new())),
sessions: Vec::new(),
transcript_cache: TranscriptCache::new(200),
scroll: ScrollState::new(),
input: InputState::new(),
misc: MiscState::new(),
dirty: true,
quit: false,
};
// Load project-specific input-line history from a file keyed by
// the first workspace root's SHA256 hash.
let base_dir = state.memory_dir.parent().unwrap_or(&state.memory_dir);
if let Some(root) = state.workspace_roots.first() {
if let Ok(abs_root) = std::fs::canonicalize(root) {
use sha2::Digest;
let mut hasher = sha2::Sha256::new();
hasher.update(abs_root.to_string_lossy().as_bytes());
let hash_hex = hex::encode(hasher.finalize());
let folder_name = abs_root
.file_name()
.map_or_else(|| "root".to_string(), |n| n.to_string_lossy().to_string());
let history_filename = format!("{}-{}.txt", folder_name, &hash_hex[..8]);
let history_dir = base_dir.join("history");
let _ = std::fs::create_dir_all(&history_dir);
let history_file = history_dir.join(history_filename);
if let Ok(content) = std::fs::read_to_string(&history_file) {
let history: Vec<String> = content
.lines()
.map(std::string::ToString::to_string)
.filter(|s| !s.is_empty())
.collect();
state.input.history = history;
}
state.input.history_file = Some(history_file);
}
}
state
}
/// Spawn the background thread that walks every workspace root and
/// populates `mention_index` for `@file` mention autocomplete.
///
/// Callers that DO need the index (single-process mode, the daemon)
/// call this explicitly after construction.
pub fn spawn_mention_index_build(&self) {
let mention_index = self.mention_index.clone();
let roots = self.workspace_roots.clone();
std::thread::spawn(move || {
const MAX_MENTION_ENTRIES: usize = 50_000;
let mut paths = Vec::new();
'roots: for (i, root) in roots.iter().enumerate() {
for entry in ignore::Walk::new(root).flatten() {
if !entry.path().is_file() {
continue;
}
let rel = entry.path().strip_prefix(root).unwrap_or(entry.path());
let rel_str = rel.display().to_string();
let formatted = if i == 0 {
rel_str
} else {
format!("[{i}]{rel_str}")
};
paths.push(formatted);
if paths.len() >= MAX_MENTION_ENTRIES {
break 'roots;
}
}
}
mention_index.set(paths);
});
}
/// Whether an agent turn is currently running.
pub fn turn_in_flight(&self) -> bool {
self.turn_in_flight.lock().map_or_else(
|_| {
tracing::warn!("[state] turn_in_flight mutex poisoned");
false
},
|g| *g,
)
}
/// Shut down every running LSP server process.
pub fn shutdown_lsp(&mut self) {
if let Ok(mut mgr) = self.lsp_manager.lock() {
mgr.shutdown_all();
}
}
/// Append a message to the transcript, evicting the oldest entry once
/// `max_lines` is exceeded.
pub fn push_transcript(&mut self, msg: ChatMessageDisplay) {
self.transcript_cache.messages.push(msg);
if self.transcript_cache.messages.len() > self.transcript_cache.max_lines {
self.transcript_cache.messages.remove(0);
}
self.transcript_cache.dirty = true;
self.dirty = true;
}
/// Mark the app state as dirty, triggering a TUI re-render on the next frame.
pub fn mark_dirty(&mut self) {
self.dirty = true;
}
/// Queue a toast notification for display and mark the app dirty.
pub fn push_toast(&mut self, toast: Toast) {
self.misc.push_toast(toast);
self.mark_dirty();
}
/// Push an info toast with the given message.
pub fn toast_info(&mut self, msg: impl Into<String>) {
self.push_toast(Toast::new(ToastKind::Info, msg.into()));
}
/// Push a success toast with the given message.
pub fn toast_success(&mut self, msg: impl Into<String>) {
self.push_toast(Toast::new(ToastKind::Success, msg.into()));
}
/// Push a warning toast with the given message.
pub fn toast_warning(&mut self, msg: impl Into<String>) {
self.push_toast(Toast::new(ToastKind::Warning, msg.into()));
}
/// Push an error toast with the given message.
pub fn toast_error(&mut self, msg: impl Into<String>) {
self.push_toast(Toast::new(ToastKind::Error, msg.into()));
}
/// Resolve the base directory that stores this session (grandparent of
/// `session_dir`).
pub fn store_base_dir(&self) -> PathBuf {
self.session_dir
.parent()
.and_then(|p| p.parent())
.map_or_else(
|| {
tracing::warn!(
"[state] session_dir '{}' has no grandparent, using parent",
self.session_dir.display()
);
self.session_dir.parent().map_or_else(
|| {
tracing::warn!(
"[state] session_dir '{}' has no parent at all, using itself",
self.session_dir.display()
);
self.session_dir.clone()
},
std::path::Path::to_path_buf,
)
},
std::path::Path::to_path_buf,
)
}
/// Persist the current settings to the store and swallow any error.
pub fn save_settings(&self) {
let _ = JsonSettingsRepository::new()
.save(&self.store_base_dir(), &self.settings);
}
}
// ---------------------------------------------------------------------------
// SessionLockGuard — RAII guard that releases a session lock on drop
// ---------------------------------------------------------------------------
/// RAII guard that releases a session lock on drop.
pub struct SessionLockGuard {
lock_repo: FileSystemSessionLockRepository,
session_dir: PathBuf,
}
impl SessionLockGuard {
/// Create a new guard. Caller must have already acquired the lock.
pub fn new(lock_repo: FileSystemSessionLockRepository, session_dir: PathBuf) -> Self {
tracing::debug!("acquired session lock for {:?}", session_dir);
Self {
lock_repo,
session_dir,
}
}
}
impl Drop for SessionLockGuard {
fn drop(&mut self) {
tracing::debug!("releasing session lock for {:?}", self.session_dir);
let _ = self.lock_repo.unlock(&self.session_dir);
}
}
// ---------------------------------------------------------------------------
// DaemonState — wraps AppStateRest with IPC socket metadata
// ---------------------------------------------------------------------------
/// The daemon's overall state: owns the application state and the IPC socket
/// metadata for client connections.
pub struct DaemonState {
/// The canonical application state for this daemon session.
pub app_state: AppStateRest,
/// The daemon session's unique identifier (same as `app_state.session_id`).
pub session_id: String,
/// Path to the bound Unix socket, if any.
pub socket_path: Option<String>,
}
impl DaemonState {
/// Wrap an `AppStateRest` into a `DaemonState`.
pub fn new(app_state: AppStateRest) -> Self {
let session_id = app_state.session_id.clone();
DaemonState {
app_state,
session_id,
socket_path: None,
}
}
/// Set the socket path after binding.
pub fn set_socket_path(&mut self, path: String) {
self.socket_path = Some(path);
}
}
// ---------------------------------------------------------------------------
// Session creation
// ---------------------------------------------------------------------------
/// Create a new daemon session: store, session directory, exclusive lock,
/// application state, and tokio runtime.
///
/// Flow: create the store → create a new session directory → attempt an
/// exclusive lock → build `AppStateRest` → spawn mention-index builder →
/// load session list → start a tokio runtime.
///
/// Return: (store, lock guard, app_state, tokio_runtime).
pub fn create_session() -> Result<(
zesdex_infrastructure::Store,
SessionLockGuard,
AppStateRest,
tokio::runtime::Runtime,
)> {
tracing::info!("creating new daemon session");
let store = zesdex_infrastructure::Store::new();
store.ensure_dirs()?;
let session_id = uuid::Uuid::new_v4().to_string();
let session_dir = store.base_dir.join("sessions").join(&session_id);
std::fs::create_dir_all(&session_dir)?;
let lock_repo = FileSystemSessionLockRepository::new();
if !lock_repo.try_lock(&session_dir)? {
anyhow::bail!(
"session already active (another zesdex process holds the lock for this session directory)"
);
}
let session_lock_guard = SessionLockGuard::new(lock_repo, session_dir.clone());
let workspace_roots = vec![std::env::current_dir()?];
let mut state = AppStateRest::new(workspace_roots, &session_dir, store.memory_dir.clone());
state.spawn_mention_index_build();
let session_repo =
zesdex_infrastructure::persistence::FileSystemSessionRepository::new();
state.sessions = session_repo
.list_sessions(&store.base_dir)
.unwrap_or_default();
let rt = tokio::runtime::Runtime::new()?;
Ok((store, session_lock_guard, state, rt))
}