tauri cleanup

This commit is contained in:
lovebird 2025-09-20 13:48:45 +02:00
parent 005a909534
commit 3a59b33a91
12 changed files with 628 additions and 525 deletions

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -0,0 +1,222 @@
use tauri::{Manager, Emitter};
use serde::{Serialize, Deserialize};
use dirs;
use crate::{log_json, Counter, DebugMessages, DebugPayload};
#[derive(Serialize, Deserialize)]
pub struct Payload {
pub prompt: String,
pub files: Vec<String>,
pub dst: String,
}
#[derive(Serialize, Deserialize)]
pub struct IPCMessage {
#[serde(rename = "type")]
pub message_type: String,
pub data: serde_json::Value,
pub timestamp: Option<u64>,
pub id: Option<String>,
}
// Core command handlers
#[tauri::command]
pub fn submit_prompt(prompt: &str, files: Vec<String>, dst: &str, window: tauri::Window) {
log_json("info", "submit_prompt command called", Some(serde_json::json!({
"prompt": prompt,
"files": files,
"dst": dst
})));
let payload = Payload {
prompt: prompt.to_string(),
files,
dst: dst.to_string(),
};
let json_payload = serde_json::to_string(&payload).unwrap();
log_json("info", "Sending JSON payload to stdout", Some(serde_json::json!({
"payload_length": json_payload.len()
})));
println!("{}", json_payload);
let _ = window.app_handle().exit(0);
}
#[tauri::command]
pub fn log_error_to_console(error: &str) {
eprintln!("[WebView ERROR forwarded]: {}", error);
}
#[tauri::command]
pub fn resolve_path_relative_to_home(absolute_path: String) -> Result<String, String> {
let home_dir = dirs::home_dir().ok_or_else(|| "Could not find home directory".to_string())?;
let path_to_resolve = std::path::Path::new(&absolute_path);
let relative_path = pathdiff::diff_paths(path_to_resolve, home_dir)
.ok_or_else(|| "Failed to calculate relative path from home directory".to_string())?;
Ok(relative_path.to_string_lossy().to_string())
}
// Debug message handlers
#[tauri::command]
pub fn add_debug_message(message: String, level: String, data: Option<serde_json::Value>, state: tauri::State<'_, DebugMessages>) -> Result<(), String> {
log_json(&level, &format!("Frontend: {}", message), data.clone());
let debug_payload = DebugPayload {
level,
message,
data,
};
let mut messages = state.0.lock().unwrap();
messages.push(debug_payload);
if messages.len() > 100 {
let len = messages.len();
messages.drain(0..len - 100);
}
Ok(())
}
#[tauri::command]
pub fn get_debug_messages(state: tauri::State<'_, DebugMessages>) -> Result<Vec<DebugPayload>, String> {
let messages = state.0.lock().unwrap();
Ok(messages.clone())
}
#[tauri::command]
pub fn clear_debug_messages(state: tauri::State<'_, DebugMessages>) -> Result<(), String> {
let mut messages = state.0.lock().unwrap();
messages.clear();
Ok(())
}
// Counter handlers (legacy)
#[tauri::command]
pub fn increment_counter(state: tauri::State<'_, Counter>) -> Result<u32, String> {
let mut counter = state.0.lock().unwrap();
*counter += 1;
Ok(*counter)
}
#[tauri::command]
pub fn get_counter(state: tauri::State<'_, Counter>) -> Result<u32, String> {
let counter = state.0.lock().unwrap();
Ok(*counter)
}
#[tauri::command]
pub fn reset_counter(state: tauri::State<'_, Counter>) -> Result<u32, String> {
let mut counter = state.0.lock().unwrap();
*counter = 0;
Ok(0)
}
// IPC communication handlers
#[tauri::command]
pub fn send_ipc_message(message_type: String, data: serde_json::Value, _window: tauri::Window) -> Result<(), String> {
let ipc_message = IPCMessage {
message_type,
data,
timestamp: Some(std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64),
id: Some(format!("msg_{}_{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis(),
rand::random::<u32>())),
};
let json_message = serde_json::to_string(&ipc_message).unwrap();
println!("{}", json_message);
Ok(())
}
#[tauri::command]
pub fn send_message_to_stdout(message: String) -> Result<(), String> {
println!("{}", message);
Ok(())
}
// Image generation handlers
#[tauri::command]
pub fn generate_image_via_backend(prompt: String, files: Vec<String>, dst: String) -> Result<(), String> {
let request = serde_json::json!({
"type": "generate_request",
"prompt": prompt,
"files": files,
"dst": dst,
"timestamp": std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis()
});
println!("{}", serde_json::to_string(&request).unwrap());
Ok(())
}
#[tauri::command]
pub fn request_config_from_images(_app: tauri::AppHandle) -> Result<(), String> {
let request = serde_json::json!({
"type": "config_request",
"timestamp": std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis()
});
println!("{}", serde_json::to_string(&request).unwrap());
Ok(())
}
#[tauri::command]
pub fn request_file_deletion(path: String) -> Result<(), String> {
let request = serde_json::json!({
"type": "delete_request",
"path": path,
});
println!("{}", serde_json::to_string(&request).unwrap());
Ok(())
}
// Legacy direct handlers (not used via stdin)
#[tauri::command]
pub fn forward_config_to_frontend(prompt: Option<String>, dst: Option<String>, api_key: Option<String>, files: Vec<String>, app: tauri::AppHandle) -> Result<(), String> {
let config_data = serde_json::json!({
"prompt": prompt,
"dst": dst,
"apiKey": api_key,
"files": files
});
if let Err(e) = app.emit("config-received", &config_data) {
return Err(format!("Failed to emit config: {}", e));
}
Ok(())
}
#[tauri::command]
pub fn forward_image_to_frontend(base64: String, mime_type: String, filename: String, app: tauri::AppHandle) -> Result<(), String> {
let image_data = serde_json::json!({
"base64": base64,
"mimeType": mime_type,
"filename": filename
});
if let Err(e) = app.emit("image-received", &image_data) {
return Err(format!("Failed to emit image: {}", e));
}
Ok(())
}

View File

@ -1,6 +1,10 @@
use tauri::{Manager, Emitter};
use tauri::Manager;
use serde::{Serialize, Deserialize};
use dirs;
mod handlers;
mod stdin_processor;
pub use handlers::*;
#[derive(Serialize)]
struct LogMessage {
@ -11,7 +15,7 @@ struct LogMessage {
timestamp: u64,
}
fn log_json(level: &str, message: &str, data: Option<serde_json::Value>) {
pub fn log_json(level: &str, message: &str, data: Option<serde_json::Value>) {
let log_msg = LogMessage {
level: level.to_string(),
message: message.to_string(),
@ -24,298 +28,17 @@ fn log_json(level: &str, message: &str, data: Option<serde_json::Value>) {
eprintln!("{}", serde_json::to_string(&log_msg).unwrap_or_else(|_| format!("{{\"level\":\"error\",\"message\":\"Failed to serialize log message\"}}")));
}
struct Counter(std::sync::Mutex<u32>);
struct DebugMessages(std::sync::Mutex<Vec<DebugPayload>>);
#[derive(Serialize, Deserialize)]
struct Payload {
prompt: String,
files: Vec<String>,
dst: String,
}
#[derive(Serialize, Deserialize)]
struct IPCMessage {
#[serde(rename = "type")]
message_type: String,
data: serde_json::Value,
timestamp: Option<u64>,
id: Option<String>,
}
#[derive(Serialize, Deserialize)]
struct CounterPayload {
count: u32,
message: Option<String>,
}
// App state structures
pub struct Counter(pub std::sync::Mutex<u32>);
pub struct DebugMessages(pub std::sync::Mutex<Vec<DebugPayload>>);
#[derive(Serialize, Deserialize, Clone)]
struct DebugPayload {
level: String,
message: String,
data: Option<serde_json::Value>,
pub struct DebugPayload {
pub level: String,
pub message: String,
pub data: Option<serde_json::Value>,
}
#[derive(Serialize, Deserialize)]
struct ImagePayload {
base64: String,
#[serde(rename = "mimeType")]
mime_type: String,
filename: Option<String>,
}
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
fn submit_prompt(prompt: &str, files: Vec<String>, dst: &str, window: tauri::Window) {
log_json("info", "submit_prompt command called", Some(serde_json::json!({
"prompt": prompt,
"files": files,
"dst": dst
})));
let payload = Payload {
prompt: prompt.to_string(),
files,
dst: dst.to_string(),
};
let json_payload = serde_json::to_string(&payload).unwrap();
log_json("info", "Sending JSON payload to stdout", Some(serde_json::json!({
"payload_length": json_payload.len()
})));
println!("{}", json_payload); // The actual payload - ONLY this should go to stdout
let _ = window.app_handle().exit(0);
}
#[tauri::command]
fn log_error_to_console(error: &str) {
eprintln!("[WebView ERROR forwarded]: {}", error);
}
#[tauri::command]
fn resolve_path_relative_to_home(absolute_path: String) -> Result<String, String> {
eprintln!("[RUST LOG]: resolve_path_relative_to_home command called.");
eprintln!("[RUST LOG]: - Received absolute path: {}", absolute_path);
let home_dir = dirs::home_dir().ok_or_else(|| "Could not find home directory".to_string())?;
let path_to_resolve = std::path::Path::new(&absolute_path);
let relative_path = pathdiff::diff_paths(path_to_resolve, home_dir)
.ok_or_else(|| "Failed to calculate relative path from home directory".to_string())?;
let result = relative_path.to_string_lossy().to_string();
eprintln!("[RUST LOG]: - Resolved to path relative to home: {}", result);
Ok(result)
}
#[tauri::command]
fn increment_counter(state: tauri::State<'_, Counter>) -> Result<u32, String> {
eprintln!("[RUST LOG]: increment_counter command called.");
let mut counter = state.0.lock().unwrap();
*counter += 1;
let current_value = *counter;
eprintln!("[RUST LOG]: - Counter incremented to: {}", current_value);
Ok(current_value)
}
#[tauri::command]
fn get_counter(state: tauri::State<'_, Counter>) -> Result<u32, String> {
eprintln!("[RUST LOG]: get_counter command called.");
let counter = state.0.lock().unwrap();
let current_value = *counter;
eprintln!("[RUST LOG]: - Current counter value: {}", current_value);
Ok(current_value)
}
#[tauri::command]
fn reset_counter(state: tauri::State<'_, Counter>) -> Result<u32, String> {
eprintln!("[RUST LOG]: reset_counter command called.");
let mut counter = state.0.lock().unwrap();
*counter = 0;
eprintln!("[RUST LOG]: - Counter reset to: 0");
Ok(0)
}
#[tauri::command]
fn add_debug_message(message: String, level: String, data: Option<serde_json::Value>, state: tauri::State<'_, DebugMessages>) -> Result<(), String> {
// Forward frontend debug messages to CLI via structured logging
log_json(&level, &format!("Frontend: {}", message), data.clone());
let debug_payload = DebugPayload {
level,
message,
data,
};
let mut messages = state.0.lock().unwrap();
messages.push(debug_payload);
// Keep only the last 100 messages to prevent memory issues
if messages.len() > 100 {
let len = messages.len();
messages.drain(0..len - 100);
}
Ok(())
}
#[tauri::command]
fn get_debug_messages(state: tauri::State<'_, DebugMessages>) -> Result<Vec<DebugPayload>, String> {
eprintln!("[RUST LOG]: get_debug_messages command called.");
let messages = state.0.lock().unwrap();
let result = messages.clone();
eprintln!("[RUST LOG]: - Returning {} debug messages", result.len());
Ok(result)
}
#[tauri::command]
fn clear_debug_messages(state: tauri::State<'_, DebugMessages>) -> Result<(), String> {
eprintln!("[RUST LOG]: clear_debug_messages command called.");
let mut messages = state.0.lock().unwrap();
messages.clear();
eprintln!("[RUST LOG]: - Debug messages cleared");
Ok(())
}
#[tauri::command]
fn send_ipc_message(message_type: String, data: serde_json::Value, _window: tauri::Window) -> Result<(), String> {
eprintln!("[RUST LOG]: send_ipc_message command called.");
eprintln!("[RUST LOG]: - Type: {}", message_type);
eprintln!("[RUST LOG]: - Data: {}", data);
let ipc_message = IPCMessage {
message_type,
data,
timestamp: Some(std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64),
id: Some(format!("msg_{}_{}",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis(),
rand::random::<u32>())),
};
let json_message = serde_json::to_string(&ipc_message).unwrap();
eprintln!("[RUST LOG]: - Sending IPC message to stdout: {}", json_message);
println!("{}", json_message);
Ok(())
}
#[tauri::command]
fn send_message_to_stdout(message: String) -> Result<(), String> {
eprintln!("[RUST LOG]: send_message_to_stdout command called.");
eprintln!("[RUST LOG]: - Message: {}", message);
// Send directly to stdout (this will be captured by images.ts)
println!("{}", message);
Ok(())
}
#[tauri::command]
fn generate_image_via_backend(prompt: String, files: Vec<String>, dst: String) -> Result<(), String> {
eprintln!("[RUST LOG]: generate_image_via_backend called");
eprintln!("[RUST LOG]: - Prompt: {}", prompt);
eprintln!("[RUST LOG]: - Files: {:?}", files);
eprintln!("[RUST LOG]: - Dst: {}", dst);
// Send generation request to images.ts via stdout
let request = serde_json::json!({
"type": "generate_request",
"prompt": prompt,
"files": files,
"dst": dst,
"timestamp": std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis()
});
println!("{}", serde_json::to_string(&request).unwrap());
eprintln!("[RUST LOG]: Generation request sent to images.ts");
Ok(())
}
#[tauri::command]
fn request_config_from_images(_app: tauri::AppHandle) -> Result<(), String> {
eprintln!("[RUST LOG]: request_config_from_images called");
// Send request to images.ts via stdout
let request = serde_json::json!({
"type": "config_request",
"timestamp": std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis()
});
println!("{}", serde_json::to_string(&request).unwrap());
eprintln!("[RUST LOG]: Config request sent to images.ts");
Ok(())
}
#[tauri::command]
fn forward_config_to_frontend(prompt: Option<String>, dst: Option<String>, api_key: Option<String>, files: Vec<String>, app: tauri::AppHandle) -> Result<(), String> {
eprintln!("[RUST LOG]: forward_config_to_frontend called");
let config_data = serde_json::json!({
"prompt": prompt,
"dst": dst,
"apiKey": api_key,
"files": files
});
if let Err(e) = app.emit("config-received", &config_data) {
eprintln!("[RUST LOG]: Failed to emit config-received: {}", e);
return Err(format!("Failed to emit config: {}", e));
}
eprintln!("[RUST LOG]: Config forwarded to frontend successfully");
Ok(())
}
#[tauri::command]
fn forward_image_to_frontend(base64: String, mime_type: String, filename: String, app: tauri::AppHandle) -> Result<(), String> {
eprintln!("[RUST LOG]: forward_image_to_frontend called for {}", filename);
let image_data = serde_json::json!({
"base64": base64,
"mimeType": mime_type,
"filename": filename
});
if let Err(e) = app.emit("image-received", &image_data) {
eprintln!("[RUST LOG]: Failed to emit image-received: {}", e);
return Err(format!("Failed to emit image: {}", e));
}
eprintln!("[RUST LOG]: Image forwarded to frontend successfully");
Ok(())
}
#[tauri::command]
fn request_file_deletion(path: String) -> Result<(), String> {
log_json("info", "request_file_deletion command called", Some(serde_json::json!({
"path": path
})));
let request = serde_json::json!({
"type": "delete_request",
"path": path,
});
println!("{}", serde_json::to_string(&request).unwrap());
log_json("info", "Deletion request sent to images.ts", None);
Ok(())
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
@ -352,153 +75,9 @@ pub fn run() {
}
let app_handle = app.handle().clone();
// Test our new JSON logging
log_json("info", "Tauri app starting with improved logging", Some(serde_json::json!({
"test": true,
"message": "This is a test of the new structured logging system"
})));
// Listen for stdin commands from images.ts
std::thread::spawn(move || {
use std::io::{self, BufRead, BufReader};
let stdin = io::stdin();
let reader = BufReader::new(stdin);
log_json("info", "Stdin listener thread started", None);
for line in reader.lines() {
if let Ok(line_content) = line {
if line_content.trim().is_empty() {
continue;
}
// Log stdin command but hide binary data
if line_content.contains("\"base64\"") {
log_json("debug", "Received stdin command with base64 data", Some(serde_json::json!({
"content_length": line_content.len()
})));
} else {
log_json("debug", "Received stdin command", Some(serde_json::json!({
"content": line_content
})));
}
// Parse command from images.ts
if let Ok(command) = serde_json::from_str::<serde_json::Value>(&line_content) {
if let Some(cmd) = command.get("cmd").and_then(|v| v.as_str()) {
log_json("info", "Processing command", Some(serde_json::json!({
"command": cmd
})));
match cmd {
"forward_config_to_frontend" => {
log_json("info", "Forwarding config to frontend", Some(serde_json::json!({
"has_prompt": command.get("prompt").is_some(),
"has_dst": command.get("dst").is_some(),
"has_api_key": command.get("apiKey").is_some(),
"file_count": command.get("files").and_then(|f| f.as_array()).map(|a| a.len()).unwrap_or(0)
})));
let config_data = serde_json::json!({
"prompt": command.get("prompt"),
"dst": command.get("dst"),
"apiKey": command.get("apiKey"),
"files": command.get("files")
});
if let Err(e) = app_handle.emit("config-received", &config_data) {
log_json("error", "Failed to emit config-received", Some(serde_json::json!({
"error": e.to_string()
})));
} else {
log_json("info", "Config emitted successfully to frontend", None);
}
}
"forward_image_to_frontend" => {
if let (Some(filename), Some(base64), Some(mime_type)) = (
command.get("filename").and_then(|v| v.as_str()),
command.get("base64").and_then(|v| v.as_str()),
command.get("mimeType").and_then(|v| v.as_str())
) {
log_json("info", "Forwarding image to frontend", Some(serde_json::json!({
"filename": filename,
"mime_type": mime_type,
"base64_size": base64.len()
})));
let image_data = serde_json::json!({
"base64": base64,
"mimeType": mime_type,
"filename": filename
});
if let Err(e) = app_handle.emit("image-received", &image_data) {
log_json("error", "Failed to emit image-received", Some(serde_json::json!({
"error": e.to_string(),
"filename": filename
})));
} else {
log_json("info", "Image emitted successfully", Some(serde_json::json!({
"filename": filename
})));
}
}
}
"generation_result" => {
eprintln!("[RUST LOG]: Forwarding generation result to frontend");
if let Err(e) = app_handle.emit("generation-result", &command) {
eprintln!("[RUST LOG]: Failed to emit generation-result: {}", e);
} else {
eprintln!("[RUST LOG]: Generation result emitted successfully");
}
}
"generation_error" => {
eprintln!("[RUST LOG]: Forwarding generation error to frontend");
if let Err(e) = app_handle.emit("generation-error", &command) {
eprintln!("[RUST LOG]: Failed to emit generation-error: {}", e);
} else {
eprintln!("[RUST LOG]: Generation error emitted successfully");
}
}
"generation_complete" => {
eprintln!("[RUST LOG]: Generation completed successfully");
if let Err(e) = app_handle.emit("generation-complete", &command) {
eprintln!("[RUST LOG]: Failed to emit generation-complete: {}", e);
} else {
eprintln!("[RUST LOG]: Generation complete emitted successfully");
}
}
"file_deleted_successfully" => {
if let Some(path) = command.get("path").and_then(|v| v.as_str()) {
eprintln!("[RUST LOG]: Received confirmation of file deletion: {}", path);
if let Err(e) = app_handle.emit("file-deleted-successfully", &serde_json::json!({ "path": path })) {
eprintln!("[RUST LOG]: Failed to emit file-deleted-successfully: {}", e);
}
}
}
"file_deletion_error" => {
if let (Some(path), Some(error)) = (
command.get("path").and_then(|v| v.as_str()),
command.get("error").and_then(|v| v.as_str())
) {
eprintln!("[RUST LOG]: Received file deletion error for {}: {}", path, error);
if let Err(e) = app_handle.emit("file-deletion-error", &serde_json::json!({ "path": path, "error": error })) {
eprintln!("[RUST LOG]: Failed to emit file-deletion-error: {}", e);
}
}
}
_ => {
eprintln!("[RUST LOG]: Unknown command: {}", cmd);
}
}
}
} else {
eprintln!("[RUST LOG]: Failed to parse command as JSON");
}
}
}
eprintln!("[RUST LOG]: Stdin listener thread ended");
});
// Start stdin listener in separate module
stdin_processor::start_stdin_listener(app_handle);
Ok(())
})

View File

@ -0,0 +1,158 @@
use tauri::Emitter;
use std::io::{BufRead, BufReader};
use crate::log_json;
pub fn start_stdin_listener(app_handle: tauri::AppHandle) {
std::thread::spawn(move || {
let stdin = std::io::stdin();
let reader = BufReader::new(stdin);
for line in reader.lines() {
if let Ok(line_content) = line {
if line_content.trim().is_empty() {
continue;
}
// Parse command from images.ts
if let Ok(command) = serde_json::from_str::<serde_json::Value>(&line_content) {
if let Some(cmd) = command.get("cmd").and_then(|v| v.as_str()) {
log_json("info", "Processing command", Some(serde_json::json!({
"command": cmd
})));
match cmd {
"forward_config_to_frontend" => {
handle_config_forward(&command, &app_handle);
}
"forward_image_to_frontend" => {
handle_image_forward(&command, &app_handle);
}
"generation_result" => {
if let Err(e) = app_handle.emit("generation-result", &command) {
log_json("error", "Failed to emit generation-result", Some(serde_json::json!({
"error": e.to_string()
})));
}
}
"generation_error" => {
if let Err(e) = app_handle.emit("generation-error", &command) {
log_json("error", "Failed to emit generation-error", Some(serde_json::json!({
"error": e.to_string()
})));
}
}
"generation_complete" => {
if let Err(e) = app_handle.emit("generation-complete", &command) {
log_json("error", "Failed to emit generation-complete", Some(serde_json::json!({
"error": e.to_string()
})));
}
}
"file_deleted_successfully" => {
if let Some(path) = command.get("path").and_then(|v| v.as_str()) {
if let Err(e) = app_handle.emit("file-deleted-successfully", &serde_json::json!({ "path": path })) {
log_json("error", "Failed to emit file-deleted-successfully", Some(serde_json::json!({
"error": e.to_string()
})));
}
}
}
"file_deletion_error" => {
if let (Some(path), Some(error)) = (
command.get("path").and_then(|v| v.as_str()),
command.get("error").and_then(|v| v.as_str())
) {
if let Err(e) = app_handle.emit("file-deletion-error", &serde_json::json!({ "path": path, "error": error })) {
log_json("error", "Failed to emit file-deletion-error", Some(serde_json::json!({
"error": e.to_string()
})));
}
}
}
_ => {
log_json("warn", "Unknown command received", Some(serde_json::json!({
"command": cmd
})));
}
}
}
} else {
log_json("warn", "Failed to parse stdin as JSON", Some(serde_json::json!({
"content_length": line_content.len()
})));
}
}
}
log_json("info", "Stdin listener thread ended", None);
});
}
fn handle_config_forward(command: &serde_json::Value, app_handle: &tauri::AppHandle) {
log_json("info", "Forwarding config to frontend", Some(serde_json::json!({
"has_prompt": command.get("prompt").is_some(),
"has_dst": command.get("dst").is_some(),
"has_api_key": command.get("apiKey").is_some(),
"file_count": command.get("files").and_then(|f| f.as_array()).map(|a| a.len()).unwrap_or(0)
})));
// Extract values, handling both null and undefined
let prompt = command.get("prompt")
.and_then(|v| if v.is_null() { None } else { v.as_str().map(|s| s.to_string()) });
let dst = command.get("dst")
.and_then(|v| if v.is_null() { None } else { v.as_str().map(|s| s.to_string()) });
let api_key = command.get("apiKey")
.and_then(|v| if v.is_null() { None } else { v.as_str().map(|s| s.to_string()) });
let files = command.get("files")
.and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(|f| f.as_str().map(|s| s.to_string())).collect::<Vec<String>>())
.unwrap_or_else(Vec::new);
let config_data = serde_json::json!({
"prompt": prompt,
"dst": dst,
"apiKey": api_key,
"files": files
});
if let Err(e) = app_handle.emit("config-received", &config_data) {
log_json("error", "Failed to emit config-received", Some(serde_json::json!({
"error": e.to_string()
})));
} else {
log_json("info", "Config emitted successfully to frontend", None);
}
}
fn handle_image_forward(command: &serde_json::Value, app_handle: &tauri::AppHandle) {
if let (Some(filename), Some(base64), Some(mime_type)) = (
command.get("filename").and_then(|v| v.as_str()),
command.get("base64").and_then(|v| v.as_str()),
command.get("mimeType").and_then(|v| v.as_str())
) {
log_json("info", "Forwarding image to frontend", Some(serde_json::json!({
"filename": filename,
"mime_type": mime_type,
"base64_size": base64.len()
})));
let image_data = serde_json::json!({
"base64": base64,
"mimeType": mime_type,
"filename": filename
});
if let Err(e) = app_handle.emit("image-received", &image_data) {
log_json("error", "Failed to emit image-received", Some(serde_json::json!({
"error": e.to_string(),
"filename": filename
})));
} else {
log_json("info", "Image emitted successfully", Some(serde_json::json!({
"filename": filename
})));
}
}
}

View File

@ -26,7 +26,7 @@ function App() {
const [apiKey, setApiKey] = useState("");
const [isDarkMode, setIsDarkMode] = useState(false);
const [debugMessages, setDebugMessages] = useState<any[]>([]);
const [showDebugPanel, setShowDebugPanel] = useState(true); // Default open for debugging
const [showDebugPanel, setShowDebugPanel] = useState(false); // Hidden in production
// Initialize logging system and connect to UI
useEffect(() => {
@ -140,10 +140,6 @@ function App() {
}
};
// Legacy function for compatibility - just use log directly now
const addDebugMessage = (level: 'info' | 'warn' | 'error' | 'debug', message: string, data?: any) => {
log[level](message, data);
};
const addImageFromUrl = async (url: string) => {
try {
@ -416,7 +412,7 @@ function App() {
await generateImage(prompt, imagesToUse);
// Don't clear prompt - let user iterate
} else {
addDebugMessage('error', 'API key required for image generation');
log.error('API key required for image generation');
}
}
@ -591,7 +587,6 @@ function App() {
{showDebugPanel && (
<DebugPanel
debugMessages={debugMessages}
addDebugMessage={addDebugMessage}
sendIPCMessage={sendIPCMessage}
clearDebugMessages={clearDebugMessages}
ipcInitialized={ipcInitialized}

View File

@ -1,9 +1,64 @@
import React from 'react';
import { tauriApi } from '../lib/tauriApi';
import log from '../lib/log';
// Safe JSON stringify to prevent circular reference crashes
function safeStringify(obj: any, maxDepth = 3): string {
const seen = new WeakSet();
function serialize(value: any, depth: number): any {
if (depth > maxDepth) {
return '[Max depth reached]';
}
if (value === null || value === undefined) {
return value;
}
if (typeof value !== 'object') {
return value;
}
if (seen.has(value)) {
return '[Circular Reference]';
}
seen.add(value);
if (Array.isArray(value)) {
return value.slice(0, 10).map((item, index) => {
if (index >= 10) return '[Truncated]';
return serialize(item, depth + 1);
});
}
const result: any = {};
const keys = Object.keys(value).slice(0, 20); // Limit keys
for (const key of keys) {
try {
result[key] = serialize(value[key], depth + 1);
} catch (e) {
result[key] = '[Serialization Error]';
}
}
if (Object.keys(value).length > 20) {
result['[truncated]'] = `${Object.keys(value).length - 20} more keys...`;
}
return result;
}
try {
return JSON.stringify(serialize(obj, 0), null, 2);
} catch (e) {
return `[Serialization failed: ${e instanceof Error ? e.message : 'Unknown error'}]`;
}
}
interface DebugPanelProps {
debugMessages: any[];
addDebugMessage: (level: 'info' | 'warn' | 'error' | 'debug', message: string, data?: any) => void;
sendIPCMessage: (messageType: string, data: any) => void;
clearDebugMessages: () => void;
ipcInitialized: boolean;
@ -14,7 +69,6 @@ interface DebugPanelProps {
const DebugPanel: React.FC<DebugPanelProps> = ({
debugMessages,
addDebugMessage,
sendIPCMessage,
clearDebugMessages,
ipcInitialized,
@ -39,7 +93,7 @@ const DebugPanel: React.FC<DebugPanelProps> = ({
? await tauriApi.path.join(dataDir, '.kbot-gui.json')
: 'N/A';
addDebugMessage('info', 'System Info & Store Paths', {
log.info('System Info & Store Paths', {
platform: navigator.platform,
userAgent: navigator.userAgent,
isTauri: tauriApi.isTauri(),
@ -50,7 +104,7 @@ const DebugPanel: React.FC<DebugPanelProps> = ({
windowLocation: window.location.href
});
} catch (error) {
addDebugMessage('error', 'Failed to get system info', { error: (error as Error).message });
log.error('Failed to get system info', { error: (error as Error).message });
}
}}
className="glass-button text-sm px-4 py-2 rounded-lg"
@ -58,7 +112,7 @@ const DebugPanel: React.FC<DebugPanelProps> = ({
Test Info
</button>
<button
onClick={() => addDebugMessage('error', 'Test error message')}
onClick={() => log.error('Test error message')}
className="glass-button text-sm px-4 py-2 rounded-lg border-red-400/50 text-red-600 hover:bg-red-500/20"
>
Test Error
@ -67,7 +121,7 @@ const DebugPanel: React.FC<DebugPanelProps> = ({
onClick={() => sendIPCMessage('test-message', { content: 'Hello from GUI', timestamp: Date.now() })}
className="glass-button text-sm px-4 py-2 rounded-lg border-blue-400/50 text-blue-600 hover:bg-blue-500/20"
>
Send IPC - DEPRECATED
Send IPC
</button>
<button
onClick={clearDebugMessages}
@ -81,7 +135,7 @@ const DebugPanel: React.FC<DebugPanelProps> = ({
<div className="glass-card max-h-96 overflow-y-auto">
{debugMessages.length === 0 ? (
<div className="p-4 text-center text-slate-500 dark:text-slate-400">
No debug messages yet. Click the test buttons above to generate some.
No debug messages yet.
</div>
) : (
<div className="divide-y divide-slate-200/50 dark:divide-slate-700/50">
@ -103,7 +157,7 @@ const DebugPanel: React.FC<DebugPanelProps> = ({
<div className="text-sm text-slate-700 dark:text-slate-300 mb-1">{msg.message}</div>
{msg.data && (
<div className="text-xs text-slate-500 dark:text-slate-400 bg-slate-100/50 dark:bg-slate-800/50 p-2 rounded font-mono max-h-40 overflow-y-auto">
{JSON.stringify(msg.data, null, 2)}
{safeStringify(msg.data)}
</div>
)}
</div>

View File

@ -36,31 +36,35 @@ export function useTauriListeners({
let unlistenDeleteError: (() => void) | undefined;
const setupListeners = async () => {
// Initialize the app using the centralized init system
const initCallbacks: InitCallbacks = {
setPrompt,
setDst,
setApiKey,
setPrompts,
setIpcInitialized
};
// Initialize APIs first
const { isTauri: isTauriEnv } = await tauriApi.ensureTauriApi();
const initState = await initializeApp(initCallbacks);
if (!initState.isTauriEnv) {
if (!isTauriEnv) {
log.warn('Tauri APIs not available, running in browser mode.');
return;
}
// Set up event listeners FIRST, before sending any requests
const listeners = await Promise.all([
tauriApi.listen(TauriEvent.CONFIG_RECEIVED, async (event: any) => {
const data = event.payload;
// Complete initialization using the centralized system
log.info('📨 Config received from backend');
// Process config and update state
const initCallbacks: InitCallbacks = {
setPrompt,
setDst,
setApiKey,
setPrompts,
setIpcInitialized
};
try {
await completeInitialization(data, initCallbacks);
log.info('✅ App initialization completed');
} catch (error) {
log.error('Failed to complete initialization after config received', {
log.error('Failed to complete initialization', {
error: (error as Error).message
});
}
@ -138,6 +142,16 @@ export function useTauriListeners({
]);
[unlistenConfig, unlistenImage, unlistenError, unlistenComplete, unlistenDeleted, unlistenDeleteError] = listeners;
// Send config request after listeners are ready
try {
await tauriApi.requestConfigFromImages();
log.info('📤 Config request sent');
} catch (error) {
log.error('Failed to request config', {
error: (error as Error).message
});
}
};
setupListeners();

View File

@ -58,12 +58,72 @@ export function getLogState() {
};
}
/**
* Safely serialize data to prevent circular references
*/
function safeStringify(obj: any, maxDepth = 3): any {
const seen = new WeakSet();
function serialize(value: any, depth: number): any {
if (depth > maxDepth) {
return '[Max depth reached]';
}
if (value === null || value === undefined) {
return value;
}
if (typeof value !== 'object') {
return value;
}
if (seen.has(value)) {
return '[Circular Reference]';
}
seen.add(value);
if (Array.isArray(value)) {
return value.slice(0, 10).map((item, index) => {
if (index >= 10) return '[Truncated]';
return serialize(item, depth + 1);
});
}
const result: any = {};
const keys = Object.keys(value).slice(0, 20); // Limit keys
for (const key of keys) {
try {
result[key] = serialize(value[key], depth + 1);
} catch (e) {
result[key] = '[Serialization Error]';
}
}
if (Object.keys(value).length > 20) {
result['[truncated]'] = `${Object.keys(value).length - 20} more keys...`;
}
return result;
}
try {
return serialize(obj, 0);
} catch (e) {
return { error: 'Serialization failed', type: typeof obj };
}
}
/**
* Send message to backend via IPC
*/
async function sendToBackend(message: LogMessage) {
try {
await tauriApi.addDebugMessage(message.message, message.level, message.data);
// Safely serialize data to prevent circular reference issues
const safeData = message.data ? safeStringify(message.data) : message.data;
await tauriApi.addDebugMessage(message.message, message.level, safeData);
} catch (error) {
console.warn('Failed to send log message to backend:', error);
// Don't create infinite loop by logging this error
@ -89,7 +149,17 @@ function log(level: LogLevel, message: string, data?: any) {
debug: '🔍'
}[level];
console[consoleMethod](`${prefix} [${timestamp}] ${message}`, data || '');
// Safely log to console
if (data) {
try {
const safeData = safeStringify(data);
console[consoleMethod](`${prefix} [${timestamp}] ${message}`, safeData);
} catch (e) {
console[consoleMethod](`${prefix} [${timestamp}] ${message} [Data serialization failed]`);
}
} else {
console[consoleMethod](`${prefix} [${timestamp}] ${message}`);
}
// Store locally for UI
logState.localMessages.push(logMessage);

View File

@ -32,55 +32,53 @@ const apiInitializationPromise = (async () => {
}
try {
// Check if we're in Tauri environment first
if (typeof window !== 'undefined' && (window as any).__TAURI__) {
console.log('🔍 Tauri environment detected, loading APIs...');
// Load all Tauri APIs in parallel for better performance
const [
windowApi,
webviewApi,
coreApi,
eventApi,
dialogApi,
fsApi,
httpApi,
pathApi
] = await Promise.all([
import('@tauri-apps/api/window'),
import('@tauri-apps/api/webview'),
import('@tauri-apps/api/core'),
import('@tauri-apps/api/event'),
import('@tauri-apps/plugin-dialog'),
import('@tauri-apps/plugin-fs'),
import('@tauri-apps/plugin-http'),
import('@tauri-apps/api/path')
]);
// Try to load Tauri APIs - if they fail, we're in browser mode
console.log('🔍 Attempting to load Tauri APIs...');
// Load all Tauri APIs in parallel for better performance
const [
windowApi,
webviewApi,
coreApi,
eventApi,
dialogApi,
fsApi,
httpApi,
pathApi
] = await Promise.all([
import('@tauri-apps/api/window'),
import('@tauri-apps/api/webview'),
import('@tauri-apps/api/core'),
import('@tauri-apps/api/event'),
import('@tauri-apps/plugin-dialog'),
import('@tauri-apps/plugin-fs'),
import('@tauri-apps/plugin-http'),
import('@tauri-apps/api/path')
]);
// Assign all APIs
getCurrentWindow = windowApi.getCurrentWindow;
getCurrentWebview = webviewApi.getCurrentWebview;
invoke = coreApi.invoke;
listen = eventApi.listen;
open = dialogApi.open;
save = dialogApi.save;
readFile = fsApi.readFile;
writeFile = fsApi.writeFile;
readTextFile = fsApi.readTextFile;
writeTextFile = fsApi.writeTextFile;
BaseDirectory = fsApi.BaseDirectory;
fetch = httpApi.fetch;
appConfigDir = pathApi.appConfigDir;
appDataDir = pathApi.appDataDir;
join = pathApi.join;
// Test if we can actually use the APIs (this will throw if not in Tauri)
await windowApi.getCurrentWindow();
// Assign all APIs
getCurrentWindow = windowApi.getCurrentWindow;
getCurrentWebview = webviewApi.getCurrentWebview;
invoke = coreApi.invoke;
listen = eventApi.listen;
open = dialogApi.open;
save = dialogApi.save;
readFile = fsApi.readFile;
writeFile = fsApi.writeFile;
readTextFile = fsApi.readTextFile;
writeTextFile = fsApi.writeTextFile;
BaseDirectory = fsApi.BaseDirectory;
fetch = httpApi.fetch;
appConfigDir = pathApi.appConfigDir;
appDataDir = pathApi.appDataDir;
join = pathApi.join;
isTauri = true;
apiInitialized = true;
console.log('✅ All Tauri APIs loaded successfully');
} else {
console.log('🌐 No Tauri environment detected, running in browser mode');
isTauri = false;
}
isTauri = true;
apiInitialized = true;
console.log('✅ All Tauri APIs loaded successfully');
} catch (e) {
console.warn('❌ Failed to load Tauri APIs, falling back to browser mode:', e);
isTauri = false;

View File

@ -6,6 +6,15 @@ import App from "./App";
// Check if we're running in Tauri environment
const isTauri = typeof window !== 'undefined' && '__TAURI__' in window;
// Safe stringify for error handling
function safeStringifyError(obj: any): string {
try {
return JSON.stringify(obj, null, 2);
} catch (e) {
return `[Serialization failed: ${e instanceof Error ? e.message : 'Unknown error'}] - Object type: ${typeof obj}`;
}
}
let invoke: any;
// Safe invoke function that works in both Tauri and browser environments
@ -33,7 +42,7 @@ window.addEventListener('error', event => {
colno: event.colno,
error: event.error ? event.error.stack : 'No stack available',
};
safeInvoke('log_error_to_console', { error: `[JavaScript Error] ${JSON.stringify(errorPayload, null, 2)}` });
safeInvoke('log_error_to_console', { error: `[JavaScript Error] ${safeStringifyError(errorPayload)}` });
});
window.addEventListener('unhandledrejection', event => {
@ -43,7 +52,7 @@ window.addEventListener('unhandledrejection', event => {
stack: reason.stack || 'No stack available',
reason: String(reason)
};
safeInvoke('log_error_to_console', { error: `[Unhandled Promise Rejection] ${JSON.stringify(errorPayload, null, 2)}` });
safeInvoke('log_error_to_console', { error: `[Unhandled Promise Rejection] ${safeStringifyError(errorPayload)}` });
});
// Hijack console.log to send messages to the Rust backend (only in Tauri mode)
@ -57,7 +66,7 @@ if (isTauri) {
originalConsoleLog(...args);
// Format the arguments into a single string
const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)).join(' ');
const message = args.map(arg => typeof arg === 'object' ? safeStringifyError(arg) : String(arg)).join(' ');
// Send the message to the Rust backend (with delay to ensure invoke is loaded)
setTimeout(() => {
@ -70,7 +79,7 @@ if (isTauri) {
originalConsoleError(...args);
// Format the arguments into a single string
const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)).join(' ');
const message = args.map(arg => typeof arg === 'object' ? safeStringifyError(arg) : String(arg)).join(' ');
// Send the message to the Rust backend (with delay to ensure invoke is loaded)
setTimeout(() => {
@ -83,7 +92,7 @@ if (isTauri) {
originalConsoleWarn(...args);
// Format the arguments into a single string
const message = args.map(arg => typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)).join(' ');
const message = args.map(arg => typeof arg === 'object' ? safeStringifyError(arg) : String(arg)).join(' ');
// Send the message to the Rust backend (with delay to ensure invoke is loaded)
setTimeout(() => {

View File

@ -188,8 +188,10 @@ async function launchGuiAndGetPrompt(argv: any): Promise<string | null> {
files: absoluteIncludes
};
tauriProcess.stdin?.write(JSON.stringify(configResponse) + '\n');
logger.info('📤 Sent config response to GUI');
const jsonString = JSON.stringify(configResponse);
logger.info('📤 About to write to stdin:', jsonString);
tauriProcess.stdin?.write(jsonString + '\n');
logger.info('📤 Sent config response to GUI', configResponse);
// Send image data
for (const imagePath of absoluteIncludes) {