Simplify Conversations to a single default thread (#22)

* refactor: update Conversations component and routing logic

- Replaced the Conversations component rendering in AppRoutes with a redirect to the conversations list.
- Simplified the Conversations component by removing unused imports and state variables.
- Introduced default thread creation logic to ensure a conversation is always available.
- Streamlined the effect hooks for managing thread selection and message fetching.

* refactor: remove TDLib integration and clean up related code

- Eliminated TDLib-related code and comments across multiple files to streamline the project.
- Updated build logic to remove unnecessary checks for TDLib resources.
- Refined comments for clarity regarding background service shutdown and storage mechanisms.
- Adjusted the QuickJS runtime support module to reflect the removal of TDLib integration.

* refactor conversations to single default thread UI
This commit is contained in:
Steven Enamakel
2026-03-26 19:38:51 -07:00
committed by GitHub
parent 5d6c9e7333
commit 4dffd9526e
11 changed files with 455 additions and 1351 deletions
-8
View File
@@ -5,11 +5,3 @@
# Generated by Tauri
# will have schema files for capabilities auto-completion
/gen/schemas
# TDLib and dependencies copied by build.rs for macOS bundling
/libraries/
# TDLib built from source (see scripts/build-tdlib-from-source.sh)
/tdlib-local/
/tdlib-build/
/tdlib-cache/
+1 -17
View File
@@ -1,5 +1,4 @@
use std::env;
use std::path::PathBuf;
fn main() {
maybe_override_tauri_config_for_local_builds();
@@ -9,13 +8,8 @@ fn main() {
fn maybe_override_tauri_config_for_local_builds() {
let profile = env::var("PROFILE").unwrap_or_default();
let skip_resources = env::var("TAURI_SKIP_RESOURCES").is_ok() || profile == "test";
let is_release = profile == "release";
let manifest_dir =
PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"));
let tdlib_framework_path = manifest_dir.join("libraries/libtdjson.1.8.29.dylib");
let skip_missing_frameworks = !is_release && !tdlib_framework_path.exists();
if !skip_resources && !skip_missing_frameworks {
if !skip_resources {
return;
}
@@ -23,9 +17,6 @@ fn maybe_override_tauri_config_for_local_builds() {
if skip_resources {
merge_config["bundle"]["resources"] = serde_json::json!([]);
}
if skip_missing_frameworks {
merge_config["bundle"]["macOS"]["frameworks"] = serde_json::json!([]);
}
match serde_json::to_string(&merge_config) {
Ok(json) => {
@@ -33,16 +24,9 @@ fn maybe_override_tauri_config_for_local_builds() {
if skip_resources {
println!("cargo:warning=TAURI resources disabled for local build");
}
if skip_missing_frameworks {
println!(
"cargo:warning=TAURI macOS frameworks disabled because {} is missing",
tdlib_framework_path.display()
);
}
}
Err(err) => {
println!("cargo:warning=Failed to serialize TAURI_CONFIG override: {err}");
}
}
}
+1 -2
View File
@@ -1242,8 +1242,7 @@ pub fn run() {
}
}
// Gracefully shut down TDLib before process exit to prevent
// use-after-free crash in the blocking receive loop.
// Gracefully shut down background services before process exit.
#[cfg(not(any(target_os = "android", target_os = "ios")))]
RunEvent::Exit => {
log::info!("[app] Exit event received, shutting down");
+5 -5
View File
@@ -17,7 +17,7 @@ use crate::runtime::skill_registry::SkillRegistry;
use crate::runtime::socket_manager::SocketManager;
use crate::runtime::types::{events, SkillMessage, SkillSnapshot, SkillStatus, ToolResult};
use crate::runtime::qjs_skill_instance::{BridgeDeps, QjsSkillInstance};
// IdbStorage removed with TDLib cleanup
// IdbStorage removed during runtime cleanup
/// The central runtime engine using QuickJS.
pub struct RuntimeEngine {
@@ -463,20 +463,20 @@ impl RuntimeEngine {
}
/// Read a KV value from a skill's database.
/// TODO: Removed with TDLib cleanup - reimplement if needed
/// TODO: Removed during runtime cleanup - reimplement if needed
pub fn kv_get(&self, _skill_id: &str, _key: &str) -> Result<serde_json::Value, String> {
Err("KV storage removed with TDLib cleanup".to_string())
Err("KV storage removed during runtime cleanup".to_string())
}
/// Write a KV value into a skill's database.
/// TODO: Removed with TDLib cleanup - reimplement if needed
/// TODO: Removed during runtime cleanup - reimplement if needed
pub fn kv_set(
&self,
_skill_id: &str,
_key: &str,
_value: &serde_json::Value,
) -> Result<(), String> {
Err("KV storage removed with TDLib cleanup".to_string())
Err("KV storage removed during runtime cleanup".to_string())
}
/// Route a JSON-RPC method call.
+2 -70
View File
@@ -1,7 +1,7 @@
/**
* Bootstrap JavaScript for QuickJS Runtime
*
* Provides browser-like API shims that tdweb expects.
* Provides browser-like API shims for skill execution.
* These shims call Rust "ops" for actual I/O.
*/
@@ -343,7 +343,7 @@ WebSocket._instances = new Map();
globalThis.WebSocket = WebSocket;
// ============================================================================
// IndexedDB API (for tdweb persistence)
// IndexedDB API (persistent local storage)
// ============================================================================
class IDBFactory {
open(name, version = 1) {
@@ -940,74 +940,6 @@ globalThis.skills = {
},
};
// ============================================================================
// TDLib Bridge API (telegram skill only)
// ============================================================================
// Provides native TDLib access for the telegram skill.
// This is only available on desktop - Android uses Tauri invoke() instead.
globalThis.tdlib = {
/**
* Check if TDLib ops are available.
* @returns {boolean} True on desktop, false on mobile/web.
*/
isAvailable: function () {
try {
return typeof __ops?.tdlib_is_available === 'function'
? __ops.tdlib_is_available()
: false;
} catch (e) {
return false;
}
},
/**
* Create client and set TDLib parameters in a single atomic call.
* API credentials are stored on the Rust side only.
* @param {string} dataDir - Path to store TDLib data files.
* @returns {Promise<number>} Client ID.
*/
ensureInitialized: async function (dataDir) {
return await __ops.tdlib_ensure_initialized(dataDir);
},
/**
* Create a TDLib client with the given data directory.
* @param {string} dataDir - Path to store TDLib data files.
* @returns {Promise<number>} Client ID (always 1 for singleton).
*/
createClient: function (dataDir) {
return __ops.tdlib_create_client(dataDir);
},
/**
* Send a TDLib request and wait for the response.
* @param {string} requestJson - JSON-serialized TDLib API request with @type field.
* @returns {Promise<string>} JSON-serialized TDLib response.
*/
send: async function (requestJson) {
return await __ops.tdlib_send(requestJson);
},
/**
* Receive the next TDLib update (with timeout).
* @param {number} [timeoutMs=1000] - Timeout in milliseconds.
* @returns {Promise<object|null>} Update object or null if timeout.
*/
receive: async function (timeoutMs = 1000) {
const resultJson = await __ops.tdlib_receive(timeoutMs);
return resultJson ? JSON.parse(resultJson) : null;
},
/**
* Destroy the TDLib client and clean up resources.
* @returns {Promise<void>}
*/
destroy: async function () {
return await __ops.tdlib_destroy();
},
};
// ============================================================================
// Model Bridge API (routes to cloud backend)
// ============================================================================
+2 -5
View File
@@ -1,13 +1,10 @@
//! TDLib Runtime Module
//! QuickJS Runtime Support Module
//!
//! Provides a QuickJS JavaScript runtime (via rquickjs) for running
//! skill JavaScript code and TDLib integration. Provides a browser-like
//! skill JavaScript code and supporting browser-like shims.
//! environment for skill execution.
pub mod qjs_ops;
pub mod service;
pub mod storage;
#[allow(unused_imports)]
pub use service::{TdClientAdapter, TdClientConfig, TdUpdate, TdlibV8Service};
pub use storage::IdbStorage;
@@ -1,457 +0,0 @@
//! TdlibV8Service — High-level TDLib service using V8 runtime.
//!
//! Manages TDLib client instances running in V8 with tdweb.
//! Provides async send/receive interface and update broadcasting.
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use tokio::sync::{broadcast, mpsc, oneshot};
use super::storage::IdbStorage;
/// Configuration for a TDLib client.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(dead_code)]
pub struct TdClientConfig {
/// API ID from my.telegram.org
pub api_id: i32,
/// API hash from my.telegram.org
pub api_hash: String,
/// Database directory name
pub database_directory: String,
/// Files directory name
pub files_directory: String,
/// Use test DC
#[serde(default)]
pub use_test_dc: bool,
/// Use file database
#[serde(default = "default_true")]
pub use_file_database: bool,
/// Use chat info database
#[serde(default = "default_true")]
pub use_chat_info_database: bool,
/// Use message database
#[serde(default = "default_true")]
pub use_message_database: bool,
/// System language code
#[serde(default = "default_lang")]
pub system_language_code: String,
/// Device model
#[serde(default = "default_device")]
pub device_model: String,
/// Application version
#[serde(default = "default_version")]
pub application_version: String,
}
#[allow(dead_code)]
fn default_true() -> bool {
true
}
#[allow(dead_code)]
fn default_lang() -> String {
"en".to_string()
}
#[allow(dead_code)]
fn default_device() -> String {
"Desktop".to_string()
}
#[allow(dead_code)]
fn default_version() -> String {
"1.0.0".to_string()
}
impl Default for TdClientConfig {
fn default() -> Self {
Self {
api_id: 0,
api_hash: String::new(),
database_directory: "tdlib".to_string(),
files_directory: "tdlib_files".to_string(),
use_test_dc: false,
use_file_database: true,
use_chat_info_database: true,
use_message_database: true,
system_language_code: "en".to_string(),
device_model: "Desktop".to_string(),
application_version: "1.0.0".to_string(),
}
}
}
/// A TDLib update received from the server.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(dead_code)]
pub struct TdUpdate {
/// The update type (e.g., "updateNewMessage")
#[serde(rename = "@type")]
pub update_type: String,
/// The full update data
#[serde(flatten)]
pub data: serde_json::Value,
}
/// Messages sent to the TDLib service.
#[derive(Debug)]
#[allow(dead_code)]
pub enum TdServiceMessage {
/// Send a TDLib query
Send {
user_id: String,
query: serde_json::Value,
reply: oneshot::Sender<Result<serde_json::Value, String>>,
},
/// Get current auth state
GetAuthState {
user_id: String,
reply: oneshot::Sender<Result<serde_json::Value, String>>,
},
/// Create a new client
CreateClient {
user_id: String,
config: TdClientConfig,
reply: oneshot::Sender<Result<(), String>>,
},
/// Destroy a client
DestroyClient {
user_id: String,
reply: oneshot::Sender<Result<(), String>>,
},
/// Stop the service
Stop,
}
/// State for a single TDLib client.
#[allow(dead_code)]
struct TdClientState {
/// Current authorization state
auth_state: serde_json::Value,
/// Whether the client is ready
is_ready: bool,
}
/// TDLib V8 Service that manages TDLib clients.
#[allow(dead_code)]
pub struct TdlibV8Service {
/// Data directory for TDLib databases
data_dir: PathBuf,
/// Storage layer for IndexedDB emulation
storage: IdbStorage,
/// Message sender for the service
tx: mpsc::Sender<TdServiceMessage>,
/// Update broadcaster
update_tx: broadcast::Sender<(String, TdUpdate)>,
}
#[allow(dead_code)]
impl TdlibV8Service {
/// Create a new TDLib V8 service.
pub async fn new(data_dir: PathBuf) -> Result<Self, String> {
let storage = IdbStorage::new(&data_dir)?;
let (tx, _rx) = mpsc::channel(64);
let (update_tx, _) = broadcast::channel(256);
let service = Self {
data_dir,
storage,
tx,
update_tx,
};
Ok(service)
}
/// Get a sender for the service.
pub fn sender(&self) -> mpsc::Sender<TdServiceMessage> {
self.tx.clone()
}
/// Subscribe to TDLib updates.
/// Returns a receiver that will receive (user_id, update) tuples.
pub fn subscribe(&self) -> broadcast::Receiver<(String, TdUpdate)> {
self.update_tx.subscribe()
}
/// Send a query to TDLib.
pub async fn send(
&self,
user_id: &str,
query: serde_json::Value,
) -> Result<serde_json::Value, String> {
let (reply_tx, reply_rx) = oneshot::channel();
self.tx
.send(TdServiceMessage::Send {
user_id: user_id.to_string(),
query,
reply: reply_tx,
})
.await
.map_err(|e| format!("Failed to send query: {e}"))?;
reply_rx
.await
.map_err(|_| "Service did not respond".to_string())?
}
/// Get the current authorization state.
pub async fn get_auth_state(&self, user_id: &str) -> Result<serde_json::Value, String> {
let (reply_tx, reply_rx) = oneshot::channel();
self.tx
.send(TdServiceMessage::GetAuthState {
user_id: user_id.to_string(),
reply: reply_tx,
})
.await
.map_err(|e| format!("Failed to get auth state: {e}"))?;
reply_rx
.await
.map_err(|_| "Service did not respond".to_string())?
}
/// Create a new TDLib client for a user.
pub async fn create_client(
&self,
user_id: &str,
config: TdClientConfig,
) -> Result<(), String> {
let (reply_tx, reply_rx) = oneshot::channel();
self.tx
.send(TdServiceMessage::CreateClient {
user_id: user_id.to_string(),
config,
reply: reply_tx,
})
.await
.map_err(|e| format!("Failed to create client: {e}"))?;
reply_rx
.await
.map_err(|_| "Service did not respond".to_string())?
}
/// Destroy a TDLib client.
pub async fn destroy_client(&self, user_id: &str) -> Result<(), String> {
let (reply_tx, reply_rx) = oneshot::channel();
self.tx
.send(TdServiceMessage::DestroyClient {
user_id: user_id.to_string(),
reply: reply_tx,
})
.await
.map_err(|e| format!("Failed to destroy client: {e}"))?;
reply_rx
.await
.map_err(|_| "Service did not respond".to_string())?
}
}
/// TDLib client adapter that wraps a V8 runtime.
///
/// This is a placeholder for the full tdweb integration.
/// In the full implementation, this would:
/// 1. Load the tdweb JavaScript bundle
/// 2. Initialize TdClient with the WASM module
/// 3. Handle send/receive through the V8 runtime
#[allow(dead_code)]
pub struct TdClientAdapter {
user_id: String,
config: TdClientConfig,
data_dir: PathBuf,
storage: IdbStorage,
auth_state: serde_json::Value,
query_id: u64,
}
#[allow(dead_code)]
impl TdClientAdapter {
/// Create a new TDLib client adapter.
pub fn new(
user_id: String,
config: TdClientConfig,
data_dir: PathBuf,
storage: IdbStorage,
) -> Self {
Self {
user_id,
config,
data_dir,
storage,
auth_state: serde_json::json!({
"@type": "authorizationStateWaitTdlibParameters"
}),
query_id: 0,
}
}
/// Initialize the TDLib client.
///
/// This would load the tdweb bundle and initialize the WASM module.
pub async fn init(&mut self) -> Result<(), String> {
log::info!("[tdlib:{}] Initializing TDLib client", self.user_id);
// TODO: Load tdweb.js and libtdjson.wasm
// TODO: Create TdClient instance in V8
// TODO: Set up update handlers
// For now, simulate initialization
self.auth_state = serde_json::json!({
"@type": "authorizationStateWaitTdlibParameters"
});
Ok(())
}
/// Send a query to TDLib.
pub async fn send(&mut self, query: serde_json::Value) -> Result<serde_json::Value, String> {
self.query_id += 1;
let query_id = self.query_id;
log::debug!(
"[tdlib:{}] Sending query {}: {:?}",
self.user_id,
query_id,
query.get("@type")
);
// TODO: Execute query through V8/tdweb
// For now, return a placeholder response
let query_type = query
.get("@type")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
match query_type {
"getAuthorizationState" => Ok(self.auth_state.clone()),
"setTdlibParameters" => {
self.auth_state = serde_json::json!({
"@type": "authorizationStateWaitPhoneNumber"
});
Ok(serde_json::json!({ "@type": "ok" }))
}
_ => Ok(serde_json::json!({
"@type": "error",
"code": 400,
"message": "TDLib not fully initialized - tdweb integration pending"
})),
}
}
/// Get the current authorization state.
pub fn get_auth_state(&self) -> serde_json::Value {
self.auth_state.clone()
}
/// Destroy the client.
pub async fn destroy(&mut self) -> Result<(), String> {
log::info!("[tdlib:{}] Destroying TDLib client", self.user_id);
// TODO: Properly close TDLib client
// TODO: Cleanup V8 runtime
Ok(())
}
}
/// JavaScript code to initialize the TDLib bridge in V8.
///
/// This provides the `__tdlib_send` and `__tdlib_get_auth_state` functions
/// that the bootstrap.js exposes to skills.
#[allow(dead_code)]
pub const TDLIB_BRIDGE_JS: &str = r#"
// TDLib Bridge for V8 Runtime
// This will be replaced with actual tdweb integration
(function() {
// Client state
const clients = new Map();
let defaultClient = null;
// Create a mock client for now
class MockTdClient {
constructor(options) {
this.options = options;
this.authState = { '@type': 'authorizationStateWaitTdlibParameters' };
this.queryId = 0;
this.callbacks = new Map();
}
async send(query) {
this.queryId++;
const queryType = query['@type'];
console.log('[TDLib] Send:', queryType);
// Handle known query types
switch (queryType) {
case 'getAuthorizationState':
return this.authState;
case 'setTdlibParameters':
this.authState = { '@type': 'authorizationStateWaitPhoneNumber' };
return { '@type': 'ok' };
case 'setAuthenticationPhoneNumber':
this.authState = { '@type': 'authorizationStateWaitCode' };
return { '@type': 'ok' };
default:
return {
'@type': 'error',
'code': 400,
'message': 'TDLib not fully initialized - waiting for tdweb integration'
};
}
}
getAuthState() {
return this.authState;
}
}
// Initialize default client
function initClient(userId, options) {
const client = new MockTdClient(options);
clients.set(userId, client);
if (!defaultClient) {
defaultClient = client;
}
return client;
}
// Get or create client
function getClient(userId) {
if (!clients.has(userId)) {
initClient(userId, {});
}
return clients.get(userId);
}
// Export to global scope
globalThis.__tdlib_send = async function(userId, query) {
const client = getClient(userId);
return await client.send(query);
};
globalThis.__tdlib_get_auth_state = function(userId) {
const client = getClient(userId);
return client.getAuthState();
};
globalThis.__tdlib_init = function(userId, options) {
return initClient(userId, options);
};
console.log('[TDLib] Bridge initialized (mock mode)');
})();
"#;
@@ -1,7 +1,7 @@
//! IndexedDB Storage Layer (SQLite-backed)
//!
//! Provides persistent storage for:
//! - IndexedDB API (for tdweb)
//! - IndexedDB API
//! - Skill databases (db bridge)
//! - Skill key-value store (store bridge)
@@ -12,7 +12,7 @@ use std::path::{Path, PathBuf};
use std::sync::Arc;
/// Result of opening an IndexedDB database.
/// Used by the IndexedDB emulation layer for tdweb.
/// Used by the IndexedDB emulation layer.
#[derive(Debug, Clone, serde::Serialize)]
pub struct IdbOpenResult {
/// Whether a version upgrade is needed.
+1 -1
View File
@@ -124,7 +124,7 @@ const AppRoutes = () => {
path="/conversations/:threadId"
element={
<ProtectedRoute requireAuth={true}>
<Conversations />
<Navigate to="/conversations" replace />
</ProtectedRoute>
}
/>
File diff suppressed because it is too large Load Diff
+3
View File
@@ -33,6 +33,9 @@ export interface ChatCompletionRequest {
messages: ChatMessage[];
tools?: Tool[];
tool_choice?: 'auto' | 'none' | { type: 'function'; function: { name: string } };
openhuman?: {
trace_tools?: boolean;
};
stream?: boolean;
temperature?: number;
max_tokens?: number;