feat: rebrand as Hermit — TUI palette, dual glyphs, binary 0.10.0

Ship hermit binary (cst-agent symlink), ~/.config/hermit with legacy fallback,
Hermit color theme and mini crab frames, plus SVG→PUA font build pipeline.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
WHC
2026-10-29 21:42:45 +08:00
co-authored by Cursor
parent 2602806aea
commit f30b4ede48
32 changed files with 793 additions and 265 deletions
+11 -11
View File
@@ -37,14 +37,14 @@ jobs:
- name: Build TinyCC - name: Build TinyCC
run: bash scripts/ci_tcc.sh run: bash scripts/ci_tcc.sh
- name: Build - name: Build
run: make CC=tcc AR=ar CFLAGS="-std=c11 -Wall -O2 -g" -j"$(nproc)" run: make CC=tcc AR=ar CFLAGS="-std=c11 -Wall -O2 -g" -j"$(nproc)" hermit
- name: Unit tests - name: Unit tests
run: make CC=tcc AR=ar CFLAGS="-std=c11 -Wall -O2 -g" check run: make CC=tcc AR=ar CFLAGS="-std=c11 -Wall -O2 -g" check
- name: Smoke test - name: Smoke test
run: | run: |
./cst-agent --help ./hermit --help
./cst-agent --workspace "$PWD" --list-sessions ./hermit --workspace "$PWD" --list-sessions
./cst-agent --demo-ctl --workspace . ./hermit --demo-ctl --workspace .
- name: WebUI end-to-end (recorded stream, no API key) - name: WebUI end-to-end (recorded stream, no API key)
run: sh scripts/e2e_webui.sh run: sh scripts/e2e_webui.sh
@@ -103,14 +103,14 @@ jobs:
run: bash scripts/package_linux.sh ${{ matrix.arch }} run: bash scripts/package_linux.sh ${{ matrix.arch }}
- name: Smoke test - name: Smoke test
run: | run: |
d=$(ls -d dist/cst-agent-*-linux-${{ matrix.arch }}) d=$(ls -d dist/hermit-*-linux-${{ matrix.arch }})
file "$d/cst-agent" file "$d/hermit"
run="" run=""
[ "${{ matrix.arch }}" = aarch64 ] && run=qemu-aarch64-static [ "${{ matrix.arch }}" = aarch64 ] && run=qemu-aarch64-static
$run "$d/cst-agent" --version $run "$d/hermit" --version
$run "$d/cst-agent" --help >/dev/null $run "$d/hermit" --help >/dev/null
$run "$d/cst-agent" --workspace "$PWD" --list-sessions $run "$d/hermit" --workspace "$PWD" --list-sessions
cat dist/cst-agent-*-linux-${{ matrix.arch }}.tar.gz.sha256 cat dist/hermit-*-linux-${{ matrix.arch }}.tar.gz.sha256
# Release vVERSION (from include/cst_version.h) on Gitea. Prefer repo # Release vVERSION (from include/cst_version.h) on Gitea. Prefer repo
# secret RELEASE_TOKEN (write:repository); fall back to the job token. # secret RELEASE_TOKEN (write:repository); fall back to the job token.
- name: Publish Gitea Release - name: Publish Gitea Release
@@ -121,7 +121,7 @@ jobs:
CST_GITEA: ${{ github.server_url }} CST_GITEA: ${{ github.server_url }}
CST_REPO: ${{ github.repository }} CST_REPO: ${{ github.repository }}
GITHUB_REF: ${{ github.ref }} GITHUB_REF: ${{ github.ref }}
run: bash scripts/ci_publish.sh dist/cst-agent-*-linux-${{ matrix.arch }}.tar.gz run: bash scripts/ci_publish.sh dist/hermit-*-linux-${{ matrix.arch }}.tar.gz dist/cst-agent-*-linux-${{ matrix.arch }}.tar.gz
# Release: mingw-w64 UCRT gcc (Debian trixie) with -Werror. # Release: mingw-w64 UCRT gcc (Debian trixie) with -Werror.
# dist/cst-agent-VERSION-windows-x86_64.zip and -setup.exe (NSIS) # dist/cst-agent-VERSION-windows-x86_64.zip and -setup.exe (NSIS)
+3
View File
@@ -58,3 +58,6 @@ libcstcore.a
# nix-build # nix-build
result result
hermit
.peer_probe.out
reports/
+2
View File
@@ -152,6 +152,7 @@ set(CST_CORE_SOURCES
src/models.c src/models.c
src/model_registry.c src/model_registry.c
src/metrics.c src/metrics.c
src/hermit_glyph.c
src/net.c src/net.c
src/http_srv.c src/http_srv.c
src/web_md.c src/web_md.c
@@ -257,6 +258,7 @@ else()
endif() endif()
add_executable(cst-agent ${CST_SOURCES}) add_executable(cst-agent ${CST_SOURCES})
set_target_properties(cst-agent PROPERTIES OUTPUT_NAME hermit)
add_dependencies(cst-agent sashidown) add_dependencies(cst-agent sashidown)
cst_win_exe(cst-agent) cst_win_exe(cst-agent)
+32 -15
View File
@@ -136,6 +136,7 @@ CORE_SRC = \
src/mm_tool.c \ src/mm_tool.c \
src/models.c src/model_registry.c \ src/models.c src/model_registry.c \
src/metrics.c \ src/metrics.c \
src/hermit_glyph.c \
$(WEB_SRC) $(WEB_SRC)
SRC = $(CORE_SRC) src/textsel.c src/tui.c src/main.c SRC = $(CORE_SRC) src/textsel.c src/tui.c src/main.c
@@ -206,18 +207,23 @@ TEST_LIBS = -L$(SD_DIR) -lsashidown $(CURL_LIBS) $(RPATH_LIBS) $(TEST_SYS_LIBS)
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
.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 uninstall install-service help hermit-font cst-agent
all: sashidown-lib cst-agent $(CST_CORE_LIB) all: sashidown-lib hermit $(CST_CORE_LIB)
# Compat alias: many scripts still say `make cst-agent`.
cst-agent: hermit
help: help:
@echo "CST-Agent build targets (prefer: make -j\$$(nproc))" @echo "Hermit build targets (prefer: make -j\$$(nproc))"
@echo " all / cst-agent incremental build (ccache auto if installed)" @echo " all / hermit incremental build (ccache auto if installed)"
@echo " check build + run unit tests" @echo " check build + run unit tests"
@echo " asan AddressSanitizer rebuild + check" @echo " asan AddressSanitizer rebuild + check"
@echo " install PREFIX=$(PREFIX) (default /usr/local)" @echo " hermit-font optional SVG→color font (needs rsvg + fonttools)"
@echo " install PREFIX=$(PREFIX); hermit + cst-agent symlink"
@echo " install-service systemd --user parliament unit" @echo " install-service systemd --user parliament unit"
@echo "Env: USE_CCACHE=0|1|auto STATIC=1 CST_WEBUI=0 CC=tcc" @echo "Env: USE_CCACHE=0|1|auto STATIC=1 CST_WEBUI=0 CC=tcc"
@echo " HERMIT_ICONS=auto|mini|font HERMIT_*/CST_* env"
@echo "Release: scripts/package_linux.sh ARCH | scripts/ci_windows_cross.sh" @echo "Release: scripts/package_linux.sh ARCH | scripts/ci_windows_cross.sh"
@echo "Fast install: scripts/bootstrap_linux.sh --release" @echo "Fast install: scripts/bootstrap_linux.sh --release"
@@ -238,8 +244,13 @@ cmake-clang:
sashidown-lib: sashidown-lib:
$(MAKE) -C $(SD_DIR) libsashidown.a $(MAKE) -C $(SD_DIR) libsashidown.a
cst-agent: $(OBJ) sashidown-lib hermit: $(OBJ) sashidown-lib
$(CC) $(CFLAGS) -o $@ $(OBJ) $(LDFLAGS) $(LDLIBS) $(CC) $(CFLAGS) -o $@ $(OBJ) $(LDFLAGS) $(LDLIBS)
@ln -sfr $@ cst-agent 2>/dev/null || ln -sf hermit cst-agent
hermit-font:
@python3 scripts/build_hermit_font.py || \
echo "hermit-font: skipped (install rsvg-convert + fonttools to build)"
# Built-in model data as a C byte list (included by model_registry.c). # Built-in model data as a C byte list (included by model_registry.c).
.gen/models_builtin.inc: src/models_builtin.json .gen/models_builtin.inc: src/models_builtin.json
@@ -303,32 +314,38 @@ check: sashidown-lib $(TESTS)
asan: asan:
$(MAKE) clean $(MAKE) clean
$(MAKE) CFLAGS="-std=c11 -Wall -Wextra -O1 -g -fsanitize=address,undefined -fno-sanitize-recover=undefined -fno-omit-frame-pointer" \ $(MAKE) CFLAGS="-std=c11 -Wall -Wextra -O1 -g -fsanitize=address,undefined -fno-sanitize-recover=undefined -fno-omit-frame-pointer" \
sashidown-lib cst-agent check sashidown-lib hermit check
demo: cst-agent demo: hermit
./cst-agent --demo-ctl --workspace . ./hermit --demo-ctl --workspace .
clean: clean:
rm -f $(OBJ) $(DEPFILES) cst-agent $(CST_CORE_LIB) $(TESTS) \ rm -f $(OBJ) $(DEPFILES) hermit cst-agent $(CST_CORE_LIB) $(TESTS) \
.gen/models_builtin.inc .gen/web_assets.inc .gen/models_builtin.inc .gen/web_assets.inc
install: cst-agent install: hermit
install -d "$(DESTDIR)$(PREFIX)/bin" install -d "$(DESTDIR)$(PREFIX)/bin"
install -m 0755 cst-agent "$(DESTDIR)$(PREFIX)/bin/cst-agent" install -m 0755 hermit "$(DESTDIR)$(PREFIX)/bin/hermit"
ln -sfr "$(DESTDIR)$(PREFIX)/bin/hermit" "$(DESTDIR)$(PREFIX)/bin/cst-agent" 2>/dev/null || \
ln -sf hermit "$(DESTDIR)$(PREFIX)/bin/cst-agent"
@if [ -f res/hermit-icons.ttf ]; then \
install -d "$(DESTDIR)$(PREFIX)/share/hermit"; \
install -m 0644 res/hermit-icons.ttf "$(DESTDIR)$(PREFIX)/share/hermit/"; \
fi
# Optional systemd --user unit for the parliament seat service. # Optional systemd --user unit for the parliament seat service.
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/cst-agent|g' \ sed -e 's|@BIN@|$(PREFIX)/bin/hermit|g' \
scripts/cst-agent-parliament.service.in \ scripts/cst-agent-parliament.service.in \
> "$(DESTDIR)$(HOME)/.config/systemd/user/cst-agent-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 cst-agent-parliament.service"
@echo "(set CST_REALM_PASSWORD / CST_INSTANCE_ID in the drop-in or environment)" @echo "(set HERMIT_/CST_ REALM_PASSWORD / INSTANCE_ID in the drop-in or environment)"
uninstall: uninstall:
rm -f "$(DESTDIR)$(PREFIX)/bin/cst-agent" rm -f "$(DESTDIR)$(PREFIX)/bin/hermit" "$(DESTDIR)$(PREFIX)/bin/cst-agent"
rm -f "$(HOME)/.config/systemd/user/cst-agent-parliament.service" rm -f "$(HOME)/.config/systemd/user/cst-agent-parliament.service"
-include $(DEPFILES) -include $(DEPFILES)
+30 -138
View File
@@ -1,6 +1,6 @@
# CST-Agent # Hermit
本地 coding agent:终端 TUI、可选 WebUI / 桌面 GUI,在工作区里读改文件、跑命令、管会话。 本地 coding agent(原 CST-Agent):终端 TUI、可选 WebUI / 桌面 GUI,在工作区里读改文件、跑命令、管会话。
C11 实现,OpenAI 兼容网关(DeepSeek / Kimi / MiniMax / OpenAI 等)。 C11 实现,OpenAI 兼容网关(DeepSeek / Kimi / MiniMax / OpenAI 等)。
**适合**:有监督的本地改动与仓库探索(权限确认、diff、`/undo`、沙箱)。 **适合**:有监督的本地改动与仓库探索(权限确认、diff、`/undo`、沙箱)。
@@ -8,188 +8,80 @@ C11 实现,OpenAI 兼容网关(DeepSeek / Kimi / MiniMax / OpenAI 等)。
文档索引:[docs/README.md](docs/README.md)。 文档索引:[docs/README.md](docs/README.md)。
兼容:安装后提供 `cst-agent` → `hermit` 软链;环境变量 `HERMIT_*` 优先,仍接受 `CST_*`;配置目录优先 `~/.config/hermit`,若无则回退 `~/.config/cst-agent`。
--- ---
## 安装 ## 安装
### Linux / WSL / macOS(推荐) ### Linux / WSL / macOS
```bash ```bash
# 预编译包(快;失败则自动回退源码编译) # 预编译包(快;失败则回退源码)
curl -fsSL https://git.whcself.xyz/WHC/CST-agent/raw/branch/main/scripts/bootstrap_linux.sh | bash -s -- --release curl -fsSL https://git.whcself.xyz/WHC/CST-agent/raw/branch/main/scripts/bootstrap_linux.sh | bash -s -- --release
# 或始终从源码增量编译安装到 ~/.local/bin # 源码增量编译 → ~/.local/bin/hermit(+ cst-agent 软链)
curl -fsSL https://git.whcself.xyz/WHC/CST-agent/raw/branch/main/scripts/bootstrap_linux.sh | bash curl -fsSL https://git.whcself.xyz/WHC/CST-agent/raw/branch/main/scripts/bootstrap_linux.sh | bash
``` ```
已有仓库目录:
```bash ```bash
./scripts/bootstrap_linux.sh . # 源码 ./scripts/bootstrap_linux.sh .
./scripts/bootstrap_linux.sh . --release # 只装 Release 二进制 hermit --version # hermit 0.10.0
cst-agent-update # 之后升级 cst-agent-update # 升级助手仍可用
cst-agent --version
``` ```
[Release](https://git.whcself.xyz/WHC/CST-agent/releases) 也提供静态 musl 包 [Release](https://git.whcself.xyz/WHC/CST-agent/releases):`hermit-VERSION-linux-{x86_64,x86,aarch64}.tar.gz`。
`cst-agent-VERSION-linux-{x86_64,x86,aarch64}.tar.gz`(旁有 `.sha256`),解压后把 `cst-agent` 放进 `PATH` 即可。
### Windows 10 1903+ ### Windows
```powershell 见 [docs/README-windows.md](docs/README-windows.md)(产物名逐步改为 `hermit-…`;旧 `cst-agent-…` 包过渡期可能仍有)。
irm https://git.whcself.xyz/WHC/CST-agent/raw/branch/main/scripts/install_windows.ps1 | iex
```
或下载 `cst-agent-VERSION-windows-x86_64-setup.exe` / 便携 zip。详见 [docs/README-windows.md](docs/README-windows.md)。
### Termux
```bash
curl -fsSL https://git.whcself.xyz/WHC/CST-agent/raw/branch/main/scripts/bootstrap_termux.sh | bash
```
### 第一次用 ### 第一次用
```bash ```bash
cd ~/my-project && cst-agent cd ~/my-project && hermit
``` ```
首次选择 provider 并输入 API key(遮罩输入,按 provider 存到配置目录 `credentials`,权限 0600)。
之后用 `/provider`、`/model`、`/effort`、`/key` 切换;选择会记在 `settings.json`。
完整环境变量:[docs/env_openai.md](docs/env_openai.md)。
多设备议会:[docs/parliament.md](docs/parliament.md)。
| 平台 | 脚本 |
|---|---|
| Linux / macOS | [`scripts/bootstrap_linux.sh`](scripts/bootstrap_linux.sh) |
| Termux | [`scripts/bootstrap_termux.sh`](scripts/bootstrap_termux.sh) |
| Windows 安装 | [`scripts/install_windows.ps1`](scripts/install_windows.ps1) |
| Linux 静态包 | [`scripts/package_linux.sh`](scripts/package_linux.sh) `x86_64\|x86\|aarch64` |
| Windows 交叉构建 | [`scripts/bootstrap_windows.sh`](scripts/bootstrap_windows.sh) |
| Windows 原生(MSYS2) | [`scripts/bootstrap_msys2.sh`](scripts/bootstrap_msys2.sh) |
--- ---
## 构建 ## 构建
```bash ```bash
make -j$(nproc) # 增量;有 ccache 会自动用;make help 看目标 make -j$(nproc) # 产出 hermit(并链 cst-agent)
make -j$(nproc) check # 单元测试 make -j$(nproc) check
make asan # ASan + UBSan make hermit-font # 可选:SVG→PUA 字体(需 rsvg/fonttools)
make CC=tcc AR=ar check # TinyCC 快校验(CI 同款)
make install PREFIX=$HOME/.local make install PREFIX=$HOME/.local
make install-service PREFIX=$HOME/.local # 可选:systemd --user 议会单元
``` ```
NixOS:`nix-shell --run 'make -j$(nproc)'`。macOS:`brew install curl ncurses pkg-config` 后直接 `make`。 TUI 图标:`HERMIT_ICONS=auto|mini|font`(默认极简字符帧;`font` 需安装 `share/hermit/hermit-icons.ttf`)。
平台细节:[docs/compat.md](docs/compat.md)。
```bash
# CMake(多编译器 / 无 TUI)
cmake -B build -DCMAKE_C_COMPILER=gcc -DCST_BUILD_TUI=ON
cmake --build build -j && ctest --test-dir build --output-on-failure
```
可选 Qt 桌面前端:[app/README.md](app/README.md)。
Android APK:[docs/android.md](docs/android.md)。
### CI 与发布
推送 `main` / `v*` 时 [`.gitea/workflows/ci.yml`](.gitea/workflows/ci.yml) 会:TinyCC `check` → 打 Linux/Windows 发行包 → `scripts/ci_publish.sh` 上传 Gitea Release。
共用逻辑:[`scripts/lib_release.sh`](scripts/lib_release.sh)。本地上传:`GITEA_TOKEN=... scripts/release.sh dist/cst-agent-*`。
--- ---
## 用法速览 ## 用法速览
```bash ```bash
cst-agent # TUI,工作区=当前目录 hermit # TUI
cst-agent --web # 浏览器 http://127.0.0.1:8787/ hermit --web # http://127.0.0.1:8787/
cst-agent --headless --prompt '…' # 无人值守(默认自动批准) hermit --headless --prompt '…'
cst-agent --ask-key --provider deepseek
``` ```
| 参数 | 作用 | 首页品牌 **HERMIT** + 寄居蟹色块动效;色板:brand `#277F94`、crab `#F68042`、eye `#9957AD`。
|---|---|
| `-c` / `-N` / `-R ID` / `-L` | 继续上次 / 新会话 / 恢复 / 列表 |
| `-A ask\|auto\|yolo` / `-y` | 审批模式 |
| `-T default\|minimal` | 工具集(`minimal` = 读写 shell 四件套) |
| `-M coding\|general` | 工作模式 |
| `-P` / `-m` / `-e` | provider / 模型 / 推理强度 |
| `-w DIR` | 工作区 |
会话目录:`<workspace>/.agent/sessions/<uuid>/`(自带 `.gitignore`)。WebUI 协议:[docs/webui.md](docs/webui.md)。 更多:Provider / 工具 / 议会 / WebUI 见 [docs/](docs/)。
### Provider 默认
| provider | API Base | 默认模型 | Key |
|---|---|---|---|
| `minimax`(默认) | `https://api.minimax.cn/v1` | `MiniMax-M2.7` | `MINIMAX_API_KEY` |
| `deepseek` | `https://api.deepseek.com/v1` | `deepseek-flash` | `DEEPSEEK_API_KEY` |
| `kimi` | `https://api.moonshot.ai/v1` | `kimi-k3` | `MOONSHOT_API_KEY` |
| `kimi-code` | `https://api.kimi.ai/coding/v1` | `k3` | `KIMI_API_KEY` |
| `openai` | `https://api.openai.com/v1` | `gpt-4.1-mini` | `OPENAI_API_KEY` |
| `teamorouter` | `https://api.teamorouter.com/v1` | `/models` 首个 | `TEAMOROUTER_API_KEY` |
优先级:CLI → 已保存 credentials → 环境变量 → TUI 询问。详见 [docs/env_openai.md](docs/env_openai.md)。
### 内置能力(摘要)
- **文件**:`read_file` / `list_dir` / `glob` / `grep` / `edit_file` / `apply_patch` / `write_file` / `delete_file` / `move_file`
- **命令**:`run_shell`(沙箱 + 可中断;超时 `CST_SHELL_TIMEOUT`)
- **网络**:`web_fetch`;Kimi/DeepSeek 另有官方 `web_search`
- **任务**:`todo_write`、`ask_user`、`task` 子 agent、后台 `job_*`(完成会推 TUI / 下一回合笔记)
- **扩展**:Skills、插件、MCP([docs/plugins.md](docs/plugins.md));worker 编排([docs/workers.md](docs/workers.md))
同一步里连续只读调用会自动并行(默认最多 8,`CST_PAR_MAX` 可调)。截断结果带 `[truncated … evidence=ID]`,用 `evidence_read` 续读。
### 审批与沙箱
Shift+Tab / `/approval` 在 `ask` → `auto` → `yolo` 间切换。`auto` 下 Linux 优先 Landlock(否则 seccomp),macOS `sandbox_init`,Windows Low IL;详见源码与 [docs/compat.md](docs/compat.md)。
写文件前自动快照,`/undo` 可回滚;工作区 `AGENTS.md` 会注入系统提示。
### TUI 常用
| 按键 / 命令 | 说明 |
|---|---|
| Enter / Esc | 发送(busy 时入队)/ 取消当前回合 |
| Tab / Shift+Tab | 补全或 Build↔Plan / 审批模式 |
| Ctrl+P / Ctrl+T | 命令面板 / 任务面板 |
| Ctrl+X 再按字母 | Leader 快捷键(`/keys` 或 F1 查看) |
| `/provider` `/model` `/effort` `/key` | 切换后端 |
| `/realm` `/motion` | 议会登录提示、Seat Deck 动效 |
| `/web` `/quit` | 起 WebUI / 退出 |
完整快捷键与斜杠命令仍以 TUI 内 `/help`、`/keys` 为准。
### 工具协议(可选)
默认 `native-eos`(OpenAI `tools[]`,流结束后执行)。也可 `native-inc`、文本帧 `ctl` / `ctl-heredoc`、`xml`(`CST_PROTOCOL` / `/protocol`)。日常使用保持默认即可。
--- ---
## 平台 ## CI 与发布
| OS | Arch | TUI | 备注 | `.gitea/workflows/ci.yml`:TinyCC check → 打 Linux/Windows 包 → `scripts/ci_publish.sh`。
|---|---|---|---| 共用逻辑:[scripts/lib_release.sh](scripts/lib_release.sh)。
| Linux glibc/musl | x86_64 / x86 / aarch64 | ncursesw | 静态 Release 为 musl |
| WSL2 | x86_64 | ncursesw | 工作区放在 Linux 文件系统 |
| macOS | arm64 / x86_64 | Homebrew ncurses | |
| Termux | aarch64 等 | Termux ncurses | |
| Windows 10 1903+ | x86_64 | 捆绑 ncurses | shell 为 busybox-w32 |
--- ## 开发者
## 开发者备忘
```bash ```bash
make check make check
scripts/prompt_smoke.sh # 系统提示契约(无 API) scripts/prompt_smoke.sh
scripts/e2e_webui.sh # WebUI 录制流(无 API) scripts/e2e_webui.sh
scripts/e2e_fixture.sh mini_calc --provider deepseek # 可选:带 key 的夹具
``` ```
`fixtures/` 下是可重复的本地任务夹具(`TASK.md` + `check.sh`),用于手工或脚本验收,不是产品依赖。 `icons/agent-motion/` 为 SVG 资产;TUI 默认用字体序列极简轨,`make hermit-font` 生成 PUA 字体轨。
许可证与第三方:`vendor/sashidown`、`vendor/cjson`(MIT)、`vendor/monocypher` 随仓库提供。
+6 -7
View File
@@ -1,16 +1,15 @@
# 文档索引 # Hermit 文档索引
面向日常使用的说明。二进制名仍是 `cst-agent`,配置目录默认 `~/.config/cst-agent/`。 二进制:`hermit`(兼容软链 `cst-agent`)。配置目录:`~/.config/hermit`(若无则回退 `~/.config/cst-agent`)。
| 文档 | 内容 | | 文档 | 内容 |
|---|---| |---|---|
| [env_openai.md](env_openai.md) | Provider、模型、API Key、环境变量 | | [env_openai.md](env_openai.md) | Provider、模型、API Key、环境变量(`HERMIT_*` / `CST_*`) |
| [parliament.md](parliament.md) | 多设备议会(Realm / 席位 / 同步) | | [parliament.md](parliament.md) | 多设备议会 |
| [workers.md](workers.md) | 后台任务、worker、job 完成报告 | | [workers.md](workers.md) | 后台任务、worker、job 完成报告 |
| [webui.md](webui.md) | 内置 HTTP + SSE WebUI | | [webui.md](webui.md) | 内置 HTTP + SSE WebUI |
| [plugins.md](plugins.md) | Skills、插件、MCP | | [plugins.md](plugins.md) | Skills、插件、MCP |
| [compat.md](compat.md) | 平台兼容与验收清单 | | [compat.md](compat.md) | 平台兼容 |
| [README-windows.md](README-windows.md) | Windows 安装、沙箱、构建 | | [README-windows.md](README-windows.md) | Windows |
| [android.md](android.md) | Android / APK | | [android.md](android.md) | Android / APK |
| [windows_selftest.md](windows_selftest.md) | Windows 自测步骤 |
| [archive/](archive/) | 历史设计笔记(非产品文档) | | [archive/](archive/) | 历史设计笔记(非产品文档) |
+21
View File
@@ -0,0 +1,21 @@
# Hermit / 寄居蟹 · 扁平 SVG 动图包
设计原则:遵循原始草图。纯色块、橙色方形身体、紫色眼睛、短脚;无渐变、无阴影、无高光。蓝色终端方块就是寄居蟹的壳。
6 个状态 × 2 个版本,共 12 个 SVG:
- idle:等待输入
- question:请求问题
- thinking:思考,黄色小钳子托腮
- coding:写码,>_ 眼睛
- success:完成
- error:遇到问题
icons/{状态}-shell.svg 是大的带壳图标;icons/{状态}.svg 是小的无壳图标。透明背景、CSS 循环动画,支持系统减少动态效果。
直接打开 index.html 查看,支持深浅背景、暂停、速度调整、自动轮播以及两个版本的 SVG 下载。无联网依赖。
接入示例:<img src="icons/idle.svg" width="24" alt="等待输入">。大图标建议 64px 及以上,小图标建议 24–32px;16px 时状态细节较弱。使用内联 SVG 时可通过 --speed 设置速度。
普通字符终端不直接渲染 SVG;图标适用于支持图形的终端界面、Webview 或 CLI 伴随应用。
更新:橘杏色身体、莓紫眼睛、低饱和蓝绿终端壳。预览主角色以 620ms 缓入缓出插值连续改变眼睛路径与身体姿态,支持中途切换;不重建主角色。状态符号淡入淡出。独立 SVG 保留各状态循环动效;跨状态变形逻辑位于 index.html。
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 80" class="coding" role="img" aria-label="Hermit 写码 带壳"><title>Hermit · 写码</title><style>svg{--speed:1}.hermit-body{transform-origin:32px 35px;animation:hermit-idle calc(3.8s / var(--speed)) cubic-bezier(.45,0,.55,1) infinite}.hermit-eyes{transform-box:fill-box;transform-origin:center;animation:hermit-blink calc(5.4s / var(--speed)) ease-in-out infinite}.question .hermit-body{animation-name:hermit-ask}.coding .hermit-eyes{animation:none}.coding .hermit-body{animation-name:hermit-code;animation-duration:calc(1.4s / var(--speed))}.success .hermit-body{animation-name:hermit-joy}.error .hermit-body{animation-name:hermit-care}.hermit-mark{animation:hermit-mark calc(2.8s / var(--speed)) ease-in-out infinite}.thinking .hermit-eyes{animation:hermit-look calc(3.8s / var(--speed)) ease-in-out infinite}@keyframes hermit-idle{0%,100%{transform:translateY(0) rotate(-1deg) scale(1,1)}50%{transform:translateY(-1.2px) rotate(1deg) scale(1.015,.985)}}@keyframes hermit-blink{0%,42%,48%,100%{transform:scaleY(1)}45%{transform:scaleY(.12)}}@keyframes hermit-ask{0%,100%{transform:rotate(-2deg) translateY(0)}45%,65%{transform:rotate(6deg) translateY(-1.3px)}}@keyframes hermit-code{0%,100%{transform:translateY(0) rotate(-1.4deg)}50%{transform:translateY(-1px) rotate(1.4deg)}}@keyframes hermit-joy{0%,100%{transform:translateY(0) scale(1.02,.98)}45%{transform:translateY(-2.8px) rotate(-3deg) scale(.98,1.02)}70%{transform:translateY(-1px) rotate(2deg)}}@keyframes hermit-care{0%,70%,100%{transform:rotate(0)}78%{transform:rotate(-3deg)}88%{transform:rotate(3deg)}}@keyframes hermit-mark{0%,100%{transform:translateY(0) rotate(0)}50%{transform:translateY(-1.7px) rotate(3deg)}}@keyframes hermit-look{0%,25%,100%{transform:translate(0,0)}45%,70%{transform:translate(1.5px,-.6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><rect x="8" y="12" width="68" height="47" rx="3" fill="#277f94"/><path d="m20 25 5 6-5 6m14 1h10" stroke="#a9d9d7" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none"/><g transform="translate(34 23)"><g class="hermit-body"><path d="M23 36v5m18-5v5" stroke="#a4574f" stroke-width="6" stroke-linecap="round"/><rect x="12" y="15" width="36" height="25" rx="4" fill="#f68042"/><g class="hermit-eyes" fill="none" stroke="#9957ad" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="m27 21 4 4-4 4m10-1h5"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="7 5 52 42" class="coding" role="img" aria-label="Hermit 写码 无壳"><title>Hermit · 写码</title><style>svg{--speed:1}.hermit-body{transform-origin:32px 35px;animation:hermit-idle calc(3.8s / var(--speed)) cubic-bezier(.45,0,.55,1) infinite}.hermit-eyes{transform-box:fill-box;transform-origin:center;animation:hermit-blink calc(5.4s / var(--speed)) ease-in-out infinite}.question .hermit-body{animation-name:hermit-ask}.coding .hermit-eyes{animation:none}.coding .hermit-body{animation-name:hermit-code;animation-duration:calc(1.4s / var(--speed))}.success .hermit-body{animation-name:hermit-joy}.error .hermit-body{animation-name:hermit-care}.hermit-mark{animation:hermit-mark calc(2.8s / var(--speed)) ease-in-out infinite}.thinking .hermit-eyes{animation:hermit-look calc(3.8s / var(--speed)) ease-in-out infinite}@keyframes hermit-idle{0%,100%{transform:translateY(0) rotate(-1deg) scale(1,1)}50%{transform:translateY(-1.2px) rotate(1deg) scale(1.015,.985)}}@keyframes hermit-blink{0%,42%,48%,100%{transform:scaleY(1)}45%{transform:scaleY(.12)}}@keyframes hermit-ask{0%,100%{transform:rotate(-2deg) translateY(0)}45%,65%{transform:rotate(6deg) translateY(-1.3px)}}@keyframes hermit-code{0%,100%{transform:translateY(0) rotate(-1.4deg)}50%{transform:translateY(-1px) rotate(1.4deg)}}@keyframes hermit-joy{0%,100%{transform:translateY(0) scale(1.02,.98)}45%{transform:translateY(-2.8px) rotate(-3deg) scale(.98,1.02)}70%{transform:translateY(-1px) rotate(2deg)}}@keyframes hermit-care{0%,70%,100%{transform:rotate(0)}78%{transform:rotate(-3deg)}88%{transform:rotate(3deg)}}@keyframes hermit-mark{0%,100%{transform:translateY(0) rotate(0)}50%{transform:translateY(-1.7px) rotate(3deg)}}@keyframes hermit-look{0%,25%,100%{transform:translate(0,0)}45%,70%{transform:translate(1.5px,-.6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><g class="hermit-body"><path d="M23 36v5m18-5v5" stroke="#a4574f" stroke-width="6" stroke-linecap="round"/><rect x="12" y="15" width="36" height="25" rx="4" fill="#f68042"/><g class="hermit-eyes" fill="none" stroke="#9957ad" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="m27 21 4 4-4 4m10-1h5"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 80" class="error" role="img" aria-label="Hermit 遇到问题 带壳"><title>Hermit · 遇到问题</title><style>svg{--speed:1}.hermit-body{transform-origin:32px 35px;animation:hermit-idle calc(3.8s / var(--speed)) cubic-bezier(.45,0,.55,1) infinite}.hermit-eyes{transform-box:fill-box;transform-origin:center;animation:hermit-blink calc(5.4s / var(--speed)) ease-in-out infinite}.question .hermit-body{animation-name:hermit-ask}.coding .hermit-eyes{animation:none}.coding .hermit-body{animation-name:hermit-code;animation-duration:calc(1.4s / var(--speed))}.success .hermit-body{animation-name:hermit-joy}.error .hermit-body{animation-name:hermit-care}.hermit-mark{animation:hermit-mark calc(2.8s / var(--speed)) ease-in-out infinite}.thinking .hermit-eyes{animation:hermit-look calc(3.8s / var(--speed)) ease-in-out infinite}@keyframes hermit-idle{0%,100%{transform:translateY(0) rotate(-1deg) scale(1,1)}50%{transform:translateY(-1.2px) rotate(1deg) scale(1.015,.985)}}@keyframes hermit-blink{0%,42%,48%,100%{transform:scaleY(1)}45%{transform:scaleY(.12)}}@keyframes hermit-ask{0%,100%{transform:rotate(-2deg) translateY(0)}45%,65%{transform:rotate(6deg) translateY(-1.3px)}}@keyframes hermit-code{0%,100%{transform:translateY(0) rotate(-1.4deg)}50%{transform:translateY(-1px) rotate(1.4deg)}}@keyframes hermit-joy{0%,100%{transform:translateY(0) scale(1.02,.98)}45%{transform:translateY(-2.8px) rotate(-3deg) scale(.98,1.02)}70%{transform:translateY(-1px) rotate(2deg)}}@keyframes hermit-care{0%,70%,100%{transform:rotate(0)}78%{transform:rotate(-3deg)}88%{transform:rotate(3deg)}}@keyframes hermit-mark{0%,100%{transform:translateY(0) rotate(0)}50%{transform:translateY(-1.7px) rotate(3deg)}}@keyframes hermit-look{0%,25%,100%{transform:translate(0,0)}45%,70%{transform:translate(1.5px,-.6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><rect x="8" y="12" width="68" height="47" rx="3" fill="#277f94"/><path d="m20 25 5 6-5 6m14 1h10" stroke="#a9d9d7" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none"/><g transform="translate(34 23)"><g class="hermit-body"><path d="M23 36v5m18-5v5" stroke="#a4574f" stroke-width="6" stroke-linecap="round"/><rect x="12" y="15" width="36" height="25" rx="4" fill="#f68042"/><g class="hermit-eyes" fill="none" stroke="#9957ad" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="m27 22 5 2m6 0 5-2"/></g><path class="hermit-mark" d="M52 12v6m0 5v.1" stroke="#9957ad" stroke-width="3" stroke-linecap="round"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="7 5 52 42" class="error" role="img" aria-label="Hermit 遇到问题 无壳"><title>Hermit · 遇到问题</title><style>svg{--speed:1}.hermit-body{transform-origin:32px 35px;animation:hermit-idle calc(3.8s / var(--speed)) cubic-bezier(.45,0,.55,1) infinite}.hermit-eyes{transform-box:fill-box;transform-origin:center;animation:hermit-blink calc(5.4s / var(--speed)) ease-in-out infinite}.question .hermit-body{animation-name:hermit-ask}.coding .hermit-eyes{animation:none}.coding .hermit-body{animation-name:hermit-code;animation-duration:calc(1.4s / var(--speed))}.success .hermit-body{animation-name:hermit-joy}.error .hermit-body{animation-name:hermit-care}.hermit-mark{animation:hermit-mark calc(2.8s / var(--speed)) ease-in-out infinite}.thinking .hermit-eyes{animation:hermit-look calc(3.8s / var(--speed)) ease-in-out infinite}@keyframes hermit-idle{0%,100%{transform:translateY(0) rotate(-1deg) scale(1,1)}50%{transform:translateY(-1.2px) rotate(1deg) scale(1.015,.985)}}@keyframes hermit-blink{0%,42%,48%,100%{transform:scaleY(1)}45%{transform:scaleY(.12)}}@keyframes hermit-ask{0%,100%{transform:rotate(-2deg) translateY(0)}45%,65%{transform:rotate(6deg) translateY(-1.3px)}}@keyframes hermit-code{0%,100%{transform:translateY(0) rotate(-1.4deg)}50%{transform:translateY(-1px) rotate(1.4deg)}}@keyframes hermit-joy{0%,100%{transform:translateY(0) scale(1.02,.98)}45%{transform:translateY(-2.8px) rotate(-3deg) scale(.98,1.02)}70%{transform:translateY(-1px) rotate(2deg)}}@keyframes hermit-care{0%,70%,100%{transform:rotate(0)}78%{transform:rotate(-3deg)}88%{transform:rotate(3deg)}}@keyframes hermit-mark{0%,100%{transform:translateY(0) rotate(0)}50%{transform:translateY(-1.7px) rotate(3deg)}}@keyframes hermit-look{0%,25%,100%{transform:translate(0,0)}45%,70%{transform:translate(1.5px,-.6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><g class="hermit-body"><path d="M23 36v5m18-5v5" stroke="#a4574f" stroke-width="6" stroke-linecap="round"/><rect x="12" y="15" width="36" height="25" rx="4" fill="#f68042"/><g class="hermit-eyes" fill="none" stroke="#9957ad" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="m27 22 5 2m6 0 5-2"/></g><path class="hermit-mark" d="M52 12v6m0 5v.1" stroke="#9957ad" stroke-width="3" stroke-linecap="round"/></g></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 80" class="idle" role="img" aria-label="Hermit 等待输入 带壳"><title>Hermit · 等待输入</title><style>svg{--speed:1}.hermit-body{transform-origin:32px 35px;animation:hermit-idle calc(3.8s / var(--speed)) cubic-bezier(.45,0,.55,1) infinite}.hermit-eyes{transform-box:fill-box;transform-origin:center;animation:hermit-blink calc(5.4s / var(--speed)) ease-in-out infinite}.question .hermit-body{animation-name:hermit-ask}.coding .hermit-eyes{animation:none}.coding .hermit-body{animation-name:hermit-code;animation-duration:calc(1.4s / var(--speed))}.success .hermit-body{animation-name:hermit-joy}.error .hermit-body{animation-name:hermit-care}.hermit-mark{animation:hermit-mark calc(2.8s / var(--speed)) ease-in-out infinite}.thinking .hermit-eyes{animation:hermit-look calc(3.8s / var(--speed)) ease-in-out infinite}@keyframes hermit-idle{0%,100%{transform:translateY(0) rotate(-1deg) scale(1,1)}50%{transform:translateY(-1.2px) rotate(1deg) scale(1.015,.985)}}@keyframes hermit-blink{0%,42%,48%,100%{transform:scaleY(1)}45%{transform:scaleY(.12)}}@keyframes hermit-ask{0%,100%{transform:rotate(-2deg) translateY(0)}45%,65%{transform:rotate(6deg) translateY(-1.3px)}}@keyframes hermit-code{0%,100%{transform:translateY(0) rotate(-1.4deg)}50%{transform:translateY(-1px) rotate(1.4deg)}}@keyframes hermit-joy{0%,100%{transform:translateY(0) scale(1.02,.98)}45%{transform:translateY(-2.8px) rotate(-3deg) scale(.98,1.02)}70%{transform:translateY(-1px) rotate(2deg)}}@keyframes hermit-care{0%,70%,100%{transform:rotate(0)}78%{transform:rotate(-3deg)}88%{transform:rotate(3deg)}}@keyframes hermit-mark{0%,100%{transform:translateY(0) rotate(0)}50%{transform:translateY(-1.7px) rotate(3deg)}}@keyframes hermit-look{0%,25%,100%{transform:translate(0,0)}45%,70%{transform:translate(1.5px,-.6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><rect x="8" y="12" width="68" height="47" rx="3" fill="#277f94"/><path d="m20 25 5 6-5 6m14 1h10" stroke="#a9d9d7" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none"/><g transform="translate(34 23)"><g class="hermit-body"><path d="M23 36v5m18-5v5" stroke="#a4574f" stroke-width="6" stroke-linecap="round"/><rect x="12" y="15" width="36" height="25" rx="4" fill="#f68042"/><g class="hermit-eyes" fill="none" stroke="#9957ad" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M29 23v5m10-5v5"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="7 5 52 42" class="idle" role="img" aria-label="Hermit 等待输入 无壳"><title>Hermit · 等待输入</title><style>svg{--speed:1}.hermit-body{transform-origin:32px 35px;animation:hermit-idle calc(3.8s / var(--speed)) cubic-bezier(.45,0,.55,1) infinite}.hermit-eyes{transform-box:fill-box;transform-origin:center;animation:hermit-blink calc(5.4s / var(--speed)) ease-in-out infinite}.question .hermit-body{animation-name:hermit-ask}.coding .hermit-eyes{animation:none}.coding .hermit-body{animation-name:hermit-code;animation-duration:calc(1.4s / var(--speed))}.success .hermit-body{animation-name:hermit-joy}.error .hermit-body{animation-name:hermit-care}.hermit-mark{animation:hermit-mark calc(2.8s / var(--speed)) ease-in-out infinite}.thinking .hermit-eyes{animation:hermit-look calc(3.8s / var(--speed)) ease-in-out infinite}@keyframes hermit-idle{0%,100%{transform:translateY(0) rotate(-1deg) scale(1,1)}50%{transform:translateY(-1.2px) rotate(1deg) scale(1.015,.985)}}@keyframes hermit-blink{0%,42%,48%,100%{transform:scaleY(1)}45%{transform:scaleY(.12)}}@keyframes hermit-ask{0%,100%{transform:rotate(-2deg) translateY(0)}45%,65%{transform:rotate(6deg) translateY(-1.3px)}}@keyframes hermit-code{0%,100%{transform:translateY(0) rotate(-1.4deg)}50%{transform:translateY(-1px) rotate(1.4deg)}}@keyframes hermit-joy{0%,100%{transform:translateY(0) scale(1.02,.98)}45%{transform:translateY(-2.8px) rotate(-3deg) scale(.98,1.02)}70%{transform:translateY(-1px) rotate(2deg)}}@keyframes hermit-care{0%,70%,100%{transform:rotate(0)}78%{transform:rotate(-3deg)}88%{transform:rotate(3deg)}}@keyframes hermit-mark{0%,100%{transform:translateY(0) rotate(0)}50%{transform:translateY(-1.7px) rotate(3deg)}}@keyframes hermit-look{0%,25%,100%{transform:translate(0,0)}45%,70%{transform:translate(1.5px,-.6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><g class="hermit-body"><path d="M23 36v5m18-5v5" stroke="#a4574f" stroke-width="6" stroke-linecap="round"/><rect x="12" y="15" width="36" height="25" rx="4" fill="#f68042"/><g class="hermit-eyes" fill="none" stroke="#9957ad" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M29 23v5m10-5v5"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 80" class="question" role="img" aria-label="Hermit 请求问题 带壳"><title>Hermit · 请求问题</title><style>svg{--speed:1}.hermit-body{transform-origin:32px 35px;animation:hermit-idle calc(3.8s / var(--speed)) cubic-bezier(.45,0,.55,1) infinite}.hermit-eyes{transform-box:fill-box;transform-origin:center;animation:hermit-blink calc(5.4s / var(--speed)) ease-in-out infinite}.question .hermit-body{animation-name:hermit-ask}.coding .hermit-eyes{animation:none}.coding .hermit-body{animation-name:hermit-code;animation-duration:calc(1.4s / var(--speed))}.success .hermit-body{animation-name:hermit-joy}.error .hermit-body{animation-name:hermit-care}.hermit-mark{animation:hermit-mark calc(2.8s / var(--speed)) ease-in-out infinite}.thinking .hermit-eyes{animation:hermit-look calc(3.8s / var(--speed)) ease-in-out infinite}@keyframes hermit-idle{0%,100%{transform:translateY(0) rotate(-1deg) scale(1,1)}50%{transform:translateY(-1.2px) rotate(1deg) scale(1.015,.985)}}@keyframes hermit-blink{0%,42%,48%,100%{transform:scaleY(1)}45%{transform:scaleY(.12)}}@keyframes hermit-ask{0%,100%{transform:rotate(-2deg) translateY(0)}45%,65%{transform:rotate(6deg) translateY(-1.3px)}}@keyframes hermit-code{0%,100%{transform:translateY(0) rotate(-1.4deg)}50%{transform:translateY(-1px) rotate(1.4deg)}}@keyframes hermit-joy{0%,100%{transform:translateY(0) scale(1.02,.98)}45%{transform:translateY(-2.8px) rotate(-3deg) scale(.98,1.02)}70%{transform:translateY(-1px) rotate(2deg)}}@keyframes hermit-care{0%,70%,100%{transform:rotate(0)}78%{transform:rotate(-3deg)}88%{transform:rotate(3deg)}}@keyframes hermit-mark{0%,100%{transform:translateY(0) rotate(0)}50%{transform:translateY(-1.7px) rotate(3deg)}}@keyframes hermit-look{0%,25%,100%{transform:translate(0,0)}45%,70%{transform:translate(1.5px,-.6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><rect x="8" y="12" width="68" height="47" rx="3" fill="#277f94"/><path d="m20 25 5 6-5 6m14 1h10" stroke="#a9d9d7" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none"/><g transform="translate(34 23)"><g class="hermit-body"><path d="M23 36v5m18-5v5" stroke="#a4574f" stroke-width="6" stroke-linecap="round"/><rect x="12" y="15" width="36" height="25" rx="4" fill="#f68042"/><g class="hermit-eyes" fill="none" stroke="#9957ad" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M30 22v4m10-4v4"/></g><path class="hermit-mark" d="M47 13c0-6 8-6 8-1 0 3-4 3-4 6m0 5v.1" stroke="#9957ad" stroke-width="3" stroke-linecap="round" fill="none"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="7 5 52 42" class="question" role="img" aria-label="Hermit 请求问题 无壳"><title>Hermit · 请求问题</title><style>svg{--speed:1}.hermit-body{transform-origin:32px 35px;animation:hermit-idle calc(3.8s / var(--speed)) cubic-bezier(.45,0,.55,1) infinite}.hermit-eyes{transform-box:fill-box;transform-origin:center;animation:hermit-blink calc(5.4s / var(--speed)) ease-in-out infinite}.question .hermit-body{animation-name:hermit-ask}.coding .hermit-eyes{animation:none}.coding .hermit-body{animation-name:hermit-code;animation-duration:calc(1.4s / var(--speed))}.success .hermit-body{animation-name:hermit-joy}.error .hermit-body{animation-name:hermit-care}.hermit-mark{animation:hermit-mark calc(2.8s / var(--speed)) ease-in-out infinite}.thinking .hermit-eyes{animation:hermit-look calc(3.8s / var(--speed)) ease-in-out infinite}@keyframes hermit-idle{0%,100%{transform:translateY(0) rotate(-1deg) scale(1,1)}50%{transform:translateY(-1.2px) rotate(1deg) scale(1.015,.985)}}@keyframes hermit-blink{0%,42%,48%,100%{transform:scaleY(1)}45%{transform:scaleY(.12)}}@keyframes hermit-ask{0%,100%{transform:rotate(-2deg) translateY(0)}45%,65%{transform:rotate(6deg) translateY(-1.3px)}}@keyframes hermit-code{0%,100%{transform:translateY(0) rotate(-1.4deg)}50%{transform:translateY(-1px) rotate(1.4deg)}}@keyframes hermit-joy{0%,100%{transform:translateY(0) scale(1.02,.98)}45%{transform:translateY(-2.8px) rotate(-3deg) scale(.98,1.02)}70%{transform:translateY(-1px) rotate(2deg)}}@keyframes hermit-care{0%,70%,100%{transform:rotate(0)}78%{transform:rotate(-3deg)}88%{transform:rotate(3deg)}}@keyframes hermit-mark{0%,100%{transform:translateY(0) rotate(0)}50%{transform:translateY(-1.7px) rotate(3deg)}}@keyframes hermit-look{0%,25%,100%{transform:translate(0,0)}45%,70%{transform:translate(1.5px,-.6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><g class="hermit-body"><path d="M23 36v5m18-5v5" stroke="#a4574f" stroke-width="6" stroke-linecap="round"/><rect x="12" y="15" width="36" height="25" rx="4" fill="#f68042"/><g class="hermit-eyes" fill="none" stroke="#9957ad" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M30 22v4m10-4v4"/></g><path class="hermit-mark" d="M47 13c0-6 8-6 8-1 0 3-4 3-4 6m0 5v.1" stroke="#9957ad" stroke-width="3" stroke-linecap="round" fill="none"/></g></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 80" class="success" role="img" aria-label="Hermit 完成 带壳"><title>Hermit · 完成</title><style>svg{--speed:1}.hermit-body{transform-origin:32px 35px;animation:hermit-idle calc(3.8s / var(--speed)) cubic-bezier(.45,0,.55,1) infinite}.hermit-eyes{transform-box:fill-box;transform-origin:center;animation:hermit-blink calc(5.4s / var(--speed)) ease-in-out infinite}.question .hermit-body{animation-name:hermit-ask}.coding .hermit-eyes{animation:none}.coding .hermit-body{animation-name:hermit-code;animation-duration:calc(1.4s / var(--speed))}.success .hermit-body{animation-name:hermit-joy}.error .hermit-body{animation-name:hermit-care}.hermit-mark{animation:hermit-mark calc(2.8s / var(--speed)) ease-in-out infinite}.thinking .hermit-eyes{animation:hermit-look calc(3.8s / var(--speed)) ease-in-out infinite}@keyframes hermit-idle{0%,100%{transform:translateY(0) rotate(-1deg) scale(1,1)}50%{transform:translateY(-1.2px) rotate(1deg) scale(1.015,.985)}}@keyframes hermit-blink{0%,42%,48%,100%{transform:scaleY(1)}45%{transform:scaleY(.12)}}@keyframes hermit-ask{0%,100%{transform:rotate(-2deg) translateY(0)}45%,65%{transform:rotate(6deg) translateY(-1.3px)}}@keyframes hermit-code{0%,100%{transform:translateY(0) rotate(-1.4deg)}50%{transform:translateY(-1px) rotate(1.4deg)}}@keyframes hermit-joy{0%,100%{transform:translateY(0) scale(1.02,.98)}45%{transform:translateY(-2.8px) rotate(-3deg) scale(.98,1.02)}70%{transform:translateY(-1px) rotate(2deg)}}@keyframes hermit-care{0%,70%,100%{transform:rotate(0)}78%{transform:rotate(-3deg)}88%{transform:rotate(3deg)}}@keyframes hermit-mark{0%,100%{transform:translateY(0) rotate(0)}50%{transform:translateY(-1.7px) rotate(3deg)}}@keyframes hermit-look{0%,25%,100%{transform:translate(0,0)}45%,70%{transform:translate(1.5px,-.6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><rect x="8" y="12" width="68" height="47" rx="3" fill="#277f94"/><path d="m20 25 5 6-5 6m14 1h10" stroke="#a9d9d7" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none"/><g transform="translate(34 23)"><g class="hermit-body"><path d="M23 36v5m18-5v5" stroke="#a4574f" stroke-width="6" stroke-linecap="round"/><rect x="12" y="15" width="36" height="25" rx="4" fill="#f68042"/><g class="hermit-eyes" fill="none" stroke="#9957ad" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="m27 25 3-3 3 3m5 0 3-3 3 3"/></g></g></g></svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="7 5 52 42" class="success" role="img" aria-label="Hermit 完成 无壳"><title>Hermit · 完成</title><style>svg{--speed:1}.hermit-body{transform-origin:32px 35px;animation:hermit-idle calc(3.8s / var(--speed)) cubic-bezier(.45,0,.55,1) infinite}.hermit-eyes{transform-box:fill-box;transform-origin:center;animation:hermit-blink calc(5.4s / var(--speed)) ease-in-out infinite}.question .hermit-body{animation-name:hermit-ask}.coding .hermit-eyes{animation:none}.coding .hermit-body{animation-name:hermit-code;animation-duration:calc(1.4s / var(--speed))}.success .hermit-body{animation-name:hermit-joy}.error .hermit-body{animation-name:hermit-care}.hermit-mark{animation:hermit-mark calc(2.8s / var(--speed)) ease-in-out infinite}.thinking .hermit-eyes{animation:hermit-look calc(3.8s / var(--speed)) ease-in-out infinite}@keyframes hermit-idle{0%,100%{transform:translateY(0) rotate(-1deg) scale(1,1)}50%{transform:translateY(-1.2px) rotate(1deg) scale(1.015,.985)}}@keyframes hermit-blink{0%,42%,48%,100%{transform:scaleY(1)}45%{transform:scaleY(.12)}}@keyframes hermit-ask{0%,100%{transform:rotate(-2deg) translateY(0)}45%,65%{transform:rotate(6deg) translateY(-1.3px)}}@keyframes hermit-code{0%,100%{transform:translateY(0) rotate(-1.4deg)}50%{transform:translateY(-1px) rotate(1.4deg)}}@keyframes hermit-joy{0%,100%{transform:translateY(0) scale(1.02,.98)}45%{transform:translateY(-2.8px) rotate(-3deg) scale(.98,1.02)}70%{transform:translateY(-1px) rotate(2deg)}}@keyframes hermit-care{0%,70%,100%{transform:rotate(0)}78%{transform:rotate(-3deg)}88%{transform:rotate(3deg)}}@keyframes hermit-mark{0%,100%{transform:translateY(0) rotate(0)}50%{transform:translateY(-1.7px) rotate(3deg)}}@keyframes hermit-look{0%,25%,100%{transform:translate(0,0)}45%,70%{transform:translate(1.5px,-.6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><g class="hermit-body"><path d="M23 36v5m18-5v5" stroke="#a4574f" stroke-width="6" stroke-linecap="round"/><rect x="12" y="15" width="36" height="25" rx="4" fill="#f68042"/><g class="hermit-eyes" fill="none" stroke="#9957ad" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="m27 25 3-3 3 3m5 0 3-3 3 3"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 80" class="thinking" role="img" aria-label="Hermit 思考 带壳"><title>Hermit · 思考</title><style>svg{--speed:1}.hermit-body{transform-origin:32px 35px;animation:hermit-idle calc(3.8s / var(--speed)) cubic-bezier(.45,0,.55,1) infinite}.hermit-eyes{transform-box:fill-box;transform-origin:center;animation:hermit-blink calc(5.4s / var(--speed)) ease-in-out infinite}.question .hermit-body{animation-name:hermit-ask}.coding .hermit-eyes{animation:none}.coding .hermit-body{animation-name:hermit-code;animation-duration:calc(1.4s / var(--speed))}.success .hermit-body{animation-name:hermit-joy}.error .hermit-body{animation-name:hermit-care}.hermit-mark{animation:hermit-mark calc(2.8s / var(--speed)) ease-in-out infinite}.thinking .hermit-eyes{animation:hermit-look calc(3.8s / var(--speed)) ease-in-out infinite}@keyframes hermit-idle{0%,100%{transform:translateY(0) rotate(-1deg) scale(1,1)}50%{transform:translateY(-1.2px) rotate(1deg) scale(1.015,.985)}}@keyframes hermit-blink{0%,42%,48%,100%{transform:scaleY(1)}45%{transform:scaleY(.12)}}@keyframes hermit-ask{0%,100%{transform:rotate(-2deg) translateY(0)}45%,65%{transform:rotate(6deg) translateY(-1.3px)}}@keyframes hermit-code{0%,100%{transform:translateY(0) rotate(-1.4deg)}50%{transform:translateY(-1px) rotate(1.4deg)}}@keyframes hermit-joy{0%,100%{transform:translateY(0) scale(1.02,.98)}45%{transform:translateY(-2.8px) rotate(-3deg) scale(.98,1.02)}70%{transform:translateY(-1px) rotate(2deg)}}@keyframes hermit-care{0%,70%,100%{transform:rotate(0)}78%{transform:rotate(-3deg)}88%{transform:rotate(3deg)}}@keyframes hermit-mark{0%,100%{transform:translateY(0) rotate(0)}50%{transform:translateY(-1.7px) rotate(3deg)}}@keyframes hermit-look{0%,25%,100%{transform:translate(0,0)}45%,70%{transform:translate(1.5px,-.6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><rect x="8" y="12" width="68" height="47" rx="3" fill="#277f94"/><path d="m20 25 5 6-5 6m14 1h10" stroke="#a9d9d7" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" fill="none"/><g transform="translate(34 23)"><g class="hermit-body"><path d="M23 36v5m18-5v5" stroke="#a4574f" stroke-width="6" stroke-linecap="round"/><rect x="12" y="15" width="36" height="25" rx="4" fill="#f68042"/><g class="hermit-eyes" fill="none" stroke="#9957ad" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M31 22v3m10-3v3"/></g><path class="hermit-mark" d="M43 32v7h-8" stroke="#e9ba47" stroke-width="6" fill="none" stroke-linecap="round" stroke-linejoin="round"/></g></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="7 5 52 42" class="thinking" role="img" aria-label="Hermit 思考 无壳"><title>Hermit · 思考</title><style>svg{--speed:1}.hermit-body{transform-origin:32px 35px;animation:hermit-idle calc(3.8s / var(--speed)) cubic-bezier(.45,0,.55,1) infinite}.hermit-eyes{transform-box:fill-box;transform-origin:center;animation:hermit-blink calc(5.4s / var(--speed)) ease-in-out infinite}.question .hermit-body{animation-name:hermit-ask}.coding .hermit-eyes{animation:none}.coding .hermit-body{animation-name:hermit-code;animation-duration:calc(1.4s / var(--speed))}.success .hermit-body{animation-name:hermit-joy}.error .hermit-body{animation-name:hermit-care}.hermit-mark{animation:hermit-mark calc(2.8s / var(--speed)) ease-in-out infinite}.thinking .hermit-eyes{animation:hermit-look calc(3.8s / var(--speed)) ease-in-out infinite}@keyframes hermit-idle{0%,100%{transform:translateY(0) rotate(-1deg) scale(1,1)}50%{transform:translateY(-1.2px) rotate(1deg) scale(1.015,.985)}}@keyframes hermit-blink{0%,42%,48%,100%{transform:scaleY(1)}45%{transform:scaleY(.12)}}@keyframes hermit-ask{0%,100%{transform:rotate(-2deg) translateY(0)}45%,65%{transform:rotate(6deg) translateY(-1.3px)}}@keyframes hermit-code{0%,100%{transform:translateY(0) rotate(-1.4deg)}50%{transform:translateY(-1px) rotate(1.4deg)}}@keyframes hermit-joy{0%,100%{transform:translateY(0) scale(1.02,.98)}45%{transform:translateY(-2.8px) rotate(-3deg) scale(.98,1.02)}70%{transform:translateY(-1px) rotate(2deg)}}@keyframes hermit-care{0%,70%,100%{transform:rotate(0)}78%{transform:rotate(-3deg)}88%{transform:rotate(3deg)}}@keyframes hermit-mark{0%,100%{transform:translateY(0) rotate(0)}50%{transform:translateY(-1.7px) rotate(3deg)}}@keyframes hermit-look{0%,25%,100%{transform:translate(0,0)}45%,70%{transform:translate(1.5px,-.6px)}}@media(prefers-reduced-motion:reduce){*{animation:none!important}}</style><g class="hermit-body"><path d="M23 36v5m18-5v5" stroke="#a4574f" stroke-width="6" stroke-linecap="round"/><rect x="12" y="15" width="36" height="25" rx="4" fill="#f68042"/><g class="hermit-eyes" fill="none" stroke="#9957ad" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M31 22v3m10-3v3"/></g><path class="hermit-mark" d="M43 32v7h-8" stroke="#e9ba47" stroke-width="6" fill="none" stroke-linecap="round" stroke-linejoin="round"/></g></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.
+63
View File
@@ -0,0 +1,63 @@
#ifndef CST_HERMIT_GLYPH_H
#define CST_HERMIT_GLYPH_H
#include <stddef.h>
#include <wchar.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Product display name (user-visible). Binary install name is "hermit". */
#define HERMIT_NAME "Hermit"
#define HERMIT_BIN "hermit"
#define HERMIT_LEGACY_BIN "cst-agent"
#define HERMIT_CONFIG_DIR "hermit"
#define HERMIT_LEGACY_CONFIG_DIR "cst-agent"
/* Icon states mirror icons/agent-motion SVG set. */
typedef enum {
HERMIT_ST_IDLE = 0,
HERMIT_ST_QUESTION,
HERMIT_ST_THINKING,
HERMIT_ST_CODING,
HERMIT_ST_SUCCESS,
HERMIT_ST_ERROR,
HERMIT_ST_N
} hermit_state_t;
typedef enum {
HERMIT_ICON_AUTO = 0,
HERMIT_ICON_MINI,
HERMIT_ICON_FONT
} hermit_icon_mode_t;
/* HERMIT_<SUFFIX> then CST_<SUFFIX>. Never returns NULL (may be ""). */
const char *hermit_getenv(const char *suffix);
hermit_icon_mode_t hermit_icon_mode(void);
/* Effective mode after AUTO resolution (ascii forces MINI). */
hermit_icon_mode_t hermit_icon_mode_eff(int ascii);
/* Small badge UTF-8 (or ASCII). frame advances animation. */
const char *hermit_mini_badge(hermit_state_t st, int frame, int ascii);
/* Home shell crab: up to *nlines strings (not owned). Returns line count. */
int hermit_shell_art(hermit_state_t st, int with_shell, int ascii,
const char **lines, int max_lines);
/* Color roles for art cells: 0=body(crab) 1=shell 2=eye 3=leg 4=prompt 5=mark */
int hermit_shell_cell_role(hermit_state_t st, int with_shell, int row, int col);
/* PUA base U+E700; layout: shell*HERMIT_ST_N*HERMIT_FONT_FRAMES + st*FRAMES + frame */
#define HERMIT_FONT_PUA_BASE 0xE700
#define HERMIT_FONT_FRAMES 8
wchar_t hermit_font_codepoint(hermit_state_t st, int frame, int with_shell);
/* Map agent UI signals → icon state. */
hermit_state_t hermit_state_from_ui(int busy, int ask, int error);
#ifdef __cplusplus
}
#endif
#endif
+2 -2
View File
@@ -2,8 +2,8 @@
#define CST_VERSION_H #define CST_VERSION_H
#define CST_VERSION_MAJOR 0 #define CST_VERSION_MAJOR 0
#define CST_VERSION_MINOR 9 #define CST_VERSION_MINOR 10
#define CST_VERSION_PATCH 34 #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)
+17 -10
View File
@@ -44,7 +44,9 @@ run_make() {
install_bin() { install_bin() {
local prefix="$1" bin="$2" local prefix="$1" bin="$2"
mkdir -p "$prefix/bin" mkdir -p "$prefix/bin"
install -m 0755 "$bin" "$prefix/bin/cst-agent" install -m 0755 "$bin" "$prefix/bin/hermit"
ln -sfr "$prefix/bin/hermit" "$prefix/bin/cst-agent" 2>/dev/null || \
ln -sf hermit "$prefix/bin/cst-agent"
} }
install_helpers() { install_helpers() {
@@ -60,7 +62,7 @@ EOF
#!$(command -v bash) #!$(command -v bash)
# Manage the parliament seat service (foreground under systemd, or --service). # Manage the parliament seat service (foreground under systemd, or --service).
set -euo pipefail set -euo pipefail
BIN="$prefix/bin/cst-agent" BIN="$prefix/bin/hermit"
cmd="\${1:-status}" cmd="\${1:-status}"
case "\$cmd" in case "\$cmd" in
status) status)
@@ -102,19 +104,24 @@ install_from_release() {
if [[ -z "$ver" ]]; then if [[ -z "$ver" ]]; then
ver="$(cst_version)" ver="$(cst_version)"
fi fi
local name="cst-agent-$ver-linux-$arch.tar.gz" local name="hermit-$ver-linux-$arch.tar.gz"
# One-cycle legacy asset name fallback handled below if download fails.
local tmp local tmp
tmp="$(mktemp -d /tmp/cst-rel.XXXXXX)" tmp="$(mktemp -d /tmp/cst-rel.XXXXXX)"
# shellcheck disable=SC2046 # shellcheck disable=SC2046
local legacy="cst-agent-$ver-linux-$arch.tar.gz"
if ! cst_fetch "$tmp/$name" $(cst_release_asset_urls "$ver" "$name"); then if ! cst_fetch "$tmp/$name" $(cst_release_asset_urls "$ver" "$name"); then
echo "release download failed for $name; falling back to source build" >&2 if ! cst_fetch "$tmp/$legacy" $(cst_release_asset_urls "$ver" "$legacy"); then
echo "release download failed; falling back to source build" >&2
rm -rf "$tmp" rm -rf "$tmp"
return 1 return 1
fi fi
name="$legacy"
fi
tar -C "$tmp" -xzf "$tmp/$name" tar -C "$tmp" -xzf "$tmp/$name"
local bin local bin
bin="$(find "$tmp" -type f -name cst-agent | head -1)" bin="$(find "$tmp" -type f \( -name hermit -o -name cst-agent \) | head -1)"
[[ -x "$bin" ]] || { echo "tarball missing cst-agent" >&2; rm -rf "$tmp"; return 1; } [[ -x "$bin" ]] || { echo "tarball missing hermit/cst-agent" >&2; rm -rf "$tmp"; return 1; }
install_bin "$prefix" "$bin" install_bin "$prefix" "$bin"
rm -rf "$tmp" rm -rf "$tmp"
echo "==> installed release $ver ($arch) -> $prefix/bin/cst-agent" echo "==> installed release $ver ($arch) -> $prefix/bin/cst-agent"
@@ -177,7 +184,7 @@ main() {
if install_from_release "$prefix" "$dir"; then if install_from_release "$prefix" "$dir"; then
install_helpers "$prefix" "$dir" install_helpers "$prefix" "$dir"
echo echo
echo "OK: $("$prefix/bin/cst-agent" --version) (release install)" echo "OK: $("$prefix/bin/hermit" --version) (release install)"
case ":$PATH:" in case ":$PATH:" in
*":$prefix/bin:"*) ;; *":$prefix/bin:"*) ;;
*) echo "Add to PATH: export PATH=\"$prefix/bin:\$PATH\"" ;; *) echo "Add to PATH: export PATH=\"$prefix/bin:\$PATH\"" ;;
@@ -209,14 +216,14 @@ main() {
install_helpers "$prefix" "$dir" install_helpers "$prefix" "$dir"
echo echo
echo "OK: $("$prefix/bin/cst-agent" --version)" echo "OK: $("$prefix/bin/hermit" --version)"
case ":$PATH:" in case ":$PATH:" in
*":$prefix/bin:"*) ;; *":$prefix/bin:"*) ;;
*) echo "Add to PATH: export PATH=\"$prefix/bin:\$PATH\"" ;; *) echo "Add to PATH: export PATH=\"$prefix/bin:\$PATH\"" ;;
esac esac
echo "Run 'cst-agent' inside a project directory; upgrade with 'cst-agent-update'." echo "Run 'hermit' (or 'cst-agent') in a project; upgrade with 'cst-agent-update'."
echo "Fast upgrade from Release: 'cst-agent-update --release'." echo "Fast upgrade from Release: 'cst-agent-update --release'."
echo "Parliament seat service helper: 'cst-agent-service status|install-unit'." echo "Parliament helper: 'cst-agent-service status|install-unit'."
echo "The first start asks for the provider and its API key and saves them." echo "The first start asks for the provider and its API key and saves them."
} }
+202
View File
@@ -0,0 +1,202 @@
#!/usr/bin/env python3
"""Batch-convert icons/agent-motion SVG states into a color emoji-style font.
Outputs:
res/hermit-icons.ttf (CBDT/CBLC bitmap glyphs when fonttools works)
.gen/hermit_font_map.inc (PUA codepoint table for the TUI)
PUA layout matches cst_hermit_glyph.h:
U+E700 + (shell * N_STATE + state) * N_FRAME + frame
Requires (optional): rsvg-convert or cairosvg, pillow, fonttools.
Without them, still writes hermit_font_map.inc so FONT mode can emit codepoints.
"""
from __future__ import annotations
import struct
import subprocess
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
ICONS = ROOT / "icons" / "agent-motion" / "icons"
OUT_TTF = ROOT / "res" / "hermit-icons.ttf"
OUT_INC = ROOT / ".gen" / "hermit_font_map.inc"
STATES = ["idle", "question", "thinking", "coding", "success", "error"]
N_FRAME = 8
PUA_BASE = 0xE700
def codepoint(shell: int, state: int, frame: int) -> int:
return PUA_BASE + (shell * len(STATES) + state) * N_FRAME + frame
def write_map_inc() -> None:
OUT_INC.parent.mkdir(parents=True, exist_ok=True)
lines = [
"/* Generated by scripts/build_hermit_font.py — do not edit. */",
f"#define HERMIT_FONT_PUA_BASE_GEN 0x{PUA_BASE:X}",
f"#define HERMIT_FONT_FRAMES_GEN {N_FRAME}",
f"#define HERMIT_FONT_STATES_GEN {len(STATES)}",
"/* state names: " + ", ".join(STATES) + " */",
"",
]
OUT_INC.write_text("\n".join(lines) + "\n", encoding="utf-8")
print(f"wrote {OUT_INC}")
def render_svg(svg: Path, png: Path, size: int) -> bool:
png.parent.mkdir(parents=True, exist_ok=True)
try:
if subprocess.call(
["rsvg-convert", "-w", str(size), "-h", str(size), "-o", str(png), str(svg)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
) == 0:
return png.is_file()
except FileNotFoundError:
pass
try:
import cairosvg # type: ignore
cairosvg.svg2png(url=str(svg), write_to=str(png), output_width=size, output_height=size)
return png.is_file()
except Exception:
return False
def build_ttf(png_by_cp: dict[int, Path]) -> bool:
try:
from fontTools.fontBuilder import FontBuilder # type: ignore
from fontTools.ttLib import TTFont # type: ignore
from fontTools.ttLib.tables.E_B_D_T_ import table_E_B_D_T_ # noqa: F401
from fontTools.ttLib.tables.C_B_D_T_ import table_C_B_D_T_ # noqa: F401
from fontTools.ttLib.tables.C_B_L_C_ import table_C_B_L_C_ # noqa: F401
from fontTools.ttLib.tables._g_l_y_f import Glyph # type: ignore
from fontTools.pens.ttGlyphPen import TTGlyphPen # type: ignore
except Exception as e:
print(f"fonttools CBDT path unavailable: {e}", file=sys.stderr)
return False
# Minimal TTF with empty glyf + Unicode cmap to PUA; color bitmaps via nanoemoji-less stub:
# Prefer shipping PNGs + cmap so terminals that map PUA→font face still get something.
try:
from PIL import Image # type: ignore
except Exception as e:
print(f"pillow required for font pack: {e}", file=sys.stderr)
return False
# Simplified: write a placeholder TTF that only has cmap entries; bitmaps
# need full CBDT authoring. For now embed as sbix-like custom is heavy —
# create a valid empty-outline font with correct cmap so install works, and
# document that full color CBDT needs `nanoemoji` when available.
glyph_order = [".notdef"]
cmap = {}
for cp, png in sorted(png_by_cp.items()):
name = f"u{cp:04X}"
glyph_order.append(name)
cmap[cp] = name
fb = FontBuilder(1024, isTTF=True)
fb.setupGlyphOrder(glyph_order)
fb.setupCharacterMap(cmap)
pen = TTGlyphPen(None)
empty = pen.glyph()
glyphs = {".notdef": empty}
for name in glyph_order[1:]:
glyphs[name] = empty
fb.setupGlyf(glyphs)
fb.setupHorizontalMetrics({n: (1024, 0) for n in glyph_order})
fb.setupHorizontalHeader(ascent=1024, descent=0)
fb.setupNameTable(
{
"familyName": "Hermit Icons",
"styleName": "Regular",
"uniqueFontIdentifier": "Hermit Icons",
"fullName": "Hermit Icons",
"psName": "HermitIcons",
"version": "0.10",
}
)
fb.setupOS2()
fb.setupPost()
OUT_TTF.parent.mkdir(parents=True, exist_ok=True)
fb.save(str(OUT_TTF))
print(f"wrote {OUT_TTF} (cmap + empty outlines; color CBDT via nanoemoji optional)")
# Touch PNGs kept beside for tooling
dump = ROOT / "res" / "hermit-icon-frames"
dump.mkdir(parents=True, exist_ok=True)
for cp, png in png_by_cp.items():
target = dump / f"u{cp:04X}.png"
target.write_bytes(png.read_bytes())
print(f"wrote {len(png_by_cp)} frames under {dump}")
return True
def try_nanoemoji(svgs: list[Path]) -> bool:
"""If nanoemoji is installed, build a proper color font from SVGs."""
if not svgs:
return False
try:
with tempfile.TemporaryDirectory() as td:
td = Path(td)
for i, s in enumerate(svgs):
(td / f"{i:03d}.svg").write_bytes(s.read_bytes())
out = td / "HermitIcons.ttf"
rc = subprocess.call(
["nanoemoji", f"--output_file={out}", *sorted(str(p) for p in td.glob("*.svg"))],
cwd=str(td),
)
if rc == 0 and out.is_file():
OUT_TTF.parent.mkdir(parents=True, exist_ok=True)
OUT_TTF.write_bytes(out.read_bytes())
print(f"wrote {OUT_TTF} via nanoemoji")
return True
except FileNotFoundError:
pass
except Exception as e:
print(f"nanoemoji failed: {e}", file=sys.stderr)
return False
def main() -> int:
write_map_inc()
if not ICONS.is_dir():
print(f"missing {ICONS}", file=sys.stderr)
return 1
svg_list: list[Path] = []
png_by_cp: dict[int, Path] = {}
with tempfile.TemporaryDirectory() as td:
td = Path(td)
for shell, suffix in ((0, ""), (1, "-shell")):
for si, st in enumerate(STATES):
svg = ICONS / f"{st}{suffix}.svg"
if not svg.is_file():
print(f"skip missing {svg.name}", file=sys.stderr)
continue
svg_list.append(svg)
# One master frame per state for now; duplicate across N_FRAME.
png = td / f"{shell}_{st}.png"
if not render_svg(svg, png, 64):
print(f"render failed: {svg.name}", file=sys.stderr)
continue
for fr in range(N_FRAME):
png_by_cp[codepoint(shell, si, fr)] = png
if try_nanoemoji(svg_list):
return 0
if png_by_cp and build_ttf(png_by_cp):
return 0
print(
"font binary not built (install rsvg-convert/cairosvg + pillow + fonttools "
"or nanoemoji). Map .inc is ready for PUA emission.",
file=sys.stderr,
)
return 0 # map is enough for compile
if __name__ == "__main__":
sys.exit(main())
+3 -1
View File
@@ -1,5 +1,5 @@
[Unit] [Unit]
Description=CST-Agent parliament seat service Description=Hermit parliament seat service
Documentation=file://%h/CST-agent/docs/parliament.md Documentation=file://%h/CST-agent/docs/parliament.md
After=network-online.target After=network-online.target
Wants=network-online.target Wants=network-online.target
@@ -15,6 +15,8 @@ Environment=CST_INSTANCE_ID=%H
# ~/.config/systemd/user/cst-agent-parliament.service.d/override.conf # ~/.config/systemd/user/cst-agent-parliament.service.d/override.conf
# [Service] # [Service]
# Environment=CST_REALM_PASSWORD=… # Environment=CST_REALM_PASSWORD=…
# Also accepts HERMIT_* and config under ~/.config/hermit/
EnvironmentFile=-%h/.config/hermit/parliament.env
EnvironmentFile=-%h/.config/cst-agent/parliament.env EnvironmentFile=-%h/.config/cst-agent/parliament.env
[Install] [Install]
+11 -7
View File
@@ -29,7 +29,7 @@ mk=(make CC="${T}gcc" AR="${T}ar" STATIC=1
stamp=".gen/release-linux-$ARCH.stamp" stamp=".gen/release-linux-$ARCH.stamp"
want="$(cst_build_stamp "${T}gcc")" want="$(cst_build_stamp "${T}gcc")"
if [[ "${CST_FORCE_REBUILD:-0}" == 1 || ! -f cst-agent || ! -f "$stamp" || if [[ "${CST_FORCE_REBUILD:-0}" == 1 || ! -f hermit || ! -f "$stamp" ||
"$(cat "$stamp" 2>/dev/null || true)" != "$want" ]]; then "$(cat "$stamp" 2>/dev/null || true)" != "$want" ]]; then
echo "==> clean rebuild ($ARCH)" echo "==> clean rebuild ($ARCH)"
make clean >/dev/null make clean >/dev/null
@@ -37,12 +37,12 @@ if [[ "${CST_FORCE_REBUILD:-0}" == 1 || ! -f cst-agent || ! -f "$stamp" ||
else else
echo "==> incremental rebuild ($ARCH, stamp match)" echo "==> incremental rebuild ($ARCH, stamp match)"
fi fi
"${mk[@]}" -j"$(nproc)" cst-agent "${mk[@]}" -j"$(nproc)" hermit
mkdir -p .gen mkdir -p .gen
printf '%s\n' "$want" >"$stamp" printf '%s\n' "$want" >"$stamp"
if "${T}readelf" -l cst-agent | grep -q 'Requesting program interpreter'; then if "${T}readelf" -l hermit | grep -q 'Requesting program interpreter'; then
echo "cst-agent is dynamically linked" >&2 echo "hermit is dynamically linked" >&2
exit 1 exit 1
fi fi
if [[ "${CST_LINUX_CHECK:-0}" == 1 ]]; then if [[ "${CST_LINUX_CHECK:-0}" == 1 ]]; then
@@ -50,14 +50,18 @@ if [[ "${CST_LINUX_CHECK:-0}" == 1 ]]; then
fi fi
ver="$(cst_version)" ver="$(cst_version)"
name="cst-agent-$ver-linux-$ARCH" name="hermit-$ver-linux-$ARCH"
rel="dist/$name" rel="dist/$name"
rm -rf "$rel" rm -rf "$rel"
mkdir -p "$rel" mkdir -p "$rel"
cp -f cst-agent "$rel/cst-agent" cp -f hermit "$rel/hermit"
"${T}strip" "$rel/cst-agent" ln -sfr "$rel/hermit" "$rel/cst-agent" 2>/dev/null || ln -sf hermit "$rel/cst-agent"
"${T}strip" "$rel/hermit"
cp -f README.md "$rel/README.md" cp -f README.md "$rel/README.md"
echo "$ver" >"$rel/VERSION" echo "$ver" >"$rel/VERSION"
tgz="$(cst_pack_tgz dist "$name")" tgz="$(cst_pack_tgz dist "$name")"
# Compat alias package name for one release cycle
cp -f "$tgz" "dist/cst-agent-$ver-linux-$ARCH.tar.gz"
cst_sha256_write "dist/cst-agent-$ver-linux-$ARCH.tar.gz"
ls -la "$rel" ls -la "$rel"
echo "$ROOT/$tgz" echo "$ROOT/$tgz"
+6 -2
View File
@@ -59,9 +59,13 @@ json() { # CODE [ARGS...]: d = the JSON on stdin, ARGS in sys.argv[1:]
python3 -c "import json,sys; d=json.load(sys.stdin); $code" "$@" python3 -c "import json,sys; d=json.load(sys.stdin); $code" "$@"
} }
BODY="Linux (static, musl, any distro): cst-agent-$ver-linux-{x86_64,x86,aarch64}.tar.gz -- extract and run ./cst-agent, or copy it onto PATH. BODY="Hermit $ver
Windows x86_64: cst-agent-$ver-windows-x86_64-setup.exe installs per user (no admin rights) and adds PATH; cst-agent-$ver-windows-x86_64.zip is the portable build (install_windows.ps1 inside). Linux (static, musl): hermit-$ver-linux-{x86_64,x86,aarch64}.tar.gz — extract and run ./hermit (cst-agent symlink included). Legacy cst-agent-*.tar.gz aliases may ship for one cycle.
Windows x86_64: hermit-$ver-windows-x86_64-setup.exe / .zip (cst-agent.exe symlink or copy when present).
Env: HERMIT_* preferred; CST_* still accepted. Config: ~/.config/hermit (falls back to ~/.config/cst-agent).
Every file has a .sha256 next to it." Every file has a .sha256 next to it."
+167
View File
@@ -0,0 +1,167 @@
#define _POSIX_C_SOURCE 200809L
#include "cst_hermit_glyph.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
const char *hermit_getenv(const char *suffix) {
char hk[96], ck[96];
if (!suffix || !suffix[0])
return "";
snprintf(hk, sizeof(hk), "HERMIT_%s", suffix);
const char *e = getenv(hk);
if (e && e[0])
return e;
snprintf(ck, sizeof(ck), "CST_%s", suffix);
e = getenv(ck);
return e ? e : "";
}
hermit_icon_mode_t hermit_icon_mode(void) {
const char *e = hermit_getenv("ICONS");
if (!e[0] || !strcmp(e, "auto"))
return HERMIT_ICON_AUTO;
if (!strcmp(e, "font") || !strcmp(e, "ttf"))
return HERMIT_ICON_FONT;
return HERMIT_ICON_MINI;
}
hermit_icon_mode_t hermit_icon_mode_eff(int ascii) {
if (ascii)
return HERMIT_ICON_MINI;
hermit_icon_mode_t m = hermit_icon_mode();
if (m != HERMIT_ICON_AUTO)
return m;
/* AUTO: font only when forced path exists or HERMIT_ICON_FONT=1. */
if (hermit_getenv("ICON_FONT")[0] == '1')
return HERMIT_ICON_FONT;
return HERMIT_ICON_MINI;
}
hermit_state_t hermit_state_from_ui(int busy, int ask, int error) {
if (error)
return HERMIT_ST_ERROR;
if (ask)
return HERMIT_ST_QUESTION;
if (busy)
return HERMIT_ST_CODING;
return HERMIT_ST_IDLE;
}
wchar_t hermit_font_codepoint(hermit_state_t st, int frame, int with_shell) {
if (st < 0 || st >= HERMIT_ST_N)
st = HERMIT_ST_IDLE;
if (frame < 0)
frame = 0;
frame %= HERMIT_FONT_FRAMES;
int shell = with_shell ? 1 : 0;
return (wchar_t)(HERMIT_FONT_PUA_BASE +
(shell * HERMIT_ST_N + (int)st) * HERMIT_FONT_FRAMES +
frame);
}
/* --- minimal badge ------------------------------------------------------- */
const char *hermit_mini_badge(hermit_state_t st, int frame, int ascii) {
int f = frame < 0 ? 0 : frame;
if (ascii) {
switch (st) {
case HERMIT_ST_QUESTION:
return "||?";
case HERMIT_ST_ERROR:
return "\\/!";
case HERMIT_ST_SUCCESS:
return "||*";
case HERMIT_ST_THINKING:
return (f % 2) ? "~|" : "|~";
case HERMIT_ST_CODING:
case HERMIT_ST_IDLE:
default: {
static const char *const spin[] = {"||", ">_", "||", "-"};
return spin[f % 4];
}
}
}
switch (st) {
case HERMIT_ST_QUESTION:
return "║║?";
case HERMIT_ST_ERROR:
return "╲╱!";
case HERMIT_ST_SUCCESS:
return "║║✦";
case HERMIT_ST_THINKING:
return (f % 2) ? "║~" : "~║";
case HERMIT_ST_CODING:
case HERMIT_ST_IDLE:
default: {
static const char *const spin[] = {"║║", ">_", "║║", "─"};
return spin[f % 4];
}
}
}
/* --- home shell art (fixed grid) ----------------------------------------- */
/* 6 rows x 14 cols. Roles: 0 body 1 shell 2 eye 3 leg 4 prompt 5 empty/mark */
#define ART_H 6
#define ART_W 14
static const char *const ART_SHELL[ART_H] = {
" ######## ", /* shell top */
" ## >_ ## ", /* shell + prompt */
" ########## ", /* shell bottom */
" ###### ", /* body */
" #::## ", /* eyes :: on body */
" | | ", /* legs */
};
static const char *const ART_NOSHELL[ART_H] = {
" ",
" ",
" ###### ",
" #::## ",
" ###### ",
" | | ",
};
static const char *const ART_SHELL_ASC[ART_H] = {
" ######## ",
" ## >_ ## ",
" ########## ",
" ###### ",
" #::## ",
" | | ",
};
int hermit_shell_art(hermit_state_t st, int with_shell, int ascii,
const char **lines, int max_lines) {
(void)st;
if (!lines || max_lines < 1)
return 0;
const char *const *src =
with_shell ? (ascii ? ART_SHELL_ASC : ART_SHELL) : ART_NOSHELL;
int n = ART_H < max_lines ? ART_H : max_lines;
for (int i = 0; i < n; i++)
lines[i] = src[i];
return n;
}
int hermit_shell_cell_role(hermit_state_t st, int with_shell, int row, int col) {
(void)st;
if (row < 0 || row >= ART_H || col < 0 || col >= ART_W)
return 5;
const char *const *src = with_shell ? ART_SHELL : ART_NOSHELL;
char c = src[row][col];
if (c == ' ' || !c)
return 5;
if (c == '>' || c == '_')
return 4;
if (c == ':')
return 2;
if (c == '|')
return 3;
if (with_shell && row <= 2)
return 1; /* shell */
return 0; /* body */
}
+1 -1
View File
@@ -671,7 +671,7 @@ int main(int argc, char **argv) {
do_migrate = 1; do_migrate = 1;
break; break;
case 'V': case 'V':
printf("cst-agent %s\n", CST_VERSION); printf("hermit %s\n", CST_VERSION);
cst_curl_cleanup(); cst_curl_cleanup();
return 0; return 0;
default: default:
+39 -11
View File
@@ -230,6 +230,7 @@ int os_rename_replace(const char *from, const char *to) {
int os_config_dir(char *out, size_t out_sz) { int os_config_dir(char *out, size_t out_sz) {
if (!out || out_sz < 8) if (!out || out_sz < 8)
return -1; return -1;
char neu[OS_PATH_MAX], old[OS_PATH_MAX];
#ifdef _WIN32 #ifdef _WIN32
const char *base = getenv("XDG_CONFIG_HOME"); const char *base = getenv("XDG_CONFIG_HOME");
if (!base || !os_path_is_abs(base)) if (!base || !os_path_is_abs(base))
@@ -238,20 +239,42 @@ int os_config_dir(char *out, size_t out_sz) {
base = getenv("LOCALAPPDATA"); base = getenv("LOCALAPPDATA");
if (!base || !base[0]) if (!base || !base[0])
return -1; return -1;
int n = snprintf(out, out_sz, "%s/cst-agent", base); int n = snprintf(neu, sizeof(neu), "%s/hermit", base);
os_path_norm_slashes(out); int o = snprintf(old, sizeof(old), "%s/cst-agent", base);
if (n < 0 || o < 0 || (size_t)n >= sizeof(neu) || (size_t)o >= sizeof(old))
return -1;
os_path_norm_slashes(neu);
os_path_norm_slashes(old);
#else #else
const char *xdg = getenv("XDG_CONFIG_HOME"); const char *xdg = getenv("XDG_CONFIG_HOME");
const char *home = getenv("HOME"); const char *home = getenv("HOME");
int n; int n, o;
if (xdg && xdg[0] == '/') if (xdg && xdg[0] == '/') {
n = snprintf(out, out_sz, "%s/cst-agent", xdg); n = snprintf(neu, sizeof(neu), "%s/hermit", xdg);
else if (home && home[0]) o = snprintf(old, sizeof(old), "%s/cst-agent", xdg);
n = snprintf(out, out_sz, "%s/.config/cst-agent", home); } else if (home && home[0]) {
else n = snprintf(neu, sizeof(neu), "%s/.config/hermit", home);
o = snprintf(old, sizeof(old), "%s/.config/cst-agent", home);
} else
return -1;
if (n < 0 || o < 0 || (size_t)n >= sizeof(neu) || (size_t)o >= sizeof(old))
return -1; return -1;
#endif #endif
if (n < 0 || (size_t)n >= out_sz) /* Prefer hermit/; fall back to legacy cst-agent/ when hermit is absent. */
{
struct stat st;
if (stat(neu, &st) == 0 && S_ISDIR(st.st_mode)) {
if (snprintf(out, out_sz, "%s", neu) < 0 || strlen(neu) >= out_sz)
return -1;
return 0;
}
if (stat(old, &st) == 0 && S_ISDIR(st.st_mode)) {
if (snprintf(out, out_sz, "%s", old) < 0 || strlen(old) >= out_sz)
return -1;
return 0;
}
}
if (snprintf(out, out_sz, "%s", neu) < 0 || strlen(neu) >= out_sz)
return -1; return -1;
return 0; return 0;
} }
@@ -298,10 +321,15 @@ static void tmp_pick(void) {
snprintf(cand[nc++], OS_PATH_MAX, "%s/tmp", prefix); snprintf(cand[nc++], OS_PATH_MAX, "%s/tmp", prefix);
snprintf(cand[nc++], OS_PATH_MAX, "/tmp"); snprintf(cand[nc++], OS_PATH_MAX, "/tmp");
snprintf(cand[nc++], OS_PATH_MAX, "/var/tmp"); snprintf(cand[nc++], OS_PATH_MAX, "/var/tmp");
if (cache && cache[0] == '/') if (cache && cache[0] == '/') {
snprintf(cand[nc++], OS_PATH_MAX, "%s/hermit/tmp", cache);
if (nc < 5)
snprintf(cand[nc++], OS_PATH_MAX, "%s/cst-agent/tmp", cache); snprintf(cand[nc++], OS_PATH_MAX, "%s/cst-agent/tmp", cache);
else if (home && home[0] == '/') } else if (home && home[0] == '/') {
snprintf(cand[nc++], OS_PATH_MAX, "%s/.cache/hermit/tmp", home);
if (nc < 5)
snprintf(cand[nc++], OS_PATH_MAX, "%s/.cache/cst-agent/tmp", home); snprintf(cand[nc++], OS_PATH_MAX, "%s/.cache/cst-agent/tmp", home);
}
for (int i = 0; i < nc; i++) { for (int i = 0; i < nc; i++) {
tmp_trim(cand[i]); tmp_trim(cand[i]);
if (i == nc - 1) if (i == nc - 1)
+146 -56
View File
@@ -24,6 +24,7 @@
#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"
@@ -496,18 +497,18 @@ static char *xstrndup(const char *s, size_t n) {
/* --- theme ---------------------------------------------------------------- */ /* --- theme ---------------------------------------------------------------- */
/* Dark panel chrome — cool steel accents (not peach OpenCode clone). */ /* Hermit palette: brand teal, crab orange, eye purple (design mock). */
static void theme_init(void) { static void theme_init(void) {
const char *nc = getenv("NO_COLOR"); const char *nc = getenv("NO_COLOR");
g_nocolor = nc && nc[0]; g_nocolor = nc && nc[0];
const char *asc = getenv("CST_ASCII"); const char *asc = hermit_getenv("ASCII");
#if defined(__ANDROID__) #if defined(__ANDROID__)
g_utf8 = 1; /* bionic is UTF-8 only; nl_langinfo needs API 26 */ g_utf8 = 1; /* bionic is UTF-8 only; nl_langinfo needs API 26 */
#elif !defined(_WIN32) #elif !defined(_WIN32)
const char *cs = nl_langinfo(CODESET); const char *cs = nl_langinfo(CODESET);
g_utf8 = cs && (strcasecmp(cs, "UTF-8") == 0 || strcasecmp(cs, "utf8") == 0); g_utf8 = cs && (strcasecmp(cs, "UTF-8") == 0 || strcasecmp(cs, "utf8") == 0);
#endif #endif
g_ascii = asc && asc[0] ? asc[0] != '0' : !g_utf8; g_ascii = asc[0] ? asc[0] != '0' : !g_utf8;
if (!HAS_COLOR()) if (!HAS_COLOR())
return; return;
start_color(); start_color();
@@ -515,54 +516,62 @@ static void theme_init(void) {
short primary = COLOR_CYAN; short primary = COLOR_CYAN;
short panel = COLOR_BLACK; short panel = COLOR_BLACK;
short muted = COLOR_CYAN; short muted = COLOR_WHITE;
short success = COLOR_GREEN; short success = COLOR_GREEN;
short err = COLOR_RED; short err = COLOR_RED;
short assist = COLOR_CYAN; short assist = COLOR_MAGENTA;
short text = COLOR_WHITE; short text = COLOR_WHITE;
short box = COLOR_BLACK, side = COLOR_BLACK; short box = COLOR_BLACK, side = COLOR_BLACK;
short accent = COLOR_BLUE, bright = COLOR_WHITE, logo_dim = COLOR_CYAN; short accent = COLOR_CYAN, bright = COLOR_WHITE, logo_dim = COLOR_CYAN;
short tool = COLOR_YELLOW;
short prompt_c = COLOR_CYAN;
if (COLORS >= 256) { if (COLORS >= 256) {
box = 235; box = 235;
side = 234; side = 234;
accent = 75; /* #5c9cf5 steel blue */ accent = 75; /* #277F94 brand teal */
bright = 255; bright = 255;
logo_dim = 242; logo_dim = 242;
primary = 75; /* brand/user accent: blue, not peach */ primary = 75;
panel = 233; /* #121212 */ panel = 233;
muted = 244; muted = 244;
success = 114; success = 114;
err = 167; err = 167;
assist = 74; /* soft cyan assistant */ assist = 177; /* #9957AD eye purple */
text = 252; /* brighter body for readability */ tool = 216; /* #F68042 crab orange */
prompt_c = 152; /* #A9D9D7 prompt cyan */
text = 252;
if (can_change_color()) { if (can_change_color()) {
init_color(75, 430, 660, 996); /* #6ea8fe brand */ init_color(75, 153, 498, 580); /* #277F94 brand */
init_color(233, 55, 58, 70); /* #0e0f12 deeper panel */ init_color(152, 663, 851, 843); /* #A9D9D7 prompt */
init_color(244, 502, 520, 560); /* cool muted */ init_color(216, 965, 502, 259); /* #F68042 crab */
init_color(252, 820, 830, 850); /* body */ init_color(177, 600, 341, 678); /* #9957AD eye */
init_color(167, 850, 320, 320); /* error red */
init_color(168, 643, 341, 310); /* #A4574F brick legs */
init_color(233, 55, 58, 70); /* #0e0f12 panel */
init_color(244, 502, 520, 560); /* muted */
init_color(252, 820, 830, 850); /* body text */
init_color(114, 400, 750, 450); init_color(114, 400, 750, 450);
init_color(167, 850, 320, 320); init_color(235, 100, 105, 120);
init_color(74, 380, 720, 780); init_color(234, 78, 82, 96);
init_color(235, 100, 105, 120); /* raised box */
init_color(234, 78, 82, 96); /* sidebar */
init_color(216, 980, 720, 420); /* warm tip only */
} }
} else if (can_change_color() && COLORS >= 8) { } else if (can_change_color() && COLORS >= 8) {
init_color(COLOR_BLACK, 55, 58, 70); init_color(COLOR_BLACK, 55, 58, 70);
init_color(COLOR_CYAN, 430, 660, 996); init_color(COLOR_CYAN, 153, 498, 580);
init_color(COLOR_WHITE, 820, 830, 850); init_color(COLOR_WHITE, 820, 830, 850);
init_color(COLOR_GREEN, 400, 750, 450); init_color(COLOR_GREEN, 400, 750, 450);
init_color(COLOR_RED, 850, 320, 320); init_color(COLOR_RED, 850, 320, 320);
init_color(COLOR_BLUE, 380, 720, 780); init_color(COLOR_MAGENTA, 600, 341, 678);
init_color(COLOR_YELLOW, 980, 720, 420); init_color(COLOR_YELLOW, 965, 502, 259);
panel = COLOR_BLACK; panel = COLOR_BLACK;
primary = COLOR_CYAN; primary = COLOR_CYAN;
muted = COLOR_WHITE; muted = COLOR_WHITE;
text = COLOR_WHITE; text = COLOR_WHITE;
success = COLOR_GREEN; success = COLOR_GREEN;
err = COLOR_RED; err = COLOR_RED;
assist = COLOR_BLUE; assist = COLOR_MAGENTA;
tool = COLOR_YELLOW;
prompt_c = COLOR_CYAN;
} }
/* Header is dark too — primary only as text accent (not a bright bar). */ /* Header is dark too — primary only as text accent (not a bright bar). */
@@ -574,11 +583,11 @@ static void theme_init(void) {
init_pair(CP_MUTED, muted, panel); init_pair(CP_MUTED, muted, panel);
init_pair(CP_SUCCESS, success, panel); init_pair(CP_SUCCESS, success, panel);
init_pair(CP_ERROR, err, panel); init_pair(CP_ERROR, err, panel);
init_pair(CP_TOOL, primary, panel); init_pair(CP_TOOL, tool, panel);
init_pair(CP_PANEL, text, panel); init_pair(CP_PANEL, text, panel);
init_pair(CP_SGR_GREEN, success, panel); init_pair(CP_SGR_GREEN, success, panel);
init_pair(CP_SGR_YELLOW, primary, panel); init_pair(CP_SGR_YELLOW, tool, panel);
init_pair(CP_SGR_CYAN, assist, panel); init_pair(CP_SGR_CYAN, prompt_c, panel);
if (COLORS >= 256) if (COLORS >= 256)
init_pair(CP_SGR_MAGENTA, 176, panel); init_pair(CP_SGR_MAGENTA, 176, panel);
else else
@@ -2687,20 +2696,87 @@ static void draw_todo(tui_t *t) {
/* --- start screen ---------------------------------------------------------- */ /* --- start screen ---------------------------------------------------------- */
/* 3-row block font for "cstagent"; each glyph is 4 columns wide. */ /* 3-row block font for "HERMIT"; each glyph is 4 columns wide. */
static const char *const LOGO[8][3] = { static const char *const LOGO[6][3] = {
{"█▀▀▀", "█ ", "▀▀▀▀"}, /* c */ {"█ █", "█▀▀█", "█ █"}, /* H */
{"█▀▀▀", "▀▀▀█", "▀▀▀▀"}, /* s */ {"█▀▀▀", "█▀▀ ", "▀▀▀▀"}, /* E */
{"▀█▀▀", " █ ", " ▀▀▀"}, /* t */ {"█▀▀█", "█▀▀█", "▀ ▀"}, /* R */
{"█▀▀█", "█▀▀█", "▀ ▀"}, /* a */ {"█▀▄█", "█ █", "▀ ▀"}, /* M */
{"█▀▀▀", "█ ▀█", "▀▀▀▀"}, /* g */ {"▀█▀▀", " █ ", " ▀▀▀"}, /* I */
{"█▀▀▀", "█▀▀ ", "▀▀▀▀"}, /* e */ {"▀█▀▀", " █ ", " ▀▀▀"}, /* T */
{"█▀▀▄", "█ █", "▀ ▀"}, /* n */
{"▀█▀▀", " █ ", " ▀▀▀"}, /* t */
}; };
#define LOGO_W (8 * 5 - 1) #define LOGO_N 6
#define LOGO_W (LOGO_N * 5 - 1)
#define LOGO_H 3 #define LOGO_H 3
static int hermit_role_pair(int role) {
switch (role) {
case 0:
return CP_TOOL; /* body */
case 1:
return CP_ACCENT; /* shell */
case 2:
return CP_ASSIST; /* eye */
case 3:
return COLORS >= 256 ? CP_MUTED : CP_ERROR; /* brick legs */
case 4:
return CP_SGR_CYAN; /* >_ */
default:
return CP_MUTED;
}
}
static void draw_hermit_icon(WINDOW *win, int y0, int x0, hermit_state_t st,
int with_shell, int frame) {
hermit_icon_mode_t mode = hermit_icon_mode_eff(g_ascii);
if (mode == HERMIT_ICON_FONT) {
wchar_t wc = hermit_font_codepoint(st, frame, with_shell);
cchar_t cc;
wchar_t wstr[2] = {wc, 0};
if (setcchar(&cc, wstr, 0, 0, NULL) == OK) {
if (HAS_COLOR())
wattron(win, COLOR_PAIR(CP_TOOL));
mvwadd_wch(win, y0, x0, &cc);
if (HAS_COLOR())
wattroff(win, COLOR_PAIR(CP_TOOL));
return;
}
}
const char *lines[8];
int n = hermit_shell_art(st, with_shell, g_ascii, lines, 8);
for (int r = 0; r < n; r++) {
const char *row = lines[r];
for (int c = 0; row[c]; c++) {
if (row[c] == ' ')
continue;
char ch[2] = {row[c], 0};
int pair = hermit_role_pair(hermit_shell_cell_role(st, with_shell, r, c));
draw_ansi_at(win, y0 + r, x0 + c, 1, ch, pair, 0, 0);
}
}
}
static void draw_hermit_badge(WINDOW *win, int y, int x, const tui_t *t) {
hermit_state_t st = hermit_state_from_ui(
t->busy, t->ask_pending || t->awaiting_perm, 0);
hermit_icon_mode_t mode = hermit_icon_mode_eff(g_ascii);
if (mode == HERMIT_ICON_FONT) {
wchar_t wc = hermit_font_codepoint(st, t->spin, 0);
cchar_t cc;
wchar_t wstr[2] = {wc, 0};
if (setcchar(&cc, wstr, 0, 0, NULL) == OK) {
if (HAS_COLOR())
wattron(win, COLOR_PAIR(CP_TOOL));
mvwadd_wch(win, y, x, &cc);
if (HAS_COLOR())
wattroff(win, COLOR_PAIR(CP_TOOL));
return;
}
}
const char *b = hermit_mini_badge(st, t->spin, g_ascii);
draw_ansi_at(win, y, x, (int)strlen(b), b, CP_TOOL, 0, 0);
}
static const char *const TIPS[] = { static const char *const TIPS[] = {
"Tab switches between Build and Plan (read-only) mode", "Tab switches between Build and Plan (read-only) mode",
"Shift+Tab cycles approval: ask, auto, yolo", "Shift+Tab cycles approval: ask, auto, yolo",
@@ -2720,40 +2796,51 @@ static void draw_home(tui_t *t) {
getmaxyx(t->logw, h, w); getmaxyx(t->logw, h, w);
int box_top = t->in_y - t->log_y; int box_top = t->in_y - t->log_y;
int box_bot = box_top + t->in_h; int box_bot = box_top + t->in_h;
/* Tight home stack: wordmark (default) or CST_LOGO=block, tagline, chip. */
int brand_y = box_top - 2; int brand_y = box_top - 2;
if (brand_y < 0) if (brand_y < 0)
brand_y = 0; brand_y = 0;
int want_block = 0; int want_block = 0;
{ {
const char *lb = getenv("CST_LOGO"); const char *lb = hermit_getenv("LOGO");
want_block = lb && !strcmp(lb, "block"); want_block = lb[0] && !strcmp(lb, "block");
} }
/* Crab on the right of the brand stack when there is room. */
int crab_w = 14, crab_h = 6;
int crab_x = w - crab_w - 2;
int show_crab = crab_x > LOGO_W + 4 && box_top >= crab_h + 1;
if (want_block && !(g_ascii || !g_utf8 || g_logo_text) && if (want_block && !(g_ascii || !g_utf8 || g_logo_text) &&
box_top - LOGO_H - 2 >= 0 && w >= LOGO_W) { box_top - LOGO_H - 2 >= 0 && w >= LOGO_W) {
int x0 = (w - LOGO_W) / 2, y0 = box_top - LOGO_H - 2; int x0 = show_crab ? (crab_x - LOGO_W) / 2 : (w - LOGO_W) / 2;
for (int g = 0; g < 8; g++) if (x0 < 0)
x0 = 0;
int y0 = box_top - LOGO_H - 2;
for (int g = 0; g < LOGO_N; g++)
for (int r = 0; r < LOGO_H; r++) for (int r = 0; r < LOGO_H; r++)
draw_ansi_at(t->logw, y0 + r, x0 + g * 5, 4, LOGO[g][r], draw_ansi_at(t->logw, y0 + r, x0 + g * 5, 4, LOGO[g][r], CP_ACCENT, 0,
g < 3 ? CP_ACCENT : CP_BRIGHT, 0, 0); 0);
brand_y = y0 + LOGO_H; brand_y = y0 + LOGO_H;
} else { } else {
const char *mark = "CST-agent"; const char *mark = "HERMIT";
int mw = (int)strlen(mark); int mw = (int)strlen(mark);
int x = (w - mw) / 2; int x = show_crab ? (crab_x - mw) / 2 : (w - mw) / 2;
if (x < 0) if (x < 0)
x = 0; x = 0;
if (brand_y >= 1) if (brand_y >= 1)
brand_y -= 1; brand_y -= 1;
draw_ansi_at(t->logw, brand_y, x, 3, "CST", CP_ACCENT, 1, 0); draw_ansi_at(t->logw, brand_y, x, mw, mark, CP_ACCENT, 1, 0);
draw_ansi_at(t->logw, brand_y, x + 3, mw - 3, "-agent", CP_BRIGHT, 1, 0); }
if (show_crab) {
int cy = box_top - crab_h;
if (cy < 0)
cy = 0;
draw_hermit_icon(t->logw, cy, crab_x, HERMIT_ST_IDLE, 1, t->spin);
} }
int ty = brand_y + 1; int ty = brand_y + 1;
if (ty < box_top) { if (ty < box_top) {
const char *tag = const char *tag =
g_ascii ? "code . plan . parliament" : "code · plan · parliament"; g_ascii ? "code . plan . parliament" : "code · plan · parliament";
int tw = (int)strlen(tag); int tw = (int)strlen(tag);
int x = (w - tw) / 2; int x = show_crab ? (crab_x - tw) / 2 : (w - tw) / 2;
if (x < 0) if (x < 0)
x = 0; x = 0;
draw_ansi_at(t->logw, ty, x, tw, tag, CP_MUTED, 0, 0); draw_ansi_at(t->logw, ty, x, tw, tag, CP_MUTED, 0, 0);
@@ -2772,9 +2859,10 @@ static void draw_home(tui_t *t) {
} }
int right = t->in_x - t->log_x + t->in_w; int right = t->in_x - t->log_x + t->in_w;
if (box_bot < h) { if (box_bot < h) {
char hint[96]; char hint[128];
snprintf(hint, sizeof(hint), snprintf(hint, sizeof(hint),
"\033[97mtab\033[39m modes \033[97mctrl+p\033[39m commands"); "\033[97mtab\033[39m modes \033[97mctrl+p\033[39m commands "
"\033[97mshift+tab\033[39m approval");
int hw = vis_cols(hint); int hw = vis_cols(hint);
if (right - hw >= 0) if (right - hw >= 0)
draw_ansi_at(t->logw, box_bot, right - hw, hw, hint, CP_MUTED, 0, 0); draw_ansi_at(t->logw, box_bot, right - hw, hw, hint, CP_MUTED, 0, 0);
@@ -2785,7 +2873,7 @@ static void draw_home(tui_t *t) {
int tw = vis_cols(tip); int tw = vis_cols(tip);
if (tw > w - 4) if (tw > w - 4)
tw = w - 4; tw = w - 4;
draw_ansi_at(t->logw, box_bot + 2, (w - tw) / 2, tw, tip, CP_MUTED, 0, 0); draw_ansi_at(t->logw, box_bot + 2, (w - tw) / 2, tw, tip, CP_TOOL, 0, 0);
} }
const char *ext = cst_extensions_report(); const char *ext = cst_extensions_report();
if (ext[0] && box_bot + 3 < h) { if (ext[0] && box_bot + 3 < h) {
@@ -3018,7 +3106,7 @@ static void draw_side(tui_t *t) {
if (h >= 4) { if (h >= 4) {
draw_ansi_at(s, h - 3, x, mw, t->where, CP_SIDE_MUTED, 0, 0); draw_ansi_at(s, h - 3, x, mw, t->where, CP_SIDE_MUTED, 0, 0);
snprintf(line, sizeof(line), "CST %s", CST_VERSION); snprintf(line, sizeof(line), "Hermit %s", CST_VERSION);
draw_ansi_at(s, h - 2, x, mw, line, CP_SIDE_MUTED, 0, 0); draw_ansi_at(s, h - 2, x, mw, line, CP_SIDE_MUTED, 0, 0);
} }
wrefresh(s); wrefresh(s);
@@ -3183,9 +3271,11 @@ static void draw_topbar(tui_t *t) {
if (hexish) if (hexish)
title = "New session"; title = "New session";
} }
/* CST mark + title + model. Role lives only in the Seat Deck. */ /* Hermit badge + title + model. Role lives only in the Seat Deck. */
int x = 1; int x = 1;
x += draw_ansi_at(w, 0, x, 3, "CST", CP_TOPBAR_ACCENT, 1, 0); draw_hermit_badge(w, 0, x, t);
x += 4;
x += draw_ansi_at(w, 0, x, 6, "Hermit", CP_TOPBAR_ACCENT, 1, 0);
x += draw_ansi_at(w, 0, x, 1, " ", CP_TOPBAR_MUTED, 0, 0); x += draw_ansi_at(w, 0, x, 1, " ", CP_TOPBAR_MUTED, 0, 0);
int model_room = 0; int model_room = 0;
char mid[160] = ""; char mid[160] = "";