feat: Hermit 1.1.0 — HERMIT_* env, Tauri desktop shell
ci / tcc-check (push) Failing after 40s
ci / linux (aarch64, 0) (push) Skipped
ci / linux (x86, 1) (push) Skipped
ci / linux (x86_64, 1) (push) Skipped
ci / windows (push) Skipped

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:
WHC
2026-10-30 13:15:47 +08:00
co-authored by Cursor
parent 15c6c4a02a
commit ea38247161
71 changed files with 15336 additions and 143 deletions
+4515
View File
File diff suppressed because it is too large Load Diff
+28
View File
@@ -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 = []
+29
View File
@@ -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

+173
View File
@@ -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.
}
+31
View File
@@ -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
}
+14
View File
@@ -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
+73
View File
@@ -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");
}
+5
View File
@@ -0,0 +1,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
hermit_desktop_lib::run();
}
+53
View File
@@ -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"
}
}
}
}