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
|
hermit
|
||||||
.peer_probe.out
|
.peer_probe.out
|
||||||
reports/
|
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
|
# 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.
|
# 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
|
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 \
|
.PHONY: all clean check demo sashidown-lib cmake-gcc cmake-clang asan install \
|
||||||
uninstall install-service help hermit-font cst-agent
|
uninstall install-service help hermit-font cst-agent
|
||||||
@@ -347,11 +348,11 @@ install: hermit
|
|||||||
'BIN="$(PREFIX)/bin/hermit"' \
|
'BIN="$(PREFIX)/bin/hermit"' \
|
||||||
'cmd="$${1:-status}"' \
|
'cmd="$${1:-status}"' \
|
||||||
'case "$$cmd" in' \
|
'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)" ;;' \
|
'install-unit) make -C "$(HERMIT_SRC)" install-service PREFIX="$(PREFIX)" ;;' \
|
||||||
'start) systemctl --user start 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 cst-agent-parliament.service; "$$BIN" --service-stop || true ;;' \
|
'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 cst-agent-parliament.service ;;' \
|
'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 ;;' \
|
'*) echo "usage: hermit-service {status|install-unit|start|stop|restart}" >&2; exit 2 ;;' \
|
||||||
'esac' \
|
'esac' \
|
||||||
> "$(DESTDIR)$(PREFIX)/bin/hermit-service"
|
> "$(DESTDIR)$(PREFIX)/bin/hermit-service"
|
||||||
@@ -383,11 +384,15 @@ install: hermit
|
|||||||
install-service: install
|
install-service: install
|
||||||
install -d "$(DESTDIR)$(HOME)/.config/systemd/user"
|
install -d "$(DESTDIR)$(HOME)/.config/systemd/user"
|
||||||
sed -e 's|@BIN@|$(PREFIX)/bin/hermit|g' \
|
sed -e 's|@BIN@|$(PREFIX)/bin/hermit|g' \
|
||||||
scripts/cst-agent-parliament.service.in \
|
scripts/hermit-parliament.service.in \
|
||||||
> "$(DESTDIR)$(HOME)/.config/systemd/user/cst-agent-parliament.service"
|
> "$(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 "Installed user unit. Enable with:"
|
||||||
@echo " systemctl --user daemon-reload"
|
@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)"
|
@echo "(set HERMIT_/CST_ REALM_PASSWORD / INSTANCE_ID in the drop-in or environment)"
|
||||||
|
|
||||||
uninstall:
|
uninstall:
|
||||||
@@ -398,6 +403,7 @@ uninstall:
|
|||||||
rm -f "$(DESTDIR)$(PREFIX)/share/fonts/hermit/hermit-icons.ttf"
|
rm -f "$(DESTDIR)$(PREFIX)/share/fonts/hermit/hermit-icons.ttf"
|
||||||
rm -rf "$(DESTDIR)$(PREFIX)/share/hermit/frames"
|
rm -rf "$(DESTDIR)$(PREFIX)/share/hermit/frames"
|
||||||
rm -f "$(HOME)/.local/share/fonts/hermit-icons.ttf"
|
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"
|
rm -f "$(HOME)/.config/systemd/user/cst-agent-parliament.service"
|
||||||
|
|
||||||
-include $(DEPFILES)
|
-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
|
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]) {
|
if (m_cfg.api_key[0] == 0 && m_cfg.provider[0]) {
|
||||||
char key[512] = "";
|
char key[512] = "";
|
||||||
if (cst_creds_resolve(m_cfg.provider, key, sizeof(key), nullptr) != 0) {
|
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])
|
if (env && env[0])
|
||||||
snprintf(key, sizeof(key), "%s", env);
|
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
|
# NixOS and other nix users: legacy Qt Quick front end (frozen; Tauri app is
|
||||||
# it embeds, built and installed the nix way.
|
# apps/desktop). CLI remains scripts/bootstrap_linux.sh.
|
||||||
#
|
#
|
||||||
# nix-build # ./result/bin/cst-agent-app, run it in place
|
# nix-build # ./result/bin/hermit-app (Qt, transitional)
|
||||||
# nix-env -f . -i cst-agent # into the user profile: PATH, menu entry, icon
|
# nix-env -f . -i hermit
|
||||||
#
|
|
||||||
# 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.
|
|
||||||
{ pkgs ? import <nixpkgs> { } }:
|
{ pkgs ? import <nixpkgs> { } }:
|
||||||
|
|
||||||
let
|
let
|
||||||
@@ -21,7 +16,7 @@ let
|
|||||||
builtins.head (builtins.match "#define ${re} ([0-9]+)" hit);
|
builtins.head (builtins.match "#define ${re} ([0-9]+)" hit);
|
||||||
in
|
in
|
||||||
pkgs.stdenv.mkDerivation {
|
pkgs.stdenv.mkDerivation {
|
||||||
pname = "cst-agent";
|
pname = "hermit";
|
||||||
version =
|
version =
|
||||||
"${vLine "CST_VERSION_MAJOR"}.${vLine "CST_VERSION_MINOR"}.${vLine "CST_VERSION_PATCH"}";
|
"${vLine "CST_VERSION_MAJOR"}.${vLine "CST_VERSION_MINOR"}.${vLine "CST_VERSION_PATCH"}";
|
||||||
|
|
||||||
@@ -91,8 +86,8 @@ pkgs.stdenv.mkDerivation {
|
|||||||
|
|
||||||
meta = {
|
meta = {
|
||||||
description =
|
description =
|
||||||
"Local coding agent: Qt Quick front end with the engine in-process";
|
"Hermit: local coding agent (legacy Qt Quick front end; prefer apps/desktop)";
|
||||||
homepage = "https://git.whcself.xyz/WHC/CST-agent";
|
homepage = "https://git.whcself.xyz/WHC/Hermit";
|
||||||
platforms = pkgs.lib.platforms.linux;
|
platforms = pkgs.lib.platforms.linux;
|
||||||
mainProgram = "cst-agent-app";
|
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_H
|
||||||
|
|
||||||
#define CST_VERSION_MAJOR 1
|
#define CST_VERSION_MAJOR 1
|
||||||
#define CST_VERSION_MINOR 0
|
#define CST_VERSION_MINOR 1
|
||||||
#define CST_VERSION_PATCH 2
|
#define CST_VERSION_PATCH 0
|
||||||
|
|
||||||
#define CST_VERSION_STR_(x) #x
|
#define CST_VERSION_STR_(x) #x
|
||||||
#define CST_VERSION_STR(x) CST_VERSION_STR_(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. */
|
* text as GBK and the screen would show mojibake. */
|
||||||
int os_utf8_decode(const char *s, size_t avail, unsigned long *cp);
|
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 ------------------------------------------------
|
/* --- libc gaps on Windows ------------------------------------------------
|
||||||
* MinGW-w64 provides neither strcasestr() (a GNU extension) nor
|
* MinGW-w64 provides neither strcasestr() (a GNU extension) nor
|
||||||
* setenv()/unsetenv() (POSIX): they are simply absent from its headers.
|
* setenv()/unsetenv() (POSIX): they are simply absent from its headers.
|
||||||
|
|||||||
@@ -106,7 +106,9 @@ cmd="\${1:-status}"
|
|||||||
case "\$cmd" in
|
case "\$cmd" in
|
||||||
status)
|
status)
|
||||||
"\$BIN" --version
|
"\$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
|
systemctl --user status cst-agent-parliament.service --no-pager
|
||||||
else
|
else
|
||||||
"\$BIN" --service-stop >/dev/null 2>&1 || true
|
"\$BIN" --service-stop >/dev/null 2>&1 || true
|
||||||
@@ -116,9 +118,9 @@ status)
|
|||||||
install-unit)
|
install-unit)
|
||||||
make -C "$dir" install-service PREFIX="$prefix"
|
make -C "$dir" install-service PREFIX="$prefix"
|
||||||
;;
|
;;
|
||||||
start) systemctl --user start 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 cst-agent-parliament.service; "\$BIN" --service-stop || true ;;
|
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 cst-agent-parliament.service ;;
|
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
|
echo "usage: hermit-service {status|install-unit|start|stop|restart}" >&2
|
||||||
exit 2
|
exit 2
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
#!/data/data/com.termux/files/usr/bin/bash
|
#!/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.
|
# checkout is fast-forwarded, rebuilt and reinstalled.
|
||||||
#
|
#
|
||||||
# bootstrap_termux.sh [DIR] [--no-pull]
|
# 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
|
set -euo pipefail
|
||||||
|
|
||||||
main() {
|
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
|
local dir="" pull=1 changed=0 arg
|
||||||
for arg in "$@"; do
|
for arg in "$@"; do
|
||||||
case "$arg" in
|
case "$arg" in
|
||||||
@@ -21,10 +21,12 @@ main() {
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
if [[ -z "$dir" ]]; then
|
if [[ -z "$dir" ]]; then
|
||||||
if [[ -n "${CST_AGENT_DIR:-}" ]]; then
|
if [[ -n "${HERMIT_DIR:-${CST_AGENT_DIR:-}}" ]]; then
|
||||||
dir="$CST_AGENT_DIR"
|
dir="${HERMIT_DIR:-$CST_AGENT_DIR}"
|
||||||
elif [[ -f Makefile && -f src/main.c && -d .git ]]; then
|
elif [[ -f Makefile && -f src/main.c && -d .git ]]; then
|
||||||
dir="$(pwd)"
|
dir="$(pwd)"
|
||||||
|
elif [[ -d "$HOME/Hermit/.git" ]]; then
|
||||||
|
dir="$HOME/Hermit"
|
||||||
else
|
else
|
||||||
dir="$HOME/CST-agent"
|
dir="$HOME/CST-agent"
|
||||||
fi
|
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"
|
!define OUTFILE "cst-agent-${VERSION}-windows-x86_64-setup.exe"
|
||||||
!endif
|
!endif
|
||||||
|
|
||||||
!define APP "CST-Agent"
|
!define APP "Hermit"
|
||||||
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\cst-agent"
|
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\hermit"
|
||||||
|
|
||||||
!include "MUI2.nsh"
|
!include "MUI2.nsh"
|
||||||
!include "LogicLib.nsh"
|
!include "LogicLib.nsh"
|
||||||
|
|||||||
+9
-8
@@ -20,6 +20,7 @@
|
|||||||
#include "cst_util.h"
|
#include "cst_util.h"
|
||||||
#include "mm_tool.h"
|
#include "mm_tool.h"
|
||||||
#include "os_compat.h"
|
#include "os_compat.h"
|
||||||
|
#include "cst_hermit_glyph.h"
|
||||||
|
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
@@ -695,7 +696,7 @@ static const char NUDGE_FINAL[] =
|
|||||||
#define MAX_CTL_RETRIES 3
|
#define MAX_CTL_RETRIES 3
|
||||||
|
|
||||||
static int par_max_cap(void) {
|
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;
|
int n = e && e[0] ? atoi(e) : PAR_MAX_DEFAULT;
|
||||||
if (n < 1)
|
if (n < 1)
|
||||||
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. */
|
* request filled 85% of the context window the oldest half is dropped. */
|
||||||
static int window_start(cst_agent_t *a, int n) {
|
static int window_start(cst_agent_t *a, int n) {
|
||||||
int start = a->win_start > 0 && a->win_start < n ? a->win_start : 0;
|
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 win = e ? atoi(e) : 0;
|
||||||
int count = 0;
|
int count = 0;
|
||||||
for (int i = start; i < n; i++)
|
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)
|
if (a->tools_json && sig == a->tools_sig)
|
||||||
return a->tools_json;
|
return a->tools_json;
|
||||||
if (!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_mcp_wait_ready(e && atoi(e) >= 0 ? atoi(e) : 3000);
|
||||||
}
|
}
|
||||||
cst_agent_config_t c = a->cfg;
|
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);
|
(unsigned long long)h);
|
||||||
req->cache_key = a->cache_key;
|
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;
|
return;
|
||||||
free(a->notes);
|
free(a->notes);
|
||||||
a->notes = NULL;
|
a->notes = NULL;
|
||||||
const char *e = getenv("CST_NO_PROJECT_NOTES");
|
const char *e = hermit_getenv("NO_PROJECT_NOTES");
|
||||||
if (e && e[0] == '1')
|
if (e && e[0] == '1')
|
||||||
return;
|
return;
|
||||||
static const char *const names[] = {"AGENTS.md", "CST.md"};
|
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
|
* 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. */
|
* every step, so a 1M-token window is not worth filling. */
|
||||||
static long ctx_budget(const cst_agent_t *a) {
|
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)
|
if (e && atol(e) > 0)
|
||||||
return atol(e);
|
return atol(e);
|
||||||
long win = cst_model_context_tokens(a->cfg.model);
|
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. */
|
/* 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) {
|
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')
|
if (e && e[0] == '0')
|
||||||
return;
|
return;
|
||||||
long budget = ctx_budget(a);
|
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. */
|
* flaky gateway does not end the turn blank. CST_EMPTY_RETRIES overrides. */
|
||||||
{
|
{
|
||||||
int empty_cap = a->cfg.headless ? 4 : 2;
|
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)
|
if (env_er && atoi(env_er) > 0)
|
||||||
empty_cap = atoi(env_er);
|
empty_cap = atoi(env_er);
|
||||||
if (empty_retries < empty_cap && step + 1 < a->cfg.max_steps) {
|
if (empty_retries < empty_cap && step + 1 < a->cfg.max_steps) {
|
||||||
|
|||||||
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
#include "cst_ext.h"
|
#include "cst_ext.h"
|
||||||
|
#include "cst_hermit_glyph.h"
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -21,7 +22,7 @@ void cst_extensions_wait(FILE *log) {
|
|||||||
if (total == 0)
|
if (total == 0)
|
||||||
return;
|
return;
|
||||||
/* Default 8s (was 30s); override with CST_MCP_WAIT_MS. */
|
/* 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;
|
int ms = e && atoi(e) >= 0 ? atoi(e) : 8000;
|
||||||
if (ms > 60000)
|
if (ms > 60000)
|
||||||
ms = 60000;
|
ms = 60000;
|
||||||
|
|||||||
+2
-12
@@ -6,18 +6,8 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
const char *hermit_getenv(const char *suffix) {
|
/* hermit_getenv lives in os_compat.c so light test binaries that link
|
||||||
char hk[96], ck[96];
|
* os_compat without hermit_glyph still resolve HERMIT_/CST_ dual env. */
|
||||||
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 : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
static int file_ok(const char *path) {
|
static int file_ok(const char *path) {
|
||||||
os_stat_t st;
|
os_stat_t st;
|
||||||
|
|||||||
+22
-20
@@ -7,6 +7,7 @@
|
|||||||
#include "cst_tools.h"
|
#include "cst_tools.h"
|
||||||
#include "cst_util.h"
|
#include "cst_util.h"
|
||||||
#include "os_compat.h"
|
#include "os_compat.h"
|
||||||
|
#include "cst_hermit_glyph.h"
|
||||||
|
|
||||||
#include <curl/curl.h>
|
#include <curl/curl.h>
|
||||||
#include <stdatomic.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 */
|
static atomic_int state; /* 0 unchecked, 1 checking, 2 done */
|
||||||
int expect = 0;
|
int expect = 0;
|
||||||
if (atomic_compare_exchange_strong(&state, &expect, 1)) {
|
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)) {
|
if (ca_file_ok(env)) {
|
||||||
snprintf(ca, sizeof(ca), "%s", env);
|
snprintf(ca, sizeof(ca), "%s", env);
|
||||||
} else {
|
} else {
|
||||||
@@ -120,7 +121,7 @@ void cst_curl_apply_ca(void *curl) {
|
|||||||
static size_t wire_tool_cap(const cst_llm_request_t *req) {
|
static size_t wire_tool_cap(const cst_llm_request_t *req) {
|
||||||
if (req->tool_out_cap)
|
if (req->tool_out_cap)
|
||||||
return 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)
|
if (e && atol(e) > 256)
|
||||||
return (size_t)atol(e);
|
return (size_t)atol(e);
|
||||||
return CST_DEFAULT_TOOL_OUT_CAP;
|
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))
|
if (req->send_reasoning || is_kimi_k27(req->model))
|
||||||
cJSON_AddStringToObject(thinking, "keep", "all");
|
cJSON_AddStringToObject(thinking, "keep", "all");
|
||||||
}
|
}
|
||||||
if (kimi && (!getenv("CST_NO_CACHE_HINTS") ||
|
{
|
||||||
!getenv("CST_NO_CACHE_HINTS")[0] ||
|
const char *noch = hermit_getenv("NO_CACHE_HINTS");
|
||||||
getenv("CST_NO_CACHE_HINTS")[0] == '0')) {
|
if (kimi && (!noch[0] || noch[0] == '0')) {
|
||||||
/* Kimi's implicit cache defaults to 5m. Agent sessions commonly pause
|
/* 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. */
|
* longer than that, so opt into the 1h tier; users can select 5m/1h. */
|
||||||
const char *ttl = getenv("CST_KIMI_CACHE_TTL");
|
const char *ttl = hermit_getenv("KIMI_CACHE_TTL");
|
||||||
if (!ttl || (strcmp(ttl, "5m") != 0 && strcmp(ttl, "1h") != 0))
|
if (!ttl || (strcmp(ttl, "5m") != 0 && strcmp(ttl, "1h") != 0))
|
||||||
ttl = "1h";
|
ttl = "1h";
|
||||||
cJSON *cache = cJSON_AddObjectToObject(root, "prompt_cache_options");
|
cJSON *cache = cJSON_AddObjectToObject(root, "prompt_cache_options");
|
||||||
cJSON_AddStringToObject(cache, "mode", "implicit");
|
cJSON_AddStringToObject(cache, "mode", "implicit");
|
||||||
cJSON_AddStringToObject(cache, "ttl", ttl);
|
cJSON_AddStringToObject(cache, "ttl", ttl);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cJSON_AddTrueToObject(root, "stream");
|
cJSON_AddTrueToObject(root, "stream");
|
||||||
const char *nousage = getenv("CST_NO_STREAM_USAGE");
|
const char *nousage = hermit_getenv("NO_STREAM_USAGE");
|
||||||
if (!nousage || !nousage[0] || nousage[0] == '0') {
|
if (!nousage[0] || nousage[0] == '0') {
|
||||||
cJSON *so = cJSON_AddObjectToObject(root, "stream_options");
|
cJSON *so = cJSON_AddObjectToObject(root, "stream_options");
|
||||||
cJSON_AddTrueToObject(so, "include_usage");
|
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 */
|
cJSON *pending = NULL; /* images from tool results, waiting for a break */
|
||||||
size_t img_budget = CST_ATTACH_MAX_TOTAL;
|
size_t img_budget = CST_ATTACH_MAX_TOTAL;
|
||||||
size_t doc_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 =
|
const char *img_off =
|
||||||
noimg && noimg[0] && noimg[0] != '0'
|
noimg && noimg[0] && noimg[0] != '0'
|
||||||
? "image sending is disabled (CST_NO_IMAGES)"
|
? "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 &&
|
if (req->cache_style == CST_CACHE_OPENAI && req->cache_key &&
|
||||||
req->cache_key[0]) {
|
req->cache_key[0]) {
|
||||||
cJSON_AddStringToObject(root, "prompt_cache_key", req->cache_key);
|
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])
|
if (ret && ret[0])
|
||||||
cJSON_AddStringToObject(root, "prompt_cache_retention", ret);
|
cJSON_AddStringToObject(root, "prompt_cache_retention", ret);
|
||||||
} else if (req->cache_style == CST_CACHE_ANTHROPIC) {
|
} 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;
|
return -1;
|
||||||
}
|
}
|
||||||
size_t chunk = 16;
|
size_t chunk = 16;
|
||||||
const char *e = getenv("CST_REPLAY_CHUNK");
|
const char *e = hermit_getenv("REPLAY_CHUNK");
|
||||||
if (e && atoi(e) > 0)
|
if (e && atoi(e) > 0)
|
||||||
chunk = (size_t)atoi(e);
|
chunk = (size_t)atoi(e);
|
||||||
char buf[4096];
|
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);
|
int seq = atomic_fetch_add(&g_trace_seq, 1);
|
||||||
const char *rec_dir = getenv("CST_RECORD_SSE");
|
const char *rec_dir = hermit_getenv("RECORD_SSE");
|
||||||
const char *rep_dir = getenv("CST_REPLAY_SSE");
|
const char *rep_dir = hermit_getenv("REPLAY_SSE");
|
||||||
if (rec_dir && rec_dir[0]) {
|
if (rec_dir && rec_dir[0]) {
|
||||||
os_mkdir_p(rec_dir, 0755);
|
os_mkdir_p(rec_dir, 0755);
|
||||||
char p[OS_PATH_MAX];
|
char p[OS_PATH_MAX];
|
||||||
|
|||||||
+35
-35
@@ -8,6 +8,7 @@
|
|||||||
#include "cst_version.h"
|
#include "cst_version.h"
|
||||||
#include "cst_webui.h"
|
#include "cst_webui.h"
|
||||||
#include "os_compat.h"
|
#include "os_compat.h"
|
||||||
|
#include "cst_hermit_glyph.h"
|
||||||
|
|
||||||
#include <curl/curl.h>
|
#include <curl/curl.h>
|
||||||
#include <getopt.h>
|
#include <getopt.h>
|
||||||
@@ -67,7 +68,7 @@ static void usage(const char *argv0) {
|
|||||||
" --no-orchestrate override workspace orchestration setting\n"
|
" --no-orchestrate override workspace orchestration setting\n"
|
||||||
" --realm-new create Realm file keys (legacy/dev)\n"
|
" --realm-new create Realm file keys (legacy/dev)\n"
|
||||||
" --realm-password-init create password-derived Realm (no sk on disk)\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"
|
" --join unlock; join as member seat (no chair steal)\n"
|
||||||
" --service run parliament service (discover+context sync)\n"
|
" --service run parliament service (discover+context sync)\n"
|
||||||
" --service-fg same as --service, stay in foreground\n"
|
" --service-fg same as --service, stay in foreground\n"
|
||||||
@@ -95,15 +96,14 @@ static void usage(const char *argv0) {
|
|||||||
"\n"
|
"\n"
|
||||||
"API keys: the TUI asks for a missing key and saves it per provider\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"
|
" 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: provider, models, effort and mode are remembered in\n"
|
||||||
" settings.json next to the credentials.\n"
|
" settings.json next to the credentials (~/.config/hermit).\n"
|
||||||
"Env: CST_PROVIDER CST_MODEL CST_EFFORT CST_MODE CST_PROTOCOL\n"
|
"Env: HERMIT_* preferred, CST_* accepted. Common:\n"
|
||||||
" CST_TOOLS CST_CONFIRM CST_MAX_STEPS CST_TOOL_OUT_CAP\n"
|
" PROVIDER MODEL EFFORT MODE PROTOCOL TOOLS CONFIRM APPROVAL\n"
|
||||||
" CST_CTX_WINDOW CST_CTX_TOKENS CST_AUTO_COMPACT=0\n"
|
" MAX_STEPS TOOL_OUT_CAP CTX_WINDOW AUTO_COMPACT=0 SANDBOX=off\n"
|
||||||
" CST_SHELL_TIMEOUT CST_METRICS=0 CST_RECORD_SSE CST_REPLAY_SSE\n"
|
" REALM_PASSWORD INSTANCE_ID WEB_PORT WEB_HOST WEB_TOKEN\n"
|
||||||
" CST_MODELS CST_APPROVAL=ask|auto|yolo CST_AUTO_APPROVE=1 (=yolo)\n"
|
" SHELL_TIMEOUT METRICS=0 RECORD_SSE REPLAY_SSE MODELS NO_COLOR\n"
|
||||||
" CST_SANDBOX=off CST_SHELL CST_NO_PROJECT_NOTES=1 NO_COLOR\n"
|
|
||||||
"TUI: type /help inside for commands and keys\n",
|
"TUI: type /help inside for commands and keys\n",
|
||||||
argv0);
|
argv0);
|
||||||
}
|
}
|
||||||
@@ -342,36 +342,36 @@ int main(int argc, char **argv) {
|
|||||||
cfg.api_key[0] = 0;
|
cfg.api_key[0] = 0;
|
||||||
cfg.model[0] = 0;
|
cfg.model[0] = 0;
|
||||||
cfg.protocol = CST_PROTO_NATIVE_EOS;
|
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] &&
|
if (env_proto && env_proto[0] &&
|
||||||
cst_protocol_parse(env_proto, &cfg.protocol) != 0) {
|
cst_protocol_parse(env_proto, &cfg.protocol) != 0) {
|
||||||
fprintf(stderr, "unknown CST_PROTOCOL=%s\n", env_proto);
|
fprintf(stderr, "unknown CST_PROTOCOL=%s\n", env_proto);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
cfg.max_steps = CST_DEFAULT_MAX_STEPS;
|
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)
|
if (env_steps && atoi(env_steps) > 0)
|
||||||
cfg.max_steps = atoi(env_steps);
|
cfg.max_steps = atoi(env_steps);
|
||||||
cfg.tool_out_cap = CST_DEFAULT_TOOL_OUT_CAP;
|
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)
|
if (env_cap && atol(env_cap) > 0)
|
||||||
cfg.tool_out_cap = (size_t)atol(env_cap);
|
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.headless = 0;
|
||||||
cfg.confirm_shell = 1;
|
cfg.confirm_shell = 1;
|
||||||
cfg.confirm_write = 1;
|
cfg.confirm_write = 1;
|
||||||
cfg.confirm_net = 1;
|
cfg.confirm_net = 1;
|
||||||
cfg.auto_approve = 0;
|
cfg.auto_approve = 0;
|
||||||
cfg.tool_profile = CST_TOOLS_DEFAULT;
|
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] &&
|
if (env_tools && env_tools[0] &&
|
||||||
cst_tool_profile_parse(env_tools, &cfg.tool_profile) != 0) {
|
cst_tool_profile_parse(env_tools, &cfg.tool_profile) != 0) {
|
||||||
fprintf(stderr, "unknown CST_TOOLS=%s (default | minimal)\n", env_tools);
|
fprintf(stderr, "unknown CST_TOOLS=%s (default | minimal)\n", env_tools);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
if (getenv("CST_AUTO_APPROVE"))
|
if (hermit_getenv("AUTO_APPROVE")[0])
|
||||||
cfg.auto_approve = 1;
|
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) {
|
if (env_confirm && parse_confirm(env_confirm, &cfg) != 0) {
|
||||||
fprintf(stderr, "bad CST_CONFIRM=%s (shell,write,net | none)\n",
|
fprintf(stderr, "bad CST_CONFIRM=%s (shell,write,net | none)\n",
|
||||||
env_confirm);
|
env_confirm);
|
||||||
@@ -379,7 +379,7 @@ int main(int argc, char **argv) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int approval = -1;
|
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) {
|
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);
|
fprintf(stderr, "bad CST_APPROVAL=%s (ask | auto | yolo)\n", env_appr);
|
||||||
return 1;
|
return 1;
|
||||||
@@ -405,14 +405,14 @@ int main(int argc, char **argv) {
|
|||||||
cst_webui_opts_t web;
|
cst_webui_opts_t web;
|
||||||
memset(&web, 0, sizeof(web));
|
memset(&web, 0, sizeof(web));
|
||||||
web.port = CST_WEBUI_DEFAULT_PORT; /* 0 would mean "pick a free one" */
|
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) {
|
if (env && env[0] && parse_port(env, &web.port) != 0) {
|
||||||
fprintf(stderr, "CST_WEB_PORT=%s is not a port (0-65535)\n", env);
|
fprintf(stderr, "CST_WEB_PORT=%s is not a port (0-65535)\n", env);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
web.host = getenv("CST_WEB_HOST");
|
web.host = hermit_getenv("WEB_HOST");
|
||||||
web.token = getenv("CST_WEB_TOKEN");
|
web.token = hermit_getenv("WEB_TOKEN");
|
||||||
web.root = getenv("CST_WEB_ROOT");
|
web.root = hermit_getenv("WEB_ROOT");
|
||||||
|
|
||||||
static struct option opts[] = {
|
static struct option opts[] = {
|
||||||
{"workspace", required_argument, 0, 'w'},
|
{"workspace", required_argument, 0, 'w'},
|
||||||
@@ -693,13 +693,13 @@ int main(int argc, char **argv) {
|
|||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
if (do_realm_pw_init) {
|
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);
|
int rc = cst_parliament_cli_realm_password_init(pw);
|
||||||
cst_curl_cleanup();
|
cst_curl_cleanup();
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
if (do_login) {
|
if (do_login) {
|
||||||
const char *pw = getenv("CST_REALM_PASSWORD");
|
const char *pw = hermit_getenv("REALM_PASSWORD");
|
||||||
char iidbuf[64];
|
char iidbuf[64];
|
||||||
const char *iid = parliament_instance;
|
const char *iid = parliament_instance;
|
||||||
if (!iid || !iid[0]) {
|
if (!iid || !iid[0]) {
|
||||||
@@ -711,7 +711,7 @@ int main(int argc, char **argv) {
|
|||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
if (do_join) {
|
if (do_join) {
|
||||||
const char *pw = getenv("CST_REALM_PASSWORD");
|
const char *pw = hermit_getenv("REALM_PASSWORD");
|
||||||
char iidbuf[64];
|
char iidbuf[64];
|
||||||
const char *iid = parliament_instance;
|
const char *iid = parliament_instance;
|
||||||
if (!iid || !iid[0]) {
|
if (!iid || !iid[0]) {
|
||||||
@@ -734,7 +734,7 @@ int main(int argc, char **argv) {
|
|||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
if (do_service || do_service_fg) {
|
if (do_service || do_service_fg) {
|
||||||
const char *pw = getenv("CST_REALM_PASSWORD");
|
const char *pw = hermit_getenv("REALM_PASSWORD");
|
||||||
char iidbuf[64];
|
char iidbuf[64];
|
||||||
const char *iid = parliament_instance;
|
const char *iid = parliament_instance;
|
||||||
if (!iid || !iid[0]) {
|
if (!iid || !iid[0]) {
|
||||||
@@ -754,13 +754,13 @@ int main(int argc, char **argv) {
|
|||||||
if (do_peer_listen) {
|
if (do_peer_listen) {
|
||||||
const char *iid = parliament_instance ? parliament_instance : realm_enroll_id;
|
const char *iid = parliament_instance ? parliament_instance : realm_enroll_id;
|
||||||
if (!iid)
|
if (!iid)
|
||||||
iid = getenv("CST_INSTANCE_ID");
|
iid = hermit_getenv("INSTANCE_ID");
|
||||||
int rc = cst_parliament_cli_peer_listen(peer_listen_port, iid);
|
int rc = cst_parliament_cli_peer_listen(peer_listen_port, iid);
|
||||||
cst_curl_cleanup();
|
cst_curl_cleanup();
|
||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
if (do_peer_dial) {
|
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];
|
char host[256];
|
||||||
int port = 0;
|
int port = 0;
|
||||||
host[0] = 0;
|
host[0] = 0;
|
||||||
@@ -780,7 +780,7 @@ int main(int argc, char **argv) {
|
|||||||
return rc;
|
return rc;
|
||||||
}
|
}
|
||||||
if (do_discover) {
|
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);
|
int rc = cst_parliament_cli_discover(iid, discover_peer_port, discover_wait_ms);
|
||||||
cst_curl_cleanup();
|
cst_curl_cleanup();
|
||||||
return rc;
|
return rc;
|
||||||
@@ -795,7 +795,7 @@ int main(int argc, char **argv) {
|
|||||||
cst_realm_dir(rdir, sizeof(rdir)) == 0 &&
|
cst_realm_dir(rdir, sizeof(rdir)) == 0 &&
|
||||||
cst_realm_is_password_mode(rdir) && !cst_parliament_session_ok(NULL, 0)) {
|
cst_realm_is_password_mode(rdir) && !cst_parliament_session_ok(NULL, 0)) {
|
||||||
char pwbuf[256];
|
char pwbuf[256];
|
||||||
const char *pw = getenv("CST_REALM_PASSWORD");
|
const char *pw = hermit_getenv("REALM_PASSWORD");
|
||||||
if ((!pw || !pw[0]) && isatty(0) &&
|
if ((!pw || !pw[0]) && isatty(0) &&
|
||||||
cst_prompt_secret("Realm password to open agent (hidden): ", pwbuf,
|
cst_prompt_secret("Realm password to open agent (hidden): ", pwbuf,
|
||||||
sizeof(pwbuf)) == 0)
|
sizeof(pwbuf)) == 0)
|
||||||
@@ -861,20 +861,20 @@ int main(int argc, char **argv) {
|
|||||||
}
|
}
|
||||||
snprintf(cfg.provider, sizeof(cfg.provider), "%s", prov->name);
|
snprintf(cfg.provider, sizeof(cfg.provider), "%s", prov->name);
|
||||||
if (!cfg.effort[0]) {
|
if (!cfg.effort[0]) {
|
||||||
const char *e = getenv("CST_EFFORT");
|
const char *e = hermit_getenv("EFFORT");
|
||||||
if (!cst_effort_valid(e))
|
if (!cst_effort_valid(e))
|
||||||
e = saved.effort;
|
e = saved.effort;
|
||||||
snprintf(cfg.effort, sizeof(cfg.effort), "%s",
|
snprintf(cfg.effort, sizeof(cfg.effort), "%s",
|
||||||
cst_effort_valid(e) ? e : CST_DEFAULT_EFFORT);
|
cst_effort_valid(e) ? e : CST_DEFAULT_EFFORT);
|
||||||
}
|
}
|
||||||
if (!cfg.mode[0]) {
|
if (!cfg.mode[0]) {
|
||||||
const char *m = getenv("CST_MODE");
|
const char *m = hermit_getenv("MODE");
|
||||||
if (!cst_mode_valid(m))
|
if (!cst_mode_valid(m))
|
||||||
m = saved.mode;
|
m = saved.mode;
|
||||||
snprintf(cfg.mode, sizeof(cfg.mode), "%s", cst_mode_name(m));
|
snprintf(cfg.mode, sizeof(cfg.mode), "%s", cst_mode_name(m));
|
||||||
}
|
}
|
||||||
if (!cfg.api_base[0]) {
|
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])
|
if (!env_base || !env_base[0])
|
||||||
env_base = getenv("OPENAI_BASE_URL");
|
env_base = getenv("OPENAI_BASE_URL");
|
||||||
if (!env_base || !env_base[0])
|
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);
|
env_base && env_base[0] ? env_base : prov->api_base);
|
||||||
}
|
}
|
||||||
if (!cfg.model[0]) {
|
if (!cfg.model[0]) {
|
||||||
const char *env_model = getenv("CST_MODEL");
|
const char *env_model = hermit_getenv("MODEL");
|
||||||
if (!env_model || !env_model[0])
|
if (!env_model || !env_model[0])
|
||||||
env_model = getenv("OPENAI_MODEL");
|
env_model = getenv("OPENAI_MODEL");
|
||||||
if (!env_model || !env_model[0])
|
if (!env_model || !env_model[0])
|
||||||
@@ -896,7 +896,7 @@ int main(int argc, char **argv) {
|
|||||||
if (!cfg.api_key[0] && !force_ask_key &&
|
if (!cfg.api_key[0] && !force_ask_key &&
|
||||||
cst_creds_resolve(prov->name, cfg.api_key, sizeof(cfg.api_key), NULL) !=
|
cst_creds_resolve(prov->name, cfg.api_key, sizeof(cfg.api_key), NULL) !=
|
||||||
0) {
|
0) {
|
||||||
const char *k = getenv("CST_API_KEY");
|
const char *k = hermit_getenv("API_KEY");
|
||||||
if (k && k[0])
|
if (k && k[0])
|
||||||
snprintf(cfg.api_key, sizeof(cfg.api_key), "%s", k);
|
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)) {
|
if (!cfg.api_key[0] && (cfg.headless || web_mode)) {
|
||||||
fprintf(stderr,
|
fprintf(stderr,
|
||||||
"no API key for %s: start cst-agent once to enter it, or run "
|
"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);
|
cfg.provider, cfg.provider);
|
||||||
cst_curl_cleanup();
|
cst_curl_cleanup();
|
||||||
return 1;
|
return 1;
|
||||||
|
|||||||
+3
-2
@@ -1,4 +1,5 @@
|
|||||||
#include "cst_metrics.h"
|
#include "cst_metrics.h"
|
||||||
|
#include "cst_hermit_glyph.h"
|
||||||
|
|
||||||
#include "cJSON.h"
|
#include "cJSON.h"
|
||||||
#include "cst_agent.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) {
|
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)
|
if (off && strcmp(off, "0") == 0)
|
||||||
return NULL;
|
return NULL;
|
||||||
cst_metrics_t *m = calloc(1, sizeof(*m));
|
cst_metrics_t *m = calloc(1, sizeof(*m));
|
||||||
if (!m)
|
if (!m)
|
||||||
return NULL;
|
return NULL;
|
||||||
const char *override = getenv("CST_METRICS_PATH");
|
const char *override = hermit_getenv("METRICS_PATH");
|
||||||
if (override && override[0]) {
|
if (override && override[0]) {
|
||||||
snprintf(m->path, sizeof(m->path), "%s", override);
|
snprintf(m->path, sizeof(m->path), "%s", override);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "cst_models.h"
|
#include "cst_models.h"
|
||||||
#include "cst_util.h"
|
#include "cst_util.h"
|
||||||
|
#include "cst_hermit_glyph.h"
|
||||||
|
|
||||||
#include "cJSON.h"
|
#include "cJSON.h"
|
||||||
#include "os_compat.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 *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);
|
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)
|
if (!out || max_out <= 0)
|
||||||
return 0;
|
return 0;
|
||||||
int n = 0;
|
int n = 0;
|
||||||
const char *env = getenv("CST_MODELS");
|
const char *env = hermit_getenv("MODELS");
|
||||||
if (env && env[0]) {
|
if (env && env[0]) {
|
||||||
char buf[1024];
|
char buf[1024];
|
||||||
snprintf(buf, sizeof(buf), "%s", env);
|
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) {
|
static int env_price(cst_price_t *out) {
|
||||||
const char *e = getenv("CST_PRICE");
|
const char *e = hermit_getenv("PRICE");
|
||||||
if (!e || !e[0])
|
if (!e || !e[0])
|
||||||
return -1;
|
return -1;
|
||||||
memset(out, 0, sizeof(*out));
|
memset(out, 0, sizeof(*out));
|
||||||
@@ -765,7 +766,7 @@ void cst_model_info(const char *provider, const char *model,
|
|||||||
memset(out, 0, sizeof(*out));
|
memset(out, 0, sizeof(*out));
|
||||||
out->vision = -1;
|
out->vision = -1;
|
||||||
unsigned have = 0;
|
unsigned have = 0;
|
||||||
const char *e = getenv("CST_CTX_TOKENS");
|
const char *e = hermit_getenv("CTX_TOKENS");
|
||||||
if (e && atol(e) > 0) {
|
if (e && atol(e) > 0) {
|
||||||
out->context = atol(e);
|
out->context = atol(e);
|
||||||
have |= F_CONTEXT;
|
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_t mi;
|
||||||
cst_model_info(provider, model, &mi);
|
cst_model_info(provider, model, &mi);
|
||||||
out->vision = mi.vision;
|
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)
|
if ((e && e[0] == '1') || !model)
|
||||||
return;
|
return;
|
||||||
out->cache_style = mi.cache_style;
|
out->cache_style = mi.cache_style;
|
||||||
|
|||||||
+17
-3
@@ -11,6 +11,7 @@
|
|||||||
#endif
|
#endif
|
||||||
#include "os_compat.h"
|
#include "os_compat.h"
|
||||||
#include "os_internal.h"
|
#include "os_internal.h"
|
||||||
|
#include "cst_hermit_glyph.h"
|
||||||
|
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <stdint.h>
|
#include <stdint.h>
|
||||||
@@ -40,6 +41,19 @@
|
|||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#endif
|
#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) {
|
void os_run_result_free(os_run_result_t *r) {
|
||||||
if (!r)
|
if (!r)
|
||||||
return;
|
return;
|
||||||
@@ -421,7 +435,7 @@ void os_set_prog_path(const char *argv0) {
|
|||||||
int os_exe_dir(char *out, size_t out_sz) {
|
int os_exe_dir(char *out, size_t out_sz) {
|
||||||
if (!out || out_sz < 2)
|
if (!out || out_sz < 2)
|
||||||
return -1;
|
return -1;
|
||||||
const char *env = getenv("CST_EXE_DIR");
|
const char *env = hermit_getenv("EXE_DIR");
|
||||||
if (env && env[0]) {
|
if (env && env[0]) {
|
||||||
snprintf(out, out_sz, "%s", env);
|
snprintf(out, out_sz, "%s", env);
|
||||||
return 0;
|
return 0;
|
||||||
@@ -480,7 +494,7 @@ static int file_exists(const char *p) {
|
|||||||
int os_resolve_shell(char *out, size_t out_sz) {
|
int os_resolve_shell(char *out, size_t out_sz) {
|
||||||
if (!out || out_sz < 2)
|
if (!out || out_sz < 2)
|
||||||
return -1;
|
return -1;
|
||||||
const char *env = getenv("CST_SHELL");
|
const char *env = hermit_getenv("SHELL");
|
||||||
if (env && env[0]) {
|
if (env && env[0]) {
|
||||||
snprintf(out, out_sz, "%s", env);
|
snprintf(out, out_sz, "%s", env);
|
||||||
return 0;
|
return 0;
|
||||||
@@ -608,7 +622,7 @@ int os_run_timeout_effective(void) {
|
|||||||
int t = run_timeout();
|
int t = run_timeout();
|
||||||
if (t > 0)
|
if (t > 0)
|
||||||
return t;
|
return t;
|
||||||
const char *e = getenv("CST_SHELL_TIMEOUT");
|
const char *e = hermit_getenv("SHELL_TIMEOUT");
|
||||||
if (e && atoi(e) > 0)
|
if (e && atoi(e) > 0)
|
||||||
return atoi(e);
|
return atoi(e);
|
||||||
return 0; /* no limit: a build, a test suite or a long clone must finish */
|
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 "cst_util.h"
|
||||||
#include "monocypher.h"
|
#include "monocypher.h"
|
||||||
#include "os_compat.h"
|
#include "os_compat.h"
|
||||||
|
#include "cst_hermit_glyph.h"
|
||||||
|
|
||||||
#include <ctype.h>
|
#include <ctype.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
@@ -61,7 +62,7 @@ void cst_parliament_sync_ack(void) {
|
|||||||
int cst_parliament_default_instance(char *out, size_t n) {
|
int cst_parliament_default_instance(char *out, size_t n) {
|
||||||
if (!out || n < 2)
|
if (!out || n < 2)
|
||||||
return -1;
|
return -1;
|
||||||
const char *env = getenv("CST_INSTANCE_ID");
|
const char *env = hermit_getenv("INSTANCE_ID");
|
||||||
if (env && env[0]) {
|
if (env && env[0]) {
|
||||||
snprintf(out, n, "%s", env);
|
snprintf(out, n, "%s", env);
|
||||||
return 0;
|
return 0;
|
||||||
@@ -647,7 +648,7 @@ int cst_parliament_is_active(void) {
|
|||||||
* Password Realm alone no longer forces parliament — that starved task /
|
* Password Realm alone no longer forces parliament — that starved task /
|
||||||
* worker_* / runtime tools. Opt in via CST_COLLABORATION / settings, or
|
* worker_* / runtime tools. Opt in via CST_COLLABORATION / settings, or
|
||||||
* when an open parliament meta already exists. */
|
* when an open parliament meta already exists. */
|
||||||
const char *mode = getenv("CST_COLLABORATION");
|
const char *mode = hermit_getenv("COLLABORATION");
|
||||||
if (mode && mode[0])
|
if (mode && mode[0])
|
||||||
return strcmp(mode, "parliament") == 0;
|
return strcmp(mode, "parliament") == 0;
|
||||||
cst_settings_t s;
|
cst_settings_t s;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include "cst_peer.h"
|
#include "cst_peer.h"
|
||||||
#include "cst_realm.h"
|
#include "cst_realm.h"
|
||||||
#include "os_compat.h"
|
#include "os_compat.h"
|
||||||
|
#include "cst_hermit_glyph.h"
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -32,7 +33,7 @@ static void sleep_ms(int ms) {
|
|||||||
|
|
||||||
/* Prefer CST_REALM_PASSWORD; otherwise prompt on a TTY. */
|
/* Prefer CST_REALM_PASSWORD; otherwise prompt on a TTY. */
|
||||||
static int resolve_password(char *buf, size_t n, const char **out) {
|
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]) {
|
if (env && env[0]) {
|
||||||
*out = env;
|
*out = env;
|
||||||
return 0;
|
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)
|
if (cst_realm_dir(dir, sizeof(dir)) != 0)
|
||||||
return -1;
|
return -1;
|
||||||
if (cst_realm_is_password_mode(dir)) {
|
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])
|
if (!pw || !pw[0])
|
||||||
return -1;
|
return -1;
|
||||||
return cst_realm_unlock_password(dir, pw, realm);
|
return cst_realm_unlock_password(dir, pw, realm);
|
||||||
|
|||||||
+2
-1
@@ -3,6 +3,7 @@
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
#include "cst_sandbox.h"
|
#include "cst_sandbox.h"
|
||||||
|
#include "cst_hermit_glyph.h"
|
||||||
|
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
@@ -1244,7 +1245,7 @@ int cst_sandbox_probe(void) {
|
|||||||
if (g_kind >= 0)
|
if (g_kind >= 0)
|
||||||
return g_kind;
|
return g_kind;
|
||||||
int kind = CST_SANDBOX_NONE;
|
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 ||
|
if (e && (strcmp(e, "0") == 0 || strcmp(e, "off") == 0 ||
|
||||||
strcmp(e, "none") == 0)) {
|
strcmp(e, "none") == 0)) {
|
||||||
g_kind = kind;
|
g_kind = kind;
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
|
#include "cst_hermit_glyph.h"
|
||||||
|
|
||||||
#include <regex.h>
|
#include <regex.h>
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@
|
|||||||
#define GREP_LINE_MAX 300
|
#define GREP_LINE_MAX 300
|
||||||
|
|
||||||
static int glob_max_hits(void) {
|
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;
|
int n = e && e[0] ? atoi(e) : GLOB_MAX_DEFAULT;
|
||||||
if (n < 50)
|
if (n < 50)
|
||||||
n = 50;
|
n = 50;
|
||||||
@@ -40,7 +41,7 @@ static int glob_max_hits(void) {
|
|||||||
return n;
|
return n;
|
||||||
}
|
}
|
||||||
static int grep_max_matches(void) {
|
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;
|
int n = e && e[0] ? atoi(e) : GREP_MAX_MATCHES_DEFAULT;
|
||||||
if (n < 50)
|
if (n < 50)
|
||||||
n = 50;
|
n = 50;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <pthread.h>
|
#include <pthread.h>
|
||||||
|
#include "cst_hermit_glyph.h"
|
||||||
|
|
||||||
#include "cJSON.h"
|
#include "cJSON.h"
|
||||||
#include "cst_clipboard.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 *const CST_PLAT_DESKTOP[] = {"desktop", NULL};
|
||||||
|
|
||||||
const char *cst_platform_name(void) {
|
const char *cst_platform_name(void) {
|
||||||
const char *over = getenv("CST_PLATFORM");
|
const char *over = hermit_getenv("PLATFORM");
|
||||||
if (over && over[0])
|
if (over && over[0])
|
||||||
return over; /* tests, cross checks, a launcher that knows better */
|
return over; /* tests, cross checks, a launcher that knows better */
|
||||||
#if defined(_WIN32)
|
#if defined(_WIN32)
|
||||||
|
|||||||
@@ -24,9 +24,9 @@
|
|||||||
#include "cst_tools.h"
|
#include "cst_tools.h"
|
||||||
#include "cst_util.h"
|
#include "cst_util.h"
|
||||||
#include "cst_version.h"
|
#include "cst_version.h"
|
||||||
#include "cst_hermit_glyph.h"
|
|
||||||
#include "os_compat.h"
|
#include "os_compat.h"
|
||||||
#include "sashidown.h"
|
#include "sashidown.h"
|
||||||
|
#include "cst_hermit_glyph.h"
|
||||||
|
|
||||||
#include <locale.h>
|
#include <locale.h>
|
||||||
#include <pthread.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 ||
|
if (cst_realm_dir(dir, sizeof(dir)) != 0 ||
|
||||||
!cst_realm_is_password_mode(dir)) {
|
!cst_realm_is_password_mode(dir)) {
|
||||||
push_system_line(t, "No password Realm yet. Outside TUI run:");
|
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;
|
return;
|
||||||
}
|
}
|
||||||
char pw[256];
|
char pw[256];
|
||||||
@@ -5621,7 +5621,7 @@ static void cmd_status(tui_t *t, char *arg, int *quit) {
|
|||||||
}
|
}
|
||||||
free(pst);
|
free(pst);
|
||||||
if (cst_parliament_is_active() && !cst_parliament_session_ok(NULL, 0))
|
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_usage_t u;
|
||||||
cst_agent_usage(t->agent, &u, NULL);
|
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)arg;
|
||||||
(void)quit;
|
(void)quit;
|
||||||
#ifdef _WIN32
|
#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
|
#else
|
||||||
static pid_t spawned = 0;
|
static pid_t spawned = 0;
|
||||||
if (spawned > 0) { /* collect the one we started last time */
|
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];
|
char dir[4096], exe[4096 + 16];
|
||||||
if (os_exe_dir(dir, sizeof(dir)) != 0) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
snprintf(exe, sizeof(exe), "%s/cst-agent", dir);
|
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_todo = 1;
|
||||||
t.show_side = 1; /* Seat Deck; narrow terminals auto-hide in layout() */
|
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');
|
t.motion_on = !(mo && mo[0] == '0');
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#include "cst_agent.h"
|
#include "cst_agent.h"
|
||||||
#include "cst_attach.h"
|
#include "cst_attach.h"
|
||||||
#include "cst_test_util.h"
|
#include "cst_test_util.h"
|
||||||
|
#include "cst_hermit_glyph.h"
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -299,8 +300,8 @@ static void test_cache_hints(void) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void test_kimi_wire_format(void) {
|
static void test_kimi_wire_format(void) {
|
||||||
const char *old_no_cache = getenv("CST_NO_CACHE_HINTS");
|
const char *old_no_cache = hermit_getenv("NO_CACHE_HINTS");
|
||||||
const char *old_ttl = getenv("CST_KIMI_CACHE_TTL");
|
const char *old_ttl = hermit_getenv("KIMI_CACHE_TTL");
|
||||||
char *saved_no_cache = old_no_cache ? strdup(old_no_cache) : NULL;
|
char *saved_no_cache = old_no_cache ? strdup(old_no_cache) : NULL;
|
||||||
char *saved_ttl = old_ttl ? strdup(old_ttl) : NULL;
|
char *saved_ttl = old_ttl ? strdup(old_ttl) : NULL;
|
||||||
setenv("CST_NO_CACHE_HINTS", "0", 1);
|
setenv("CST_NO_CACHE_HINTS", "0", 1);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include "cst_perms.h"
|
#include "cst_perms.h"
|
||||||
#include "cst_sandbox.h"
|
#include "cst_sandbox.h"
|
||||||
|
#include "cst_hermit_glyph.h"
|
||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -353,7 +354,7 @@ int main(void) {
|
|||||||
if (kind == CST_SANDBOX_LANDLOCK || kind == CST_SANDBOX_SECCOMP)
|
if (kind == CST_SANDBOX_LANDLOCK || kind == CST_SANDBOX_SECCOMP)
|
||||||
test_confined(kind);
|
test_confined(kind);
|
||||||
if (kind != CST_SANDBOX_SECCOMP) {
|
if (kind != CST_SANDBOX_SECCOMP) {
|
||||||
const char *was = getenv("CST_SANDBOX");
|
const char *was = hermit_getenv("SANDBOX");
|
||||||
char saved[32] = "";
|
char saved[32] = "";
|
||||||
if (was)
|
if (was)
|
||||||
snprintf(saved, sizeof(saved), "%s", 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
|
* No build step, no dependencies. The server renders Markdown to sanitized
|
||||||
* HTML; this file lays out the conversation, the tool lines, the approval
|
* HTML; this file lays out the conversation, the tool lines, the approval
|
||||||
* gate and the anchored popovers. Protocol: docs/webui.md. */
|
* gate and the anchored popovers. Protocol: docs/webui.md. */
|
||||||
@@ -562,7 +562,7 @@ async function loadSession() {
|
|||||||
}
|
}
|
||||||
showEmpty(log.children.length === 0);
|
showEmpty(log.children.length === 0);
|
||||||
const last = [...(data.messages || [])].reverse().find((m) => m.role === 'user' && !m.note && !m.tool_result);
|
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);
|
scrollDown(true);
|
||||||
updateJump();
|
updateJump();
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||||
<meta name="color-scheme" content="dark light">
|
<meta name="color-scheme" content="dark light">
|
||||||
<title>CST-Agent</title>
|
<title>Hermit</title>
|
||||||
<link rel="stylesheet" href="style.css">
|
<link rel="stylesheet" href="style.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
<div class="side-inner">
|
<div class="side-inner">
|
||||||
<div class="side-head">
|
<div class="side-head">
|
||||||
<div class="brand">
|
<div class="brand">
|
||||||
<span class="brand-name">CST-Agent</span>
|
<span class="brand-name">Hermit</span>
|
||||||
</div>
|
</div>
|
||||||
<button id="btn-collapse" class="icon" title="收起侧栏 (Ctrl+B)">
|
<button id="btn-collapse" class="icon" title="收起侧栏 (Ctrl+B)">
|
||||||
<svg class="i"><use href="#i-panel-left"/></svg>
|
<svg class="i"><use href="#i-panel-left"/></svg>
|
||||||
@@ -158,7 +158,7 @@
|
|||||||
|
|
||||||
<div id="slash" class="slash" role="listbox" hidden></div>
|
<div id="slash" class="slash" role="listbox" hidden></div>
|
||||||
<div class="box">
|
<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">
|
<div class="box-row">
|
||||||
<button id="btn-plus" class="icon att" title="添加图片或计划模式">
|
<button id="btn-plus" class="icon att" title="添加图片或计划模式">
|
||||||
<svg class="i"><use href="#i-plus"/></svg>
|
<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
|
* Design rules
|
||||||
* - one fact, one home: sessions in the rail, model/approval/plan in the
|
* - one fact, one home: sessions in the rail, model/approval/plan in the
|
||||||
|
|||||||
Reference in New Issue
Block a user