auto-git:

[change] src-tauri/build.rs
 [change] src-tauri/src/lib.rs
 [change] src-tauri/src/main.rs
 [change] src/App.tsx
This commit is contained in:
2026-05-08 01:22:16 +02:00
parent fe2a15afca
commit 17ebfd99dd
4 changed files with 182 additions and 179 deletions

View File

@@ -1,3 +1,3 @@
fn main() { fn main() {
tauri_build::build() tauri_build::build()
} }

View File

@@ -5,236 +5,232 @@ use std::io::{BufRead, BufReader, Write};
use std::path::PathBuf; use std::path::PathBuf;
use std::process::{Command, Stdio}; use std::process::{Command, Stdio};
use tauri::{ use tauri::{
menu::{Menu, MenuItem, Submenu}, menu::{Menu, MenuItem, Submenu},
Emitter, Emitter,
}; };
fn repo_root() -> PathBuf { fn repo_root() -> PathBuf {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
manifest_dir.parent().unwrap_or(&manifest_dir).to_path_buf() manifest_dir.parent().unwrap_or(&manifest_dir).to_path_buf()
} }
fn sessions_file() -> PathBuf { fn sessions_file() -> PathBuf {
repo_root().join(".idea-hole").join("sessions.jsonl") repo_root().join(".idea-hole").join("sessions.jsonl")
} }
#[derive(Deserialize)] #[derive(Deserialize)]
struct SessionFields { struct SessionFields {
title: Option<String>, title: Option<String>,
description: Option<String>, description: Option<String>,
saved_at: Option<i64>, saved_at: Option<i64>,
} }
#[derive(Serialize)] #[derive(Serialize)]
struct SessionSummary { struct SessionSummary {
title: String, title: String,
description: String, description: String,
saved_at: i64, saved_at: i64,
} }
fn open_sessions_file() -> Result<Option<File>, String> { fn open_sessions_file() -> Result<Option<File>, String> {
match File::open(sessions_file()) { match File::open(sessions_file()) {
Ok(file) => Ok(Some(file)), Ok(file) => Ok(Some(file)),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(err) => Err(format!("Failed to open sessions file: {err}")), Err(err) => Err(format!("Failed to open sessions file: {err}")),
} }
} }
fn session_summary_from_line(line: &str) -> Option<SessionSummary> { fn session_summary_from_line(line: &str) -> Option<SessionSummary> {
let fields: SessionFields = serde_json::from_str(line).ok()?; let fields: SessionFields = serde_json::from_str(line).ok()?;
let title = fields.title.unwrap_or_default().trim().to_string(); let title = fields.title.unwrap_or_default().trim().to_string();
if title.is_empty() { if title.is_empty() {
return None; return None;
} }
Some(SessionSummary { Some(SessionSummary {
title, title,
description: fields.description.unwrap_or_default(), description: fields.description.unwrap_or_default(),
saved_at: fields.saved_at.unwrap_or_default(), saved_at: fields.saved_at.unwrap_or_default(),
}) })
} }
fn list_sessions_fast() -> Result<Vec<SessionSummary>, String> { fn list_sessions_fast() -> Result<Vec<SessionSummary>, String> {
let Some(file) = open_sessions_file()? else { let Some(file) = open_sessions_file()? else {
return Ok(Vec::new()); return Ok(Vec::new());
}; };
let mut sessions = Vec::new(); let mut sessions = Vec::new();
for line in BufReader::new(file).lines() { for line in BufReader::new(file).lines() {
let line = line.map_err(|err| format!("Failed to read sessions file: {err}"))?; let line = line.map_err(|err| format!("Failed to read sessions file: {err}"))?;
if line.trim().is_empty() { if line.trim().is_empty() {
continue; continue;
}
if let Some(summary) = session_summary_from_line(&line) {
sessions.push(summary);
}
} }
if let Some(summary) = session_summary_from_line(&line) {
sessions.push(summary);
}
}
Ok(sessions) Ok(sessions)
} }
fn load_session_fast(title: &str) -> Result<Value, String> { fn load_session_fast(title: &str) -> Result<Value, String> {
let target = title.trim(); let target = title.trim();
if target.is_empty() { if target.is_empty() {
return Ok(Value::Null); return Ok(Value::Null);
}
let Some(file) = open_sessions_file()? else {
return Ok(Value::Null);
};
for line in BufReader::new(file).lines() {
let line = line.map_err(|err| format!("Failed to read sessions file: {err}"))?;
if line.trim().is_empty() {
continue;
} }
let Some(summary) = session_summary_from_line(&line) else { let Some(file) = open_sessions_file()? else {
continue; return Ok(Value::Null);
}; };
if summary.title == target {
return serde_json::from_str(&line)
.map_err(|err| format!("Failed to parse saved session: {err}"));
}
}
Ok(Value::Null) for line in BufReader::new(file).lines() {
let line = line.map_err(|err| format!("Failed to read sessions file: {err}"))?;
if line.trim().is_empty() {
continue;
}
let Some(summary) = session_summary_from_line(&line) else {
continue;
};
if summary.title == target {
return serde_json::from_str(&line)
.map_err(|err| format!("Failed to parse saved session: {err}"));
}
}
Ok(Value::Null)
} }
fn run_python(script: &str, input: &str) -> Result<Vec<u8>, String> { fn run_python(script: &str, input: &str) -> Result<Vec<u8>, String> {
let cwd = repo_root(); let cwd = repo_root();
let mut last_err: Option<String> = None; let mut last_err: Option<String> = None;
for candidate in ["python3", "python"] { for candidate in ["python3", "python"] {
let mut child = match Command::new(candidate) let mut child = match Command::new(candidate)
.arg(script) .arg(script)
.current_dir(&cwd) .current_dir(&cwd)
.stdin(Stdio::piped()) .stdin(Stdio::piped())
.stdout(Stdio::piped()) .stdout(Stdio::piped())
.stderr(Stdio::piped()) .stderr(Stdio::piped())
.spawn() .spawn()
{ {
Ok(child) => child, Ok(child) => child,
Err(err) => { Err(err) => {
last_err = Some(format!("{candidate}: {err}")); last_err = Some(format!("{candidate}: {err}"));
continue; continue;
} }
}; };
if let Some(mut stdin) = child.stdin.take() { if let Some(mut stdin) = child.stdin.take() {
if let Err(err) = stdin.write_all(input.as_bytes()) { if let Err(err) = stdin.write_all(input.as_bytes()) {
return Err(format!("Failed to write to Python stdin: {err}")); return Err(format!("Failed to write to Python stdin: {err}"));
} }
}
let output = match child.wait_with_output() {
Ok(output) => output,
Err(err) => {
last_err = Some(format!("{candidate}: {err}"));
continue;
}
};
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("Python backend failed: {stderr}"));
}
return Ok(output.stdout);
} }
let output = match child.wait_with_output() { Err(last_err.unwrap_or_else(|| "Python not found".to_string()))
Ok(output) => output,
Err(err) => {
last_err = Some(format!("{candidate}: {err}"));
continue;
}
};
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("Python backend failed: {stderr}"));
}
return Ok(output.stdout);
}
Err(last_err.unwrap_or_else(|| "Python not found".to_string()))
} }
#[tauri::command] #[tauri::command]
fn run_python_action(action: String, payload: Value) -> Result<Value, String> { fn run_python_action(action: String, payload: Value) -> Result<Value, String> {
match action.as_str() { match action.as_str() {
"list_sessions" => { "list_sessions" => {
return serde_json::to_value(list_sessions_fast()?).map_err(|err| err.to_string()); return serde_json::to_value(list_sessions_fast()?).map_err(|err| err.to_string());
}
"load_session" => {
let title = payload.get("title").and_then(Value::as_str).unwrap_or("");
return load_session_fast(title);
}
_ => {}
} }
"load_session" => {
let title = payload.get("title").and_then(Value::as_str).unwrap_or(""); let script = repo_root().join("concept_api.py");
return load_session_fast(title); if !script.exists() {
return Err("concept_api.py not found".to_string());
} }
_ => {}
}
let script = repo_root().join("concept_api.py"); let req = serde_json::json!({
if !script.exists() { "action": action,
return Err("concept_api.py not found".to_string()); "payload": payload,
} });
let input = serde_json::to_string(&req).map_err(|e| e.to_string())?;
let req = serde_json::json!({ let stdout = run_python(script.to_string_lossy().as_ref(), &input)?;
"action": action, let resp: Value = serde_json::from_slice(&stdout).map_err(|e| e.to_string())?;
"payload": payload, if resp.get("ok") == Some(&Value::Bool(true)) {
}); Ok(resp.get("data").cloned().unwrap_or(Value::Null))
let input = serde_json::to_string(&req).map_err(|e| e.to_string())?; } else {
let stdout = run_python(script.to_string_lossy().as_ref(), &input)?; let error = resp
let resp: Value = serde_json::from_slice(&stdout).map_err(|e| e.to_string())?; .get("error")
if resp.get("ok") == Some(&Value::Bool(true)) { .and_then(|v| v.as_str())
Ok(resp.get("data").cloned().unwrap_or(Value::Null)) .unwrap_or("Unknown backend error");
} else { Err(error.to_string())
let error = resp }
.get("error")
.and_then(|v| v.as_str())
.unwrap_or("Unknown backend error");
Err(error.to_string())
}
} }
#[tauri::command] #[tauri::command]
fn open_path(path: String) -> Result<(), String> { fn open_path(path: String) -> Result<(), String> {
let path = PathBuf::from(path); let path = PathBuf::from(path);
if !path.exists() { if !path.exists() {
return Err(format!("Path does not exist: {}", path.display())); return Err(format!("Path does not exist: {}", path.display()));
} }
let mut command = if cfg!(target_os = "macos") { let mut command = if cfg!(target_os = "macos") {
let mut command = Command::new("open"); let mut command = Command::new("open");
command.arg(&path); command.arg(&path);
command command
} else if cfg!(target_os = "windows") { } else if cfg!(target_os = "windows") {
let mut command = Command::new("cmd"); let mut command = Command::new("cmd");
command.arg("/C").arg("start").arg("").arg(&path); command.arg("/C").arg("start").arg("").arg(&path);
command command
} else { } else {
let mut command = Command::new("xdg-open"); let mut command = Command::new("xdg-open");
command.arg(&path); command.arg(&path);
command command
}; };
command command
.spawn() .spawn()
.map(|_| ()) .map(|_| ())
.map_err(|err| format!("Failed to open {}: {err}", path.display())) .map_err(|err| format!("Failed to open {}: {err}", path.display()))
} }
pub fn run() { pub fn run() {
tauri::Builder::default() tauri::Builder::default()
.plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_dialog::init())
.menu(|handle| { .menu(|handle| {
let new_item = MenuItem::with_id(handle, "file-new", "New", true, None::<&str>)?; let new_item = MenuItem::with_id(handle, "file-new", "New", true, None::<&str>)?;
let open_item = MenuItem::with_id(handle, "file-open", "Open", true, None::<&str>)?; let open_item = MenuItem::with_id(handle, "file-open", "Open", true, None::<&str>)?;
let save_item = MenuItem::with_id(handle, "file-save", "Save", true, None::<&str>)?; let save_item = MenuItem::with_id(handle, "file-save", "Save", true, None::<&str>)?;
let file_menu = Submenu::with_items( let file_menu =
handle, Submenu::with_items(handle, "File", true, &[&new_item, &open_item, &save_item])?;
"File",
true,
&[&new_item, &open_item, &save_item],
)?;
let open_settings = let open_settings =
MenuItem::with_id(handle, "settings-open", "Open Settings", true, None::<&str>)?; MenuItem::with_id(handle, "settings-open", "Open Settings", true, None::<&str>)?;
let settings_menu = Submenu::with_items(handle, "Settings", true, &[&open_settings])?; let settings_menu = Submenu::with_items(handle, "Settings", true, &[&open_settings])?;
Menu::with_items(handle, &[&file_menu, &settings_menu]) Menu::with_items(handle, &[&file_menu, &settings_menu])
}) })
.on_menu_event(|app, event| match event.id().as_ref() { .on_menu_event(|app, event| match event.id().as_ref() {
"file-new" | "file-open" | "file-save" | "settings-open" => { "file-new" | "file-open" | "file-save" | "settings-open" => {
let _ = app.emit("app-menu-action", event.id().as_ref()); let _ = app.emit("app-menu-action", event.id().as_ref());
} }
_ => {} _ => {}
}) })
.invoke_handler(tauri::generate_handler![run_python_action, open_path]) .invoke_handler(tauri::generate_handler![run_python_action, open_path])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");
} }

View File

@@ -1,3 +1,3 @@
fn main() { fn main() {
concept_maker::run(); concept_maker::run();
} }

View File

@@ -91,6 +91,7 @@ export default function App() {
const [sessionModalOpen, setSessionModalOpen] = useState(false); const [sessionModalOpen, setSessionModalOpen] = useState(false);
const [sessions, setSessions] = useState<SessionSummary[]>([]); const [sessions, setSessions] = useState<SessionSummary[]>([]);
const [sessionsLoading, setSessionsLoading] = useState(false);
const [priorModalOpen, setPriorModalOpen] = useState(false); const [priorModalOpen, setPriorModalOpen] = useState(false);
const [priorData, setPriorData] = useState<PriorArtResponse | null>(null); const [priorData, setPriorData] = useState<PriorArtResponse | null>(null);
@@ -539,13 +540,18 @@ export default function App() {
}; };
const onOpenSession = async () => { const onOpenSession = async () => {
setSessionModalOpen(true);
setSessionsLoading(true);
setSessions([]);
try { try {
const list = await runBackend<SessionSummary[]>("list_sessions", {}); const list = await runBackend<SessionSummary[]>("list_sessions", {});
setSessions(list.sort((a, b) => (b.saved_at || 0) - (a.saved_at || 0))); setSessions(list.sort((a, b) => (b.saved_at || 0) - (a.saved_at || 0)));
setSessionModalOpen(true);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
setSessionModalOpen(false);
window.alert("Failed to load sessions."); window.alert("Failed to load sessions.");
} finally {
setSessionsLoading(false);
} }
}; };
@@ -844,9 +850,10 @@ export default function App() {
}} }}
> >
<h3 style={{ marginTop: 0 }}>Open Session</h3> <h3 style={{ marginTop: 0 }}>Open Session</h3>
{sessions.length === 0 && <p>No saved sessions yet.</p>} {sessionsLoading && <p>Loading sessions...</p>}
{!sessionsLoading && sessions.length === 0 && <p>No saved sessions yet.</p>}
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}> <div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
{sessions.map((s) => ( {!sessionsLoading && sessions.map((s) => (
<button <button
key={s.title} key={s.title}
className="ghost" className="ghost"