tauri logging | prompts

This commit is contained in:
babayaga 2025-09-18 19:22:14 +02:00
parent e3dd519686
commit 7975a782f4
15 changed files with 578 additions and 101 deletions

File diff suppressed because one or more lines are too long

Binary file not shown.

View File

@ -6,7 +6,11 @@
"permissions": [
{
"identifier": "fs:scope",
"allow": [{ "path": "$HOME/**" }, { "path": "$EXE/**" }]
"allow": [
{ "path": "$HOME/**" },
{ "path": "$EXE/**" },
{ "path": "$APPDATA/**" }
]
},
"core:default",
"fs:default",

View File

@ -2,6 +2,28 @@ use tauri::{Manager, Emitter};
use serde::{Serialize, Deserialize};
use dirs;
#[derive(Serialize)]
struct LogMessage {
level: String,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
data: Option<serde_json::Value>,
timestamp: u64,
}
fn log_json(level: &str, message: &str, data: Option<serde_json::Value>) {
let log_msg = LogMessage {
level: level.to_string(),
message: message.to_string(),
data,
timestamp: std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64,
};
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>>);
@ -45,11 +67,11 @@ struct ImagePayload {
// 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) {
// Use eprintln! for debug logs so they go to stderr, not stdout
eprintln!("[RUST LOG]: submit_prompt command called.");
eprintln!("[RUST LOG]: - Prompt: {}", prompt);
eprintln!("[RUST LOG]: - Files: {:?}", files);
eprintln!("[RUST LOG]: - Dst: {}", dst);
log_json("info", "submit_prompt command called", Some(serde_json::json!({
"prompt": prompt,
"files": files,
"dst": dst
})));
let payload = Payload {
prompt: prompt.to_string(),
@ -58,7 +80,9 @@ fn submit_prompt(prompt: &str, files: Vec<String>, dst: &str, window: tauri::Win
};
let json_payload = serde_json::to_string(&payload).unwrap();
eprintln!("[RUST LOG]: - Sending JSON payload to stdout: {}", json_payload);
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);
}
@ -115,9 +139,8 @@ fn reset_counter(state: tauri::State<'_, Counter>) -> Result<u32, String> {
#[tauri::command]
fn add_debug_message(message: String, level: String, data: Option<serde_json::Value>, state: tauri::State<'_, DebugMessages>) -> Result<(), String> {
eprintln!("[RUST LOG]: add_debug_message command called.");
eprintln!("[RUST LOG]: - Level: {}", level);
eprintln!("[RUST LOG]: - Message: {}", message);
// Forward frontend debug messages to CLI via structured logging
log_json(&level, &format!("Frontend: {}", message), data.clone());
let debug_payload = DebugPayload {
level,
@ -134,7 +157,6 @@ fn add_debug_message(message: String, level: String, data: Option<serde_json::Va
messages.drain(0..len - 100);
}
eprintln!("[RUST LOG]: - Debug message added. Total messages: {}", messages.len());
Ok(())
}
@ -280,8 +302,9 @@ fn forward_image_to_frontend(base64: String, mime_type: String, filename: String
#[tauri::command]
fn request_file_deletion(path: String) -> Result<(), String> {
eprintln!("[RUST LOG]: request_file_deletion command called.");
eprintln!("[RUST LOG]: - Path: {}", path);
log_json("info", "request_file_deletion command called", Some(serde_json::json!({
"path": path
})));
let request = serde_json::json!({
"type": "delete_request",
@ -289,7 +312,7 @@ fn request_file_deletion(path: String) -> Result<(), String> {
});
println!("{}", serde_json::to_string(&request).unwrap());
eprintln!("[RUST LOG]: Deletion request sent to images.ts");
log_json("info", "Deletion request sent to images.ts", None);
Ok(())
}
@ -330,13 +353,19 @@ 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);
eprintln!("[RUST LOG]: Stdin listener thread started");
log_json("info", "Stdin listener thread started", None);
for line in reader.lines() {
if let Ok(line_content) = line {
@ -345,25 +374,31 @@ pub fn run() {
}
// Log stdin command but hide binary data
let log_content = if line_content.contains("\"base64\"") {
format!("[COMMAND WITH BASE64 DATA - {} chars]", line_content.len())
if line_content.contains("\"base64\"") {
log_json("debug", "Received stdin command with base64 data", Some(serde_json::json!({
"content_length": line_content.len()
})));
} else {
line_content.clone()
};
eprintln!("[RUST LOG]: Received stdin command: {}", log_content);
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()) {
eprintln!("[RUST LOG]: Processing command: {}", cmd);
log_json("info", "Processing command", Some(serde_json::json!({
"command": cmd
})));
match cmd {
"forward_config_to_frontend" => {
eprintln!("[RUST LOG]: Forwarding config to frontend");
eprintln!("[RUST LOG]: - prompt: {:?}", command.get("prompt"));
eprintln!("[RUST LOG]: - dst: {:?}", command.get("dst"));
eprintln!("[RUST LOG]: - apiKey: {:?}", command.get("apiKey").map(|_| "[REDACTED]"));
eprintln!("[RUST LOG]: - files: {:?}", command.get("files"));
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"),
@ -373,9 +408,11 @@ pub fn run() {
});
if let Err(e) = app_handle.emit("config-received", &config_data) {
eprintln!("[RUST LOG]: Failed to emit config-received: {}", e);
log_json("error", "Failed to emit config-received", Some(serde_json::json!({
"error": e.to_string()
})));
} else {
eprintln!("[RUST LOG]: Config emitted successfully to frontend");
log_json("info", "Config emitted successfully to frontend", None);
}
}
"forward_image_to_frontend" => {
@ -384,7 +421,11 @@ pub fn run() {
command.get("base64").and_then(|v| v.as_str()),
command.get("mimeType").and_then(|v| v.as_str())
) {
eprintln!("[RUST LOG]: Forwarding image to frontend: {}", filename);
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,
@ -392,9 +433,14 @@ pub fn run() {
});
if let Err(e) = app_handle.emit("image-received", &image_data) {
eprintln!("[RUST LOG]: Failed to emit image-received: {}", e);
log_json("error", "Failed to emit image-received", Some(serde_json::json!({
"error": e.to_string(),
"filename": filename
})));
} else {
eprintln!("[RUST LOG]: Image emitted successfully: {}", filename);
log_json("info", "Image emitted successfully", Some(serde_json::json!({
"filename": filename
})));
}
}
}

View File

@ -1,5 +1,5 @@
import { useState, useEffect } from "react";
import { ImageFile } from "./types";
import { ImageFile, PromptTemplate } from "./types";
import { useTauriListeners } from "./hooks/useTauriListeners";
import { tauriApi } from "./lib/tauriApi";
import Header from "./components/Header";
@ -29,6 +29,95 @@ function App() {
const [messageToSend, setMessageToSend] = useState("");
const [generationTimeoutId, setGenerationTimeoutId] = useState<NodeJS.Timeout | null>(null);
const [currentIndex, setCurrentIndex] = useState(0);
const [prompts, setPrompts] = useState<PromptTemplate[]>([]);
const STORE_FILE_NAME = '.kbot-gui.json';
useEffect(() => {
const loadPrompts = async () => {
addDebugMessage('debug', '🔄 Store loading useEffect triggered');
try {
if (tauriApi.isTauri()) {
addDebugMessage('info', '📂 Attempting to load prompts from store...');
const configDir = await tauriApi.path.appDataDir();
addDebugMessage('debug', `📁 Data directory: ${configDir}`);
const storePath = await tauriApi.path.join(configDir, STORE_FILE_NAME);
addDebugMessage('debug', `📄 Store path resolved to: ${storePath}`);
const content = await tauriApi.fs.readTextFile(storePath);
addDebugMessage('debug', `📖 File content length: ${content?.length || 0}`);
if (content) {
const data = JSON.parse(content);
addDebugMessage('debug', `📋 Parsed store data:`, data);
if (data.prompts) {
setPrompts(data.prompts);
addDebugMessage('info', `✅ Loaded ${data.prompts.length} prompts from store`);
} else {
addDebugMessage('warn', '⚠️ Store file exists but has no prompts array');
}
} else {
addDebugMessage('info', '📭 Store file is empty');
}
} else {
addDebugMessage('warn', '🌐 Not in Tauri environment, skipping store load');
}
} catch (e) {
const error = e as Error;
addDebugMessage('info', `📂 Prompt store not found or failed to load. A new one will be created on save.`, {
error: error.message,
errorName: error.name,
storePath: STORE_FILE_NAME
});
}
};
loadPrompts();
}, []);
const importPrompts = async () => {
try {
const selected = await tauriApi.dialog.open({
multiple: false,
filters: [{ name: 'JSON', extensions: ['json'] }]
});
if (typeof selected === 'string') {
const contents = await tauriApi.fs.readTextFile(selected);
const newPrompts = JSON.parse(contents);
if (newPrompts.prompts && Array.isArray(newPrompts.prompts)) {
setPrompts(newPrompts.prompts);
savePrompts(newPrompts.prompts);
addDebugMessage('info', `✅ Prompts imported successfully from: ${selected}`);
} else {
addDebugMessage('error', 'Invalid prompts file format.');
}
}
} catch (error) {
addDebugMessage('error', 'Failed to import prompts', { error: (error as Error).message });
}
};
const exportPrompts = async () => {
addDebugMessage('info', 'Attempting to export prompts...');
try {
const path = await tauriApi.dialog.save({
defaultPath: 'kbot-prompts.json',
filters: [{ name: 'JSON', extensions: ['json'] }]
});
if (path) {
addDebugMessage('debug', `📂 Export path selected: ${path}`);
const dataToWrite = JSON.stringify({ prompts }, null, 2);
addDebugMessage('debug', '📋 Data to be exported:', { promptCount: prompts.length, dataLength: dataToWrite.length });
addDebugMessage('debug', '💾 About to call writeTextFile...');
await tauriApi.fs.writeTextFile(path, dataToWrite);
addDebugMessage('info', `✅ Prompts exported successfully to: ${path}`);
} else {
addDebugMessage('info', 'Export dialog was cancelled.');
}
} catch (error) {
addDebugMessage('error', 'Failed to export prompts', { error: (error as Error).message });
}
};
const deleteFilePermanently = async (pathToDelete: string) => {
addDebugMessage('info', `Requesting deletion of file: ${pathToDelete}`);
@ -123,7 +212,7 @@ function App() {
generationTimeoutId,
setGenerationTimeoutId,
setIsGenerating,
prompt
prompt,
});
const addFiles = async (newPaths: string[]) => {
@ -400,6 +489,31 @@ function App() {
setMessageToSend('');
};
const savePrompts = async (promptsToSave: PromptTemplate[]) => {
if (tauriApi.isTauri()) {
try {
addDebugMessage('debug', '💾 Starting save prompts process...');
const dataDir = await tauriApi.path.appDataDir();
addDebugMessage('debug', `📁 Got data dir: ${dataDir}`);
const storePath = await tauriApi.path.join(dataDir, STORE_FILE_NAME);
addDebugMessage('debug', `📄 Store path: ${storePath}`);
const dataToSave = JSON.stringify({ prompts: promptsToSave }, null, 2);
addDebugMessage('debug', `💾 Data to save:`, { promptCount: promptsToSave.length, dataLength: dataToSave.length });
await tauriApi.fs.writeTextFile(storePath, dataToSave);
addDebugMessage('info', `✅ Prompts saved to ${storePath}`);
} catch (error) {
addDebugMessage('error', 'Failed to save prompts', {
error: (error as Error).message,
errorName: (error as Error).name,
errorStack: (error as Error).stack
});
}
} else {
addDebugMessage('warn', '🌐 Not in Tauri, cannot save prompts');
}
};
async function openFilePicker() {
if (!tauriApi.isTauri()) {
// Browser fallback: create file input
@ -517,6 +631,11 @@ function App() {
addFiles={addFiles}
currentIndex={currentIndex}
setCurrentIndex={setCurrentIndex}
prompts={prompts}
setPrompts={setPrompts}
savePrompts={savePrompts}
importPrompts={importPrompts}
exportPrompts={exportPrompts}
/>
{/* Debug Panel */}

View File

@ -1,4 +1,5 @@
import React from 'react';
import { tauriApi } from '../lib/tauriApi';
interface DebugPanelProps {
debugMessages: any[];
@ -28,7 +29,30 @@ const DebugPanel: React.FC<DebugPanelProps> = ({
<h2 className="text-2xl font-bold accent-text">Debug Panel</h2>
<div className="flex gap-3">
<button
onClick={() => addDebugMessage('info', 'Test info message', { test: 'data' })}
onClick={async () => {
try {
// Get all relevant paths and store info
const dataDir = tauriApi.isTauri()
? await tauriApi.path.appDataDir()
: 'N/A (not in Tauri)';
const storePath = tauriApi.isTauri() && dataDir !== 'N/A (not in Tauri)'
? await tauriApi.path.join(dataDir, '.kbot-gui.json')
: 'N/A';
addDebugMessage('info', 'System Info & Store Paths', {
platform: navigator.platform,
userAgent: navigator.userAgent,
isTauri: tauriApi.isTauri(),
dataDir,
storePath,
cwd: 'Available in Node.js only',
timestamp: new Date().toISOString(),
windowLocation: window.location.href
});
} catch (error) {
addDebugMessage('error', 'Failed to get system info', { error: (error as Error).message });
}
}}
className="glass-button text-sm px-4 py-2 rounded-lg"
>
Test Info

View File

@ -1,7 +1,8 @@
import React from 'react';
import { ImageFile } from '../types';
import { ImageFile, PromptTemplate } from '../types';
import ImageGallery from './ImageGallery';
import { useDropZone } from '../hooks/useDropZone';
import PromptManager from './PromptManager';
interface PromptFormProps {
prompt: string;
@ -24,6 +25,11 @@ interface PromptFormProps {
addFiles: (paths: string[]) => void;
currentIndex: number;
setCurrentIndex: (index: number) => void;
prompts: PromptTemplate[];
setPrompts: (prompts: PromptTemplate[]) => void;
savePrompts: (prompts: PromptTemplate[]) => void;
importPrompts: () => void;
exportPrompts: () => void;
}
const PromptForm: React.FC<PromptFormProps> = ({
@ -46,7 +52,12 @@ const PromptForm: React.FC<PromptFormProps> = ({
onImageSaveAs,
addFiles,
currentIndex,
setCurrentIndex
setCurrentIndex,
prompts,
setPrompts,
savePrompts,
importPrompts,
exportPrompts,
}) => {
const selectedCount = getSelectedImages().length;
const { ref: dropZoneRef, dragIn } = useDropZone({ onDrop: addFiles });
@ -80,6 +91,19 @@ const PromptForm: React.FC<PromptFormProps> = ({
/>
</div>
<PromptManager
prompts={prompts}
onSelectPrompt={setPrompt}
currentPrompt={prompt}
onSavePrompt={(name, text) => {
const newPrompts = [...prompts, { name, text }];
setPrompts(newPrompts);
savePrompts(newPrompts);
}}
onImportPrompts={importPrompts}
onExportPrompts={exportPrompts}
/>
<div>
<label htmlFor="output-path" className="block text-sm font-semibold text-slate-700 dark:text-slate-300 mb-2">
Output File Path

View File

@ -0,0 +1,93 @@
import React, { useState } from 'react';
import { PromptTemplate } from '../types';
interface PromptManagerProps {
prompts: PromptTemplate[];
onSelectPrompt: (promptText: string) => void;
onSavePrompt: (name: string, text: string) => void;
onImportPrompts: () => void;
onExportPrompts: () => void;
currentPrompt: string;
}
const PromptManager: React.FC<PromptManagerProps> = ({
prompts,
onSelectPrompt,
onSavePrompt,
onImportPrompts,
onExportPrompts,
currentPrompt,
}) => {
const [showSaveModal, setShowSaveModal] = useState(false);
const [newPromptName, setNewPromptName] = useState('');
const handleSaveClick = () => {
if (currentPrompt.trim()) {
setShowSaveModal(true);
}
};
const handleSaveConfirm = () => {
if (newPromptName.trim() && currentPrompt.trim()) {
onSavePrompt(newPromptName, currentPrompt);
setNewPromptName('');
setShowSaveModal(false);
}
};
return (
<div className="mt-4">
<div className="flex flex-wrap items-center gap-2">
<span className="text-sm font-semibold text-slate-700 dark:text-slate-300">Templates:</span>
{prompts.map((prompt) => (
<button
key={prompt.name}
type="button"
onClick={() => onSelectPrompt(prompt.text)}
className="glass-button text-xs px-3 py-1 rounded-full"
title={prompt.text}
>
{prompt.name}
</button>
))}
</div>
<div className="flex gap-2 mt-4">
<button type="button" onClick={handleSaveClick} className="glass-button text-sm px-4 py-2 rounded-lg" disabled={!currentPrompt.trim()}>
Save Current Prompt
</button>
<button type="button" onClick={onImportPrompts} className="glass-button text-sm px-4 py-2 rounded-lg">
Import
</button>
<button type="button" onClick={onExportPrompts} className="glass-button text-sm px-4 py-2 rounded-lg">
Export
</button>
</div>
{showSaveModal && (
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
<div className="bg-white dark:bg-slate-800 p-6 rounded-lg shadow-xl">
<h3 className="text-lg font-bold mb-4">Save Prompt Template</h3>
<input
type="text"
value={newPromptName}
onChange={(e) => setNewPromptName(e.target.value)}
placeholder="Enter template name"
className="w-full glass-input p-2 rounded-md mb-4"
/>
<div className="flex justify-end gap-2">
<button onClick={() => setShowSaveModal(false)} className="glass-button px-4 py-2 rounded-lg">
Cancel
</button>
<button onClick={handleSaveConfirm} className="glass-button px-4 py-2 rounded-lg border-blue-500">
Save
</button>
</div>
</div>
</div>
)}
</div>
);
};
export default PromptManager;

View File

@ -1,16 +1,11 @@
export enum TauriCommand {
RESOLVE_PATH_RELATIVE_TO_HOME = 'resolve_path_relative_to_home',
LOG_ERROR_TO_CONSOLE = 'log_error_to_console',
SUBMIT_PROMPT = 'submit_prompt',
GENERATE_IMAGE_VIA_BACKEND = 'generate_image_via_backend',
REQUEST_CONFIG_FROM_IMAGES = 'request_config_from_images',
INCREMENT_COUNTER = 'increment_counter',
RESET_COUNTER = 'reset_counter',
GET_COUNTER = 'get_counter',
LOG_ERROR = 'log_error_to_console',
RESOLVE_PATH = 'resolve_path_relative_to_home',
ADD_DEBUG_MESSAGE = 'add_debug_message',
CLEAR_DEBUG_MESSAGES = 'clear_debug_messages',
SEND_IPC_MESSAGE = 'send_ipc_message',
SEND_MESSAGE_TO_STDOUT = 'send_message_to_stdout',
REQUEST_CONFIG = 'request_config_from_images',
GENERATE_IMAGE = 'generate_image_via_backend',
REQUEST_FILE_DELETION = 'request_file_deletion',
}

View File

@ -41,8 +41,9 @@ export function useTauriListeners({
const setupListeners = async () => {
await tauriApi.ensureTauriApi();
if (tauriApi.isTauri()) {
addDebugMessage('info', 'IPC system initialized successfully');
if (!tauriApi.isTauri()) {
addDebugMessage('warn', 'Tauri APIs not available, running in browser mode.');
return;
}
const listeners = await Promise.all([

View File

@ -1,4 +1,8 @@
import { TauriCommand } from '../constants';
import {
TauriCommand,
TauriEvent
} from '../constants';
import { PromptTemplate } from '../types';
// Dynamically import Tauri APIs
let invoke: any;
@ -6,11 +10,16 @@ let open: any;
let save: any;
let readFile: any;
let writeFile: any;
let readTextFile: any;
let writeTextFile: any;
let BaseDirectory: any;
let listen: any;
let getCurrentWindow: any;
let getCurrentWebview: any;
let fetch: any;
let appConfigDir: any;
let appDataDir: any;
let join: any;
let isTauri = false;
const isBrowser = typeof window !== 'undefined';
@ -39,10 +48,17 @@ const apiInitializationPromise = (async () => {
const fsApi = await import('@tauri-apps/plugin-fs');
readFile = fsApi.readFile;
writeFile = fsApi.writeFile;
readTextFile = fsApi.readTextFile;
writeTextFile = fsApi.writeTextFile;
BaseDirectory = fsApi.BaseDirectory;
const httpApi = await import('@tauri-apps/plugin-http');
fetch = httpApi.fetch;
const pathApi = await import('@tauri-apps/api/path');
appConfigDir = pathApi.appConfigDir;
appDataDir = pathApi.appDataDir;
join = pathApi.join;
} catch (e) {
console.warn('Tauri APIs not available, running in browser mode.');
isTauri = false;
@ -58,7 +74,7 @@ export const ensureTauriApi = async () => {
};
// Safe invoke function
export const safeInvoke = async <T>(command: TauriCommand, args?: any): Promise<T | null> => {
export const safeInvoke = async <T>(command: TauriCommand | string, args?: any): Promise<T | null> => {
await ensureTauriApi();
if (isTauri && typeof invoke === 'function') {
try {
@ -99,6 +115,16 @@ export const tauriApi = {
return writeFile(...args);
}
},
readTextFile: async (...args: Parameters<typeof readTextFile>) => {
await ensureTauriApi();
return readTextFile ? readTextFile(...args) : '';
},
writeTextFile: async (...args: Parameters<typeof writeTextFile>) => {
await ensureTauriApi();
if (writeTextFile) {
return writeTextFile(...args);
}
},
BaseDirectory: () => BaseDirectory,
},
dialog: {
@ -111,6 +137,20 @@ export const tauriApi = {
return save ? save(...args) : null;
},
},
path: {
appConfigDir: async () => {
await ensureTauriApi();
return appConfigDir ? appConfigDir() : '';
},
appDataDir: async () => {
await ensureTauriApi();
return appDataDir ? appDataDir() : '';
},
join: async (...args: Parameters<typeof join>) => {
await ensureTauriApi();
return join ? join(...args) : '';
},
},
window: {
getCurrent: async () => {
await ensureTauriApi();
@ -125,28 +165,28 @@ export const tauriApi = {
},
// Add typed wrappers for your app's specific commands
resolvePathRelativeToHome: (absolutePath: string) =>
safeInvoke<string>(TauriCommand.RESOLVE_PATH_RELATIVE_TO_HOME, { absolutePath }),
safeInvoke<string>(TauriCommand.RESOLVE_PATH, { absolutePath }),
submitPrompt: (data: { prompt: string; files: string[]; dst: string }) =>
safeInvoke(TauriCommand.SUBMIT_PROMPT, data),
generateImageViaBackend: (data: { prompt: string; files: string[]; dst: string }) =>
safeInvoke(TauriCommand.GENERATE_IMAGE_VIA_BACKEND, data),
safeInvoke(TauriCommand.GENERATE_IMAGE, data),
requestConfigFromImages: () =>
safeInvoke(TauriCommand.REQUEST_CONFIG_FROM_IMAGES),
safeInvoke(TauriCommand.REQUEST_CONFIG),
addDebugMessage: (message: string, level: string, data?: any) =>
safeInvoke(TauriCommand.ADD_DEBUG_MESSAGE, { message, level, data }),
clearDebugMessages: () =>
safeInvoke(TauriCommand.CLEAR_DEBUG_MESSAGES),
safeInvoke('clear_debug_messages'),
sendMessageToStdout: (message: string) =>
safeInvoke(TauriCommand.SEND_MESSAGE_TO_STDOUT, { message }),
safeInvoke('send_message_to_stdout', { message }),
logErrorToConsole: (error: string) =>
safeInvoke(TauriCommand.LOG_ERROR_TO_CONSOLE, { error }),
safeInvoke(TauriCommand.LOG_ERROR, { error }),
sendIPCMessage: (messageType: string, data: any) =>
safeInvoke(TauriCommand.SEND_IPC_MESSAGE, { messageType, data }),

View File

@ -6,11 +6,11 @@ export interface ImageFile {
}
export interface GeneratedImage {
id: string;
src: string;
prompt: string;
timestamp: number;
saved?: boolean;
selectedForNext?: boolean;
timeoutId?: NodeJS.Timeout;
filename: string;
}
export interface PromptTemplate {
name: string;
text: string;
}

View File

@ -41,7 +41,7 @@
"remark-parse": "11.0.0",
"remark-stringify": "11.0.0",
"ts-retry": "6.0.0",
"tslog": "^4.9.3",
"tslog": "4.9.3",
"turndown": "7.2.0",
"unified": "11.0.5",
"unist-util-visit": "5.0.0",

View File

@ -92,7 +92,7 @@
"remark-parse": "11.0.0",
"remark-stringify": "11.0.0",
"ts-retry": "6.0.0",
"tslog": "^4.9.3",
"tslog": "4.9.3",
"turndown": "7.2.0",
"unified": "11.0.5",
"unist-util-visit": "5.0.0",

View File

@ -7,6 +7,7 @@ import {
statSync,
unlinkSync
} from 'node:fs';
import { ILogObj, Logger } from 'tslog';
import { variables } from '../variables.js';
import { resolve } from '@polymech/commons';
@ -14,7 +15,6 @@ import { isArray, isString } from '@polymech/core/primitives';
import { OptionsSchema } from '../zod_schema.js';
import { createImage, editImage } from '../lib/images-google.js';
import { getLogger } from '../index.js';
import { prompt as resolvePrompt } from '../prompt.js';
import { spawn } from 'node:child_process';
import { loadConfig } from '../config.js';
@ -119,11 +119,15 @@ export const ImageOptionsSchema = () => {
}
async function launchGuiAndGetPrompt(argv: any): Promise<string | null> {
const logger = getLogger(argv);
const logger = new Logger<ILogObj>({
minLevel: 0, // Show all logs for debugging
prettyLogTemplate: "{{yyyy}}-{{mm}}-{{dd}} {{hh}}:{{MM}}:{{ss}}.{{ms}}\t{{logLevelName}}\t"
});
return new Promise((_resolve, reject) => {
logger.info('🚀 Starting GUI application with improved logging');
const guiAppPath = getGuiAppPath();
console.log('guiAppPath', guiAppPath);
logger.info('📁 GUI app path:', guiAppPath);
if (!exists(guiAppPath)) {
return reject(new Error(`GUI application not found at: ${guiAppPath}. Please build it first by running 'npm run tauri build' in 'gui/tauri-app'.`));
}
@ -177,7 +181,6 @@ async function launchGuiAndGetPrompt(argv: any): Promise<string | null> {
const includes = argv.include ? (Array.isArray(argv.include) ? argv.include : [argv.include]) : [];
const absoluteIncludes = includes.map(p => path.resolve(p));
// Send config via stdin (Tauri will call forward_config_to_frontend)
const configResponse = {
cmd: 'forward_config_to_frontend',
prompt: argv.prompt || null,
@ -198,14 +201,6 @@ async function launchGuiAndGetPrompt(argv: any): Promise<string | null> {
const mimeType = path.extname(imagePath).toLowerCase() === '.png' ? 'image/png' : 'image/jpeg';
const filename = path.basename(imagePath);
// Verify base64 encoding
logger.info(`📸 Image encoding check: ${filename}`, {
bufferSize: imageBuffer.length,
base64Size: base64.length,
base64Sample: base64.substring(0, 50),
isValidBase64: /^[A-Za-z0-9+/]*={0,2}$/.test(base64)
});
const imageResponse = {
cmd: 'forward_image_to_frontend',
base64,
@ -332,7 +327,7 @@ async function launchGuiAndGetPrompt(argv: any): Promise<string | null> {
}
} catch (e) {
// Not a JSON message, add to regular output
console.log('GUI stdout chunk:', JSON.stringify(line));
logger.info('GUI stdout chunk:', JSON.stringify(line));
output += line + '\n';
}
}
@ -340,18 +335,88 @@ async function launchGuiAndGetPrompt(argv: any): Promise<string | null> {
tauriProcess.stderr.on('data', (data) => {
const chunk = data.toString();
console.log('GUI stderr chunk:', JSON.stringify(chunk));
const lines = chunk.split('\n').filter(line => line.trim());
for (const line of lines) {
try {
const logMessage = JSON.parse(line);
if (logMessage.level && logMessage.message) {
// This is a structured log from Rust
// Suppress noisy debug messages
if (logMessage.message === 'add_debug_message command called.' ||
logMessage.message.includes('Debug message added. Total messages:')) {
return; // Skip these noisy logs
}
// Parse and prettify stdin command logs
if (logMessage.message === 'Received stdin command' && logMessage.data?.content) {
try {
const command = JSON.parse(logMessage.data.content);
if (command.cmd) {
logger.info(`🦀 📨 Stdin: ${command.cmd}`, {
prompt: command.prompt ? `"${command.prompt.substring(0, 50)}${command.prompt.length > 50 ? '...' : ''}"` : undefined,
dst: command.dst,
files: command.files?.length ? `${command.files.length} files` : undefined,
hasApiKey: !!command.apiKey
});
return;
}
} catch (e) {
// Fall through to regular logging
}
}
switch (logMessage.level.toLowerCase()) {
case 'debug':
logger.debug(`🦀 ${logMessage.message}`, logMessage.data);
break;
case 'info':
logger.info(`🦀 ${logMessage.message}`, logMessage.data);
break;
case 'warn':
logger.warn(`🦀 ${logMessage.message}`, logMessage.data);
break;
case 'error':
logger.error(`🦀 ${logMessage.message}`, logMessage.data);
break;
default:
logger.info(`🦀 ${logMessage.message}`, logMessage.data);
}
} else {
// Fallback for non-JSON logs - show all non-JSON content
logger.info('🦀', line);
}
} catch (e) {
// Not JSON, show it unless it's the old verbose [RUST LOG] format
if (line.includes('[RUST LOG]')) {
// Suppress add_debug_message noise
if (line.includes('add_debug_message command called') ||
line.includes('Debug message added. Total messages:')) {
return; // Skip these
}
// Skip the old verbose format, but keep important parts
if (line.includes('command called') || line.includes('emitted successfully') || line.includes('Failed to')) {
const cleanedLine = line.replace(/^\[RUST LOG\]:\s*/, '').replace(/^\s*-\s*/, '');
logger.info('🦀', cleanedLine);
}
} else if (line.trim()) {
// Show all other stderr content
logger.info('🦀', line);
}
}
}
errorOutput += chunk;
});
tauriProcess.on('close', (code) => {
console.log('GUI process closed with code:', code);
console.log('Final stdout:', JSON.stringify(output));
console.log('Final stderr:', JSON.stringify(errorOutput));
logger.info('GUI process closed with code:', code);
logger.info('Final stdout:', JSON.stringify(output));
logger.info('Final stderr:', JSON.stringify(errorOutput));
if (code === 0) {
const trimmedOutput = output.trim();
console.log('Attempting to parse JSON:', JSON.stringify(trimmedOutput));
logger.info('Attempting to parse JSON:', JSON.stringify(trimmedOutput));
_resolve(trimmedOutput || null);
} else {
reject(new Error(`Tauri app exited with code ${code}. stderr: ${errorOutput}`));
@ -366,7 +431,7 @@ async function launchGuiAndGetPrompt(argv: any): Promise<string | null> {
export const imageCommand = async (argv: any) => {
const logger = getLogger(argv);
const logger = new Logger<ILogObj>({ minLevel: argv.logLevel || 2 });
if (argv.gui) {
try {