refactor: archive GUI clients; keep minimal TUI with hooks/API
Move Qt/Tauri/full Web SPA under archive/, ship hermit as CLI/TUI plus libcstcore and cst-webui/1 HTTP+SSE (turn lifecycle hooks, capabilities). Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+4585
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
[package]
|
||||
name = "hermit-desktop"
|
||||
version = "1.1.0"
|
||||
description = "Hermit desktop shell (Tauri 2)"
|
||||
authors = ["WHC"]
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "hermit_desktop_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
cc = "1.2"
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-shell = "2"
|
||||
tauri-plugin-dialog = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["rt-multi-thread", "sync", "macros", "time"] }
|
||||
once_cell = "1"
|
||||
|
||||
[features]
|
||||
default = ["custom-protocol"]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
# Link libcstcore from the repo (requires curl + built archive).
|
||||
engine = []
|
||||
@@ -0,0 +1,175 @@
|
||||
fn main() {
|
||||
// Optional: link a prebuilt libcstcore when HERMIT_CORE_LIB is set.
|
||||
// Example:
|
||||
// HERMIT_CORE_LIB=/path/to/libcstcore.a HERMIT_CORE_INCLUDE=/path/to/include
|
||||
// cargo build --features engine
|
||||
if std::env::var("CARGO_FEATURE_ENGINE").is_ok() {
|
||||
let manifest_dir = std::path::PathBuf::from(std::env::var("CARGO_MANIFEST_DIR").unwrap());
|
||||
// src-tauri → desktop → apps → repo root
|
||||
let repo_root = manifest_dir.join("../../..");
|
||||
let include = std::env::var("HERMIT_CORE_INCLUDE").unwrap_or_else(|_| {
|
||||
repo_root
|
||||
.join("include")
|
||||
.canonicalize()
|
||||
.map(|p| p.display().to_string())
|
||||
.unwrap_or_else(|_| repo_root.join("include").display().to_string())
|
||||
});
|
||||
|
||||
let mut build = cc::Build::new();
|
||||
build
|
||||
.file("src/ffi_bridge.c")
|
||||
.include(&include)
|
||||
.include("src")
|
||||
.warnings(false);
|
||||
// libcstcore pulls C11 + POSIX.
|
||||
build.std("c11");
|
||||
build.compile("hermit_ffi_bridge");
|
||||
println!("cargo:rerun-if-changed=src/ffi_bridge.c");
|
||||
println!("cargo:rerun-if-changed=src/ffi_bridge.h");
|
||||
|
||||
let core = std::env::var("HERMIT_CORE_LIB")
|
||||
.ok()
|
||||
.map(std::path::PathBuf::from)
|
||||
.filter(|p| p.exists())
|
||||
.or_else(|| {
|
||||
let fallback = repo_root.join("libcstcore.a");
|
||||
fallback.exists().then_some(fallback)
|
||||
});
|
||||
|
||||
if let Some(path) = core {
|
||||
if let Some(dir) = path.parent() {
|
||||
println!("cargo:rustc-link-search=native={}", dir.display());
|
||||
}
|
||||
let stem = path
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
.unwrap_or("cstcore")
|
||||
.trim_start_matches("lib");
|
||||
println!("cargo:rustc-link-lib=static={}", stem);
|
||||
// libcstcore leaves sashidown symbols unresolved (same as Makefile LDLIBS).
|
||||
let sashidown_dir = repo_root.join("vendor/sashidown");
|
||||
let sashidown_a = sashidown_dir.join("libsashidown.a");
|
||||
if sashidown_a.exists() {
|
||||
println!(
|
||||
"cargo:rustc-link-search=native={}",
|
||||
sashidown_dir
|
||||
.canonicalize()
|
||||
.unwrap_or_else(|_| sashidown_dir.clone())
|
||||
.display()
|
||||
);
|
||||
println!("cargo:rustc-link-lib=static=sashidown");
|
||||
println!("cargo:rerun-if-changed={}", sashidown_a.display());
|
||||
} else {
|
||||
println!(
|
||||
"cargo:warning=missing {} (make -C vendor/sashidown libsashidown.a)",
|
||||
sashidown_a.display()
|
||||
);
|
||||
}
|
||||
link_curl_and_sys();
|
||||
println!("cargo:rerun-if-changed={}", path.display());
|
||||
} else {
|
||||
println!(
|
||||
"cargo:warning=feature engine enabled but HERMIT_CORE_LIB unset and libcstcore.a missing"
|
||||
);
|
||||
}
|
||||
println!("cargo:rerun-if-env-changed=HERMIT_CORE_LIB");
|
||||
println!("cargo:rerun-if-env-changed=HERMIT_CORE_INCLUDE");
|
||||
println!("cargo:rerun-if-env-changed=HERMIT_CURL_LIBDIR");
|
||||
println!("cargo:rerun-if-env-changed=PKG_CONFIG_PATH");
|
||||
println!("cargo:rustc-env=HERMIT_CORE_INCLUDE={}", include);
|
||||
}
|
||||
tauri_build::build()
|
||||
}
|
||||
|
||||
/// libcstcore needs libcurl (+ pthread/m/dl). On NixOS `-lcurl` alone fails
|
||||
/// because curl is not on the default linker path — resolve via pkg-config,
|
||||
/// HERMIT_CURL_LIBDIR, or `ldd $(command -v curl)`.
|
||||
fn link_curl_and_sys() {
|
||||
if link_curl_via_pkg_config() || link_curl_via_env() || link_curl_via_ldd() {
|
||||
// ok
|
||||
} else {
|
||||
println!("cargo:warning=libcurl path not found; falling back to -lcurl");
|
||||
println!("cargo:rustc-link-lib=curl");
|
||||
}
|
||||
println!("cargo:rustc-link-lib=pthread");
|
||||
println!("cargo:rustc-link-lib=m");
|
||||
println!("cargo:rustc-link-lib=dl");
|
||||
}
|
||||
|
||||
fn apply_pkg_libs(libs: &str) -> bool {
|
||||
let mut saw_curl = false;
|
||||
for tok in libs.split_whitespace() {
|
||||
if let Some(dir) = tok.strip_prefix("-L") {
|
||||
if !dir.is_empty() {
|
||||
println!("cargo:rustc-link-search=native={dir}");
|
||||
}
|
||||
} else if let Some(lib) = tok.strip_prefix("-l") {
|
||||
if lib == "curl" {
|
||||
saw_curl = true;
|
||||
}
|
||||
println!("cargo:rustc-link-lib={lib}");
|
||||
}
|
||||
}
|
||||
saw_curl
|
||||
}
|
||||
|
||||
fn link_curl_via_pkg_config() -> bool {
|
||||
let Ok(out) = std::process::Command::new("pkg-config")
|
||||
.args(["--libs", "libcurl"])
|
||||
.output()
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
if !out.status.success() {
|
||||
return false;
|
||||
}
|
||||
let libs = String::from_utf8_lossy(&out.stdout);
|
||||
apply_pkg_libs(libs.trim())
|
||||
}
|
||||
|
||||
fn link_curl_via_env() -> bool {
|
||||
let Ok(dir) = std::env::var("HERMIT_CURL_LIBDIR") else {
|
||||
return false;
|
||||
};
|
||||
if dir.is_empty() || !std::path::Path::new(&dir).is_dir() {
|
||||
return false;
|
||||
}
|
||||
println!("cargo:rustc-link-search=native={dir}");
|
||||
println!("cargo:rustc-link-lib=curl");
|
||||
true
|
||||
}
|
||||
|
||||
fn link_curl_via_ldd() -> bool {
|
||||
// Parse: libcurl.so.4 => /nix/store/.../lib/libcurl.so.4 (0x...)
|
||||
let script = r#"
|
||||
set -e
|
||||
bin=$(command -v curl 2>/dev/null || true)
|
||||
[ -n "$bin" ] || exit 1
|
||||
ldd "$bin" 2>/dev/null | awk '/libcurl\.so/ { print $3; exit }'
|
||||
"#;
|
||||
let Ok(out) = std::process::Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg(script)
|
||||
.output()
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
if !out.status.success() {
|
||||
return false;
|
||||
}
|
||||
let so = String::from_utf8_lossy(&out.stdout);
|
||||
let so = so.trim();
|
||||
if so.is_empty() || so == "not" {
|
||||
return false;
|
||||
}
|
||||
let path = std::path::Path::new(so);
|
||||
let Some(dir) = path.parent() else {
|
||||
return false;
|
||||
};
|
||||
if !dir.is_dir() {
|
||||
return false;
|
||||
}
|
||||
println!("cargo:rustc-link-search=native={}", dir.display());
|
||||
println!("cargo:rustc-link-lib=curl");
|
||||
true
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Default Hermit desktop capabilities",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"shell:allow-open",
|
||||
"dialog:default"
|
||||
]
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"default":{"identifier":"default","description":"Default Hermit desktop capabilities","local":true,"windows":["main"],"permissions":["core:default","shell:allow-open","dialog:default"]}}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 682 B |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 233 B |
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 8.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,780 @@
|
||||
//! FFI surface against `include/cst_agent.h` via `ffi_bridge.c`.
|
||||
//!
|
||||
//! Enable with:
|
||||
//! HERMIT_CORE_LIB=$PWD/../../libcstcore.a \
|
||||
//! HERMIT_CORE_INCLUDE=$PWD/../../include \
|
||||
//! cargo build --features engine
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::sync::{Condvar, Mutex};
|
||||
|
||||
#[cfg(not(feature = "engine"))]
|
||||
pub fn engine_linked() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(feature = "engine")]
|
||||
pub fn engine_linked() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
/// Shared waiter: UI answers via `consent_answer` / `question_answer`.
|
||||
#[derive(Default)]
|
||||
pub struct ConsentGate {
|
||||
inner: Mutex<ConsentGateInner>,
|
||||
cv: Condvar,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ConsentGateInner {
|
||||
pending_id: Option<String>,
|
||||
pending_tool: String,
|
||||
pending_detail: String,
|
||||
answer: Option<String>,
|
||||
answer_text: String,
|
||||
cancel: bool,
|
||||
}
|
||||
|
||||
impl ConsentGate {
|
||||
pub fn request(&self, id: &str, tool: &str, detail: &str) -> i32 {
|
||||
let mut g = self.inner.lock().unwrap();
|
||||
g.pending_id = Some(id.to_string());
|
||||
g.pending_tool = tool.to_string();
|
||||
g.pending_detail = detail.to_string();
|
||||
g.answer = None;
|
||||
g.answer_text.clear();
|
||||
g.cancel = false;
|
||||
loop {
|
||||
if g.cancel {
|
||||
g.pending_id = None;
|
||||
return 0;
|
||||
}
|
||||
if let Some(ans) = g.answer.take() {
|
||||
g.pending_id = None;
|
||||
g.answer_text = ans.clone();
|
||||
return match ans.as_str() {
|
||||
"once" | "always_tool" | "always_prefix" => {
|
||||
if ans == "once" {
|
||||
1
|
||||
} else if ans == "always_tool" {
|
||||
2
|
||||
} else {
|
||||
3
|
||||
}
|
||||
}
|
||||
_ if !ans.is_empty() && ans != "deny" => 1,
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
g = self.cv.wait(g).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_text(&self, id: &str, tool: &str, detail: &str) -> Option<String> {
|
||||
let code = self.request(id, tool, detail);
|
||||
if code == 0 {
|
||||
return None;
|
||||
}
|
||||
let g = self.inner.lock().unwrap();
|
||||
if g.answer_text.is_empty() || g.answer_text == "once" {
|
||||
Some(String::new())
|
||||
} else {
|
||||
Some(g.answer_text.clone())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn answer(&self, id: &str, answer: &str) -> bool {
|
||||
let mut g = self.inner.lock().unwrap();
|
||||
if g.pending_id.as_deref() != Some(id) {
|
||||
return false;
|
||||
}
|
||||
g.answer = Some(answer.to_string());
|
||||
g.answer_text = answer.to_string();
|
||||
self.cv.notify_all();
|
||||
true
|
||||
}
|
||||
|
||||
pub fn cancel(&self) {
|
||||
let mut g = self.inner.lock().unwrap();
|
||||
g.cancel = true;
|
||||
self.cv.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "engine")]
|
||||
mod live {
|
||||
use super::*;
|
||||
use std::ffi::{CStr, CString};
|
||||
use std::os::raw::{c_char, c_int, c_void};
|
||||
use std::ptr;
|
||||
use std::sync::Arc;
|
||||
|
||||
#[repr(C)]
|
||||
struct HermitHooks {
|
||||
on_assistant: Option<extern "C" fn(*const c_char, usize, *mut c_void)>,
|
||||
on_thought: Option<extern "C" fn(*const c_char, usize, *mut c_void)>,
|
||||
on_tool: Option<
|
||||
extern "C" fn(
|
||||
*const c_char,
|
||||
*const c_char,
|
||||
*const c_char,
|
||||
*const c_char,
|
||||
*mut c_void,
|
||||
),
|
||||
>,
|
||||
on_tool_result: Option<
|
||||
extern "C" fn(*const c_char, *const c_char, *const c_char, usize, c_int, *mut c_void),
|
||||
>,
|
||||
on_todo: Option<extern "C" fn(*const c_char, *mut c_void)>,
|
||||
on_status: Option<extern "C" fn(*const c_char, *mut c_void)>,
|
||||
on_perm: Option<
|
||||
extern "C" fn(*const c_char, *const c_char, *const c_char, *mut c_void) -> c_int,
|
||||
>,
|
||||
on_ask: Option<
|
||||
extern "C" fn(
|
||||
*const c_char,
|
||||
*const c_char,
|
||||
*mut c_char,
|
||||
usize,
|
||||
*mut c_void,
|
||||
) -> c_int,
|
||||
>,
|
||||
ud: *mut c_void,
|
||||
}
|
||||
|
||||
extern "C" {
|
||||
fn hermit_agent_open(
|
||||
workspace: *const c_char,
|
||||
provider: *const c_char,
|
||||
model: *const c_char,
|
||||
approval: *const c_char,
|
||||
api_key: *const c_char,
|
||||
effort: *const c_char,
|
||||
err: *mut c_char,
|
||||
err_sz: usize,
|
||||
) -> *mut c_void;
|
||||
fn hermit_agent_close(agent: *mut c_void);
|
||||
fn hermit_agent_turn(
|
||||
agent: *mut c_void,
|
||||
user_text: *const c_char,
|
||||
hooks: *const HermitHooks,
|
||||
err: *mut c_char,
|
||||
err_sz: usize,
|
||||
) -> c_int;
|
||||
fn hermit_agent_cancel(agent: *mut c_void);
|
||||
fn hermit_agent_set_workspace(
|
||||
agent: *mut c_void,
|
||||
path: *const c_char,
|
||||
err: *mut c_char,
|
||||
err_sz: usize,
|
||||
) -> c_int;
|
||||
fn hermit_agent_new_session(
|
||||
agent: *mut c_void,
|
||||
id_out: *mut c_char,
|
||||
id_sz: usize,
|
||||
) -> c_int;
|
||||
fn hermit_agent_switch_session(agent: *mut c_void, id: *const c_char) -> c_int;
|
||||
fn hermit_agent_session_id(
|
||||
agent: *mut c_void,
|
||||
out: *mut c_char,
|
||||
out_sz: usize,
|
||||
) -> c_int;
|
||||
fn hermit_agent_set_plan_mode(agent: *mut c_void, on: c_int) -> c_int;
|
||||
fn hermit_agent_set_approval(agent: *mut c_void, approval: *const c_char) -> c_int;
|
||||
fn hermit_agent_set_effort(agent: *mut c_void, effort: *const c_char) -> c_int;
|
||||
fn hermit_agent_attach_file(
|
||||
agent: *mut c_void,
|
||||
path: *const c_char,
|
||||
err: *mut c_char,
|
||||
err_sz: usize,
|
||||
) -> c_int;
|
||||
fn hermit_agent_pending_clear(agent: *mut c_void);
|
||||
fn hermit_agent_compact(
|
||||
agent: *mut c_void,
|
||||
err: *mut c_char,
|
||||
err_sz: usize,
|
||||
) -> c_int;
|
||||
fn hermit_agent_usage(
|
||||
agent: *mut c_void,
|
||||
prompt: *mut i64,
|
||||
out_tokens: *mut i64,
|
||||
cost: *mut f64,
|
||||
priced: *mut c_int,
|
||||
);
|
||||
fn hermit_agent_history_json(agent: *mut c_void) -> *mut c_char;
|
||||
fn hermit_agent_todo(agent: *mut c_void) -> *mut c_char;
|
||||
fn hermit_agent_export_md(agent: *mut c_void, title: *const c_char) -> *mut c_char;
|
||||
fn hermit_creds_has(provider: *const c_char) -> c_int;
|
||||
fn free(p: *mut c_void);
|
||||
fn hermit_store_list(
|
||||
workspace: *const c_char,
|
||||
ids: *mut [c_char; 128],
|
||||
max_ids: c_int,
|
||||
) -> c_int;
|
||||
fn hermit_store_info(
|
||||
workspace: *const c_char,
|
||||
id: *const c_char,
|
||||
title: *mut c_char,
|
||||
title_sz: usize,
|
||||
mtime_ms: *mut i64,
|
||||
) -> c_int;
|
||||
fn hermit_store_title_set(
|
||||
workspace: *const c_char,
|
||||
id: *const c_char,
|
||||
title: *const c_char,
|
||||
) -> c_int;
|
||||
fn hermit_store_delete(workspace: *const c_char, id: *const c_char) -> c_int;
|
||||
fn hermit_creds_save(provider: *const c_char, key: *const c_char) -> c_int;
|
||||
}
|
||||
|
||||
pub struct LiveAgent {
|
||||
ptr: *mut c_void,
|
||||
}
|
||||
|
||||
unsafe impl Send for LiveAgent {}
|
||||
|
||||
impl Drop for LiveAgent {
|
||||
fn drop(&mut self) {
|
||||
if !self.ptr.is_null() {
|
||||
unsafe { hermit_agent_close(self.ptr) };
|
||||
self.ptr = ptr::null_mut();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn cstr_lossy(buf: &[u8]) -> String {
|
||||
CStr::from_bytes_until_nul(buf)
|
||||
.ok()
|
||||
.map(|c| c.to_string_lossy().into_owned())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn open(
|
||||
workspace: &str,
|
||||
provider: &str,
|
||||
model: &str,
|
||||
approval: &str,
|
||||
api_key: &str,
|
||||
effort: &str,
|
||||
) -> Result<LiveAgent, String> {
|
||||
let ws = CString::new(workspace).map_err(|e| e.to_string())?;
|
||||
let prov = CString::new(provider).map_err(|e| e.to_string())?;
|
||||
let modl = CString::new(model).map_err(|e| e.to_string())?;
|
||||
let appr = CString::new(approval).map_err(|e| e.to_string())?;
|
||||
let key = CString::new(api_key).map_err(|e| e.to_string())?;
|
||||
let eff = CString::new(effort).map_err(|e| e.to_string())?;
|
||||
let mut err = vec![0u8; 512];
|
||||
let ptr = unsafe {
|
||||
hermit_agent_open(
|
||||
ws.as_ptr(),
|
||||
prov.as_ptr(),
|
||||
modl.as_ptr(),
|
||||
appr.as_ptr(),
|
||||
key.as_ptr(),
|
||||
eff.as_ptr(),
|
||||
err.as_mut_ptr() as *mut c_char,
|
||||
err.len(),
|
||||
)
|
||||
};
|
||||
if ptr.is_null() {
|
||||
return Err(cstr_lossy(&err));
|
||||
}
|
||||
Ok(LiveAgent { ptr })
|
||||
}
|
||||
|
||||
pub fn cancel(agent: &LiveAgent) {
|
||||
unsafe { hermit_agent_cancel(agent.ptr) };
|
||||
}
|
||||
|
||||
pub fn set_workspace(agent: &LiveAgent, path: &str) -> Result<(), String> {
|
||||
let p = CString::new(path).map_err(|e| e.to_string())?;
|
||||
let mut err = vec![0u8; 512];
|
||||
let rc = unsafe {
|
||||
hermit_agent_set_workspace(
|
||||
agent.ptr,
|
||||
p.as_ptr(),
|
||||
err.as_mut_ptr() as *mut c_char,
|
||||
err.len(),
|
||||
)
|
||||
};
|
||||
if rc != 0 {
|
||||
return Err(cstr_lossy(&err));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_plan_mode(agent: &LiveAgent, on: bool) -> Result<(), String> {
|
||||
let rc = unsafe { hermit_agent_set_plan_mode(agent.ptr, if on { 1 } else { 0 }) };
|
||||
if rc != 0 {
|
||||
return Err("set_plan_mode failed".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_approval(agent: &LiveAgent, approval: &str) -> Result<(), String> {
|
||||
let a = CString::new(approval).map_err(|e| e.to_string())?;
|
||||
let rc = unsafe { hermit_agent_set_approval(agent.ptr, a.as_ptr()) };
|
||||
if rc != 0 {
|
||||
return Err("set_approval failed".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn set_effort(agent: &LiveAgent, effort: &str) -> Result<(), String> {
|
||||
let e = CString::new(effort).map_err(|e| e.to_string())?;
|
||||
let rc = unsafe { hermit_agent_set_effort(agent.ptr, e.as_ptr()) };
|
||||
if rc != 0 {
|
||||
return Err("set_effort failed".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn attach_file(agent: &LiveAgent, path: &str) -> Result<(), String> {
|
||||
let p = CString::new(path).map_err(|e| e.to_string())?;
|
||||
let mut err = vec![0u8; 512];
|
||||
let rc = unsafe {
|
||||
hermit_agent_attach_file(
|
||||
agent.ptr,
|
||||
p.as_ptr(),
|
||||
err.as_mut_ptr() as *mut c_char,
|
||||
err.len(),
|
||||
)
|
||||
};
|
||||
if rc != 0 {
|
||||
return Err(cstr_lossy(&err));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn pending_clear(agent: &LiveAgent) {
|
||||
unsafe { hermit_agent_pending_clear(agent.ptr) };
|
||||
}
|
||||
|
||||
pub fn compact(agent: &LiveAgent) -> Result<(), String> {
|
||||
let mut err = vec![0u8; 512];
|
||||
let rc = unsafe {
|
||||
hermit_agent_compact(agent.ptr, err.as_mut_ptr() as *mut c_char, err.len())
|
||||
};
|
||||
if rc != 0 {
|
||||
return Err(cstr_lossy(&err));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub struct UsageSnap {
|
||||
pub prompt: i64,
|
||||
pub out: i64,
|
||||
pub cost: f64,
|
||||
pub priced: bool,
|
||||
pub pct: f64,
|
||||
}
|
||||
|
||||
pub fn usage(agent: &LiveAgent) -> Result<UsageSnap, String> {
|
||||
let mut prompt = 0i64;
|
||||
let mut out = 0i64;
|
||||
let mut cost = 0f64;
|
||||
let mut priced = 0i32;
|
||||
unsafe {
|
||||
hermit_agent_usage(agent.ptr, &mut prompt, &mut out, &mut cost, &mut priced);
|
||||
}
|
||||
let pct = ((prompt + out) as f64 / 128_000.0 * 100.0).min(99.0);
|
||||
Ok(UsageSnap {
|
||||
prompt,
|
||||
out,
|
||||
cost,
|
||||
priced: priced != 0,
|
||||
pct,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn new_session(agent: &LiveAgent) -> Result<String, String> {
|
||||
let mut buf = vec![0u8; 64];
|
||||
let rc = unsafe {
|
||||
hermit_agent_new_session(agent.ptr, buf.as_mut_ptr() as *mut c_char, buf.len())
|
||||
};
|
||||
if rc != 0 {
|
||||
return Err("new_session failed".into());
|
||||
}
|
||||
Ok(cstr_lossy(&buf))
|
||||
}
|
||||
|
||||
pub fn switch_session(agent: &LiveAgent, id: &str) -> Result<(), String> {
|
||||
let i = CString::new(id).map_err(|e| e.to_string())?;
|
||||
let rc = unsafe { hermit_agent_switch_session(agent.ptr, i.as_ptr()) };
|
||||
if rc != 0 {
|
||||
return Err("switch_session failed".into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn session_history_json(agent: &LiveAgent) -> Result<String, String> {
|
||||
let ptr = unsafe { hermit_agent_history_json(agent.ptr) };
|
||||
if ptr.is_null() {
|
||||
return Ok("[]".into());
|
||||
}
|
||||
let json = unsafe { CStr::from_ptr(ptr) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
unsafe { free(ptr as *mut c_void) };
|
||||
Ok(json)
|
||||
}
|
||||
|
||||
pub fn todo_text(agent: &LiveAgent) -> String {
|
||||
let ptr = unsafe { hermit_agent_todo(agent.ptr) };
|
||||
if ptr.is_null() {
|
||||
return String::new();
|
||||
}
|
||||
let s = unsafe { CStr::from_ptr(ptr) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
unsafe { free(ptr as *mut c_void) };
|
||||
s
|
||||
}
|
||||
|
||||
pub fn export_md(agent: &LiveAgent, title: &str) -> Result<String, String> {
|
||||
let t = CString::new(title).map_err(|e| e.to_string())?;
|
||||
let ptr = unsafe { hermit_agent_export_md(agent.ptr, t.as_ptr()) };
|
||||
if ptr.is_null() {
|
||||
return Err("export failed".into());
|
||||
}
|
||||
let s = unsafe { CStr::from_ptr(ptr) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
unsafe { free(ptr as *mut c_void) };
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
pub fn creds_has(provider: &str) -> bool {
|
||||
let Ok(p) = CString::new(provider) else {
|
||||
return false;
|
||||
};
|
||||
unsafe { hermit_creds_has(p.as_ptr()) != 0 }
|
||||
}
|
||||
|
||||
pub struct TurnCallbacks {
|
||||
pub on_assistant: Box<dyn FnMut(&str) + Send>,
|
||||
pub on_thought: Box<dyn FnMut(&str) + Send>,
|
||||
pub on_tool: Box<dyn FnMut(&str, &str, &str, &str) + Send>,
|
||||
pub on_tool_result: Box<dyn FnMut(&str, &str, &str, i32) + Send>,
|
||||
pub on_todo: Box<dyn FnMut(&str) + Send>,
|
||||
pub on_status: Box<dyn FnMut(&str) + Send>,
|
||||
pub gate: Arc<ConsentGate>,
|
||||
pub question_gate: Arc<ConsentGate>,
|
||||
pub emit_consent: Box<dyn FnMut(String, String, String, Option<String>) + Send>,
|
||||
pub emit_question: Box<dyn FnMut(String, String, Vec<String>) + Send>,
|
||||
}
|
||||
|
||||
struct TurnUd {
|
||||
cb: *mut TurnCallbacks,
|
||||
}
|
||||
|
||||
unsafe fn cstr(p: *const c_char) -> String {
|
||||
if p.is_null() {
|
||||
return String::new();
|
||||
}
|
||||
CStr::from_ptr(p).to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
extern "C" fn c_assistant(text: *const c_char, len: usize, ud: *mut c_void) {
|
||||
if text.is_null() || ud.is_null() {
|
||||
return;
|
||||
}
|
||||
let s = unsafe { std::slice::from_raw_parts(text as *const u8, len) };
|
||||
let t = String::from_utf8_lossy(s);
|
||||
let turn = unsafe { &mut *(ud as *mut TurnUd) };
|
||||
let cb = unsafe { &mut *turn.cb };
|
||||
(cb.on_assistant)(&t);
|
||||
}
|
||||
|
||||
extern "C" fn c_thought(text: *const c_char, len: usize, ud: *mut c_void) {
|
||||
if text.is_null() || ud.is_null() {
|
||||
return;
|
||||
}
|
||||
let s = unsafe { std::slice::from_raw_parts(text as *const u8, len) };
|
||||
let t = String::from_utf8_lossy(s);
|
||||
let turn = unsafe { &mut *(ud as *mut TurnUd) };
|
||||
let cb = unsafe { &mut *turn.cb };
|
||||
(cb.on_thought)(&t);
|
||||
}
|
||||
|
||||
extern "C" fn c_tool(
|
||||
phase: *const c_char,
|
||||
id: *const c_char,
|
||||
name: *const c_char,
|
||||
detail: *const c_char,
|
||||
ud: *mut c_void,
|
||||
) {
|
||||
if ud.is_null() {
|
||||
return;
|
||||
}
|
||||
let phase = unsafe { cstr(phase) };
|
||||
let id = unsafe { cstr(id) };
|
||||
let name = unsafe { cstr(name) };
|
||||
let detail = unsafe { cstr(detail) };
|
||||
let turn = unsafe { &mut *(ud as *mut TurnUd) };
|
||||
let cb = unsafe { &mut *turn.cb };
|
||||
(cb.on_tool)(&phase, &id, &name, &detail);
|
||||
}
|
||||
|
||||
extern "C" fn c_tool_result(
|
||||
id: *const c_char,
|
||||
name: *const c_char,
|
||||
out: *const c_char,
|
||||
len: usize,
|
||||
exit: c_int,
|
||||
ud: *mut c_void,
|
||||
) {
|
||||
if ud.is_null() {
|
||||
return;
|
||||
}
|
||||
let id = unsafe { cstr(id) };
|
||||
let name = unsafe { cstr(name) };
|
||||
let out = if out.is_null() {
|
||||
String::new()
|
||||
} else {
|
||||
let s = unsafe { std::slice::from_raw_parts(out as *const u8, len) };
|
||||
String::from_utf8_lossy(s).into_owned()
|
||||
};
|
||||
let turn = unsafe { &mut *(ud as *mut TurnUd) };
|
||||
let cb = unsafe { &mut *turn.cb };
|
||||
(cb.on_tool_result)(&id, &name, &out, exit);
|
||||
}
|
||||
|
||||
extern "C" fn c_todo(text: *const c_char, ud: *mut c_void) {
|
||||
if ud.is_null() {
|
||||
return;
|
||||
}
|
||||
let t = unsafe { cstr(text) };
|
||||
let turn = unsafe { &mut *(ud as *mut TurnUd) };
|
||||
let cb = unsafe { &mut *turn.cb };
|
||||
(cb.on_todo)(&t);
|
||||
}
|
||||
|
||||
extern "C" fn c_status(text: *const c_char, ud: *mut c_void) {
|
||||
if ud.is_null() {
|
||||
return;
|
||||
}
|
||||
let t = unsafe { cstr(text) };
|
||||
let turn = unsafe { &mut *(ud as *mut TurnUd) };
|
||||
let cb = unsafe { &mut *turn.cb };
|
||||
(cb.on_status)(&t);
|
||||
}
|
||||
|
||||
extern "C" fn c_perm(
|
||||
tool: *const c_char,
|
||||
detail: *const c_char,
|
||||
preview: *const c_char,
|
||||
ud: *mut c_void,
|
||||
) -> c_int {
|
||||
if ud.is_null() {
|
||||
return 0;
|
||||
}
|
||||
let tool = unsafe { cstr(tool) };
|
||||
let detail = unsafe { cstr(detail) };
|
||||
let preview = if preview.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(unsafe { cstr(preview) })
|
||||
};
|
||||
let turn = unsafe { &mut *(ud as *mut TurnUd) };
|
||||
let cb = unsafe { &mut *turn.cb };
|
||||
let id = format!(
|
||||
"c{}",
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis())
|
||||
.unwrap_or(0)
|
||||
);
|
||||
(cb.emit_consent)(id.clone(), tool.clone(), detail.clone(), preview);
|
||||
cb.gate.request(&id, &tool, &detail)
|
||||
}
|
||||
|
||||
extern "C" fn c_ask(
|
||||
question: *const c_char,
|
||||
options: *const c_char,
|
||||
ans: *mut c_char,
|
||||
ans_sz: usize,
|
||||
ud: *mut c_void,
|
||||
) -> c_int {
|
||||
if ud.is_null() || ans.is_null() || ans_sz == 0 {
|
||||
return -1;
|
||||
}
|
||||
let question = unsafe { cstr(question) };
|
||||
let options_raw = unsafe { cstr(options) };
|
||||
let opts: Vec<String> = if options_raw.is_empty() {
|
||||
Vec::new()
|
||||
} else if options_raw.trim_start().starts_with('[') {
|
||||
serde_json::from_str(&options_raw).unwrap_or_default()
|
||||
} else {
|
||||
options_raw
|
||||
.split(|c| c == '\n' || c == '|')
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect()
|
||||
};
|
||||
let turn = unsafe { &mut *(ud as *mut TurnUd) };
|
||||
let cb = unsafe { &mut *turn.cb };
|
||||
let id = format!(
|
||||
"q{}",
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis())
|
||||
.unwrap_or(0)
|
||||
);
|
||||
(cb.emit_question)(id.clone(), question.clone(), opts);
|
||||
match cb.question_gate.request_text(&id, "ask_user", &question) {
|
||||
None => -1,
|
||||
Some(text) => {
|
||||
let bytes = text.as_bytes();
|
||||
let n = bytes.len().min(ans_sz - 1);
|
||||
unsafe {
|
||||
std::ptr::copy_nonoverlapping(bytes.as_ptr(), ans as *mut u8, n);
|
||||
*ans.add(n) = 0;
|
||||
}
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_turn(agent: &LiveAgent, text: &str, mut cb: TurnCallbacks) -> Result<(), String> {
|
||||
let prompt = CString::new(text).map_err(|e| e.to_string())?;
|
||||
let mut err = vec![0u8; 1024];
|
||||
let mut turn_ud = TurnUd {
|
||||
cb: &mut cb as *mut TurnCallbacks,
|
||||
};
|
||||
let hooks = HermitHooks {
|
||||
on_assistant: Some(c_assistant),
|
||||
on_thought: Some(c_thought),
|
||||
on_tool: Some(c_tool),
|
||||
on_tool_result: Some(c_tool_result),
|
||||
on_todo: Some(c_todo),
|
||||
on_status: Some(c_status),
|
||||
on_perm: Some(c_perm),
|
||||
on_ask: Some(c_ask),
|
||||
ud: &mut turn_ud as *mut TurnUd as *mut c_void,
|
||||
};
|
||||
let rc = unsafe {
|
||||
hermit_agent_turn(
|
||||
agent.ptr,
|
||||
prompt.as_ptr(),
|
||||
&hooks,
|
||||
err.as_mut_ptr() as *mut c_char,
|
||||
err.len(),
|
||||
)
|
||||
};
|
||||
if rc != 0 {
|
||||
return Err(cstr_lossy(&err));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn store_list(workspace: &str) -> Vec<(String, String, i64)> {
|
||||
let Ok(ws) = CString::new(workspace) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut ids = [[0i8; 128]; 200];
|
||||
let n = unsafe { hermit_store_list(ws.as_ptr(), ids.as_mut_ptr(), 200) };
|
||||
if n <= 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
for i in 0..(n as usize) {
|
||||
let id = unsafe { CStr::from_ptr(ids[i].as_ptr()) }
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
if id.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let Ok(id_c) = CString::new(id.as_str()) else {
|
||||
continue;
|
||||
};
|
||||
let mut title_buf = vec![0u8; 128];
|
||||
let mut mtime: i64 = 0;
|
||||
let rc = unsafe {
|
||||
hermit_store_info(
|
||||
ws.as_ptr(),
|
||||
id_c.as_ptr(),
|
||||
title_buf.as_mut_ptr() as *mut c_char,
|
||||
title_buf.len(),
|
||||
&mut mtime,
|
||||
)
|
||||
};
|
||||
let title = if rc == 0 {
|
||||
cstr_lossy(&title_buf)
|
||||
} else {
|
||||
id.clone()
|
||||
};
|
||||
out.push((id, title, mtime));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub fn store_title_set(workspace: &str, id: &str, title: &str) -> bool {
|
||||
let Ok(ws) = CString::new(workspace) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(i) = CString::new(id) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(t) = CString::new(title) else {
|
||||
return false;
|
||||
};
|
||||
unsafe { hermit_store_title_set(ws.as_ptr(), i.as_ptr(), t.as_ptr()) == 0 }
|
||||
}
|
||||
|
||||
pub fn store_delete(workspace: &str, id: &str) -> bool {
|
||||
let Ok(ws) = CString::new(workspace) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(i) = CString::new(id) else {
|
||||
return false;
|
||||
};
|
||||
unsafe { hermit_store_delete(ws.as_ptr(), i.as_ptr()) == 0 }
|
||||
}
|
||||
|
||||
pub fn creds_save(provider: &str, key: &str) -> bool {
|
||||
let Ok(p) = CString::new(provider) else {
|
||||
return false;
|
||||
};
|
||||
let Ok(k) = CString::new(key) else {
|
||||
return false;
|
||||
};
|
||||
unsafe { hermit_creds_save(p.as_ptr(), k.as_ptr()) == 0 }
|
||||
}
|
||||
|
||||
pub struct ProviderSnap {
|
||||
pub name: String,
|
||||
pub model: String,
|
||||
pub has_key: bool,
|
||||
pub key_from: Option<String>,
|
||||
}
|
||||
|
||||
pub fn providers_list() -> Result<Vec<ProviderSnap>, String> {
|
||||
let presets = [
|
||||
("deepseek", "deepseek-flash"),
|
||||
("openai", "gpt-4.1"),
|
||||
("anthropic", "claude-sonnet-4"),
|
||||
];
|
||||
Ok(presets
|
||||
.into_iter()
|
||||
.map(|(name, model)| {
|
||||
let has = creds_has(name)
|
||||
|| std::env::var(format!("{}_API_KEY", name.to_ascii_uppercase())).is_ok()
|
||||
|| (name == "openai" && std::env::var("OPENAI_API_KEY").is_ok())
|
||||
|| (name == "deepseek"
|
||||
&& (std::env::var("DEEPSEEK_API_KEY").is_ok()
|
||||
|| std::env::var("OPENAI_API_KEY").is_ok()));
|
||||
ProviderSnap {
|
||||
name: name.into(),
|
||||
model: model.into(),
|
||||
has_key: has,
|
||||
key_from: if has { Some("creds/env".into()) } else { None },
|
||||
}
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "engine")]
|
||||
pub use live::*;
|
||||
@@ -0,0 +1,517 @@
|
||||
#include "ffi_bridge.h"
|
||||
|
||||
#include "cst_agent.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
const hermit_hooks_t *h;
|
||||
} bridge_ud_t;
|
||||
|
||||
static void hook_assistant(const char *text, size_t len, void *ud) {
|
||||
bridge_ud_t *b = (bridge_ud_t *)ud;
|
||||
if (b && b->h && b->h->on_assistant)
|
||||
b->h->on_assistant(text, len, b->h->ud);
|
||||
}
|
||||
|
||||
static void hook_think(const char *text, size_t len, void *ud) {
|
||||
bridge_ud_t *b = (bridge_ud_t *)ud;
|
||||
if (b && b->h && b->h->on_thought)
|
||||
b->h->on_thought(text, len, b->h->ud);
|
||||
}
|
||||
|
||||
static void hook_tool(const char *phase, const ctl_tool_call_t *call,
|
||||
const char *detail, void *ud) {
|
||||
bridge_ud_t *b = (bridge_ud_t *)ud;
|
||||
if (!b || !b->h || !b->h->on_tool || !call)
|
||||
return;
|
||||
b->h->on_tool(phase ? phase : "", call->id, call->name,
|
||||
detail ? detail : "", b->h->ud);
|
||||
}
|
||||
|
||||
static void hook_tool_result(const ctl_tool_call_t *call, const char *out,
|
||||
size_t len, int exit_code, void *ud) {
|
||||
bridge_ud_t *b = (bridge_ud_t *)ud;
|
||||
if (!b || !b->h || !b->h->on_tool_result || !call)
|
||||
return;
|
||||
b->h->on_tool_result(call->id, call->name, out ? out : "", len, exit_code,
|
||||
b->h->ud);
|
||||
}
|
||||
|
||||
static void hook_todo(const char *text, void *ud) {
|
||||
bridge_ud_t *b = (bridge_ud_t *)ud;
|
||||
if (b && b->h && b->h->on_todo)
|
||||
b->h->on_todo(text ? text : "", b->h->ud);
|
||||
}
|
||||
|
||||
static void hook_status(const char *msg, void *ud) {
|
||||
bridge_ud_t *b = (bridge_ud_t *)ud;
|
||||
if (b && b->h && b->h->on_status)
|
||||
b->h->on_status(msg ? msg : "", b->h->ud);
|
||||
}
|
||||
|
||||
static int hook_perm(const char *tool_name, const char *detail,
|
||||
const char *preview, void *ud) {
|
||||
bridge_ud_t *b = (bridge_ud_t *)ud;
|
||||
if (!b || !b->h || !b->h->on_perm)
|
||||
return CST_PERM_DENY;
|
||||
return b->h->on_perm(tool_name ? tool_name : "", detail ? detail : "",
|
||||
preview, b->h->ud);
|
||||
}
|
||||
|
||||
static int hook_ask(const char *question, const char *options, char *ans,
|
||||
size_t ans_sz, void *ud) {
|
||||
bridge_ud_t *b = (bridge_ud_t *)ud;
|
||||
if (!b || !b->h || !b->h->on_ask)
|
||||
return -1;
|
||||
return b->h->on_ask(question ? question : "", options ? options : "", ans,
|
||||
ans_sz, b->h->ud);
|
||||
}
|
||||
|
||||
static void hook_turn_begin(const char *user_text, void *ud) {
|
||||
bridge_ud_t *b = (bridge_ud_t *)ud;
|
||||
if (b && b->h && b->h->on_turn_begin)
|
||||
b->h->on_turn_begin(user_text ? user_text : "", b->h->ud);
|
||||
}
|
||||
|
||||
static void hook_turn_end(int rc, const char *err, void *ud) {
|
||||
bridge_ud_t *b = (bridge_ud_t *)ud;
|
||||
if (b && b->h && b->h->on_turn_end)
|
||||
b->h->on_turn_end(rc, err, b->h->ud);
|
||||
}
|
||||
|
||||
static void hook_error(const char *kind, const char *message, void *ud) {
|
||||
bridge_ud_t *b = (bridge_ud_t *)ud;
|
||||
if (b && b->h && b->h->on_error)
|
||||
b->h->on_error(kind ? kind : "", message ? message : "", b->h->ud);
|
||||
}
|
||||
|
||||
void *hermit_agent_open(const char *workspace, const char *provider,
|
||||
const char *model, const char *approval,
|
||||
const char *api_key, const char *effort, char *err,
|
||||
size_t err_sz) {
|
||||
if (err && err_sz)
|
||||
err[0] = '\0';
|
||||
if (!workspace || !workspace[0]) {
|
||||
if (err && err_sz)
|
||||
snprintf(err, err_sz, "workspace required");
|
||||
return NULL;
|
||||
}
|
||||
if (cst_agent_store_ensure(workspace) != 0) {
|
||||
if (err && err_sz)
|
||||
snprintf(err, err_sz, "store ensure failed");
|
||||
return NULL;
|
||||
}
|
||||
|
||||
cst_agent_config_t cfg;
|
||||
memset(&cfg, 0, sizeof(cfg));
|
||||
snprintf(cfg.workspace_root, sizeof(cfg.workspace_root), "%s", workspace);
|
||||
if (provider && provider[0])
|
||||
snprintf(cfg.provider, sizeof(cfg.provider), "%s", provider);
|
||||
else
|
||||
snprintf(cfg.provider, sizeof(cfg.provider), "%s", "deepseek");
|
||||
if (model && model[0])
|
||||
snprintf(cfg.model, sizeof(cfg.model), "%s", model);
|
||||
else
|
||||
snprintf(cfg.model, sizeof(cfg.model), "%s", "deepseek-flash");
|
||||
snprintf(cfg.mode, sizeof(cfg.mode), "%s", CST_DEFAULT_MODE);
|
||||
cfg.protocol = CST_PROTO_NATIVE_EOS;
|
||||
cfg.max_steps = CST_DEFAULT_MAX_STEPS;
|
||||
cfg.tool_out_cap = CST_DEFAULT_TOOL_OUT_CAP;
|
||||
cfg.approval = CST_DEFAULT_APPROVAL;
|
||||
if (approval && approval[0]) {
|
||||
int a = CST_DEFAULT_APPROVAL;
|
||||
if (cst_approval_parse(approval, &a) == 0)
|
||||
cfg.approval = a;
|
||||
}
|
||||
if (effort && effort[0] && cst_effort_valid(effort))
|
||||
snprintf(cfg.effort, sizeof(cfg.effort), "%s", effort);
|
||||
|
||||
char id[64];
|
||||
if (cst_agent_store_resolve_default(cfg.workspace_root, cfg.session_path,
|
||||
sizeof(cfg.session_path), id,
|
||||
sizeof(id)) != 0) {
|
||||
if (cst_agent_store_new_id(id, sizeof(id)) != 0 ||
|
||||
cst_agent_store_path_for_id(cfg.workspace_root, id, cfg.session_path,
|
||||
sizeof(cfg.session_path)) != 0) {
|
||||
if (err && err_sz)
|
||||
snprintf(err, err_sz, "session path failed");
|
||||
return NULL;
|
||||
}
|
||||
cst_agent_store_set_current(cfg.workspace_root, id);
|
||||
}
|
||||
|
||||
if (api_key && api_key[0]) {
|
||||
snprintf(cfg.api_key, sizeof(cfg.api_key), "%s", api_key);
|
||||
} else {
|
||||
char key[512];
|
||||
const char *from = NULL;
|
||||
if (cst_creds_resolve(cfg.provider, key, sizeof(key), &from) == 0)
|
||||
snprintf(cfg.api_key, sizeof(cfg.api_key), "%s", key);
|
||||
}
|
||||
|
||||
cst_agent_t *a = cst_agent_create(&cfg);
|
||||
if (!a && err && err_sz)
|
||||
snprintf(err, err_sz, "cst_agent_create failed");
|
||||
return a;
|
||||
}
|
||||
|
||||
void hermit_agent_close(void *agent) {
|
||||
if (agent)
|
||||
cst_agent_destroy((cst_agent_t *)agent);
|
||||
}
|
||||
|
||||
int hermit_agent_turn(void *agent, const char *user_text,
|
||||
const hermit_hooks_t *hooks, char *err, size_t err_sz) {
|
||||
if (!agent || !user_text)
|
||||
return -1;
|
||||
bridge_ud_t bud = {.h = hooks};
|
||||
cst_agent_hooks_t h;
|
||||
memset(&h, 0, sizeof(h));
|
||||
h.on_assistant_delta = hook_assistant;
|
||||
h.on_think_delta = hook_think;
|
||||
h.on_tool_event = hook_tool;
|
||||
h.on_tool_result = hook_tool_result;
|
||||
h.on_todo = hook_todo;
|
||||
h.on_status = hook_status;
|
||||
h.on_permission = hook_perm;
|
||||
h.on_ask_user = hook_ask;
|
||||
h.on_turn_begin = hook_turn_begin;
|
||||
h.on_turn_end = hook_turn_end;
|
||||
h.on_error = hook_error;
|
||||
h.userdata = &bud;
|
||||
return cst_agent_run_turn_ex((cst_agent_t *)agent, user_text, &h, err,
|
||||
err_sz);
|
||||
}
|
||||
|
||||
void hermit_agent_cancel(void *agent) {
|
||||
if (agent)
|
||||
cst_agent_request_cancel((cst_agent_t *)agent);
|
||||
}
|
||||
|
||||
int hermit_agent_set_workspace(void *agent, const char *path, char *err,
|
||||
size_t err_sz) {
|
||||
if (!agent || !path)
|
||||
return -1;
|
||||
return cst_agent_set_workspace((cst_agent_t *)agent, path, err, err_sz);
|
||||
}
|
||||
|
||||
int hermit_agent_new_session(void *agent, char *id_out, size_t id_sz) {
|
||||
if (!agent)
|
||||
return -1;
|
||||
return cst_agent_new_session((cst_agent_t *)agent, id_out, id_sz);
|
||||
}
|
||||
|
||||
int hermit_agent_switch_session(void *agent, const char *id) {
|
||||
if (!agent || !id)
|
||||
return -1;
|
||||
return cst_agent_switch_session((cst_agent_t *)agent, id);
|
||||
}
|
||||
|
||||
int hermit_agent_session_id(void *agent, char *out, size_t out_sz) {
|
||||
if (!agent || !out || out_sz == 0)
|
||||
return -1;
|
||||
cst_agent_session_id((cst_agent_t *)agent, out, out_sz);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int hermit_agent_set_plan_mode(void *agent, int on) {
|
||||
if (!agent)
|
||||
return -1;
|
||||
cst_agent_config_t *cfg = cst_agent_config_mut((cst_agent_t *)agent);
|
||||
if (!cfg)
|
||||
return -1;
|
||||
cfg->plan_mode = on ? 1 : 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int hermit_agent_set_approval(void *agent, const char *approval) {
|
||||
if (!agent || !approval)
|
||||
return -1;
|
||||
cst_agent_config_t *cfg = cst_agent_config_mut((cst_agent_t *)agent);
|
||||
if (!cfg)
|
||||
return -1;
|
||||
int a = cfg->approval;
|
||||
if (cst_approval_parse(approval, &a) == 0)
|
||||
cfg->approval = a;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int hermit_agent_set_effort(void *agent, const char *effort) {
|
||||
if (!agent)
|
||||
return -1;
|
||||
cst_agent_config_t *cfg = cst_agent_config_mut((cst_agent_t *)agent);
|
||||
if (!cfg)
|
||||
return -1;
|
||||
if (!effort || !effort[0]) {
|
||||
cfg->effort[0] = '\0';
|
||||
return 0;
|
||||
}
|
||||
if (!cst_effort_valid(effort))
|
||||
return -1;
|
||||
snprintf(cfg->effort, sizeof(cfg->effort), "%s", effort);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int hermit_agent_attach_file(void *agent, const char *path, char *err,
|
||||
size_t err_sz) {
|
||||
if (!agent || !path)
|
||||
return -1;
|
||||
return cst_agent_attach_file((cst_agent_t *)agent, path, err, err_sz);
|
||||
}
|
||||
|
||||
void hermit_agent_pending_clear(void *agent) {
|
||||
if (agent)
|
||||
cst_agent_pending_clear((cst_agent_t *)agent);
|
||||
}
|
||||
|
||||
int hermit_agent_compact(void *agent, char *err, size_t err_sz) {
|
||||
if (!agent)
|
||||
return -1;
|
||||
return cst_agent_compact((cst_agent_t *)agent, err, err_sz);
|
||||
}
|
||||
|
||||
void hermit_agent_usage(void *agent, long *prompt, long *out_tokens,
|
||||
double *cost, int *priced) {
|
||||
if (!agent)
|
||||
return;
|
||||
cst_usage_t s, t;
|
||||
memset(&s, 0, sizeof(s));
|
||||
memset(&t, 0, sizeof(t));
|
||||
cst_agent_usage((cst_agent_t *)agent, &s, &t);
|
||||
if (prompt)
|
||||
*prompt = s.prompt;
|
||||
if (out_tokens)
|
||||
*out_tokens = s.out;
|
||||
if (cost)
|
||||
*cost = s.cost;
|
||||
if (priced)
|
||||
*priced = s.priced;
|
||||
}
|
||||
|
||||
static void json_escape_append(char **buf, size_t *len, size_t *cap,
|
||||
const char *s) {
|
||||
if (!s)
|
||||
s = "";
|
||||
for (const unsigned char *p = (const unsigned char *)s; *p; p++) {
|
||||
char tmp[8];
|
||||
const char *chunk;
|
||||
size_t n;
|
||||
if (*p == '"' || *p == '\\') {
|
||||
tmp[0] = '\\';
|
||||
tmp[1] = (char)*p;
|
||||
tmp[2] = 0;
|
||||
chunk = tmp;
|
||||
n = 2;
|
||||
} else if (*p < 0x20) {
|
||||
n = (size_t)snprintf(tmp, sizeof(tmp), "\\u%04x", *p);
|
||||
chunk = tmp;
|
||||
} else {
|
||||
tmp[0] = (char)*p;
|
||||
tmp[1] = 0;
|
||||
chunk = tmp;
|
||||
n = 1;
|
||||
}
|
||||
if (*len + n + 1 >= *cap) {
|
||||
*cap = (*cap + n + 1) * 2;
|
||||
char *nb = (char *)realloc(*buf, *cap);
|
||||
if (!nb)
|
||||
return;
|
||||
*buf = nb;
|
||||
}
|
||||
memcpy(*buf + *len, chunk, n);
|
||||
*len += n;
|
||||
(*buf)[*len] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
static int hist_append_obj(char **buf, size_t *len, size_t *cap, int *first,
|
||||
const char *head, const char *body) {
|
||||
if (!*first) {
|
||||
if (*len + 2 >= *cap) {
|
||||
*cap *= 2;
|
||||
char *nb = (char *)realloc(*buf, *cap);
|
||||
if (!nb)
|
||||
return -1;
|
||||
*buf = nb;
|
||||
}
|
||||
(*buf)[(*len)++] = ',';
|
||||
(*buf)[*len] = 0;
|
||||
}
|
||||
*first = 0;
|
||||
size_t hn = strlen(head);
|
||||
if (*len + hn + 8 >= *cap) {
|
||||
*cap = (*cap + hn) * 2;
|
||||
char *nb = (char *)realloc(*buf, *cap);
|
||||
if (!nb)
|
||||
return -1;
|
||||
*buf = nb;
|
||||
}
|
||||
memcpy(*buf + *len, head, hn);
|
||||
*len += hn;
|
||||
(*buf)[*len] = 0;
|
||||
json_escape_append(buf, len, cap, body ? body : "");
|
||||
if (*len + 3 >= *cap) {
|
||||
*cap *= 2;
|
||||
char *nb = (char *)realloc(*buf, *cap);
|
||||
if (!nb)
|
||||
return -1;
|
||||
*buf = nb;
|
||||
}
|
||||
(*buf)[(*len)++] = '"';
|
||||
(*buf)[(*len)++] = '}';
|
||||
(*buf)[*len] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
char *hermit_agent_history_json(void *agent) {
|
||||
if (!agent)
|
||||
return NULL;
|
||||
cst_session_t *sess = cst_agent_session((cst_agent_t *)agent);
|
||||
if (!sess)
|
||||
return NULL;
|
||||
cst_session_lock(sess);
|
||||
int n = cst_session_count(sess);
|
||||
size_t cap = 4096, len = 0;
|
||||
char *buf = (char *)malloc(cap);
|
||||
if (!buf) {
|
||||
cst_session_unlock(sess);
|
||||
return NULL;
|
||||
}
|
||||
buf[0] = '[';
|
||||
len = 1;
|
||||
int first = 1;
|
||||
for (int i = 0; i < n; i++) {
|
||||
const cst_message_t *m = cst_session_get(sess, i);
|
||||
if (!m)
|
||||
continue;
|
||||
if (m->role == CST_ROLE_SYSTEM)
|
||||
continue;
|
||||
char idbuf[64], head[288];
|
||||
snprintf(idbuf, sizeof(idbuf), "h%d", i);
|
||||
|
||||
if (m->role == CST_ROLE_ASSISTANT && m->reasoning && m->reasoning[0]) {
|
||||
snprintf(head, sizeof(head),
|
||||
"{\"kind\":\"thought\",\"id\":\"%s-r\",\"text\":\"", idbuf);
|
||||
if (hist_append_obj(&buf, &len, &cap, &first, head, m->reasoning) != 0)
|
||||
break;
|
||||
}
|
||||
|
||||
if (m->role == CST_ROLE_TOOL) {
|
||||
const char *tname =
|
||||
(m->tool_name[0] && !strpbrk(m->tool_name, "\\\"\n\r"))
|
||||
? m->tool_name
|
||||
: "tool";
|
||||
snprintf(head, sizeof(head),
|
||||
"{\"kind\":\"tool\",\"id\":\"%s\",\"name\":\"%s\",\"status\":"
|
||||
"\"ok\",\"detail\":\"%s\",\"output\":\"",
|
||||
idbuf, tname, tname);
|
||||
if (hist_append_obj(&buf, &len, &cap, &first, head,
|
||||
m->content ? m->content : "") != 0)
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (m->role == CST_ROLE_USER) {
|
||||
if (!m->content || !m->content[0])
|
||||
continue;
|
||||
snprintf(head, sizeof(head),
|
||||
"{\"kind\":\"user\",\"id\":\"%s\",\"text\":\"", idbuf);
|
||||
if (hist_append_obj(&buf, &len, &cap, &first, head, m->content) != 0)
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (m->role == CST_ROLE_ASSISTANT) {
|
||||
if (!m->content || !m->content[0])
|
||||
continue;
|
||||
snprintf(head, sizeof(head),
|
||||
"{\"kind\":\"assistant\",\"id\":\"%s\",\"text\":\"", idbuf);
|
||||
if (hist_append_obj(&buf, &len, &cap, &first, head, m->content) != 0)
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (len + 2 >= cap) {
|
||||
char *nb = (char *)realloc(buf, len + 2);
|
||||
if (nb)
|
||||
buf = nb;
|
||||
}
|
||||
buf[len++] = ']';
|
||||
buf[len] = 0;
|
||||
cst_session_unlock(sess);
|
||||
return buf;
|
||||
}
|
||||
|
||||
char *hermit_agent_todo(void *agent) {
|
||||
if (!agent)
|
||||
return NULL;
|
||||
const char *t = cst_agent_todo((cst_agent_t *)agent);
|
||||
if (!t || !t[0])
|
||||
return NULL;
|
||||
size_t n = strlen(t);
|
||||
char *out = (char *)malloc(n + 1);
|
||||
if (!out)
|
||||
return NULL;
|
||||
memcpy(out, t, n + 1);
|
||||
return out;
|
||||
}
|
||||
|
||||
char *hermit_agent_export_md(void *agent, const char *title) {
|
||||
if (!agent)
|
||||
return NULL;
|
||||
cst_session_t *sess = cst_agent_session((cst_agent_t *)agent);
|
||||
if (!sess)
|
||||
return NULL;
|
||||
char sid[128];
|
||||
cst_agent_session_id((cst_agent_t *)agent, sid, sizeof(sid));
|
||||
const cst_agent_config_t *cfg = cst_agent_config((cst_agent_t *)agent);
|
||||
const char *model = cfg && cfg->model[0] ? cfg->model : "";
|
||||
return cst_session_export_md(sess, title ? title : "Session", sid, model);
|
||||
}
|
||||
|
||||
int hermit_creds_has(const char *provider) {
|
||||
char key[512];
|
||||
const char *from = NULL;
|
||||
if (!provider || !provider[0])
|
||||
return 0;
|
||||
return cst_creds_resolve(provider, key, sizeof(key), &from) == 0 ? 1 : 0;
|
||||
}
|
||||
|
||||
int hermit_store_list(const char *workspace, char ids[][128], int max_ids) {
|
||||
return cst_agent_store_list(workspace, ids, max_ids);
|
||||
}
|
||||
|
||||
int hermit_store_info(const char *workspace, const char *id, char *title,
|
||||
size_t title_sz, long long *mtime_ms) {
|
||||
cst_session_info_t info;
|
||||
memset(&info, 0, sizeof(info));
|
||||
if (cst_agent_store_info(workspace, id, &info) != 0)
|
||||
return -1;
|
||||
if (title && title_sz) {
|
||||
if (info.title[0])
|
||||
snprintf(title, title_sz, "%s", info.title);
|
||||
else if (info.first_user[0])
|
||||
snprintf(title, title_sz, "%s", info.first_user);
|
||||
else
|
||||
snprintf(title, title_sz, "%s", id);
|
||||
}
|
||||
/* cst_session_info_t.mtime is seconds since epoch (os_lstat); JS Date wants ms. */
|
||||
if (mtime_ms)
|
||||
*mtime_ms = info.mtime > 0 ? info.mtime * 1000LL : 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int hermit_store_title_set(const char *workspace, const char *id,
|
||||
const char *title) {
|
||||
return cst_agent_store_title_set(workspace, id, title);
|
||||
}
|
||||
|
||||
int hermit_store_delete(const char *workspace, const char *id) {
|
||||
return cst_agent_store_delete(workspace, id);
|
||||
}
|
||||
|
||||
int hermit_creds_save(const char *provider, const char *key) {
|
||||
return cst_creds_save(provider, key);
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/* Thin C façade over cst_agent.h for the Tauri desktop host. */
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef void (*hermit_on_text_fn)(const char *text, size_t len, void *ud);
|
||||
typedef void (*hermit_on_tool_fn)(const char *phase, const char *id,
|
||||
const char *name, const char *detail,
|
||||
void *ud);
|
||||
typedef void (*hermit_on_tool_result_fn)(const char *id, const char *name,
|
||||
const char *out, size_t out_len,
|
||||
int exit_code, void *ud);
|
||||
typedef void (*hermit_on_todo_fn)(const char *text, void *ud);
|
||||
typedef void (*hermit_on_status_fn)(const char *msg, void *ud);
|
||||
/* Return CST_PERM_* . preview may be NULL. */
|
||||
typedef int (*hermit_on_perm_fn)(const char *tool, const char *detail,
|
||||
const char *preview, void *ud);
|
||||
/* Fill ans; return 0 ok, -1 dismiss. options is CSV or JSON-ish string. */
|
||||
typedef int (*hermit_on_ask_fn)(const char *question, const char *options,
|
||||
char *ans, size_t ans_sz, void *ud);
|
||||
typedef void (*hermit_on_turn_begin_fn)(const char *user_text, void *ud);
|
||||
typedef void (*hermit_on_turn_end_fn)(int rc, const char *err, void *ud);
|
||||
typedef void (*hermit_on_error_fn)(const char *kind, const char *message,
|
||||
void *ud);
|
||||
|
||||
typedef struct {
|
||||
hermit_on_text_fn on_assistant;
|
||||
hermit_on_text_fn on_thought;
|
||||
hermit_on_tool_fn on_tool;
|
||||
hermit_on_tool_result_fn on_tool_result;
|
||||
hermit_on_todo_fn on_todo;
|
||||
hermit_on_status_fn on_status;
|
||||
hermit_on_perm_fn on_perm;
|
||||
hermit_on_ask_fn on_ask;
|
||||
hermit_on_turn_begin_fn on_turn_begin;
|
||||
hermit_on_turn_end_fn on_turn_end;
|
||||
hermit_on_error_fn on_error;
|
||||
void *ud;
|
||||
} hermit_hooks_t;
|
||||
|
||||
void *hermit_agent_open(const char *workspace, const char *provider,
|
||||
const char *model, const char *approval,
|
||||
const char *api_key, const char *effort, char *err,
|
||||
size_t err_sz);
|
||||
void hermit_agent_close(void *agent);
|
||||
int hermit_agent_turn(void *agent, const char *user_text,
|
||||
const hermit_hooks_t *hooks, char *err, size_t err_sz);
|
||||
void hermit_agent_cancel(void *agent);
|
||||
|
||||
int hermit_agent_set_workspace(void *agent, const char *path, char *err,
|
||||
size_t err_sz);
|
||||
int hermit_agent_new_session(void *agent, char *id_out, size_t id_sz);
|
||||
int hermit_agent_switch_session(void *agent, const char *id);
|
||||
int hermit_agent_session_id(void *agent, char *out, size_t out_sz);
|
||||
int hermit_agent_set_plan_mode(void *agent, int on);
|
||||
int hermit_agent_set_approval(void *agent, const char *approval);
|
||||
int hermit_agent_set_effort(void *agent, const char *effort);
|
||||
int hermit_agent_attach_file(void *agent, const char *path, char *err,
|
||||
size_t err_sz);
|
||||
void hermit_agent_pending_clear(void *agent);
|
||||
int hermit_agent_compact(void *agent, char *err, size_t err_sz);
|
||||
void hermit_agent_usage(void *agent, long *prompt, long *out_tokens,
|
||||
double *cost, int *priced);
|
||||
|
||||
/* Malloc'd JSON array of chat items; caller frees. NULL on failure. */
|
||||
char *hermit_agent_history_json(void *agent);
|
||||
/* Malloc'd todo text or NULL. */
|
||||
char *hermit_agent_todo(void *agent);
|
||||
/* Malloc'd markdown export; caller frees. */
|
||||
char *hermit_agent_export_md(void *agent, const char *title);
|
||||
/* 1 if a saved/env key exists for provider. */
|
||||
int hermit_creds_has(const char *provider);
|
||||
|
||||
int hermit_store_list(const char *workspace, char ids[][128], int max_ids);
|
||||
int hermit_store_info(const char *workspace, const char *id, char *title,
|
||||
size_t title_sz, long long *mtime_ms);
|
||||
int hermit_store_title_set(const char *workspace, const char *id,
|
||||
const char *title);
|
||||
int hermit_store_delete(const char *workspace, const char *id);
|
||||
int hermit_creds_save(const char *provider, const char *key);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,14 @@
|
||||
/* Placeholder for future cbindgen / manual FFI against include/cst_agent.h.
|
||||
* Build with --features engine and HERMIT_CORE_LIB pointing at libcstcore.a.
|
||||
*/
|
||||
#pragma once
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* Mirrors the subset AgentCore used; real symbols come from libcstcore. */
|
||||
typedef struct cst_agent cst_agent_t;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,291 @@
|
||||
mod engine;
|
||||
mod ffi;
|
||||
|
||||
use engine::{SharedEngine, EngineState};
|
||||
use std::sync::Mutex;
|
||||
use tauri::{
|
||||
menu::{Menu, MenuItem, PredefinedMenuItem, Submenu},
|
||||
Emitter, Manager,
|
||||
};
|
||||
|
||||
#[tauri::command]
|
||||
fn session_list(state: tauri::State<'_, SharedEngine>) -> Vec<engine::SessionInfo> {
|
||||
engine::list_sessions(&state)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn session_new(state: tauri::State<'_, SharedEngine>) -> engine::SessionInfo {
|
||||
engine::new_session(&state)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn session_open(
|
||||
state: tauri::State<'_, SharedEngine>,
|
||||
id: String,
|
||||
) -> Vec<engine::ChatItem> {
|
||||
engine::open_session(&state, &id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn session_rename(
|
||||
state: tauri::State<'_, SharedEngine>,
|
||||
id: String,
|
||||
title: String,
|
||||
) -> bool {
|
||||
engine::rename_session(&state, &id, &title)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn session_delete(state: tauri::State<'_, SharedEngine>, id: String) -> bool {
|
||||
engine::delete_session(&state, &id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn session_pin(state: tauri::State<'_, SharedEngine>, id: String) -> bool {
|
||||
engine::pin_session(&state, &id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn session_set_folder(
|
||||
state: tauri::State<'_, SharedEngine>,
|
||||
id: String,
|
||||
folder: Option<String>,
|
||||
) -> bool {
|
||||
engine::session_set_folder(&state, &id, folder)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn session_compact(state: tauri::State<'_, SharedEngine>) -> String {
|
||||
engine::session_compact(&state)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn session_export(state: tauri::State<'_, SharedEngine>) -> String {
|
||||
engine::session_export(&state)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn settings_get(state: tauri::State<'_, SharedEngine>) -> engine::Settings {
|
||||
engine::settings_get(&state)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn settings_set(
|
||||
state: tauri::State<'_, SharedEngine>,
|
||||
settings: engine::Settings,
|
||||
) -> engine::Settings {
|
||||
engine::settings_set(&state, settings)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn attach_list(state: tauri::State<'_, SharedEngine>) -> Vec<engine::AttachmentInfo> {
|
||||
engine::attach_list(&state)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn attach_file(
|
||||
state: tauri::State<'_, SharedEngine>,
|
||||
path: String,
|
||||
) -> Result<engine::AttachmentInfo, String> {
|
||||
engine::attach_file(&state, path)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn attach_clear(state: tauri::State<'_, SharedEngine>) {
|
||||
engine::attach_clear(&state)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn attach_remove(state: tauri::State<'_, SharedEngine>, id: String) {
|
||||
engine::attach_remove(&state, &id)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn providers_list(state: tauri::State<'_, SharedEngine>) -> Vec<engine::ProviderInfo> {
|
||||
engine::providers_list(&state)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn providers_use(
|
||||
state: tauri::State<'_, SharedEngine>,
|
||||
provider: String,
|
||||
model: String,
|
||||
key: String,
|
||||
) -> Result<String, String> {
|
||||
engine::providers_use(&state, provider, model, key)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn usage_get(state: tauri::State<'_, SharedEngine>) -> engine::UsageInfo {
|
||||
engine::usage_get(&state)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn todo_get(state: tauri::State<'_, SharedEngine>) -> String {
|
||||
engine::todo_get(&state)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn bash_list(state: tauri::State<'_, SharedEngine>) -> Vec<engine::BashRun> {
|
||||
engine::bash_list(&state)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn workspace_files(
|
||||
state: tauri::State<'_, SharedEngine>,
|
||||
query: String,
|
||||
) -> Vec<String> {
|
||||
engine::workspace_files(&state, query)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn engine_status() -> String {
|
||||
engine::engine_status()
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
async fn send_turn(
|
||||
app: tauri::AppHandle,
|
||||
state: tauri::State<'_, SharedEngine>,
|
||||
text: String,
|
||||
) -> Result<(), String> {
|
||||
engine::send_turn(app, &state, text).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn cancel_turn(state: tauri::State<'_, SharedEngine>) {
|
||||
engine::cancel_turn(&state);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn consent_answer(state: tauri::State<'_, SharedEngine>, id: String, answer: String) {
|
||||
engine::consent_answer(&state, &id, &answer);
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn question_answer(state: tauri::State<'_, SharedEngine>, id: String, answer: String) {
|
||||
engine::question_answer(&state, &id, &answer);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn apply_linux_webview_workarounds() {
|
||||
let nvidia = std::path::Path::new("/proc/driver/nvidia/version").exists();
|
||||
let wayland = std::env::var_os("WAYLAND_DISPLAY").is_some()
|
||||
|| std::env::var("XDG_SESSION_TYPE").ok().as_deref() == Some("wayland");
|
||||
unsafe {
|
||||
if nvidia && std::env::var_os("__NV_DISABLE_EXPLICIT_SYNC").is_none() {
|
||||
std::env::set_var("__NV_DISABLE_EXPLICIT_SYNC", "1");
|
||||
}
|
||||
if nvidia && wayland && std::env::var_os("GDK_BACKEND").is_none() {
|
||||
std::env::set_var("GDK_BACKEND", "x11");
|
||||
}
|
||||
let backend = std::env::var("GDK_BACKEND").unwrap_or_default();
|
||||
let on_x11 = backend == "x11" || (!wayland && std::env::var_os("DISPLAY").is_some());
|
||||
if nvidia && on_x11 && std::env::var_os("WEBKIT_DISABLE_DMABUF_RENDERER").is_none() {
|
||||
std::env::set_var("WEBKIT_DISABLE_DMABUF_RENDERER", "1");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_menu(app: &tauri::App) -> tauri::Result<Menu<tauri::Wry>> {
|
||||
let new_session = MenuItem::with_id(app, "new_session", "New Session", true, Some("CmdOrCtrl+N"))?;
|
||||
let open_ws = MenuItem::with_id(app, "open_workspace", "Open Workspace…", true, Some("CmdOrCtrl+O"))?;
|
||||
let settings = MenuItem::with_id(app, "settings", "Settings…", true, Some("CmdOrCtrl+Comma"))?;
|
||||
let quit = PredefinedMenuItem::quit(app, Some("Quit Hermit"))?;
|
||||
|
||||
let file = Submenu::with_items(
|
||||
app,
|
||||
"File",
|
||||
true,
|
||||
&[&new_session, &open_ws, &PredefinedMenuItem::separator(app)?, &settings, &quit],
|
||||
)?;
|
||||
|
||||
let edit = Submenu::with_items(
|
||||
app,
|
||||
"Edit",
|
||||
true,
|
||||
&[
|
||||
&PredefinedMenuItem::undo(app, None)?,
|
||||
&PredefinedMenuItem::redo(app, None)?,
|
||||
&PredefinedMenuItem::separator(app)?,
|
||||
&PredefinedMenuItem::cut(app, None)?,
|
||||
&PredefinedMenuItem::copy(app, None)?,
|
||||
&PredefinedMenuItem::paste(app, None)?,
|
||||
&PredefinedMenuItem::select_all(app, None)?,
|
||||
],
|
||||
)?;
|
||||
|
||||
let view_plan = MenuItem::with_id(app, "view_plan", "Toggle Plan", true, None::<&str>)?;
|
||||
let view_bash = MenuItem::with_id(app, "view_bash", "Toggle Background Bash", true, Some("CmdOrCtrl+`"))?;
|
||||
let view = Submenu::with_items(app, "View", true, &[&view_plan, &view_bash])?;
|
||||
|
||||
let about = MenuItem::with_id(app, "about", "About Hermit", true, None::<&str>)?;
|
||||
let help = Submenu::with_items(app, "Help", true, &[&about])?;
|
||||
|
||||
Menu::with_items(app, &[&file, &edit, &view, &help])
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
#[cfg(target_os = "linux")]
|
||||
apply_linux_webview_workarounds();
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.manage(Mutex::new(EngineState::default()))
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
session_list,
|
||||
session_new,
|
||||
session_open,
|
||||
session_rename,
|
||||
session_delete,
|
||||
session_pin,
|
||||
session_set_folder,
|
||||
session_compact,
|
||||
session_export,
|
||||
settings_get,
|
||||
settings_set,
|
||||
attach_list,
|
||||
attach_file,
|
||||
attach_clear,
|
||||
attach_remove,
|
||||
providers_list,
|
||||
providers_use,
|
||||
usage_get,
|
||||
todo_get,
|
||||
bash_list,
|
||||
workspace_files,
|
||||
engine_status,
|
||||
send_turn,
|
||||
cancel_turn,
|
||||
consent_answer,
|
||||
question_answer
|
||||
])
|
||||
.setup(|app| {
|
||||
if let Ok(menu) = build_menu(app) {
|
||||
let _ = app.set_menu(menu);
|
||||
}
|
||||
if let Some(w) = app.get_webview_window("main") {
|
||||
let _ = w.set_title("Hermit");
|
||||
let _ = w.set_size(tauri::Size::Logical(tauri::LogicalSize {
|
||||
width: 1280.0,
|
||||
height: 840.0,
|
||||
}));
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
let _ = w.navigate(
|
||||
tauri::Url::parse("http://127.0.0.1:1420/").expect("dev url"),
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.on_menu_event(|app, event| {
|
||||
let id = event.id().as_ref();
|
||||
let _ = app.emit("hermit://menu", id);
|
||||
})
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running Hermit desktop");
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
hermit_desktop_lib::run();
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Hermit",
|
||||
"version": "1.1.0",
|
||||
"identifier": "xyz.whcself.hermit",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"devUrl": "http://127.0.0.1:1420",
|
||||
"beforeBuildCommand": "npm run build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"label": "main",
|
||||
"title": "Hermit",
|
||||
"width": 1280,
|
||||
"height": 840,
|
||||
"minWidth": 880,
|
||||
"minHeight": 560,
|
||||
"resizable": true,
|
||||
"fullscreen": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"linux": {
|
||||
"deb": {
|
||||
"depends": []
|
||||
}
|
||||
},
|
||||
"macOS": {
|
||||
"minimumSystemVersion": "12.0"
|
||||
},
|
||||
"windows": {
|
||||
"nsis": {
|
||||
"installMode": "currentUser"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user