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:
@@ -61,3 +61,8 @@ result
|
||||
hermit
|
||||
.peer_probe.out
|
||||
reports/
|
||||
|
||||
# Tauri desktop
|
||||
apps/desktop/node_modules/
|
||||
apps/desktop/dist/
|
||||
apps/desktop/src-tauri/target/
|
||||
|
||||
@@ -205,6 +205,7 @@ TEST_LIBS = -L$(SD_DIR) -lsashidown $(CURL_LIBS) $(RPATH_LIBS) $(TEST_SYS_LIBS)
|
||||
# cst_test_util.h has static os_* helpers; gcc drops them when unused, tcc
|
||||
# keeps them, so the small tests link the OS layer too.
|
||||
TEST_OS_SRC = src/os_compat.c src/os_win.c src/util.c
|
||||
# hermit_getenv is in os_compat.c (HERMIT_/CST_ dual env).
|
||||
|
||||
.PHONY: all clean check demo sashidown-lib cmake-gcc cmake-clang asan install \
|
||||
uninstall install-service help hermit-font cst-agent
|
||||
@@ -347,11 +348,11 @@ install: hermit
|
||||
'BIN="$(PREFIX)/bin/hermit"' \
|
||||
'cmd="$${1:-status}"' \
|
||||
'case "$$cmd" in' \
|
||||
'status) "$$BIN" --version; if command -v systemctl >/dev/null 2>&1 && systemctl --user is-active cst-agent-parliament.service >/dev/null 2>&1; then systemctl --user status cst-agent-parliament.service --no-pager; else "$$BIN" --service-stop >/dev/null 2>&1 || true; echo "parliament service: not managed by systemd --user"; fi ;;' \
|
||||
'status) "$$BIN" --version; if command -v systemctl >/dev/null 2>&1 && systemctl --user is-active hermit-parliament.service >/dev/null 2>&1; then systemctl --user status hermit-parliament.service --no-pager; elif command -v systemctl >/dev/null 2>&1 && systemctl --user is-active cst-agent-parliament.service >/dev/null 2>&1; then systemctl --user status cst-agent-parliament.service --no-pager; else "$$BIN" --service-stop >/dev/null 2>&1 || true; echo "parliament service: not managed by systemd --user"; fi ;;' \
|
||||
'install-unit) make -C "$(HERMIT_SRC)" install-service PREFIX="$(PREFIX)" ;;' \
|
||||
'start) systemctl --user start cst-agent-parliament.service ;;' \
|
||||
'stop) systemctl --user stop cst-agent-parliament.service; "$$BIN" --service-stop || true ;;' \
|
||||
'restart) systemctl --user restart cst-agent-parliament.service ;;' \
|
||||
'start) systemctl --user start hermit-parliament.service 2>/dev/null || systemctl --user start cst-agent-parliament.service ;;' \
|
||||
'stop) systemctl --user stop hermit-parliament.service 2>/dev/null || true; systemctl --user stop cst-agent-parliament.service 2>/dev/null || true; "$$BIN" --service-stop || true ;;' \
|
||||
'restart) systemctl --user restart hermit-parliament.service 2>/dev/null || systemctl --user restart cst-agent-parliament.service ;;' \
|
||||
'*) echo "usage: hermit-service {status|install-unit|start|stop|restart}" >&2; exit 2 ;;' \
|
||||
'esac' \
|
||||
> "$(DESTDIR)$(PREFIX)/bin/hermit-service"
|
||||
@@ -383,11 +384,15 @@ install: hermit
|
||||
install-service: install
|
||||
install -d "$(DESTDIR)$(HOME)/.config/systemd/user"
|
||||
sed -e 's|@BIN@|$(PREFIX)/bin/hermit|g' \
|
||||
scripts/cst-agent-parliament.service.in \
|
||||
> "$(DESTDIR)$(HOME)/.config/systemd/user/cst-agent-parliament.service"
|
||||
scripts/hermit-parliament.service.in \
|
||||
> "$(DESTDIR)$(HOME)/.config/systemd/user/hermit-parliament.service"
|
||||
ln -sfr "$(DESTDIR)$(HOME)/.config/systemd/user/hermit-parliament.service" \
|
||||
"$(DESTDIR)$(HOME)/.config/systemd/user/cst-agent-parliament.service" 2>/dev/null || \
|
||||
ln -sf hermit-parliament.service \
|
||||
"$(DESTDIR)$(HOME)/.config/systemd/user/cst-agent-parliament.service"
|
||||
@echo "Installed user unit. Enable with:"
|
||||
@echo " systemctl --user daemon-reload"
|
||||
@echo " systemctl --user enable --now cst-agent-parliament.service"
|
||||
@echo " systemctl --user enable --now hermit-parliament.service"
|
||||
@echo "(set HERMIT_/CST_ REALM_PASSWORD / INSTANCE_ID in the drop-in or environment)"
|
||||
|
||||
uninstall:
|
||||
@@ -398,6 +403,7 @@ uninstall:
|
||||
rm -f "$(DESTDIR)$(PREFIX)/share/fonts/hermit/hermit-icons.ttf"
|
||||
rm -rf "$(DESTDIR)$(PREFIX)/share/hermit/frames"
|
||||
rm -f "$(HOME)/.local/share/fonts/hermit-icons.ttf"
|
||||
rm -f "$(HOME)/.config/systemd/user/hermit-parliament.service"
|
||||
rm -f "$(HOME)/.config/systemd/user/cst-agent-parliament.service"
|
||||
|
||||
-include $(DEPFILES)
|
||||
|
||||
@@ -58,7 +58,9 @@ make hermit-font # 烘焙 PNG 帧 + hermit-icons.ttf(Pillow + fonttool
|
||||
make install PREFIX=$HOME/.local # 同时安装字体到 share/fonts/hermit 与 ~/.local/share/fonts
|
||||
```
|
||||
|
||||
TUI 图标:`HERMIT_ICONS=auto|mini|font`(默认色块动画帧,状态与 `icons/agent-motion` HTML 套件一致;`font` 输出 PUA `U+E700+`,需终端加载已安装的 Hermit Icons)。
|
||||
桌面 GUI(Tauri 2 + React,DeepSeek Harness 风格布局):见 [`apps/desktop`](apps/desktop/) 与 [docs/desktop.md](docs/desktop.md)。旧 Qt App(`app/`)已冻结。
|
||||
|
||||
TUI 图标:`HERMIT_ICONS=auto|mini|font`(默认色块动画帧,状态与 `icons/agent-motion` HTML 套件一致;`font` 输出 PUA `U+E700+`,需终端加载已安装的 Hermit Icons)。环境变量 **`HERMIT_*` 优先,`CST_*` 仍接受**。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# Deprecated: Qt front end
|
||||
|
||||
This directory is the **legacy Qt 6 / QML** Hermit GUI (`cst-agent-app`).
|
||||
|
||||
The supported desktop shell is **`apps/desktop`** (Tauri 2 + React), with a
|
||||
DeepSeek-Harness-inspired layout and an in-process C engine bridge.
|
||||
|
||||
Do not add features here. Once Tauri reaches feature parity, this tree will be
|
||||
moved to `archive/qt-app` or removed. Android APK scripts that target this tree
|
||||
are transitional.
|
||||
@@ -143,7 +143,9 @@ bool AgentCore::open(const QString &workspace, const QString &provider,
|
||||
if (m_cfg.api_key[0] == 0 && m_cfg.provider[0]) {
|
||||
char key[512] = "";
|
||||
if (cst_creds_resolve(m_cfg.provider, key, sizeof(key), nullptr) != 0) {
|
||||
const char *env = getenv("CST_API_KEY");
|
||||
const char *env = getenv("HERMIT_API_KEY");
|
||||
if (!env || !env[0])
|
||||
env = getenv("CST_API_KEY");
|
||||
if (env && env[0])
|
||||
snprintf(key, sizeof(key), "%s", env);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
node_modules/
|
||||
dist/
|
||||
src-tauri/target/
|
||||
.DS_Store
|
||||
*.log
|
||||
@@ -0,0 +1,57 @@
|
||||
# Hermit Desktop (Tauri 2)
|
||||
|
||||
Cross-platform shell for Hermit. Visual layout follows DeepSeek Harness
|
||||
(session rail · chat · tool timeline · composer); the agent engine is the
|
||||
in-process C core (`libcstcore`), not a Cordis/plugin runtime.
|
||||
|
||||
## Stack
|
||||
|
||||
- **Tauri 2** + Rust host
|
||||
- **React 19** + Vite + TypeScript UI
|
||||
- FFI to `libcstcore` (same embed path as the legacy Qt app)
|
||||
|
||||
## Dependencies
|
||||
|
||||
| Platform | Needs |
|
||||
|----------|--------|
|
||||
| All | Rust (stable), Node 20+, `pnpm` or `npm` |
|
||||
| Linux | `webkit2gtk-4.1`, `libgtk-3`, `librsvg`, `patchelf`, pkg-config, curl |
|
||||
| Windows | WebView2 runtime |
|
||||
| macOS | Xcode CLT |
|
||||
|
||||
Nix example:
|
||||
|
||||
```bash
|
||||
nix-shell -p rustc cargo nodejs_22 pkg-config webkitgtk_4_1 gtk3 librsvg openssl curl
|
||||
```
|
||||
|
||||
## Develop
|
||||
|
||||
```bash
|
||||
cd apps/desktop
|
||||
npm install
|
||||
npm run tauri dev
|
||||
```
|
||||
|
||||
Without a full engine link yet, the UI runs against the mock bridge
|
||||
(`HERMIT_DESKTOP_MOCK=1` or automatic when FFI is unavailable).
|
||||
|
||||
## Build
|
||||
|
||||
```bash
|
||||
npm run tauri build
|
||||
```
|
||||
|
||||
Artifacts land in `src-tauri/target/release/bundle/`.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
apps/desktop/
|
||||
src/ React UI (DSH-like)
|
||||
src-tauri/ Rust + Tauri + FFI
|
||||
README.md
|
||||
```
|
||||
|
||||
The legacy Qt front end under `/app` is frozen and will be archived once
|
||||
desktop feature parity lands.
|
||||
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Hermit</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+3584
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "hermit-desktop",
|
||||
"private": true,
|
||||
"version": "1.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tauri-apps/api": "^2.1.1",
|
||||
"@tauri-apps/plugin-dialog": "^2.2.0",
|
||||
"@tauri-apps/plugin-shell": "^2.2.0",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-markdown": "^9.0.3",
|
||||
"remark-gfm": "^4.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.1.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"typescript": "^5.7.2",
|
||||
"vite": "^6.0.3"
|
||||
}
|
||||
}
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,209 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Rail } from "@/components/Rail";
|
||||
import { Transcript } from "@/components/Transcript";
|
||||
import { Composer } from "@/components/Composer";
|
||||
import { ConsentModal } from "@/components/ConsentModal";
|
||||
import {
|
||||
answerConsent,
|
||||
cancelTurn,
|
||||
getSettings,
|
||||
listSessions,
|
||||
newSession,
|
||||
onEngineEvent,
|
||||
openSession,
|
||||
sendTurn,
|
||||
type ChatItem,
|
||||
type ConsentRequest,
|
||||
type SessionInfo,
|
||||
type Settings,
|
||||
} from "@/lib/bridge";
|
||||
import "@/styles/app.css";
|
||||
|
||||
export default function App() {
|
||||
const [sessions, setSessions] = useState<SessionInfo[]>([]);
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
const [items, setItems] = useState<ChatItem[]>([]);
|
||||
const [draft, setDraft] = useState("");
|
||||
const [busy, setBusy] = useState(false);
|
||||
const [query, setQuery] = useState("");
|
||||
const [settings, setSettings] = useState<Settings>({
|
||||
provider: "deepseek",
|
||||
model: "deepseek-flash",
|
||||
approval: "auto",
|
||||
workspace: ".",
|
||||
});
|
||||
const [consent, setConsent] = useState<ConsentRequest | null>(null);
|
||||
const [showSettings, setShowSettings] = useState(false);
|
||||
|
||||
const refreshSessions = useCallback(async () => {
|
||||
setSessions(await listSessions());
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void (async () => {
|
||||
setSettings(await getSettings());
|
||||
await refreshSessions();
|
||||
})();
|
||||
}, [refreshSessions]);
|
||||
|
||||
useEffect(() => {
|
||||
let un: (() => void) | undefined;
|
||||
void onEngineEvent((ev) => {
|
||||
if (ev.type === "assistant_delta") {
|
||||
setItems((prev) => {
|
||||
const last = prev[prev.length - 1];
|
||||
if (last && last.kind === "assistant") {
|
||||
return [
|
||||
...prev.slice(0, -1),
|
||||
{ ...last, text: last.text + ev.text },
|
||||
];
|
||||
}
|
||||
return [
|
||||
...prev,
|
||||
{ kind: "assistant", id: `a${Date.now()}`, text: ev.text },
|
||||
];
|
||||
});
|
||||
} else if (ev.type === "thought_delta") {
|
||||
setItems((prev) => {
|
||||
const last = prev[prev.length - 1];
|
||||
if (last && last.kind === "thought") {
|
||||
return [
|
||||
...prev.slice(0, -1),
|
||||
{ ...last, text: last.text + "\n" + ev.text },
|
||||
];
|
||||
}
|
||||
return [
|
||||
...prev,
|
||||
{ kind: "thought", id: `th${Date.now()}`, text: ev.text },
|
||||
];
|
||||
});
|
||||
} else if (ev.type === "tool_start") {
|
||||
setItems((prev) => [
|
||||
...prev,
|
||||
{
|
||||
kind: "tool",
|
||||
id: ev.id,
|
||||
name: ev.name,
|
||||
status: "running",
|
||||
detail: ev.detail,
|
||||
},
|
||||
]);
|
||||
} else if (ev.type === "tool_end") {
|
||||
setItems((prev) =>
|
||||
prev.map((it) =>
|
||||
it.kind === "tool" && it.id === ev.id
|
||||
? { ...it, status: ev.status, detail: ev.detail || it.detail }
|
||||
: it,
|
||||
),
|
||||
);
|
||||
} else if (ev.type === "consent") {
|
||||
setConsent(ev.req);
|
||||
} else if (ev.type === "turn_end") {
|
||||
setBusy(false);
|
||||
} else if (ev.type === "error") {
|
||||
setBusy(false);
|
||||
setItems((prev) => [
|
||||
...prev,
|
||||
{
|
||||
kind: "assistant",
|
||||
id: `err${Date.now()}`,
|
||||
text: `Error: ${ev.message}`,
|
||||
},
|
||||
]);
|
||||
}
|
||||
}).then((u) => {
|
||||
un = u;
|
||||
});
|
||||
return () => un?.();
|
||||
}, []);
|
||||
|
||||
const open = async (id: string) => {
|
||||
setActiveId(id);
|
||||
setItems(await openSession(id));
|
||||
};
|
||||
|
||||
const onNew = async () => {
|
||||
const s = await newSession();
|
||||
await refreshSessions();
|
||||
setActiveId(s.id);
|
||||
setItems([]);
|
||||
};
|
||||
|
||||
const onSend = async () => {
|
||||
const text = draft.trim();
|
||||
if (!text || busy) return;
|
||||
setDraft("");
|
||||
setBusy(true);
|
||||
setItems((prev) => [
|
||||
...prev,
|
||||
{ kind: "user", id: `u${Date.now()}`, text },
|
||||
]);
|
||||
await sendTurn(text);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="app">
|
||||
<Rail
|
||||
sessions={sessions}
|
||||
activeId={activeId}
|
||||
workspace={settings.workspace}
|
||||
query={query}
|
||||
onQuery={setQuery}
|
||||
onNew={() => void onNew()}
|
||||
onOpen={(id) => void open(id)}
|
||||
onSettings={() => setShowSettings(true)}
|
||||
/>
|
||||
<section className="main">
|
||||
<header className="topbar">
|
||||
<h1>
|
||||
{sessions.find((s) => s.id === activeId)?.title ?? "New session"}
|
||||
</h1>
|
||||
<div className="chips">
|
||||
<span className="chip ok">engine ready</span>
|
||||
<span className="chip">
|
||||
<strong>{settings.model}</strong>
|
||||
</span>
|
||||
</div>
|
||||
</header>
|
||||
<Transcript items={items} />
|
||||
<Composer
|
||||
value={draft}
|
||||
busy={busy}
|
||||
settings={settings}
|
||||
onChange={setDraft}
|
||||
onSend={() => void onSend()}
|
||||
onStop={() => void cancelTurn()}
|
||||
/>
|
||||
</section>
|
||||
{consent ? (
|
||||
<ConsentModal
|
||||
req={consent}
|
||||
onAnswer={(a) => {
|
||||
void answerConsent(consent.id, a);
|
||||
setConsent(null);
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{showSettings ? (
|
||||
<div className="consent" onClick={() => setShowSettings(false)}>
|
||||
<div
|
||||
className="consent-card"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h3>Settings</h3>
|
||||
<pre>{JSON.stringify(settings, null, 2)}</pre>
|
||||
<div className="consent-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="primary"
|
||||
onClick={() => setShowSettings(false)}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import type { Settings } from "@/lib/bridge";
|
||||
|
||||
type Props = {
|
||||
value: string;
|
||||
busy: boolean;
|
||||
settings: Settings;
|
||||
onChange: (v: string) => void;
|
||||
onSend: () => void;
|
||||
onStop: () => void;
|
||||
};
|
||||
|
||||
export function Composer({
|
||||
value,
|
||||
busy,
|
||||
settings,
|
||||
onChange,
|
||||
onSend,
|
||||
onStop,
|
||||
}: Props) {
|
||||
const ref = useRef<HTMLTextAreaElement>(null);
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) return;
|
||||
el.style.height = "0px";
|
||||
el.style.height = Math.min(200, el.scrollHeight) + "px";
|
||||
}, [value]);
|
||||
|
||||
return (
|
||||
<div className="composer-wrap">
|
||||
<div className="composer">
|
||||
<textarea
|
||||
ref={ref}
|
||||
rows={2}
|
||||
placeholder="Describe what you want to build, / commands, @ files…"
|
||||
value={value}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
if (!busy && value.trim()) onSend();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="composer-bar">
|
||||
<div className="composer-left">
|
||||
<span className="chip">
|
||||
<strong>{settings.model}</strong>
|
||||
</span>
|
||||
<span className="chip">
|
||||
{settings.provider} · {settings.approval}
|
||||
</span>
|
||||
</div>
|
||||
{busy ? (
|
||||
<button type="button" className="send stop" onClick={onStop}>
|
||||
Stop
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="send"
|
||||
disabled={!value.trim()}
|
||||
onClick={onSend}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import type { ConsentRequest } from "@/lib/bridge";
|
||||
|
||||
type Props = {
|
||||
req: ConsentRequest;
|
||||
onAnswer: (a: "deny" | "once" | "always_tool" | "always_prefix") => void;
|
||||
};
|
||||
|
||||
export function ConsentModal({ req, onAnswer }: Props) {
|
||||
return (
|
||||
<div className="consent">
|
||||
<div className="consent-card">
|
||||
<h3>Allow {req.tool}?</h3>
|
||||
<pre>{req.detail}</pre>
|
||||
<div className="consent-actions">
|
||||
<button type="button" onClick={() => onAnswer("deny")}>
|
||||
Deny
|
||||
</button>
|
||||
<button type="button" onClick={() => onAnswer("always_prefix")}>
|
||||
Always prefix
|
||||
</button>
|
||||
<button type="button" onClick={() => onAnswer("always_tool")}>
|
||||
Always tool
|
||||
</button>
|
||||
<button type="button" className="primary" onClick={() => onAnswer("once")}>
|
||||
Allow once
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import type { SessionInfo } from "@/lib/bridge";
|
||||
|
||||
type Props = {
|
||||
sessions: SessionInfo[];
|
||||
activeId: string | null;
|
||||
workspace: string;
|
||||
query: string;
|
||||
onQuery: (q: string) => void;
|
||||
onNew: () => void;
|
||||
onOpen: (id: string) => void;
|
||||
onSettings: () => void;
|
||||
};
|
||||
|
||||
export function Rail({
|
||||
sessions,
|
||||
activeId,
|
||||
workspace,
|
||||
query,
|
||||
onQuery,
|
||||
onNew,
|
||||
onOpen,
|
||||
onSettings,
|
||||
}: Props) {
|
||||
const filtered = sessions.filter(
|
||||
(s) => !query || s.title.toLowerCase().includes(query.toLowerCase()),
|
||||
);
|
||||
return (
|
||||
<aside className="rail">
|
||||
<div className="brand">
|
||||
<div className="brand-mark">H</div>
|
||||
<div>
|
||||
<div className="brand-name">Hermit</div>
|
||||
<div className="brand-sub">local coding agent</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button type="button" className="btn-new" onClick={onNew}>
|
||||
+ New session
|
||||
</button>
|
||||
|
||||
<div className="rail-sec">
|
||||
<span>Sessions</span>
|
||||
</div>
|
||||
<input
|
||||
className="sess-search"
|
||||
type="search"
|
||||
placeholder="Search sessions"
|
||||
value={query}
|
||||
onChange={(e) => onQuery(e.target.value)}
|
||||
/>
|
||||
<div className="sessions">
|
||||
{filtered.map((s) => (
|
||||
<button
|
||||
key={s.id}
|
||||
type="button"
|
||||
className={`sess${s.id === activeId ? " active" : ""}`}
|
||||
onClick={() => onOpen(s.id)}
|
||||
>
|
||||
<div className="sess-title">{s.title}</div>
|
||||
<div className="sess-meta">
|
||||
{new Date(s.updated_at).toLocaleString()}
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="rail-foot">
|
||||
<div className="workspace" title={workspace}>
|
||||
{workspace}
|
||||
</div>
|
||||
<div className="rail-actions">
|
||||
<button type="button" className="icon-btn" onClick={onSettings}>
|
||||
Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { ChatItem } from "@/lib/bridge";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import remarkGfm from "remark-gfm";
|
||||
|
||||
type Props = { items: ChatItem[] };
|
||||
|
||||
export function Transcript({ items }: Props) {
|
||||
if (items.length === 0) {
|
||||
return (
|
||||
<div className="transcript">
|
||||
<div className="empty">
|
||||
<h2>Meet Hermit</h2>
|
||||
<p>
|
||||
Local coding agent — read and edit your workspace, run commands with
|
||||
approval, keep sessions on disk. Describe a task to begin.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="transcript">
|
||||
{items.map((it) => {
|
||||
if (it.kind === "user") {
|
||||
return (
|
||||
<div className="msg user" key={it.id}>
|
||||
<div className="msg-role">You</div>
|
||||
<div className="bubble">{it.text}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (it.kind === "assistant") {
|
||||
return (
|
||||
<div className="msg assistant" key={it.id}>
|
||||
<div className="msg-role">Hermit</div>
|
||||
<div className="bubble">
|
||||
<ReactMarkdown remarkPlugins={[remarkGfm]}>{it.text}</ReactMarkdown>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (it.kind === "thought") {
|
||||
return (
|
||||
<details className="thought" key={it.id}>
|
||||
<summary>Thought for a while</summary>
|
||||
<div className="thought-body">{it.text}</div>
|
||||
</details>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="tool-card" key={it.id}>
|
||||
<div className="tool-head">
|
||||
<span className="dot" />
|
||||
<span className="name">{it.name}</span>
|
||||
<span className="status">{it.status}</span>
|
||||
</div>
|
||||
{it.detail ? <div className="tool-body">{it.detail}</div> : null}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
/** IPC / mock bridge for Hermit desktop. */
|
||||
|
||||
export type SessionInfo = {
|
||||
id: string;
|
||||
title: string;
|
||||
updated_at: number;
|
||||
};
|
||||
|
||||
export type ConsentRequest = {
|
||||
id: string;
|
||||
tool: string;
|
||||
detail: string;
|
||||
};
|
||||
|
||||
export type ChatItem =
|
||||
| { kind: "user"; id: string; text: string }
|
||||
| { kind: "assistant"; id: string; text: string }
|
||||
| { kind: "thought"; id: string; text: string }
|
||||
| {
|
||||
kind: "tool";
|
||||
id: string;
|
||||
name: string;
|
||||
status: "running" | "ok" | "denied" | "error";
|
||||
detail: string;
|
||||
};
|
||||
|
||||
export type EngineEvent =
|
||||
| { type: "assistant_delta"; text: string }
|
||||
| { type: "thought_delta"; text: string }
|
||||
| { type: "tool_start"; id: string; name: string; detail: string }
|
||||
| { type: "tool_end"; id: string; status: "ok" | "denied" | "error"; detail: string }
|
||||
| { type: "consent"; req: ConsentRequest }
|
||||
| { type: "turn_end"; ok: boolean }
|
||||
| { type: "error"; message: string };
|
||||
|
||||
export type Settings = {
|
||||
provider: string;
|
||||
model: string;
|
||||
approval: "ask" | "auto" | "yolo";
|
||||
workspace: string;
|
||||
};
|
||||
|
||||
type Unlisten = () => void;
|
||||
|
||||
async function tryInvoke<T>(cmd: string, args?: Record<string, unknown>): Promise<T | null> {
|
||||
try {
|
||||
const { invoke } = await import("@tauri-apps/api/core");
|
||||
return await invoke<T>(cmd, args);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const mockSessions: SessionInfo[] = [
|
||||
{
|
||||
id: "s1",
|
||||
title: "Fix home TUI layout",
|
||||
updated_at: Date.now() - 3600_000,
|
||||
},
|
||||
{
|
||||
id: "s2",
|
||||
title: "Ship hermit-update",
|
||||
updated_at: Date.now() - 86400_000,
|
||||
},
|
||||
];
|
||||
|
||||
let mockBusy = false;
|
||||
const mockListeners = new Set<(e: EngineEvent) => void>();
|
||||
|
||||
function emitMock(e: EngineEvent) {
|
||||
mockListeners.forEach((fn) => fn(e));
|
||||
}
|
||||
|
||||
export async function listSessions(): Promise<SessionInfo[]> {
|
||||
const real = await tryInvoke<SessionInfo[]>("session_list");
|
||||
return real ?? mockSessions;
|
||||
}
|
||||
|
||||
export async function newSession(): Promise<SessionInfo> {
|
||||
const real = await tryInvoke<SessionInfo>("session_new");
|
||||
if (real) return real;
|
||||
const s: SessionInfo = {
|
||||
id: `s${Date.now()}`,
|
||||
title: "New session",
|
||||
updated_at: Date.now(),
|
||||
};
|
||||
mockSessions.unshift(s);
|
||||
return s;
|
||||
}
|
||||
|
||||
export async function openSession(id: string): Promise<ChatItem[]> {
|
||||
const real = await tryInvoke<ChatItem[]>("session_open", { id });
|
||||
if (real) return real;
|
||||
if (id === "s1") {
|
||||
return [
|
||||
{ kind: "user", id: "u1", text: "右上角布局不对,帮我看看。" },
|
||||
{
|
||||
kind: "thought",
|
||||
id: "t1",
|
||||
text: "Sidebar starts at row 0 while topbar only covers the main pane…",
|
||||
},
|
||||
{
|
||||
kind: "tool",
|
||||
id: "tool1",
|
||||
name: "read_file",
|
||||
status: "ok",
|
||||
detail: "src/tui.c · draw_side / layout",
|
||||
},
|
||||
{
|
||||
kind: "assistant",
|
||||
id: "a1",
|
||||
text: "顶栏只盖主区、侧栏从第 0 行铺满,右上角会空出一块。已把 topbar 拉满并把 seat deck 对齐到顶栏下方。",
|
||||
},
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function getSettings(): Promise<Settings> {
|
||||
const real = await tryInvoke<Settings>("settings_get");
|
||||
return (
|
||||
real ?? {
|
||||
provider: "deepseek",
|
||||
model: "deepseek-flash",
|
||||
approval: "auto",
|
||||
workspace: "~/hermit",
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function sendTurn(text: string): Promise<void> {
|
||||
const real = await tryInvoke<void>("send_turn", { text });
|
||||
if (real !== null) return;
|
||||
if (mockBusy) return;
|
||||
mockBusy = true;
|
||||
emitMock({ type: "thought_delta", text: "Planning a short reply…" });
|
||||
await delay(400);
|
||||
emitMock({
|
||||
type: "tool_start",
|
||||
id: "m1",
|
||||
name: "shell",
|
||||
detail: "hermit --version",
|
||||
});
|
||||
await delay(500);
|
||||
emitMock({
|
||||
type: "tool_end",
|
||||
id: "m1",
|
||||
status: "ok",
|
||||
detail: "hermit 1.1.0",
|
||||
});
|
||||
const reply =
|
||||
"这是桌面壳的本地演示流(未链上引擎时)。接上 libcstcore 后,这里会变成真实 turn。";
|
||||
for (const ch of reply) {
|
||||
emitMock({ type: "assistant_delta", text: ch });
|
||||
await delay(12);
|
||||
}
|
||||
emitMock({ type: "turn_end", ok: true });
|
||||
mockBusy = false;
|
||||
}
|
||||
|
||||
export async function cancelTurn(): Promise<void> {
|
||||
await tryInvoke("cancel_turn");
|
||||
mockBusy = false;
|
||||
emitMock({ type: "turn_end", ok: false });
|
||||
}
|
||||
|
||||
export async function answerConsent(
|
||||
id: string,
|
||||
answer: "deny" | "once" | "always_tool" | "always_prefix",
|
||||
): Promise<void> {
|
||||
await tryInvoke("consent_answer", { id, answer });
|
||||
}
|
||||
|
||||
export async function onEngineEvent(fn: (e: EngineEvent) => void): Promise<Unlisten> {
|
||||
try {
|
||||
const { listen } = await import("@tauri-apps/api/event");
|
||||
const un = await listen<EngineEvent>("hermit://event", (ev) => fn(ev.payload));
|
||||
return () => {
|
||||
un();
|
||||
};
|
||||
} catch {
|
||||
mockListeners.add(fn);
|
||||
return () => {
|
||||
mockListeners.delete(fn);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function delay(ms: number) {
|
||||
return new Promise((r) => setTimeout(r, ms));
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import App from "./App";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,425 @@
|
||||
@import "./theme.css";
|
||||
|
||||
.app {
|
||||
display: grid;
|
||||
grid-template-columns: var(--rail-w) 1fr;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* —— left rail (DSH-like) —— */
|
||||
.rail {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 14px 12px;
|
||||
background: var(--bg-elev);
|
||||
border-right: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
.brand-mark {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 8px;
|
||||
background: linear-gradient(145deg, var(--brand), var(--brand-dim));
|
||||
box-shadow: inset 0 0 0 1px rgba(169, 217, 215, 0.25);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--accent-text);
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
}
|
||||
.brand-name {
|
||||
font-weight: 650;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
.brand-sub {
|
||||
color: var(--text-faint);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.btn-new {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
height: 40px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--brand);
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
.btn-new:hover {
|
||||
background: #2f94ab;
|
||||
}
|
||||
|
||||
.rail-sec {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 4px 6px 0;
|
||||
color: var(--text-faint);
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.sess-search {
|
||||
width: 100%;
|
||||
height: 34px;
|
||||
padding: 0 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg);
|
||||
outline: none;
|
||||
}
|
||||
.sess-search:focus {
|
||||
border-color: var(--brand);
|
||||
}
|
||||
|
||||
.sessions {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
.sess {
|
||||
text-align: left;
|
||||
padding: 10px 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
color: var(--text-muted);
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.sess:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text);
|
||||
}
|
||||
.sess.active {
|
||||
background: var(--bg-card);
|
||||
border-color: var(--border-strong);
|
||||
color: var(--text);
|
||||
}
|
||||
.sess-title {
|
||||
font-weight: 550;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.sess-meta {
|
||||
font-size: 11px;
|
||||
color: var(--text-faint);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.rail-foot {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.workspace {
|
||||
padding: 8px 10px;
|
||||
border-radius: var(--radius-sm);
|
||||
background: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
font-family: var(--mono);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.rail-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
}
|
||||
.icon-btn {
|
||||
flex: 1;
|
||||
height: 34px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.icon-btn:hover {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* —— main column —— */
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
background: var(--bg);
|
||||
}
|
||||
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 12px var(--pad);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.topbar h1 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.chips {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.chip {
|
||||
height: 28px;
|
||||
padding: 0 10px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-elev);
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
.chip strong {
|
||||
color: var(--text);
|
||||
font-weight: 550;
|
||||
}
|
||||
.chip.ok {
|
||||
border-color: rgba(61, 154, 106, 0.35);
|
||||
color: var(--ok);
|
||||
}
|
||||
|
||||
.transcript {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 20px var(--pad) 8px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
margin: auto;
|
||||
max-width: 420px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.empty h2 {
|
||||
margin: 0 0 8px;
|
||||
color: var(--text);
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
}
|
||||
.empty p {
|
||||
margin: 0;
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.msg {
|
||||
max-width: 820px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.msg.user {
|
||||
align-items: flex-end;
|
||||
}
|
||||
.msg-role {
|
||||
font-size: 11px;
|
||||
color: var(--text-faint);
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.bubble {
|
||||
padding: 12px 14px;
|
||||
border-radius: var(--radius);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: var(--shadow);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
.msg.user .bubble {
|
||||
background: linear-gradient(160deg, var(--brand-dim), #1a4a56);
|
||||
border-color: transparent;
|
||||
}
|
||||
.msg.assistant .bubble {
|
||||
background: var(--bg-elev);
|
||||
}
|
||||
|
||||
.thought,
|
||||
.tool-card {
|
||||
max-width: 820px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
background: var(--bg-elev);
|
||||
overflow: hidden;
|
||||
}
|
||||
.thought summary,
|
||||
.tool-head {
|
||||
list-style: none;
|
||||
cursor: pointer;
|
||||
padding: 10px 12px;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.thought summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
.thought[open] summary {
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.thought-body,
|
||||
.tool-body {
|
||||
padding: 10px 12px;
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.tool-head .dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: var(--crab);
|
||||
}
|
||||
.tool-head .name {
|
||||
color: var(--text);
|
||||
font-weight: 550;
|
||||
}
|
||||
.tool-head .status {
|
||||
margin-left: auto;
|
||||
color: var(--text-faint);
|
||||
}
|
||||
|
||||
.composer-wrap {
|
||||
padding: 10px var(--pad) 16px;
|
||||
}
|
||||
.composer {
|
||||
max-width: 820px;
|
||||
margin: 0 auto;
|
||||
border: 1px solid var(--border-strong);
|
||||
background: var(--bg-card);
|
||||
border-radius: 16px;
|
||||
padding: 10px 12px 8px;
|
||||
box-shadow: var(--shadow);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.composer textarea {
|
||||
width: 100%;
|
||||
min-height: 56px;
|
||||
max-height: 200px;
|
||||
resize: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
padding: 4px 2px;
|
||||
}
|
||||
.composer-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
}
|
||||
.composer-left {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.send {
|
||||
height: 34px;
|
||||
min-width: 72px;
|
||||
padding: 0 14px;
|
||||
border-radius: 999px;
|
||||
background: var(--crab);
|
||||
color: #1a100c;
|
||||
font-weight: 650;
|
||||
}
|
||||
.send:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: default;
|
||||
}
|
||||
.send.stop {
|
||||
background: var(--danger);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.consent {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.55);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
z-index: 40;
|
||||
}
|
||||
.consent-card {
|
||||
width: min(480px, calc(100% - 32px));
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-strong);
|
||||
border-radius: var(--radius);
|
||||
padding: 18px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
.consent-card h3 {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.consent-card pre {
|
||||
margin: 0 0 14px;
|
||||
padding: 10px;
|
||||
background: var(--bg);
|
||||
border-radius: var(--radius-sm);
|
||||
font-family: var(--mono);
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
max-height: 180px;
|
||||
overflow: auto;
|
||||
}
|
||||
.consent-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.consent-actions button {
|
||||
height: 34px;
|
||||
padding: 0 12px;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.consent-actions .primary {
|
||||
background: var(--brand);
|
||||
border-color: transparent;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@media (max-width: 880px) {
|
||||
.app {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.rail {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
:root {
|
||||
/* Hermit brand + DSH-like rhythm */
|
||||
--brand: #277f94;
|
||||
--brand-dim: #1d5f6e;
|
||||
--crab: #f68042;
|
||||
--eye: #9957ad;
|
||||
--ok: #3d9a6a;
|
||||
--warn: #e9ba47;
|
||||
--danger: #c45c4a;
|
||||
|
||||
--bg: #0f1114;
|
||||
--bg-elev: #161a1f;
|
||||
--bg-card: #1b2128;
|
||||
--bg-hover: #222933;
|
||||
--border: rgba(255, 255, 255, 0.06);
|
||||
--border-strong: rgba(255, 255, 255, 0.12);
|
||||
--text: #e8ecf0;
|
||||
--text-muted: #8b949e;
|
||||
--text-faint: #5c6570;
|
||||
--accent-text: #a9d9d7;
|
||||
|
||||
--radius: 12px;
|
||||
--radius-sm: 8px;
|
||||
--pad: 16px;
|
||||
--rail-w: 260px;
|
||||
--font: "IBM Plex Sans", "Segoe UI", "Noto Sans SC", system-ui, sans-serif;
|
||||
--mono: "IBM Plex Mono", "JetBrains Mono", ui-monospace, monospace;
|
||||
--shadow: 0 8px 28px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: var(--font);
|
||||
font-size: 14px;
|
||||
line-height: 1.45;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
textarea {
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--accent-text);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
border-radius: 8px;
|
||||
}
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
"baseUrl": ".",
|
||||
"paths": { "@/*": ["src/*"] }
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"root":["./src/App.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/Composer.tsx","./src/components/ConsentModal.tsx","./src/components/Rail.tsx","./src/components/Transcript.tsx","./src/lib/bridge.ts"],"version":"5.9.3"}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import path from "node:path";
|
||||
|
||||
const host = process.env.TAURI_DEV_HOST;
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: { "@": path.resolve(__dirname, "src") },
|
||||
},
|
||||
clearScreen: false,
|
||||
server: {
|
||||
port: 1420,
|
||||
strictPort: true,
|
||||
host: host || false,
|
||||
hmr: host ? { protocol: "ws", host, port: 1421 } : undefined,
|
||||
watch: { ignored: ["**/src-tauri/**"] },
|
||||
},
|
||||
envPrefix: ["VITE_", "TAURI_"],
|
||||
build: {
|
||||
target: process.env.TAURI_ENV_PLATFORM === "windows" ? "chrome105" : "safari13",
|
||||
minify: !process.env.TAURI_ENV_DEBUG ? "esbuild" : false,
|
||||
sourcemap: !!process.env.TAURI_ENV_DEBUG,
|
||||
},
|
||||
});
|
||||
+7
-12
@@ -1,13 +1,8 @@
|
||||
# NixOS and other nix users: the desktop front end (Qt Quick) with the engine
|
||||
# it embeds, built and installed the nix way.
|
||||
# NixOS and other nix users: legacy Qt Quick front end (frozen; Tauri app is
|
||||
# apps/desktop). CLI remains scripts/bootstrap_linux.sh.
|
||||
#
|
||||
# nix-build # ./result/bin/cst-agent-app, run it in place
|
||||
# nix-env -f . -i cst-agent # into the user profile: PATH, menu entry, icon
|
||||
#
|
||||
# The Qt apps hook is what makes the installed binary find Qt's plugins and
|
||||
# QML modules outside a nix-shell (this is also what keeps them out of
|
||||
# nix-collect-garbage's reach). Only the window is installed: the CLI has its
|
||||
# own installer, scripts/bootstrap_linux.sh.
|
||||
# nix-build # ./result/bin/hermit-app (Qt, transitional)
|
||||
# nix-env -f . -i hermit
|
||||
{ pkgs ? import <nixpkgs> { } }:
|
||||
|
||||
let
|
||||
@@ -21,7 +16,7 @@ let
|
||||
builtins.head (builtins.match "#define ${re} ([0-9]+)" hit);
|
||||
in
|
||||
pkgs.stdenv.mkDerivation {
|
||||
pname = "cst-agent";
|
||||
pname = "hermit";
|
||||
version =
|
||||
"${vLine "CST_VERSION_MAJOR"}.${vLine "CST_VERSION_MINOR"}.${vLine "CST_VERSION_PATCH"}";
|
||||
|
||||
@@ -91,8 +86,8 @@ pkgs.stdenv.mkDerivation {
|
||||
|
||||
meta = {
|
||||
description =
|
||||
"Local coding agent: Qt Quick front end with the engine in-process";
|
||||
homepage = "https://git.whcself.xyz/WHC/CST-agent";
|
||||
"Hermit: local coding agent (legacy Qt Quick front end; prefer apps/desktop)";
|
||||
homepage = "https://git.whcself.xyz/WHC/Hermit";
|
||||
platforms = pkgs.lib.platforms.linux;
|
||||
mainProgram = "cst-agent-app";
|
||||
};
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
# Hermit Desktop (Tauri)
|
||||
|
||||
Canonical GUI: [`apps/desktop`](../apps/desktop/). Qt under [`app/`](../app/) is frozen ([DEPRECATED](../app/DEPRECATED.md)).
|
||||
|
||||
## Shape
|
||||
|
||||
- **Shell**: Tauri 2 (Linux / Windows / macOS)
|
||||
- **UI**: React 19 — session rail, chat, tool cards, composer (DeepSeek Harness–like IA)
|
||||
- **Engine**: in-process `libcstcore` via Rust FFI (feature `engine`); without it the host uses a mock / echo path so the UI is developable
|
||||
|
||||
## Dev
|
||||
|
||||
```bash
|
||||
nix-shell -p rustc cargo nodejs_22 pkg-config webkitgtk_4_1 gtk3 librsvg openssl curl
|
||||
cd apps/desktop && npm install && npm run tauri dev
|
||||
```
|
||||
|
||||
## Product env
|
||||
|
||||
`HERMIT_*` preferred, `CST_*` accepted (see `hermit_getenv`). Config dir `~/.config/hermit`.
|
||||
@@ -2,8 +2,8 @@
|
||||
#define CST_VERSION_H
|
||||
|
||||
#define CST_VERSION_MAJOR 1
|
||||
#define CST_VERSION_MINOR 0
|
||||
#define CST_VERSION_PATCH 2
|
||||
#define CST_VERSION_MINOR 1
|
||||
#define CST_VERSION_PATCH 0
|
||||
|
||||
#define CST_VERSION_STR_(x) #x
|
||||
#define CST_VERSION_STR(x) CST_VERSION_STR_(x)
|
||||
|
||||
@@ -213,6 +213,9 @@ int os_utf8_encode(unsigned long cp, char out[4]);
|
||||
* text as GBK and the screen would show mojibake. */
|
||||
int os_utf8_decode(const char *s, size_t avail, unsigned long *cp);
|
||||
|
||||
/* HERMIT_<suffix> then CST_<suffix>. Never NULL (may be ""). */
|
||||
const char *hermit_getenv(const char *suffix);
|
||||
|
||||
/* --- libc gaps on Windows ------------------------------------------------
|
||||
* MinGW-w64 provides neither strcasestr() (a GNU extension) nor
|
||||
* setenv()/unsetenv() (POSIX): they are simply absent from its headers.
|
||||
|
||||
@@ -106,7 +106,9 @@ cmd="\${1:-status}"
|
||||
case "\$cmd" in
|
||||
status)
|
||||
"\$BIN" --version
|
||||
if command -v systemctl >/dev/null 2>&1 && systemctl --user is-active cst-agent-parliament.service >/dev/null 2>&1; then
|
||||
if command -v systemctl >/dev/null 2>&1 && systemctl --user is-active hermit-parliament.service >/dev/null 2>&1; then
|
||||
systemctl --user status hermit-parliament.service --no-pager
|
||||
elif command -v systemctl >/dev/null 2>&1 && systemctl --user is-active cst-agent-parliament.service >/dev/null 2>&1; then
|
||||
systemctl --user status cst-agent-parliament.service --no-pager
|
||||
else
|
||||
"\$BIN" --service-stop >/dev/null 2>&1 || true
|
||||
@@ -116,9 +118,9 @@ status)
|
||||
install-unit)
|
||||
make -C "$dir" install-service PREFIX="$prefix"
|
||||
;;
|
||||
start) systemctl --user start cst-agent-parliament.service ;;
|
||||
stop) systemctl --user stop cst-agent-parliament.service; "\$BIN" --service-stop || true ;;
|
||||
restart) systemctl --user restart cst-agent-parliament.service ;;
|
||||
start) systemctl --user start hermit-parliament.service 2>/dev/null || systemctl --user start cst-agent-parliament.service ;;
|
||||
stop) systemctl --user stop hermit-parliament.service 2>/dev/null || true; systemctl --user stop cst-agent-parliament.service 2>/dev/null || true; "\$BIN" --service-stop || true ;;
|
||||
restart) systemctl --user restart hermit-parliament.service 2>/dev/null || systemctl --user restart cst-agent-parliament.service ;;
|
||||
*)
|
||||
echo "usage: hermit-service {status|install-unit|start|stop|restart}" >&2
|
||||
exit 2
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
#!/data/data/com.termux/files/usr/bin/bash
|
||||
# Install or upgrade CST-Agent on Android Termux. Safe to rerun: an existing
|
||||
# Install or upgrade Hermit on Android Termux. Safe to rerun: an existing
|
||||
# checkout is fast-forwarded, rebuilt and reinstalled.
|
||||
#
|
||||
# bootstrap_termux.sh [DIR] [--no-pull]
|
||||
# cst-agent-update (installed by this script; same as rerunning it)
|
||||
# hermit-update (installed by this script; same as rerunning it)
|
||||
set -euo pipefail
|
||||
|
||||
main() {
|
||||
local repo_url="${CST_AGENT_REPO:-https://git.whcself.xyz/WHC/CST-agent.git}"
|
||||
local repo_url="${HERMIT_REPO:-${CST_AGENT_REPO:-https://git.whcself.xyz/WHC/Hermit.git}}"
|
||||
local dir="" pull=1 changed=0 arg
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
@@ -21,10 +21,12 @@ main() {
|
||||
esac
|
||||
done
|
||||
if [[ -z "$dir" ]]; then
|
||||
if [[ -n "${CST_AGENT_DIR:-}" ]]; then
|
||||
dir="$CST_AGENT_DIR"
|
||||
if [[ -n "${HERMIT_DIR:-${CST_AGENT_DIR:-}}" ]]; then
|
||||
dir="${HERMIT_DIR:-$CST_AGENT_DIR}"
|
||||
elif [[ -f Makefile && -f src/main.c && -d .git ]]; then
|
||||
dir="$(pwd)"
|
||||
elif [[ -d "$HOME/Hermit/.git" ]]; then
|
||||
dir="$HOME/Hermit"
|
||||
else
|
||||
dir="$HOME/CST-agent"
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
[Unit]
|
||||
Description=Hermit parliament seat service
|
||||
Documentation=https://git.whcself.xyz/WHC/Hermit
|
||||
After=network-online.target
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
# Prefer --service-fg under systemd so the unit tracks the real process.
|
||||
ExecStart=@BIN@ --service-fg --peer-port ${HERMIT_PEER_PORT:-${CST_PEER_PORT:-9100}}
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
Environment=HERMIT_INSTANCE_ID=%H
|
||||
# Put the Realm password in a drop-in, e.g.:
|
||||
# ~/.config/systemd/user/hermit-parliament.service.d/override.conf
|
||||
# [Service]
|
||||
# Environment=HERMIT_REALM_PASSWORD=…
|
||||
# CST_* still accepted. Config under ~/.config/hermit/ (legacy cst-agent/).
|
||||
EnvironmentFile=-%h/.config/hermit/parliament.env
|
||||
EnvironmentFile=-%h/.config/cst-agent/parliament.env
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -16,8 +16,8 @@ RequestExecutionLevel user
|
||||
!define OUTFILE "cst-agent-${VERSION}-windows-x86_64-setup.exe"
|
||||
!endif
|
||||
|
||||
!define APP "CST-Agent"
|
||||
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\cst-agent"
|
||||
!define APP "Hermit"
|
||||
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\hermit"
|
||||
|
||||
!include "MUI2.nsh"
|
||||
!include "LogicLib.nsh"
|
||||
|
||||
+9
-8
@@ -20,6 +20,7 @@
|
||||
#include "cst_util.h"
|
||||
#include "mm_tool.h"
|
||||
#include "os_compat.h"
|
||||
#include "cst_hermit_glyph.h"
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
@@ -695,7 +696,7 @@ static const char NUDGE_FINAL[] =
|
||||
#define MAX_CTL_RETRIES 3
|
||||
|
||||
static int par_max_cap(void) {
|
||||
const char *e = getenv("CST_PAR_MAX");
|
||||
const char *e = hermit_getenv("PAR_MAX");
|
||||
int n = e && e[0] ? atoi(e) : PAR_MAX_DEFAULT;
|
||||
if (n < 1)
|
||||
n = 1;
|
||||
@@ -1844,7 +1845,7 @@ int cst_message_is_note(const cst_message_t *m) {
|
||||
* request filled 85% of the context window the oldest half is dropped. */
|
||||
static int window_start(cst_agent_t *a, int n) {
|
||||
int start = a->win_start > 0 && a->win_start < n ? a->win_start : 0;
|
||||
const char *e = getenv("CST_CTX_WINDOW");
|
||||
const char *e = hermit_getenv("CTX_WINDOW");
|
||||
int win = e ? atoi(e) : 0;
|
||||
int count = 0;
|
||||
for (int i = start; i < n; i++)
|
||||
@@ -2048,7 +2049,7 @@ static const char *agent_tools_json(cst_agent_t *a) {
|
||||
if (a->tools_json && sig == a->tools_sig)
|
||||
return a->tools_json;
|
||||
if (!a->tools_json) {
|
||||
const char *e = getenv("CST_MCP_WAIT_MS");
|
||||
const char *e = hermit_getenv("MCP_WAIT_MS");
|
||||
cst_mcp_wait_ready(e && atoi(e) >= 0 ? atoi(e) : 3000);
|
||||
}
|
||||
cst_agent_config_t c = a->cfg;
|
||||
@@ -2123,7 +2124,7 @@ static void fill_request(cst_agent_t *a, cst_llm_request_t *req,
|
||||
(unsigned long long)h);
|
||||
req->cache_key = a->cache_key;
|
||||
}
|
||||
req->cache_ttl = getenv("CST_CACHE_TTL");
|
||||
req->cache_ttl = hermit_getenv("CACHE_TTL");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2482,7 +2483,7 @@ void cst_agent_reload_notes(cst_agent_t *a) {
|
||||
return;
|
||||
free(a->notes);
|
||||
a->notes = NULL;
|
||||
const char *e = getenv("CST_NO_PROJECT_NOTES");
|
||||
const char *e = hermit_getenv("NO_PROJECT_NOTES");
|
||||
if (e && e[0] == '1')
|
||||
return;
|
||||
static const char *const names[] = {"AGENTS.md", "CST.md"};
|
||||
@@ -3056,7 +3057,7 @@ static int can_continue(const cst_agent_t *a, const turn_t *t,
|
||||
* the context window, capped at 256K tokens. Even a cached prompt is billed on
|
||||
* every step, so a 1M-token window is not worth filling. */
|
||||
static long ctx_budget(const cst_agent_t *a) {
|
||||
const char *e = getenv("CST_CTX_BUDGET");
|
||||
const char *e = hermit_getenv("CTX_BUDGET");
|
||||
if (e && atol(e) > 0)
|
||||
return atol(e);
|
||||
long win = cst_model_context_tokens(a->cfg.model);
|
||||
@@ -3070,7 +3071,7 @@ static long ctx_budget(const cst_agent_t *a) {
|
||||
|
||||
/* Compact before a turn when the last request crossed the budget. */
|
||||
static void maybe_auto_compact(cst_agent_t *a, const cst_agent_hooks_t *hooks) {
|
||||
const char *e = getenv("CST_AUTO_COMPACT");
|
||||
const char *e = hermit_getenv("AUTO_COMPACT");
|
||||
if (e && e[0] == '0')
|
||||
return;
|
||||
long budget = ctx_budget(a);
|
||||
@@ -3473,7 +3474,7 @@ static int run_turn_once(cst_agent_t *a, const char *user_text,
|
||||
* flaky gateway does not end the turn blank. CST_EMPTY_RETRIES overrides. */
|
||||
{
|
||||
int empty_cap = a->cfg.headless ? 4 : 2;
|
||||
const char *env_er = getenv("CST_EMPTY_RETRIES");
|
||||
const char *env_er = hermit_getenv("EMPTY_RETRIES");
|
||||
if (env_er && atoi(env_er) > 0)
|
||||
empty_cap = atoi(env_er);
|
||||
if (empty_retries < empty_cap && step + 1 < a->cfg.max_steps) {
|
||||
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
#include "cst_ext.h"
|
||||
#include "cst_hermit_glyph.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -21,7 +22,7 @@ void cst_extensions_wait(FILE *log) {
|
||||
if (total == 0)
|
||||
return;
|
||||
/* Default 8s (was 30s); override with CST_MCP_WAIT_MS. */
|
||||
const char *e = getenv("CST_MCP_WAIT_MS");
|
||||
const char *e = hermit_getenv("MCP_WAIT_MS");
|
||||
int ms = e && atoi(e) >= 0 ? atoi(e) : 8000;
|
||||
if (ms > 60000)
|
||||
ms = 60000;
|
||||
|
||||
+2
-12
@@ -6,18 +6,8 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
const char *hermit_getenv(const char *suffix) {
|
||||
char hk[96], ck[96];
|
||||
if (!suffix || !suffix[0])
|
||||
return "";
|
||||
snprintf(hk, sizeof(hk), "HERMIT_%s", suffix);
|
||||
const char *e = getenv(hk);
|
||||
if (e && e[0])
|
||||
return e;
|
||||
snprintf(ck, sizeof(ck), "CST_%s", suffix);
|
||||
e = getenv(ck);
|
||||
return e ? e : "";
|
||||
}
|
||||
/* hermit_getenv lives in os_compat.c so light test binaries that link
|
||||
* os_compat without hermit_glyph still resolve HERMIT_/CST_ dual env. */
|
||||
|
||||
static int file_ok(const char *path) {
|
||||
os_stat_t st;
|
||||
|
||||
+22
-20
@@ -7,6 +7,7 @@
|
||||
#include "cst_tools.h"
|
||||
#include "cst_util.h"
|
||||
#include "os_compat.h"
|
||||
#include "cst_hermit_glyph.h"
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include <stdatomic.h>
|
||||
@@ -68,7 +69,7 @@ void cst_curl_apply_ca(void *curl) {
|
||||
static atomic_int state; /* 0 unchecked, 1 checking, 2 done */
|
||||
int expect = 0;
|
||||
if (atomic_compare_exchange_strong(&state, &expect, 1)) {
|
||||
const char *env = getenv("CST_CA_BUNDLE");
|
||||
const char *env = hermit_getenv("CA_BUNDLE");
|
||||
if (ca_file_ok(env)) {
|
||||
snprintf(ca, sizeof(ca), "%s", env);
|
||||
} else {
|
||||
@@ -120,7 +121,7 @@ void cst_curl_apply_ca(void *curl) {
|
||||
static size_t wire_tool_cap(const cst_llm_request_t *req) {
|
||||
if (req->tool_out_cap)
|
||||
return req->tool_out_cap;
|
||||
const char *e = getenv("CST_TOOL_OUT_CAP");
|
||||
const char *e = hermit_getenv("TOOL_OUT_CAP");
|
||||
if (e && atol(e) > 256)
|
||||
return (size_t)atol(e);
|
||||
return CST_DEFAULT_TOOL_OUT_CAP;
|
||||
@@ -484,21 +485,22 @@ char *cst_llm_build_body(const cst_llm_request_t *req) {
|
||||
if (req->send_reasoning || is_kimi_k27(req->model))
|
||||
cJSON_AddStringToObject(thinking, "keep", "all");
|
||||
}
|
||||
if (kimi && (!getenv("CST_NO_CACHE_HINTS") ||
|
||||
!getenv("CST_NO_CACHE_HINTS")[0] ||
|
||||
getenv("CST_NO_CACHE_HINTS")[0] == '0')) {
|
||||
/* Kimi's implicit cache defaults to 5m. Agent sessions commonly pause
|
||||
* longer than that, so opt into the 1h tier; users can select 5m/1h. */
|
||||
const char *ttl = getenv("CST_KIMI_CACHE_TTL");
|
||||
if (!ttl || (strcmp(ttl, "5m") != 0 && strcmp(ttl, "1h") != 0))
|
||||
ttl = "1h";
|
||||
cJSON *cache = cJSON_AddObjectToObject(root, "prompt_cache_options");
|
||||
cJSON_AddStringToObject(cache, "mode", "implicit");
|
||||
cJSON_AddStringToObject(cache, "ttl", ttl);
|
||||
{
|
||||
const char *noch = hermit_getenv("NO_CACHE_HINTS");
|
||||
if (kimi && (!noch[0] || noch[0] == '0')) {
|
||||
/* Kimi's implicit cache defaults to 5m. Agent sessions commonly pause
|
||||
* longer than that, so opt into the 1h tier; users can select 5m/1h. */
|
||||
const char *ttl = hermit_getenv("KIMI_CACHE_TTL");
|
||||
if (!ttl || (strcmp(ttl, "5m") != 0 && strcmp(ttl, "1h") != 0))
|
||||
ttl = "1h";
|
||||
cJSON *cache = cJSON_AddObjectToObject(root, "prompt_cache_options");
|
||||
cJSON_AddStringToObject(cache, "mode", "implicit");
|
||||
cJSON_AddStringToObject(cache, "ttl", ttl);
|
||||
}
|
||||
}
|
||||
cJSON_AddTrueToObject(root, "stream");
|
||||
const char *nousage = getenv("CST_NO_STREAM_USAGE");
|
||||
if (!nousage || !nousage[0] || nousage[0] == '0') {
|
||||
const char *nousage = hermit_getenv("NO_STREAM_USAGE");
|
||||
if (!nousage[0] || nousage[0] == '0') {
|
||||
cJSON *so = cJSON_AddObjectToObject(root, "stream_options");
|
||||
cJSON_AddTrueToObject(so, "include_usage");
|
||||
}
|
||||
@@ -517,7 +519,7 @@ char *cst_llm_build_body(const cst_llm_request_t *req) {
|
||||
cJSON *pending = NULL; /* images from tool results, waiting for a break */
|
||||
size_t img_budget = CST_ATTACH_MAX_TOTAL;
|
||||
size_t doc_budget = CST_ATTACH_MAX_TOTAL;
|
||||
const char *noimg = getenv("CST_NO_IMAGES");
|
||||
const char *noimg = hermit_getenv("NO_IMAGES");
|
||||
const char *img_off =
|
||||
noimg && noimg[0] && noimg[0] != '0'
|
||||
? "image sending is disabled (CST_NO_IMAGES)"
|
||||
@@ -653,7 +655,7 @@ char *cst_llm_build_body(const cst_llm_request_t *req) {
|
||||
if (req->cache_style == CST_CACHE_OPENAI && req->cache_key &&
|
||||
req->cache_key[0]) {
|
||||
cJSON_AddStringToObject(root, "prompt_cache_key", req->cache_key);
|
||||
const char *ret = getenv("CST_CACHE_RETENTION");
|
||||
const char *ret = hermit_getenv("CACHE_RETENTION");
|
||||
if (ret && ret[0])
|
||||
cJSON_AddStringToObject(root, "prompt_cache_retention", ret);
|
||||
} else if (req->cache_style == CST_CACHE_ANTHROPIC) {
|
||||
@@ -1034,7 +1036,7 @@ static int replay_run(sse_t *s, const char *dir, int seq, char *err,
|
||||
return -1;
|
||||
}
|
||||
size_t chunk = 16;
|
||||
const char *e = getenv("CST_REPLAY_CHUNK");
|
||||
const char *e = hermit_getenv("REPLAY_CHUNK");
|
||||
if (e && atoi(e) > 0)
|
||||
chunk = (size_t)atoi(e);
|
||||
char buf[4096];
|
||||
@@ -1114,8 +1116,8 @@ int cst_llm_chat_stream(const cst_llm_request_t *req, char *err,
|
||||
}
|
||||
|
||||
int seq = atomic_fetch_add(&g_trace_seq, 1);
|
||||
const char *rec_dir = getenv("CST_RECORD_SSE");
|
||||
const char *rep_dir = getenv("CST_REPLAY_SSE");
|
||||
const char *rec_dir = hermit_getenv("RECORD_SSE");
|
||||
const char *rep_dir = hermit_getenv("REPLAY_SSE");
|
||||
if (rec_dir && rec_dir[0]) {
|
||||
os_mkdir_p(rec_dir, 0755);
|
||||
char p[OS_PATH_MAX];
|
||||
|
||||
+35
-35
@@ -8,6 +8,7 @@
|
||||
#include "cst_version.h"
|
||||
#include "cst_webui.h"
|
||||
#include "os_compat.h"
|
||||
#include "cst_hermit_glyph.h"
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include <getopt.h>
|
||||
@@ -67,7 +68,7 @@ static void usage(const char *argv0) {
|
||||
" --no-orchestrate override workspace orchestration setting\n"
|
||||
" --realm-new create Realm file keys (legacy/dev)\n"
|
||||
" --realm-password-init create password-derived Realm (no sk on disk)\n"
|
||||
" --login unlock (CST_REALM_PASSWORD or prompt); claim chair\n"
|
||||
" --login unlock (HERMIT_REALM_PASSWORD / CST_… or prompt); claim chair\n"
|
||||
" --join unlock; join as member seat (no chair steal)\n"
|
||||
" --service run parliament service (discover+context sync)\n"
|
||||
" --service-fg same as --service, stay in foreground\n"
|
||||
@@ -95,15 +96,14 @@ static void usage(const char *argv0) {
|
||||
"\n"
|
||||
"API keys: the TUI asks for a missing key and saves it per provider\n"
|
||||
" in the config dir (credentials, mode 0600); /key changes it.\n"
|
||||
" Fallbacks: <provider env> (DEEPSEEK_API_KEY, ...), CST_API_KEY.\n"
|
||||
" Fallbacks: <provider env> (DEEPSEEK_API_KEY, ...), HERMIT_API_KEY / CST_API_KEY.\n"
|
||||
"Settings: provider, models, effort and mode are remembered in\n"
|
||||
" settings.json next to the credentials.\n"
|
||||
"Env: CST_PROVIDER CST_MODEL CST_EFFORT CST_MODE CST_PROTOCOL\n"
|
||||
" CST_TOOLS CST_CONFIRM CST_MAX_STEPS CST_TOOL_OUT_CAP\n"
|
||||
" CST_CTX_WINDOW CST_CTX_TOKENS CST_AUTO_COMPACT=0\n"
|
||||
" CST_SHELL_TIMEOUT CST_METRICS=0 CST_RECORD_SSE CST_REPLAY_SSE\n"
|
||||
" CST_MODELS CST_APPROVAL=ask|auto|yolo CST_AUTO_APPROVE=1 (=yolo)\n"
|
||||
" CST_SANDBOX=off CST_SHELL CST_NO_PROJECT_NOTES=1 NO_COLOR\n"
|
||||
" settings.json next to the credentials (~/.config/hermit).\n"
|
||||
"Env: HERMIT_* preferred, CST_* accepted. Common:\n"
|
||||
" PROVIDER MODEL EFFORT MODE PROTOCOL TOOLS CONFIRM APPROVAL\n"
|
||||
" MAX_STEPS TOOL_OUT_CAP CTX_WINDOW AUTO_COMPACT=0 SANDBOX=off\n"
|
||||
" REALM_PASSWORD INSTANCE_ID WEB_PORT WEB_HOST WEB_TOKEN\n"
|
||||
" SHELL_TIMEOUT METRICS=0 RECORD_SSE REPLAY_SSE MODELS NO_COLOR\n"
|
||||
"TUI: type /help inside for commands and keys\n",
|
||||
argv0);
|
||||
}
|
||||
@@ -342,36 +342,36 @@ int main(int argc, char **argv) {
|
||||
cfg.api_key[0] = 0;
|
||||
cfg.model[0] = 0;
|
||||
cfg.protocol = CST_PROTO_NATIVE_EOS;
|
||||
const char *env_proto = getenv("CST_PROTOCOL");
|
||||
const char *env_proto = hermit_getenv("PROTOCOL");
|
||||
if (env_proto && env_proto[0] &&
|
||||
cst_protocol_parse(env_proto, &cfg.protocol) != 0) {
|
||||
fprintf(stderr, "unknown CST_PROTOCOL=%s\n", env_proto);
|
||||
return 1;
|
||||
}
|
||||
cfg.max_steps = CST_DEFAULT_MAX_STEPS;
|
||||
const char *env_steps = getenv("CST_MAX_STEPS");
|
||||
const char *env_steps = hermit_getenv("MAX_STEPS");
|
||||
if (env_steps && atoi(env_steps) > 0)
|
||||
cfg.max_steps = atoi(env_steps);
|
||||
cfg.tool_out_cap = CST_DEFAULT_TOOL_OUT_CAP;
|
||||
const char *env_cap = getenv("CST_TOOL_OUT_CAP");
|
||||
const char *env_cap = hermit_getenv("TOOL_OUT_CAP");
|
||||
if (env_cap && atol(env_cap) > 0)
|
||||
cfg.tool_out_cap = (size_t)atol(env_cap);
|
||||
const char *provider_name = getenv("CST_PROVIDER");
|
||||
const char *provider_name = hermit_getenv("PROVIDER");
|
||||
cfg.headless = 0;
|
||||
cfg.confirm_shell = 1;
|
||||
cfg.confirm_write = 1;
|
||||
cfg.confirm_net = 1;
|
||||
cfg.auto_approve = 0;
|
||||
cfg.tool_profile = CST_TOOLS_DEFAULT;
|
||||
const char *env_tools = getenv("CST_TOOLS");
|
||||
const char *env_tools = hermit_getenv("TOOLS");
|
||||
if (env_tools && env_tools[0] &&
|
||||
cst_tool_profile_parse(env_tools, &cfg.tool_profile) != 0) {
|
||||
fprintf(stderr, "unknown CST_TOOLS=%s (default | minimal)\n", env_tools);
|
||||
return 1;
|
||||
}
|
||||
if (getenv("CST_AUTO_APPROVE"))
|
||||
if (hermit_getenv("AUTO_APPROVE")[0])
|
||||
cfg.auto_approve = 1;
|
||||
const char *env_confirm = getenv("CST_CONFIRM");
|
||||
const char *env_confirm = hermit_getenv("CONFIRM");
|
||||
if (env_confirm && parse_confirm(env_confirm, &cfg) != 0) {
|
||||
fprintf(stderr, "bad CST_CONFIRM=%s (shell,write,net | none)\n",
|
||||
env_confirm);
|
||||
@@ -379,7 +379,7 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
|
||||
int approval = -1;
|
||||
const char *env_appr = getenv("CST_APPROVAL");
|
||||
const char *env_appr = hermit_getenv("APPROVAL");
|
||||
if (env_appr && env_appr[0] && cst_approval_parse(env_appr, &approval) != 0) {
|
||||
fprintf(stderr, "bad CST_APPROVAL=%s (ask | auto | yolo)\n", env_appr);
|
||||
return 1;
|
||||
@@ -405,14 +405,14 @@ int main(int argc, char **argv) {
|
||||
cst_webui_opts_t web;
|
||||
memset(&web, 0, sizeof(web));
|
||||
web.port = CST_WEBUI_DEFAULT_PORT; /* 0 would mean "pick a free one" */
|
||||
const char *env = getenv("CST_WEB_PORT");
|
||||
const char *env = hermit_getenv("WEB_PORT");
|
||||
if (env && env[0] && parse_port(env, &web.port) != 0) {
|
||||
fprintf(stderr, "CST_WEB_PORT=%s is not a port (0-65535)\n", env);
|
||||
return 1;
|
||||
}
|
||||
web.host = getenv("CST_WEB_HOST");
|
||||
web.token = getenv("CST_WEB_TOKEN");
|
||||
web.root = getenv("CST_WEB_ROOT");
|
||||
web.host = hermit_getenv("WEB_HOST");
|
||||
web.token = hermit_getenv("WEB_TOKEN");
|
||||
web.root = hermit_getenv("WEB_ROOT");
|
||||
|
||||
static struct option opts[] = {
|
||||
{"workspace", required_argument, 0, 'w'},
|
||||
@@ -693,13 +693,13 @@ int main(int argc, char **argv) {
|
||||
return rc;
|
||||
}
|
||||
if (do_realm_pw_init) {
|
||||
const char *pw = getenv("CST_REALM_PASSWORD");
|
||||
const char *pw = hermit_getenv("REALM_PASSWORD");
|
||||
int rc = cst_parliament_cli_realm_password_init(pw);
|
||||
cst_curl_cleanup();
|
||||
return rc;
|
||||
}
|
||||
if (do_login) {
|
||||
const char *pw = getenv("CST_REALM_PASSWORD");
|
||||
const char *pw = hermit_getenv("REALM_PASSWORD");
|
||||
char iidbuf[64];
|
||||
const char *iid = parliament_instance;
|
||||
if (!iid || !iid[0]) {
|
||||
@@ -711,7 +711,7 @@ int main(int argc, char **argv) {
|
||||
return rc;
|
||||
}
|
||||
if (do_join) {
|
||||
const char *pw = getenv("CST_REALM_PASSWORD");
|
||||
const char *pw = hermit_getenv("REALM_PASSWORD");
|
||||
char iidbuf[64];
|
||||
const char *iid = parliament_instance;
|
||||
if (!iid || !iid[0]) {
|
||||
@@ -734,7 +734,7 @@ int main(int argc, char **argv) {
|
||||
return rc;
|
||||
}
|
||||
if (do_service || do_service_fg) {
|
||||
const char *pw = getenv("CST_REALM_PASSWORD");
|
||||
const char *pw = hermit_getenv("REALM_PASSWORD");
|
||||
char iidbuf[64];
|
||||
const char *iid = parliament_instance;
|
||||
if (!iid || !iid[0]) {
|
||||
@@ -754,13 +754,13 @@ int main(int argc, char **argv) {
|
||||
if (do_peer_listen) {
|
||||
const char *iid = parliament_instance ? parliament_instance : realm_enroll_id;
|
||||
if (!iid)
|
||||
iid = getenv("CST_INSTANCE_ID");
|
||||
iid = hermit_getenv("INSTANCE_ID");
|
||||
int rc = cst_parliament_cli_peer_listen(peer_listen_port, iid);
|
||||
cst_curl_cleanup();
|
||||
return rc;
|
||||
}
|
||||
if (do_peer_dial) {
|
||||
const char *iid = parliament_instance ? parliament_instance : getenv("CST_INSTANCE_ID");
|
||||
const char *iid = parliament_instance ? parliament_instance : hermit_getenv("INSTANCE_ID");
|
||||
char host[256];
|
||||
int port = 0;
|
||||
host[0] = 0;
|
||||
@@ -780,7 +780,7 @@ int main(int argc, char **argv) {
|
||||
return rc;
|
||||
}
|
||||
if (do_discover) {
|
||||
const char *iid = parliament_instance ? parliament_instance : getenv("CST_INSTANCE_ID");
|
||||
const char *iid = parliament_instance ? parliament_instance : hermit_getenv("INSTANCE_ID");
|
||||
int rc = cst_parliament_cli_discover(iid, discover_peer_port, discover_wait_ms);
|
||||
cst_curl_cleanup();
|
||||
return rc;
|
||||
@@ -795,7 +795,7 @@ int main(int argc, char **argv) {
|
||||
cst_realm_dir(rdir, sizeof(rdir)) == 0 &&
|
||||
cst_realm_is_password_mode(rdir) && !cst_parliament_session_ok(NULL, 0)) {
|
||||
char pwbuf[256];
|
||||
const char *pw = getenv("CST_REALM_PASSWORD");
|
||||
const char *pw = hermit_getenv("REALM_PASSWORD");
|
||||
if ((!pw || !pw[0]) && isatty(0) &&
|
||||
cst_prompt_secret("Realm password to open agent (hidden): ", pwbuf,
|
||||
sizeof(pwbuf)) == 0)
|
||||
@@ -861,20 +861,20 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
snprintf(cfg.provider, sizeof(cfg.provider), "%s", prov->name);
|
||||
if (!cfg.effort[0]) {
|
||||
const char *e = getenv("CST_EFFORT");
|
||||
const char *e = hermit_getenv("EFFORT");
|
||||
if (!cst_effort_valid(e))
|
||||
e = saved.effort;
|
||||
snprintf(cfg.effort, sizeof(cfg.effort), "%s",
|
||||
cst_effort_valid(e) ? e : CST_DEFAULT_EFFORT);
|
||||
}
|
||||
if (!cfg.mode[0]) {
|
||||
const char *m = getenv("CST_MODE");
|
||||
const char *m = hermit_getenv("MODE");
|
||||
if (!cst_mode_valid(m))
|
||||
m = saved.mode;
|
||||
snprintf(cfg.mode, sizeof(cfg.mode), "%s", cst_mode_name(m));
|
||||
}
|
||||
if (!cfg.api_base[0]) {
|
||||
const char *env_base = getenv("CST_API_BASE");
|
||||
const char *env_base = hermit_getenv("API_BASE");
|
||||
if (!env_base || !env_base[0])
|
||||
env_base = getenv("OPENAI_BASE_URL");
|
||||
if (!env_base || !env_base[0])
|
||||
@@ -883,7 +883,7 @@ int main(int argc, char **argv) {
|
||||
env_base && env_base[0] ? env_base : prov->api_base);
|
||||
}
|
||||
if (!cfg.model[0]) {
|
||||
const char *env_model = getenv("CST_MODEL");
|
||||
const char *env_model = hermit_getenv("MODEL");
|
||||
if (!env_model || !env_model[0])
|
||||
env_model = getenv("OPENAI_MODEL");
|
||||
if (!env_model || !env_model[0])
|
||||
@@ -896,7 +896,7 @@ int main(int argc, char **argv) {
|
||||
if (!cfg.api_key[0] && !force_ask_key &&
|
||||
cst_creds_resolve(prov->name, cfg.api_key, sizeof(cfg.api_key), NULL) !=
|
||||
0) {
|
||||
const char *k = getenv("CST_API_KEY");
|
||||
const char *k = hermit_getenv("API_KEY");
|
||||
if (k && k[0])
|
||||
snprintf(cfg.api_key, sizeof(cfg.api_key), "%s", k);
|
||||
}
|
||||
@@ -983,7 +983,7 @@ int main(int argc, char **argv) {
|
||||
if (!cfg.api_key[0] && (cfg.headless || web_mode)) {
|
||||
fprintf(stderr,
|
||||
"no API key for %s: start cst-agent once to enter it, or run "
|
||||
"cst-agent --ask-key --provider %s\n",
|
||||
"hermit --ask-key --provider %s\n",
|
||||
cfg.provider, cfg.provider);
|
||||
cst_curl_cleanup();
|
||||
return 1;
|
||||
|
||||
+3
-2
@@ -1,4 +1,5 @@
|
||||
#include "cst_metrics.h"
|
||||
#include "cst_hermit_glyph.h"
|
||||
|
||||
#include "cJSON.h"
|
||||
#include "cst_agent.h"
|
||||
@@ -15,13 +16,13 @@ struct cst_metrics {
|
||||
};
|
||||
|
||||
cst_metrics_t *cst_metrics_open(const char *workspace, const char *session_path) {
|
||||
const char *off = getenv("CST_METRICS");
|
||||
const char *off = hermit_getenv("METRICS");
|
||||
if (off && strcmp(off, "0") == 0)
|
||||
return NULL;
|
||||
cst_metrics_t *m = calloc(1, sizeof(*m));
|
||||
if (!m)
|
||||
return NULL;
|
||||
const char *override = getenv("CST_METRICS_PATH");
|
||||
const char *override = hermit_getenv("METRICS_PATH");
|
||||
if (override && override[0]) {
|
||||
snprintf(m->path, sizeof(m->path), "%s", override);
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "cst_models.h"
|
||||
#include "cst_util.h"
|
||||
#include "cst_hermit_glyph.h"
|
||||
|
||||
#include "cJSON.h"
|
||||
#include "os_compat.h"
|
||||
@@ -552,7 +553,7 @@ const cst_provider_t *cst_provider_find(const char *name) {
|
||||
}
|
||||
|
||||
const cst_provider_t *cst_provider_default(void) {
|
||||
const cst_provider_t *p = cst_provider_find(getenv("CST_PROVIDER"));
|
||||
const cst_provider_t *p = cst_provider_find(hermit_getenv("PROVIDER"));
|
||||
return p ? p : cst_provider_at(0);
|
||||
}
|
||||
|
||||
@@ -606,7 +607,7 @@ int cst_models_list(const char *current, char out[][128], int max_out) {
|
||||
if (!out || max_out <= 0)
|
||||
return 0;
|
||||
int n = 0;
|
||||
const char *env = getenv("CST_MODELS");
|
||||
const char *env = hermit_getenv("MODELS");
|
||||
if (env && env[0]) {
|
||||
char buf[1024];
|
||||
snprintf(buf, sizeof(buf), "%s", env);
|
||||
@@ -745,7 +746,7 @@ static void apply_cache(const char *provider, const char *model,
|
||||
}
|
||||
|
||||
static int env_price(cst_price_t *out) {
|
||||
const char *e = getenv("CST_PRICE");
|
||||
const char *e = hermit_getenv("PRICE");
|
||||
if (!e || !e[0])
|
||||
return -1;
|
||||
memset(out, 0, sizeof(*out));
|
||||
@@ -765,7 +766,7 @@ void cst_model_info(const char *provider, const char *model,
|
||||
memset(out, 0, sizeof(*out));
|
||||
out->vision = -1;
|
||||
unsigned have = 0;
|
||||
const char *e = getenv("CST_CTX_TOKENS");
|
||||
const char *e = hermit_getenv("CTX_TOKENS");
|
||||
if (e && atol(e) > 0) {
|
||||
out->context = atol(e);
|
||||
have |= F_CONTEXT;
|
||||
@@ -857,7 +858,7 @@ void cst_model_caps(const char *provider, const char *api_base,
|
||||
cst_model_info_t mi;
|
||||
cst_model_info(provider, model, &mi);
|
||||
out->vision = mi.vision;
|
||||
const char *e = getenv("CST_NO_CACHE_HINTS");
|
||||
const char *e = hermit_getenv("NO_CACHE_HINTS");
|
||||
if ((e && e[0] == '1') || !model)
|
||||
return;
|
||||
out->cache_style = mi.cache_style;
|
||||
|
||||
+17
-3
@@ -11,6 +11,7 @@
|
||||
#endif
|
||||
#include "os_compat.h"
|
||||
#include "os_internal.h"
|
||||
#include "cst_hermit_glyph.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdint.h>
|
||||
@@ -40,6 +41,19 @@
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
const char *hermit_getenv(const char *suffix) {
|
||||
char hk[96], ck[96];
|
||||
if (!suffix || !suffix[0])
|
||||
return "";
|
||||
snprintf(hk, sizeof(hk), "HERMIT_%s", suffix);
|
||||
const char *e = getenv(hk);
|
||||
if (e && e[0])
|
||||
return e;
|
||||
snprintf(ck, sizeof(ck), "CST_%s", suffix);
|
||||
e = getenv(ck);
|
||||
return e ? e : "";
|
||||
}
|
||||
|
||||
void os_run_result_free(os_run_result_t *r) {
|
||||
if (!r)
|
||||
return;
|
||||
@@ -421,7 +435,7 @@ void os_set_prog_path(const char *argv0) {
|
||||
int os_exe_dir(char *out, size_t out_sz) {
|
||||
if (!out || out_sz < 2)
|
||||
return -1;
|
||||
const char *env = getenv("CST_EXE_DIR");
|
||||
const char *env = hermit_getenv("EXE_DIR");
|
||||
if (env && env[0]) {
|
||||
snprintf(out, out_sz, "%s", env);
|
||||
return 0;
|
||||
@@ -480,7 +494,7 @@ static int file_exists(const char *p) {
|
||||
int os_resolve_shell(char *out, size_t out_sz) {
|
||||
if (!out || out_sz < 2)
|
||||
return -1;
|
||||
const char *env = getenv("CST_SHELL");
|
||||
const char *env = hermit_getenv("SHELL");
|
||||
if (env && env[0]) {
|
||||
snprintf(out, out_sz, "%s", env);
|
||||
return 0;
|
||||
@@ -608,7 +622,7 @@ int os_run_timeout_effective(void) {
|
||||
int t = run_timeout();
|
||||
if (t > 0)
|
||||
return t;
|
||||
const char *e = getenv("CST_SHELL_TIMEOUT");
|
||||
const char *e = hermit_getenv("SHELL_TIMEOUT");
|
||||
if (e && atoi(e) > 0)
|
||||
return atoi(e);
|
||||
return 0; /* no limit: a build, a test suite or a long clone must finish */
|
||||
|
||||
+3
-2
@@ -8,6 +8,7 @@
|
||||
#include "cst_util.h"
|
||||
#include "monocypher.h"
|
||||
#include "os_compat.h"
|
||||
#include "cst_hermit_glyph.h"
|
||||
|
||||
#include <ctype.h>
|
||||
#include <stdio.h>
|
||||
@@ -61,7 +62,7 @@ void cst_parliament_sync_ack(void) {
|
||||
int cst_parliament_default_instance(char *out, size_t n) {
|
||||
if (!out || n < 2)
|
||||
return -1;
|
||||
const char *env = getenv("CST_INSTANCE_ID");
|
||||
const char *env = hermit_getenv("INSTANCE_ID");
|
||||
if (env && env[0]) {
|
||||
snprintf(out, n, "%s", env);
|
||||
return 0;
|
||||
@@ -647,7 +648,7 @@ int cst_parliament_is_active(void) {
|
||||
* Password Realm alone no longer forces parliament — that starved task /
|
||||
* worker_* / runtime tools. Opt in via CST_COLLABORATION / settings, or
|
||||
* when an open parliament meta already exists. */
|
||||
const char *mode = getenv("CST_COLLABORATION");
|
||||
const char *mode = hermit_getenv("COLLABORATION");
|
||||
if (mode && mode[0])
|
||||
return strcmp(mode, "parliament") == 0;
|
||||
cst_settings_t s;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "cst_peer.h"
|
||||
#include "cst_realm.h"
|
||||
#include "os_compat.h"
|
||||
#include "cst_hermit_glyph.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -32,7 +33,7 @@ static void sleep_ms(int ms) {
|
||||
|
||||
/* Prefer CST_REALM_PASSWORD; otherwise prompt on a TTY. */
|
||||
static int resolve_password(char *buf, size_t n, const char **out) {
|
||||
const char *env = getenv("CST_REALM_PASSWORD");
|
||||
const char *env = hermit_getenv("REALM_PASSWORD");
|
||||
if (env && env[0]) {
|
||||
*out = env;
|
||||
return 0;
|
||||
@@ -239,7 +240,7 @@ static int load_realm_for_cli(cst_realm_keys_t *realm) {
|
||||
if (cst_realm_dir(dir, sizeof(dir)) != 0)
|
||||
return -1;
|
||||
if (cst_realm_is_password_mode(dir)) {
|
||||
const char *pw = getenv("CST_REALM_PASSWORD");
|
||||
const char *pw = hermit_getenv("REALM_PASSWORD");
|
||||
if (!pw || !pw[0])
|
||||
return -1;
|
||||
return cst_realm_unlock_password(dir, pw, realm);
|
||||
|
||||
+2
-1
@@ -3,6 +3,7 @@
|
||||
#endif
|
||||
|
||||
#include "cst_sandbox.h"
|
||||
#include "cst_hermit_glyph.h"
|
||||
|
||||
#include <errno.h>
|
||||
#include <stdio.h>
|
||||
@@ -1244,7 +1245,7 @@ int cst_sandbox_probe(void) {
|
||||
if (g_kind >= 0)
|
||||
return g_kind;
|
||||
int kind = CST_SANDBOX_NONE;
|
||||
const char *e = getenv("CST_SANDBOX");
|
||||
const char *e = hermit_getenv("SANDBOX");
|
||||
if (e && (strcmp(e, "0") == 0 || strcmp(e, "off") == 0 ||
|
||||
strcmp(e, "none") == 0)) {
|
||||
g_kind = kind;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include "cst_hermit_glyph.h"
|
||||
|
||||
#include <regex.h>
|
||||
|
||||
@@ -31,7 +32,7 @@
|
||||
#define GREP_LINE_MAX 300
|
||||
|
||||
static int glob_max_hits(void) {
|
||||
const char *e = getenv("CST_GLOB_MAX");
|
||||
const char *e = hermit_getenv("GLOB_MAX");
|
||||
int n = e && e[0] ? atoi(e) : GLOB_MAX_DEFAULT;
|
||||
if (n < 50)
|
||||
n = 50;
|
||||
@@ -40,7 +41,7 @@ static int glob_max_hits(void) {
|
||||
return n;
|
||||
}
|
||||
static int grep_max_matches(void) {
|
||||
const char *e = getenv("CST_GREP_MAX");
|
||||
const char *e = hermit_getenv("GREP_MAX");
|
||||
int n = e && e[0] ? atoi(e) : GREP_MAX_MATCHES_DEFAULT;
|
||||
if (n < 50)
|
||||
n = 50;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <pthread.h>
|
||||
#include "cst_hermit_glyph.h"
|
||||
|
||||
#include "cJSON.h"
|
||||
#include "cst_clipboard.h"
|
||||
@@ -342,7 +343,7 @@ int cst_tool_known(const char *name) { return cst_tool_find(name) != NULL; }
|
||||
const char *const CST_PLAT_DESKTOP[] = {"desktop", NULL};
|
||||
|
||||
const char *cst_platform_name(void) {
|
||||
const char *over = getenv("CST_PLATFORM");
|
||||
const char *over = hermit_getenv("PLATFORM");
|
||||
if (over && over[0])
|
||||
return over; /* tests, cross checks, a launcher that knows better */
|
||||
#if defined(_WIN32)
|
||||
|
||||
@@ -24,9 +24,9 @@
|
||||
#include "cst_tools.h"
|
||||
#include "cst_util.h"
|
||||
#include "cst_version.h"
|
||||
#include "cst_hermit_glyph.h"
|
||||
#include "os_compat.h"
|
||||
#include "sashidown.h"
|
||||
#include "cst_hermit_glyph.h"
|
||||
|
||||
#include <locale.h>
|
||||
#include <pthread.h>
|
||||
@@ -5467,7 +5467,7 @@ static void cmd_realm(tui_t *t, char *arg, int *quit) {
|
||||
if (cst_realm_dir(dir, sizeof(dir)) != 0 ||
|
||||
!cst_realm_is_password_mode(dir)) {
|
||||
push_system_line(t, "No password Realm yet. Outside TUI run:");
|
||||
push_system_line(t, " cst-agent --realm-password-init");
|
||||
push_system_line(t, " hermit --realm-password-init");
|
||||
return;
|
||||
}
|
||||
char pw[256];
|
||||
@@ -5621,7 +5621,7 @@ static void cmd_status(tui_t *t, char *arg, int *quit) {
|
||||
}
|
||||
free(pst);
|
||||
if (cst_parliament_is_active() && !cst_parliament_session_ok(NULL, 0))
|
||||
push_system_line(t, " tip cst-agent --login (CST_REALM_PASSWORD)");
|
||||
push_system_line(t, " tip hermit --login (HERMIT_REALM_PASSWORD)");
|
||||
}
|
||||
cst_usage_t u;
|
||||
cst_agent_usage(t->agent, &u, NULL);
|
||||
@@ -6363,7 +6363,7 @@ static void cmd_web(tui_t *t, char *arg, int *quit) {
|
||||
(void)arg;
|
||||
(void)quit;
|
||||
#ifdef _WIN32
|
||||
push_system_line(t, "open another terminal and run: cst-agent --web");
|
||||
push_system_line(t, "open another terminal and run: hermit --web");
|
||||
#else
|
||||
static pid_t spawned = 0;
|
||||
if (spawned > 0) { /* collect the one we started last time */
|
||||
@@ -6372,7 +6372,7 @@ static void cmd_web(tui_t *t, char *arg, int *quit) {
|
||||
}
|
||||
char dir[4096], exe[4096 + 16];
|
||||
if (os_exe_dir(dir, sizeof(dir)) != 0) {
|
||||
push_system_line(t, "cannot locate this binary: run cst-agent --web yourself");
|
||||
push_system_line(t, "cannot locate this binary: run hermit --web yourself");
|
||||
return;
|
||||
}
|
||||
snprintf(exe, sizeof(exe), "%s/cst-agent", dir);
|
||||
@@ -7897,7 +7897,7 @@ int cst_tui_run(cst_agent_t *agent) {
|
||||
t.show_todo = 1;
|
||||
t.show_side = 1; /* Seat Deck; narrow terminals auto-hide in layout() */
|
||||
{
|
||||
const char *mo = getenv("CST_ASCII_MOTION");
|
||||
const char *mo = hermit_getenv("ASCII_MOTION");
|
||||
t.motion_on = !(mo && mo[0] == '0');
|
||||
}
|
||||
{
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "cst_agent.h"
|
||||
#include "cst_attach.h"
|
||||
#include "cst_test_util.h"
|
||||
#include "cst_hermit_glyph.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -299,8 +300,8 @@ static void test_cache_hints(void) {
|
||||
}
|
||||
|
||||
static void test_kimi_wire_format(void) {
|
||||
const char *old_no_cache = getenv("CST_NO_CACHE_HINTS");
|
||||
const char *old_ttl = getenv("CST_KIMI_CACHE_TTL");
|
||||
const char *old_no_cache = hermit_getenv("NO_CACHE_HINTS");
|
||||
const char *old_ttl = hermit_getenv("KIMI_CACHE_TTL");
|
||||
char *saved_no_cache = old_no_cache ? strdup(old_no_cache) : NULL;
|
||||
char *saved_ttl = old_ttl ? strdup(old_ttl) : NULL;
|
||||
setenv("CST_NO_CACHE_HINTS", "0", 1);
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "cst_perms.h"
|
||||
#include "cst_sandbox.h"
|
||||
#include "cst_hermit_glyph.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -353,7 +354,7 @@ int main(void) {
|
||||
if (kind == CST_SANDBOX_LANDLOCK || kind == CST_SANDBOX_SECCOMP)
|
||||
test_confined(kind);
|
||||
if (kind != CST_SANDBOX_SECCOMP) {
|
||||
const char *was = getenv("CST_SANDBOX");
|
||||
const char *was = hermit_getenv("SANDBOX");
|
||||
char saved[32] = "";
|
||||
if (was)
|
||||
snprintf(saved, sizeof(saved), "%s", was);
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
/* CST-Agent WebUI client: HTTP + SSE against the engine in the same binary.
|
||||
/* Hermit WebUI client: HTTP + SSE against the engine in the same binary.
|
||||
* No build step, no dependencies. The server renders Markdown to sanitized
|
||||
* HTML; this file lays out the conversation, the tool lines, the approval
|
||||
* gate and the anchored popovers. Protocol: docs/webui.md. */
|
||||
@@ -562,7 +562,7 @@ async function loadSession() {
|
||||
}
|
||||
showEmpty(log.children.length === 0);
|
||||
const last = [...(data.messages || [])].reverse().find((m) => m.role === 'user' && !m.note && !m.tool_result);
|
||||
document.title = (last && last.content ? last.content.slice(0, 40) + ' · ' : '') + 'CST-Agent';
|
||||
document.title = (last && last.content ? last.content.slice(0, 40) + ' · ' : '') + 'Hermit';
|
||||
scrollDown(true);
|
||||
updateJump();
|
||||
}
|
||||
|
||||
+3
-3
@@ -4,7 +4,7 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
<meta name="color-scheme" content="dark light">
|
||||
<title>CST-Agent</title>
|
||||
<title>Hermit</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
@@ -56,7 +56,7 @@
|
||||
<div class="side-inner">
|
||||
<div class="side-head">
|
||||
<div class="brand">
|
||||
<span class="brand-name">CST-Agent</span>
|
||||
<span class="brand-name">Hermit</span>
|
||||
</div>
|
||||
<button id="btn-collapse" class="icon" title="收起侧栏 (Ctrl+B)">
|
||||
<svg class="i"><use href="#i-panel-left"/></svg>
|
||||
@@ -158,7 +158,7 @@
|
||||
|
||||
<div id="slash" class="slash" role="listbox" hidden></div>
|
||||
<div class="box">
|
||||
<textarea id="input" rows="1" placeholder="给 CST-Agent 派个任务…(/ 打开命令,F1 看快捷键)"></textarea>
|
||||
<textarea id="input" rows="1" placeholder="给 Hermit 派个任务…(/ 打开命令,F1 看快捷键)"></textarea>
|
||||
<div class="box-row">
|
||||
<button id="btn-plus" class="icon att" title="添加图片或计划模式">
|
||||
<svg class="i"><use href="#i-plus"/></svg>
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
/* CST-Agent WebUI - one stylesheet, no build step.
|
||||
/* Hermit WebUI - one stylesheet, no build step.
|
||||
*
|
||||
* Design rules
|
||||
* - one fact, one home: sessions in the rail, model/approval/plan in the
|
||||
|
||||
Reference in New Issue
Block a user