feat: Hermit 1.1.0 — HERMIT_* env, Tauri desktop shell
Wire hermit_getenv across the engine, rebrand WebUI/systemd/docs, and add apps/desktop (Tauri 2 + React) with a DeepSeek-Harness-like UI. Qt app/ is frozen; full libcstcore FFI is scaffolded behind --features engine. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Generated
+4515
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,28 @@
|
||||
[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"
|
||||
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,29 @@
|
||||
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() {
|
||||
if let Ok(lib) = std::env::var("HERMIT_CORE_LIB") {
|
||||
let path = std::path::PathBuf::from(&lib);
|
||||
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);
|
||||
println!("cargo:rustc-link-lib=curl");
|
||||
println!("cargo:rustc-link-lib=pthread");
|
||||
println!("cargo:rustc-link-lib=m");
|
||||
println!("cargo:rerun-if-changed={}", lib);
|
||||
}
|
||||
if let Ok(inc) = std::env::var("HERMIT_CORE_INCLUDE") {
|
||||
println!("cargo:rerun-if-env-changed=HERMIT_CORE_INCLUDE");
|
||||
println!("cargo:rustc-env=HERMIT_CORE_INCLUDE={}", inc);
|
||||
}
|
||||
}
|
||||
tauri_build::build()
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Default Hermit desktop capabilities",
|
||||
"windows": ["main"],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"shell:allow-open"
|
||||
]
|
||||
}
|
||||
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"]}}
|
||||
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 |
@@ -0,0 +1,173 @@
|
||||
//! Engine façade: mock by default; with `--features engine` + HERMIT_CORE_LIB
|
||||
//! this module will grow real FFI around `cst_agent.h` (AgentCore parity).
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Mutex;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SessionInfo {
|
||||
pub id: String,
|
||||
pub title: String,
|
||||
pub updated_at: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "kind")]
|
||||
pub enum ChatItem {
|
||||
#[serde(rename = "user")]
|
||||
User { id: String, text: String },
|
||||
#[serde(rename = "assistant")]
|
||||
Assistant { id: String, text: String },
|
||||
#[serde(rename = "thought")]
|
||||
Thought { id: String, text: String },
|
||||
#[serde(rename = "tool")]
|
||||
Tool {
|
||||
id: String,
|
||||
name: String,
|
||||
status: String,
|
||||
detail: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Settings {
|
||||
pub provider: String,
|
||||
pub model: String,
|
||||
pub approval: String,
|
||||
pub workspace: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum EngineEvent {
|
||||
#[serde(rename = "assistant_delta")]
|
||||
AssistantDelta { text: String },
|
||||
#[serde(rename = "thought_delta")]
|
||||
ThoughtDelta { text: String },
|
||||
#[serde(rename = "tool_start")]
|
||||
ToolStart {
|
||||
id: String,
|
||||
name: String,
|
||||
detail: String,
|
||||
},
|
||||
#[serde(rename = "tool_end")]
|
||||
ToolEnd {
|
||||
id: String,
|
||||
status: String,
|
||||
detail: String,
|
||||
},
|
||||
#[serde(rename = "consent")]
|
||||
Consent {
|
||||
req: ConsentRequest,
|
||||
},
|
||||
#[serde(rename = "turn_end")]
|
||||
TurnEnd { ok: bool },
|
||||
#[serde(rename = "error")]
|
||||
Error { message: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ConsentRequest {
|
||||
pub id: String,
|
||||
pub tool: String,
|
||||
pub detail: String,
|
||||
}
|
||||
|
||||
pub struct EngineState {
|
||||
pub settings: Settings,
|
||||
pub sessions: Vec<SessionInfo>,
|
||||
busy: bool,
|
||||
}
|
||||
|
||||
impl Default for EngineState {
|
||||
fn default() -> Self {
|
||||
let workspace = std::env::current_dir()
|
||||
.ok()
|
||||
.and_then(|p| p.to_str().map(|s| s.to_string()))
|
||||
.unwrap_or_else(|| ".".into());
|
||||
Self {
|
||||
settings: Settings {
|
||||
provider: "deepseek".into(),
|
||||
model: "deepseek-flash".into(),
|
||||
approval: "auto".into(),
|
||||
workspace,
|
||||
},
|
||||
sessions: vec![SessionInfo {
|
||||
id: "welcome".into(),
|
||||
title: "Welcome".into(),
|
||||
updated_at: chrono_now(),
|
||||
}],
|
||||
busy: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn chrono_now() -> i64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map(|d| d.as_millis() as i64)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
pub type SharedEngine = Mutex<EngineState>;
|
||||
|
||||
pub fn list_sessions(state: &SharedEngine) -> Vec<SessionInfo> {
|
||||
state.lock().unwrap().sessions.clone()
|
||||
}
|
||||
|
||||
pub fn new_session(state: &SharedEngine) -> SessionInfo {
|
||||
let mut g = state.lock().unwrap();
|
||||
let s = SessionInfo {
|
||||
id: format!("s{}", chrono_now()),
|
||||
title: "New session".into(),
|
||||
updated_at: chrono_now(),
|
||||
};
|
||||
g.sessions.insert(0, s.clone());
|
||||
s
|
||||
}
|
||||
|
||||
pub fn open_session(_state: &SharedEngine, _id: &str) -> Vec<ChatItem> {
|
||||
// Real FFI will replay transcript from cst_agent_store_*.
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
pub fn settings_get(state: &SharedEngine) -> Settings {
|
||||
state.lock().unwrap().settings.clone()
|
||||
}
|
||||
|
||||
pub async fn send_turn(app: AppHandle, state: &SharedEngine, text: String) {
|
||||
{
|
||||
let mut g = state.lock().unwrap();
|
||||
if g.busy {
|
||||
return;
|
||||
}
|
||||
g.busy = true;
|
||||
}
|
||||
|
||||
// Placeholder stream until libcstcore FFI is wired (feature = "engine").
|
||||
let _ = app.emit(
|
||||
"hermit://event",
|
||||
EngineEvent::ThoughtDelta {
|
||||
text: "Rust host received turn; FFI bridge pending full AgentCore port."
|
||||
.into(),
|
||||
},
|
||||
);
|
||||
tokio::time::sleep(std::time::Duration::from_millis(200)).await;
|
||||
let _ = app.emit(
|
||||
"hermit://event",
|
||||
EngineEvent::AssistantDelta {
|
||||
text: format!("Echo (host): {}", text),
|
||||
},
|
||||
);
|
||||
let _ = app.emit("hermit://event", EngineEvent::TurnEnd { ok: true });
|
||||
state.lock().unwrap().busy = false;
|
||||
}
|
||||
|
||||
pub fn cancel_turn(state: &SharedEngine) {
|
||||
state.lock().unwrap().busy = false;
|
||||
}
|
||||
|
||||
pub fn consent_answer(_id: &str, _answer: &str) {
|
||||
// Real path: wake the worker blocked in consent hook.
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
//! Future FFI surface against `include/cst_agent.h` (AgentCore parity).
|
||||
//!
|
||||
//! Enable with:
|
||||
//! make -C ../.. libcstcore.a # or cmake cstcore
|
||||
//! HERMIT_CORE_LIB=$PWD/../../libcstcore.a \
|
||||
//! HERMIT_CORE_INCLUDE=$PWD/../../include \
|
||||
//! cargo build --features engine
|
||||
//!
|
||||
//! Mapping (Qt AgentCore → Tauri commands/events):
|
||||
//! open / send / cancel → session_* / send_turn / cancel_turn
|
||||
//! hooks.assistant_delta → hermit://event AssistantDelta
|
||||
//! hooks.tool_* → ToolStart / ToolEnd
|
||||
//! hooks.consent (blocking) → Consent event + consent_answer wakes waiter
|
||||
//! settings / creds → settings_get (+ later settings_set)
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
#[cfg(feature = "engine")]
|
||||
mod live {
|
||||
// Bindings land here (cbindgen or manual extern "C").
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "engine"))]
|
||||
pub fn engine_linked() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[cfg(feature = "engine")]
|
||||
pub fn engine_linked() -> bool {
|
||||
true
|
||||
}
|
||||
@@ -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,73 @@
|
||||
mod engine;
|
||||
mod ffi;
|
||||
|
||||
use engine::{SharedEngine, EngineState};
|
||||
use std::sync::Mutex;
|
||||
use tauri::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 settings_get(state: tauri::State<'_, SharedEngine>) -> engine::Settings {
|
||||
engine::settings_get(&state)
|
||||
}
|
||||
|
||||
#[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(id: String, answer: String) {
|
||||
engine::consent_answer(&id, &answer);
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.manage(Mutex::new(EngineState::default()))
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
session_list,
|
||||
session_new,
|
||||
session_open,
|
||||
settings_get,
|
||||
send_turn,
|
||||
cancel_turn,
|
||||
consent_answer
|
||||
])
|
||||
.setup(|app| {
|
||||
if let Some(w) = app.get_webview_window("main") {
|
||||
let _ = w.set_title("Hermit");
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.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://localhost: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