From f30b4ede488fe553fadd026500a032cb8fc2220d Mon Sep 17 00:00:00 2001 From: WHC Date: Thu, 29 Oct 2026 21:42:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20rebrand=20as=20Hermit=20=E2=80=94=20TUI?= =?UTF-8?q?=20palette,=20dual=20glyphs,=20binary=200.10.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .gitea/workflows/ci.yml | 22 +-- .gitignore | 3 + CMakeLists.txt | 2 + Makefile | 47 +++-- README.md | 168 +++------------- docs/README.md | 13 +- icons/agent-motion/README.md | 21 ++ icons/agent-motion/icons/coding-shell.svg | 1 + icons/agent-motion/icons/coding.svg | 1 + icons/agent-motion/icons/error-shell.svg | 1 + icons/agent-motion/icons/error.svg | 1 + icons/agent-motion/icons/idle-shell.svg | 1 + icons/agent-motion/icons/idle.svg | 1 + icons/agent-motion/icons/question-shell.svg | 1 + icons/agent-motion/icons/question.svg | 1 + icons/agent-motion/icons/success-shell.svg | 1 + icons/agent-motion/icons/success.svg | 1 + icons/agent-motion/icons/thinking-shell.svg | 1 + icons/agent-motion/icons/thinking.svg | 1 + icons/agent-motion/index.html | 15 ++ icons/hermit-flat-motion-kit.zip | Bin 0 -> 20762 bytes include/cst_hermit_glyph.h | 63 ++++++ include/cst_version.h | 4 +- scripts/bootstrap_linux.sh | 31 +-- scripts/build_hermit_font.py | 202 ++++++++++++++++++++ scripts/cst-agent-parliament.service.in | 4 +- scripts/package_linux.sh | 18 +- scripts/release.sh | 8 +- src/hermit_glyph.c | 167 ++++++++++++++++ src/main.c | 2 +- src/os_compat.c | 54 ++++-- src/tui.c | 202 ++++++++++++++------ 32 files changed, 793 insertions(+), 265 deletions(-) create mode 100644 icons/agent-motion/README.md create mode 100644 icons/agent-motion/icons/coding-shell.svg create mode 100644 icons/agent-motion/icons/coding.svg create mode 100644 icons/agent-motion/icons/error-shell.svg create mode 100644 icons/agent-motion/icons/error.svg create mode 100644 icons/agent-motion/icons/idle-shell.svg create mode 100644 icons/agent-motion/icons/idle.svg create mode 100644 icons/agent-motion/icons/question-shell.svg create mode 100644 icons/agent-motion/icons/question.svg create mode 100644 icons/agent-motion/icons/success-shell.svg create mode 100644 icons/agent-motion/icons/success.svg create mode 100644 icons/agent-motion/icons/thinking-shell.svg create mode 100644 icons/agent-motion/icons/thinking.svg create mode 100644 icons/agent-motion/index.html create mode 100644 icons/hermit-flat-motion-kit.zip create mode 100644 include/cst_hermit_glyph.h create mode 100755 scripts/build_hermit_font.py create mode 100644 src/hermit_glyph.c diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 7a84acf..dbd7da9 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -37,14 +37,14 @@ jobs: - name: Build TinyCC run: bash scripts/ci_tcc.sh - 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 run: make CC=tcc AR=ar CFLAGS="-std=c11 -Wall -O2 -g" check - name: Smoke test run: | - ./cst-agent --help - ./cst-agent --workspace "$PWD" --list-sessions - ./cst-agent --demo-ctl --workspace . + ./hermit --help + ./hermit --workspace "$PWD" --list-sessions + ./hermit --demo-ctl --workspace . - name: WebUI end-to-end (recorded stream, no API key) run: sh scripts/e2e_webui.sh @@ -103,14 +103,14 @@ jobs: run: bash scripts/package_linux.sh ${{ matrix.arch }} - name: Smoke test run: | - d=$(ls -d dist/cst-agent-*-linux-${{ matrix.arch }}) - file "$d/cst-agent" + d=$(ls -d dist/hermit-*-linux-${{ matrix.arch }}) + file "$d/hermit" run="" [ "${{ matrix.arch }}" = aarch64 ] && run=qemu-aarch64-static - $run "$d/cst-agent" --version - $run "$d/cst-agent" --help >/dev/null - $run "$d/cst-agent" --workspace "$PWD" --list-sessions - cat dist/cst-agent-*-linux-${{ matrix.arch }}.tar.gz.sha256 + $run "$d/hermit" --version + $run "$d/hermit" --help >/dev/null + $run "$d/hermit" --workspace "$PWD" --list-sessions + cat dist/hermit-*-linux-${{ matrix.arch }}.tar.gz.sha256 # Release vVERSION (from include/cst_version.h) on Gitea. Prefer repo # secret RELEASE_TOKEN (write:repository); fall back to the job token. - name: Publish Gitea Release @@ -121,7 +121,7 @@ jobs: CST_GITEA: ${{ github.server_url }} CST_REPO: ${{ github.repository }} 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. # dist/cst-agent-VERSION-windows-x86_64.zip and -setup.exe (NSIS) diff --git a/.gitignore b/.gitignore index 69f3019..aaa7a68 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,6 @@ libcstcore.a # nix-build result +hermit +.peer_probe.out +reports/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 8bb87ae..890701b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -152,6 +152,7 @@ set(CST_CORE_SOURCES src/models.c src/model_registry.c src/metrics.c + src/hermit_glyph.c src/net.c src/http_srv.c src/web_md.c @@ -257,6 +258,7 @@ else() endif() add_executable(cst-agent ${CST_SOURCES}) +set_target_properties(cst-agent PROPERTIES OUTPUT_NAME hermit) add_dependencies(cst-agent sashidown) cst_win_exe(cst-agent) diff --git a/Makefile b/Makefile index 5941492..a00b32b 100644 --- a/Makefile +++ b/Makefile @@ -136,6 +136,7 @@ CORE_SRC = \ src/mm_tool.c \ src/models.c src/model_registry.c \ src/metrics.c \ + src/hermit_glyph.c \ $(WEB_SRC) 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 .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: - @echo "CST-Agent build targets (prefer: make -j\$$(nproc))" - @echo " all / cst-agent incremental build (ccache auto if installed)" + @echo "Hermit build targets (prefer: make -j\$$(nproc))" + @echo " all / hermit incremental build (ccache auto if installed)" @echo " check build + run unit tests" @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 "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 "Fast install: scripts/bootstrap_linux.sh --release" @@ -238,8 +244,13 @@ cmake-clang: sashidown-lib: $(MAKE) -C $(SD_DIR) libsashidown.a -cst-agent: $(OBJ) sashidown-lib +hermit: $(OBJ) sashidown-lib $(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). .gen/models_builtin.inc: src/models_builtin.json @@ -303,32 +314,38 @@ check: sashidown-lib $(TESTS) asan: $(MAKE) clean $(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 - ./cst-agent --demo-ctl --workspace . +demo: hermit + ./hermit --demo-ctl --workspace . 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 -install: cst-agent +install: hermit 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. install-service: install 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 \ > "$(DESTDIR)$(HOME)/.config/systemd/user/cst-agent-parliament.service" @echo "Installed user unit. Enable with:" @echo " systemctl --user daemon-reload" @echo " systemctl --user enable --now cst-agent-parliament.service" - @echo "(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: - 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" -include $(DEPFILES) diff --git a/README.md b/README.md index f3ba88e..5c40c83 100644 --- a/README.md +++ b/README.md @@ -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 等)。 **适合**:有监督的本地改动与仓库探索(权限确认、diff、`/undo`、沙箱)。 @@ -8,188 +8,80 @@ C11 实现,OpenAI 兼容网关(DeepSeek / Kimi / MiniMax / OpenAI 等)。 文档索引:[docs/README.md](docs/README.md)。 +兼容:安装后提供 `cst-agent` → `hermit` 软链;环境变量 `HERMIT_*` 优先,仍接受 `CST_*`;配置目录优先 `~/.config/hermit`,若无则回退 `~/.config/cst-agent`。 + --- ## 安装 -### Linux / WSL / macOS(推荐) +### Linux / WSL / macOS ```bash -# 预编译包(快;失败则自动回退源码编译) +# 预编译包(快;失败则回退源码) 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 ``` -已有仓库目录: - ```bash -./scripts/bootstrap_linux.sh . # 源码 -./scripts/bootstrap_linux.sh . --release # 只装 Release 二进制 -cst-agent-update # 之后升级 -cst-agent --version +./scripts/bootstrap_linux.sh . +hermit --version # hermit 0.10.0 +cst-agent-update # 升级助手仍可用 ``` -[Release](https://git.whcself.xyz/WHC/CST-agent/releases) 也提供静态 musl 包 -`cst-agent-VERSION-linux-{x86_64,x86,aarch64}.tar.gz`(旁有 `.sha256`),解压后把 `cst-agent` 放进 `PATH` 即可。 +[Release](https://git.whcself.xyz/WHC/CST-agent/releases):`hermit-VERSION-linux-{x86_64,x86,aarch64}.tar.gz`。 -### Windows 10 1903+ +### Windows -```powershell -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 -``` +见 [docs/README-windows.md](docs/README-windows.md)(产物名逐步改为 `hermit-…`;旧 `cst-agent-…` 包过渡期可能仍有)。 ### 第一次用 ```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 -make -j$(nproc) # 增量;有 ccache 会自动用;make help 看目标 -make -j$(nproc) check # 单元测试 -make asan # ASan + UBSan -make CC=tcc AR=ar check # TinyCC 快校验(CI 同款) +make -j$(nproc) # 产出 hermit(并链 cst-agent) +make -j$(nproc) check +make hermit-font # 可选:SVG→PUA 字体(需 rsvg/fonttools) 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`。 -平台细节:[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-*`。 +TUI 图标:`HERMIT_ICONS=auto|mini|font`(默认极简字符帧;`font` 需安装 `share/hermit/hermit-icons.ttf`)。 --- ## 用法速览 ```bash -cst-agent # TUI,工作区=当前目录 -cst-agent --web # 浏览器 http://127.0.0.1:8787/ -cst-agent --headless --prompt '…' # 无人值守(默认自动批准) -cst-agent --ask-key --provider deepseek +hermit # TUI +hermit --web # http://127.0.0.1:8787/ +hermit --headless --prompt '…' ``` -| 参数 | 作用 | -|---|---| -| `-c` / `-N` / `-R ID` / `-L` | 继续上次 / 新会话 / 恢复 / 列表 | -| `-A ask\|auto\|yolo` / `-y` | 审批模式 | -| `-T default\|minimal` | 工具集(`minimal` = 读写 shell 四件套) | -| `-M coding\|general` | 工作模式 | -| `-P` / `-m` / `-e` | provider / 模型 / 推理强度 | -| `-w DIR` | 工作区 | +首页品牌 **HERMIT** + 寄居蟹色块动效;色板:brand `#277F94`、crab `#F68042`、eye `#9957AD`。 -会话目录:`/.agent/sessions//`(自带 `.gitignore`)。WebUI 协议:[docs/webui.md](docs/webui.md)。 - -### 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`)。日常使用保持默认即可。 +更多:Provider / 工具 / 议会 / WebUI 见 [docs/](docs/)。 --- -## 平台 +## CI 与发布 -| OS | Arch | TUI | 备注 | -|---|---|---|---| -| 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 | +`.gitea/workflows/ci.yml`:TinyCC check → 打 Linux/Windows 包 → `scripts/ci_publish.sh`。 +共用逻辑:[scripts/lib_release.sh](scripts/lib_release.sh)。 ---- - -## 开发者备忘 +## 开发者 ```bash make check -scripts/prompt_smoke.sh # 系统提示契约(无 API) -scripts/e2e_webui.sh # WebUI 录制流(无 API) -scripts/e2e_fixture.sh mini_calc --provider deepseek # 可选:带 key 的夹具 +scripts/prompt_smoke.sh +scripts/e2e_webui.sh ``` -`fixtures/` 下是可重复的本地任务夹具(`TASK.md` + `check.sh`),用于手工或脚本验收,不是产品依赖。 - -许可证与第三方:`vendor/sashidown`、`vendor/cjson`(MIT)、`vendor/monocypher` 随仓库提供。 +`icons/agent-motion/` 为 SVG 资产;TUI 默认用字体序列极简轨,`make hermit-font` 生成 PUA 字体轨。 diff --git a/docs/README.md b/docs/README.md index 6bd4f3b..7a16ac7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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、环境变量 | -| [parliament.md](parliament.md) | 多设备议会(Realm / 席位 / 同步) | +| [env_openai.md](env_openai.md) | Provider、模型、API Key、环境变量(`HERMIT_*` / `CST_*`) | +| [parliament.md](parliament.md) | 多设备议会 | | [workers.md](workers.md) | 后台任务、worker、job 完成报告 | | [webui.md](webui.md) | 内置 HTTP + SSE WebUI | | [plugins.md](plugins.md) | Skills、插件、MCP | -| [compat.md](compat.md) | 平台兼容与验收清单 | -| [README-windows.md](README-windows.md) | Windows 安装、沙箱、构建 | +| [compat.md](compat.md) | 平台兼容 | +| [README-windows.md](README-windows.md) | Windows | | [android.md](android.md) | Android / APK | -| [windows_selftest.md](windows_selftest.md) | Windows 自测步骤 | | [archive/](archive/) | 历史设计笔记(非产品文档) | diff --git a/icons/agent-motion/README.md b/icons/agent-motion/README.md new file mode 100644 index 0000000..f0c4044 --- /dev/null +++ b/icons/agent-motion/README.md @@ -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 下载。无联网依赖。 + +接入示例:等待输入。大图标建议 64px 及以上,小图标建议 24–32px;16px 时状态细节较弱。使用内联 SVG 时可通过 --speed 设置速度。 + +普通字符终端不直接渲染 SVG;图标适用于支持图形的终端界面、Webview 或 CLI 伴随应用。 + +更新:橘杏色身体、莓紫眼睛、低饱和蓝绿终端壳。预览主角色以 620ms 缓入缓出插值连续改变眼睛路径与身体姿态,支持中途切换;不重建主角色。状态符号淡入淡出。独立 SVG 保留各状态循环动效;跨状态变形逻辑位于 index.html。 diff --git a/icons/agent-motion/icons/coding-shell.svg b/icons/agent-motion/icons/coding-shell.svg new file mode 100644 index 0000000..029ef7f --- /dev/null +++ b/icons/agent-motion/icons/coding-shell.svg @@ -0,0 +1 @@ +Hermit · 写码 \ No newline at end of file diff --git a/icons/agent-motion/icons/coding.svg b/icons/agent-motion/icons/coding.svg new file mode 100644 index 0000000..678ad0b --- /dev/null +++ b/icons/agent-motion/icons/coding.svg @@ -0,0 +1 @@ +Hermit · 写码 \ No newline at end of file diff --git a/icons/agent-motion/icons/error-shell.svg b/icons/agent-motion/icons/error-shell.svg new file mode 100644 index 0000000..5aee776 --- /dev/null +++ b/icons/agent-motion/icons/error-shell.svg @@ -0,0 +1 @@ +Hermit · 遇到问题 \ No newline at end of file diff --git a/icons/agent-motion/icons/error.svg b/icons/agent-motion/icons/error.svg new file mode 100644 index 0000000..171bd65 --- /dev/null +++ b/icons/agent-motion/icons/error.svg @@ -0,0 +1 @@ +Hermit · 遇到问题 \ No newline at end of file diff --git a/icons/agent-motion/icons/idle-shell.svg b/icons/agent-motion/icons/idle-shell.svg new file mode 100644 index 0000000..6f12497 --- /dev/null +++ b/icons/agent-motion/icons/idle-shell.svg @@ -0,0 +1 @@ +Hermit · 等待输入 \ No newline at end of file diff --git a/icons/agent-motion/icons/idle.svg b/icons/agent-motion/icons/idle.svg new file mode 100644 index 0000000..627b6ca --- /dev/null +++ b/icons/agent-motion/icons/idle.svg @@ -0,0 +1 @@ +Hermit · 等待输入 \ No newline at end of file diff --git a/icons/agent-motion/icons/question-shell.svg b/icons/agent-motion/icons/question-shell.svg new file mode 100644 index 0000000..7abb533 --- /dev/null +++ b/icons/agent-motion/icons/question-shell.svg @@ -0,0 +1 @@ +Hermit · 请求问题 \ No newline at end of file diff --git a/icons/agent-motion/icons/question.svg b/icons/agent-motion/icons/question.svg new file mode 100644 index 0000000..bb1624a --- /dev/null +++ b/icons/agent-motion/icons/question.svg @@ -0,0 +1 @@ +Hermit · 请求问题 \ No newline at end of file diff --git a/icons/agent-motion/icons/success-shell.svg b/icons/agent-motion/icons/success-shell.svg new file mode 100644 index 0000000..0a9a7ab --- /dev/null +++ b/icons/agent-motion/icons/success-shell.svg @@ -0,0 +1 @@ +Hermit · 完成 \ No newline at end of file diff --git a/icons/agent-motion/icons/success.svg b/icons/agent-motion/icons/success.svg new file mode 100644 index 0000000..ffd07f1 --- /dev/null +++ b/icons/agent-motion/icons/success.svg @@ -0,0 +1 @@ +Hermit · 完成 \ No newline at end of file diff --git a/icons/agent-motion/icons/thinking-shell.svg b/icons/agent-motion/icons/thinking-shell.svg new file mode 100644 index 0000000..ca4e350 --- /dev/null +++ b/icons/agent-motion/icons/thinking-shell.svg @@ -0,0 +1 @@ +Hermit · 思考 \ No newline at end of file diff --git a/icons/agent-motion/icons/thinking.svg b/icons/agent-motion/icons/thinking.svg new file mode 100644 index 0000000..ddcd992 --- /dev/null +++ b/icons/agent-motion/icons/thinking.svg @@ -0,0 +1 @@ +Hermit · 思考 \ No newline at end of file diff --git a/icons/agent-motion/index.html b/icons/agent-motion/index.html new file mode 100644 index 0000000..ed0377b --- /dev/null +++ b/icons/agent-motion/index.html @@ -0,0 +1,15 @@ +Hermit · Flat Motion Kit
ııHermit / 寄居蟹
AGENT MOTION KIT · VOL. 03
A LITTLE PRESENCE IN YOUR TERMINAL

一个壳,一个小伙伴。

终端是它的壳。橙色小寄居蟹,简单而有表情。

06 EXPRESSIONS
FLAT SVG / TWO SIZES
FULL ICON / 带壳
HERMIT / TERMINAL●
~/your-next-idea
❯ hermit agent

小图标 / 无壳状态

CLICK TO EXPLORE ↓

大图标带终端壳,小图标只保留寄居蟹。纯色扁平,柔和配色,表情连续变换。

原生终端通常不渲染 SVG;适用于 CLI 的图形界面、Webview 和文档。

HERMIT — SMALL FRIEND, BIG IDEAS.CRAFTED IN SVG · 2026
\ No newline at end of file diff --git a/icons/hermit-flat-motion-kit.zip b/icons/hermit-flat-motion-kit.zip new file mode 100644 index 0000000000000000000000000000000000000000..d94e7fe815ed42b5a2abaf0704ced82fc43b3166 GIT binary patch literal 20762 zcma%?Q*dVOqOHS@JGO13W81dvq+=T$+qP}nwr$%w>GhvoXRlqgYn_YlW?p_(&wR!h z?>l8BfI*M|006)NdW*HR1jo>bc`*S19LoU!@V}p$8rfNyIZzv0>N!wb**KWlSW{b= zInbI}8ydOLm^xTlUTC@7uPr8gZDs8SNsG?{;c6f<-H~zt28!o zLPeASy5xFVx1ZvQ+G-*4D-e-v+qG&SFL9p`HiPhv3z{iKMx0t2$RkB8C?I)&mM_7K zl<3gU-BTLSoL*%}txsOAJ~m!+Y6mGd1gGw( zwX$|h3sR_4cWsenp*XUL+%QXdf$0;=2{-{l+~@*}nGK{UrmQktMLk3&mtapk#-LVl zC^1k1bsl>#RXA7mGp%2Pp}75lUn6e8LM&576oBv8W52!1KXDMOfJnXvX(`{Xk04&S zla9N9-}p?SXcSoLrWkmdo2pdi3t6USKKlo9tTrbJl^@xdL7lA*O-`>teR4lM2a;Ke zxnV%NWzW?~g07or6}w8JUrL?Z#x>WZet`X)Y)vV~B8Yf zC2PAzQSgRp&(I1Ne&4kW?^;9}p=?7cvk)LQ`T4VHO`);T zpfwXuP-c(+Zefat5=g|Gp;mV~m$IgXGzZ11GHbFdPH-M775E9`!eE=GiY1LtZ(2t;K6vheFdly#_6DJP|v6z}*^% z8x?A(7&5WiR|B&i9e|(>ha9W7lAfGwRtx_}qD4ZMa0V-fKI=SaH~$SX7blJ?hP*tb z%gk*~xCKF|{)^Y2R3?Nt;lJQ9cCF_<*o6{tp@x?mW@=M^q(xQuiMC1<@9+~{bQk6^ zha6|Rhl;M0aGxA0xfXuF6or6~YO*Y=Atxack2bZl_q)O8$csyXi4TVh*Jurz_Mj2x z{t7ICLHZFI3~iIb1Wn}LP@%OaF5_1r2zJbENVPi}6m6Z%yxNhaOSd5}LFrsh2(Iea?7#0YN#j4aKvWW3F% zZXk_u-9m3P^S04D?9M<>XiIZ+MKOa3K7YM?v?yw^g5FH9>yD@WP`w8u3`i7J2=qW2 z4sG?4Z9Bj_rK$ZBaCe{Hj6>NW!3n612Sb?>cCO;R4a&BQ2Moj?sLG8}$Dwi;Xk}rr z`n>u%e7kKBEc8B8?dbC;`%+FPV>$!|6FyfF0~g)>Kjy~Be3AsJlHgV**`Yi9^psYO z7>#<;LA=WAB5b-3Q=E(W$ZI>tMMh0QD?tnRroMYPJK+pZn%>qSEFCD%v+dp7PTgb$3ObbgUP#Hz@^0Y-MlPLUcoH_F_J9<5%#xMh z;)wn4UwsT7rc2at z##=IuF#b$XwC_oPAOTt?%F)emIyR=g7Z$h!A2!4sHpf`b zhT0%pc8{m|-yy@PJciuKs46fDH!di)dhpymXi>ILQAm0~b{`CnsGU%!tMh*zO6y}chUGn%e8U-rDU zJoLgQ>!wmxSbz1OkY9qYbMU>mny@WX*ad#*M&TW-P75NWWJt-Rf8zD}o{ zXgV31y^N0fqQH2X$6XpE;>>Rp6mp47MsA&4VdD48jQDxb7!U~&p@A<<-S^)^0#-R5?{D@D56I48&JQZKx)BHwG)T?~EJq<# z^$FG&yIBwwP*4NXKrq9ctwmY*uqLwda$iZwI(H&4=Hr^nEQ)h&-BZLOqigHpYIb@; zgmdpF(o{r#afT(dqND)h717{6k~* z76@3WWf88jVrR&fOj-;367_SRC7&oF$N5(qw|XZgUeCiXTh1oUHSlc*{E_sjfpbtCJ6_v=dAb3UPcu+cDt7W zg#auM-5O=zFC9XET5jMctlHg2oMh6>C?fGEn$E4S+h2wu*fzEkwLf*@SaXvBY8tCH z4)Y=W*y@sfOu>cu0U%KjYxDaOe&;01BFgs7u&|U&H|jC5^M++Lu42!@yQ@q6Ckf=zQ#7>TIojuB_?V2C}w(97rwC_9RGrbm84>e^`7h>9jb%D47Om z;8;6j=BLx7lcQKU?Uj;DO%s2Hrd8xomfb zUDkPfzD(l5&nEEW``oB{)A6kTs8hM@01Kz_j7o(61(*E$`Y!1Q?x&rxCM+Ku)e@S` zeks-_sLD35%`@g7AnFNBV!N)HGRIZZ@S5!$4qbY4InIh(@v*6WFX)fMUFKT1U2sDm z0~6&$8E%^Bt8z|0WgH#uS_&ax!;|9_!lA`P+@bN%6A1Zcin5hyAO20`A#-=)@E9Lw zdexSJIupc12z)-gkPA$eR~?LBIGS(7 zzEr&1-OosQXS!ZN?F<>WK2Cufd%Bi8e^c+DEmSPJ7Kbi(hh1H0z5{AkaJAUomL$Z< z(A89Jt$1B+wCQ{~*?wqQmpy5JwR`V1^dvN0dO1E^R^UE$JsS*H>ZftneTLcCc-ul$ zMP|Br1R$%QL}?Qic!OFy+jP)BzZ*znGW_NMSk_GX{c?|-eV{3w&fVGGIIe!RzWy?Y znvTiP;@S3f8=TH$$d5hb6#cweko3y)`qA7NiNn^X8VFbH{Xna!=3iE2^>sStzC?TZ zeklKSdxVFx*P^}s7Fx1J?etQ4D5eiFg+;@JIqgCt_tfs><>h5>udq5#v{o`95JNNt zJi-PZzAd^{LkqA1G!s%88}3H)=llqbr#bLg`ld33kU;K8vv(+6j`CLPXg!{`QE+6i zbHR@pWj-D4_70MgT@Sj9qZQEzL|IR;54ckAWCxblS8TvkQ9d=^9v|+lf$CPONUT7i zkwQO(+eVHQlsFI%Dz*|zJe9X0N@qN0XTH21om#n6InNh0E-=Dd{ujP9dTpO8O%#n0 z$$?(h6QDyKJ$@hph_#ULJRK)*uMH+BBif~F`N4Ge$e&|G<}0!Z8Y~|WNjhZ#SV!39Xw9XG>j8%0BaGz9oOv>4Nl1Ex148f85P&Vep?BXg3d)qQkutf1jGQ< ziawz{64kLw=-(3~x#}I_^h8qa7hYh;6(yxD7EPo^x}M=Q32knSqY{jzvEjZ_OctHi z9Z8P(**G#fr0C-=K;_0C3^q0aucV0R#_Xl5Co2-l!NwV`U7z*`JR+6~*PjP%D%$0C z=444!%j8TN9CDc&-UmuDTkZxwO~+uf=;6!LuTBq7qE8YZN=Uf;l%i{)OkQZZ2739C z$BZ>8Q@t%cKm%67XzM%j)8r6LH+-!hfzI@&*zXfU{yX3`v7832fJi4!69FmI$8b~g zm|_fb@xxT0okNRHSiBGfzhb!bOF|q-mrGDYA;24&WHNCSgIqMkfn>5lL3)x9aMz&+>SO{Zi5Y{-@%;R))Tw4XSSTyy-E*W>mum^_fueYS1EteB@}l zh0H-vjqK$#`#V?qY-3P*gid?;9b>$!IQ%3GIjd*HlmoH~WAVkcVnlq`b6ZTMUrML@ z^m0RJ?UqD~h2Iw)i$$pg6saS~@CI|f1Yo%@zthaz^tJ%cJ1ZvqAS}<|A3EUxdV+9x zy>M^?K{Uc%H26;*mveg_+>i7i1>D`4sTFW$uY1P9LVA{n8nbXI14|5pIkdFF<@>LD zfvUHs?K39B*KXL($-#mf|0;T#h~H-65(b(Wd$VXsgUw8X1#hpzSKQo>95CSA&jwv> zcsv1&tNY1-Z#3bhojXt8*r zh%UMB=q6?;oLI9WZj+l>B(QW7Sssbz#fbZLkJN*(v{@>ik~gW^5g!)DN_5OcZWKl~ zC5XRsOsg7eP%2Tpb6YL}_0~uKIlGF6;%^k92jUgo_RuimIJhXOoOA z!O-yO`l#wotJ9TesL|dw5o9$iLS-~UB$Z{#DZjK zvc`pXu5sa=7C@Oy zk@!1n3s9P;q=a;P!MrV>)x-fY7DemQkTXz9R93+naayDREl2oVJ75=hB3UVy3Rsho z=-G#J?)zW}nI!wKmdTc2{uEU|cTu7zB|0*N@*SNsQI1@V#!=5sL5kiLCP?GW)Sq?U z*vFD)N{XLW*5m3_dirUb_Gg%R!2twHXn#T8#f0QFjdQ3B<6K=2Y?PMq#p!IWj}zE8PM!EYv+T6=OG(GX$^)zS1ZK( zO4Dfzc~x?3a`pB!&U?5coYES{^Bl|c3P(*9!LqBY95nC$9P8@LI9U{%N!lrlr$qeN z3FJpNk$9jCd4G1|8r@tJ}VVj13qq#QM1|mZ(G#mrL zL;fN3pGQn4a?aa7w3Y#GzdR-LaA8KLq0GH+SOD$O)}Kv}%4B+^WnZT2?`BCql6W4| z+ya3;@;rytL0r^JvUE*Z*r29f`^}aeB@bvw(u$5z=4!j6GXfo=1F%G|(+jQZ$w`aZ zo$MW5&Q|&`g;yK3)H$I5G<|BFGGej@Ni35zmt9iD`-(#vb%~Yo_&4-X!%p5;>P~aB z&4E4_i8DSX_*9ov%MP2&%N@1AClz#)NuWw8wy&VyG6MO~7ZdfY&=(tNK#qV}1@#2B zn}RrZsv`YWEe35Ssuvx7aci?(p!`a0^#Q9;GnSN0Uc?TClFX;t1MCg>V~Vxt01IckGJP95?lBu}9w%kH zkN`Z?B@|69clCs!#(h>^kv`(6TAzmvw$Em9NP(r>-&7j-1*OR z=xxE!sw7ZOeU*_FLtRucd~{V{%~}|#%A&U6pMDlGb@9tt256vooek#XKU>k-9P!zJ zB0bcqIeR#nEQF*o{eT#723&u8B2+6va=dp8jn!<2CQBO5k%B%B6{Qy9<@t#;kNCir zfu1+Y)wEaW%RfHg1OkgDUEW*|5g-Cb$s?QVOdDSTpa%&EBMH~x(MbPH#ej#3`7=BM zwHJd{id^0qp&Teq?T$DY@Cke$2yBRIvTf@j2#Q~uAE6(@G8hjCQ>-#w-|qlP0~v=U zO%WYvIKAb&3AnB%ThsL81oD(OQ}cPEy4S$u7dhP0FT&O9aDBSdYX~~860?SAUTAet zE$Jit3y|`hs%xJYPqH!17%A6d!y>gGV9CYTkTG=qLieFwt?{Zf1($lnxT7WDlJhkc zVpAqcDQWD~Zo$`GNN-8V$Xr}SyjeDUzBK?>P81D2({F>CqrXtd00tuN@uNeDXS!uc z{?z;y9pfC|1W=*kW*}jY0&!d(+?6H1gXG0rR2ZbliS%Smy$XUVGbH>;E+2UTJfM0; zKgLN}HC0fkAmZ;DYZA4u$5kb|;^5{+njh5;#CVNtc>@vuz@}3wARh@861-eAw0NDI z39;z2@d?@aP=WtjY{7wTwL0I}@uiwTyk$K{_Hn*iryuadRVvZ-=_J8HTTcP4(?X=N z$4xS@fGk+alcoW`)a@0{Abv|Xn+E&kf|L{!4%N4z)!Row869`3eJ ziYGcg$13Vh3bGyJ5MODci4#TiQQmHK!nky$Jke1^1p_CUB;=IvPe`R(Qup#T_VxvUmzV$+pGCd-ClHe=9)oK~K3Z&zY_ZWl1^_w-Ek8 zEu!Ycr-+9??_Dio7FLOKZqA)f!(+8!oB(5`IZW7z#UP@!nZ=z`+dX-~>?QqFBwMM! z@3ut?-Ji{k)0r|_)WBBT@=ud+s3RDGWgc`NdEr{G;SP}=M zx-ri-B|aI$G2Hh)+;u@%B}~nYq2@+-)D2kXHe09;!qm!ZLEM^6>@2}vzJv@B_?KN3 z0ybWo=Z2-SfWF?1shN5B)OcA_()xW?h>{%x>7=bTL};Ej1*eB4aTf^)kj$VhON5>t zArXfv9RfgBNViNu`7W<*-e3&=7*(*)@Rd?wO4rV;yoK)(!Op;c+d<9C2S}F~Sfa21 za6)LMq9>e$h#3th4{C2V4$0HGktkzfh-s_vgkW587xp#2qevs>0w8CCg=37J{@76P z3PGAiJ&X?tSkS5gVorj4y@=GH{dS+(c-uG&#Vv8>2erWo>LglCU;!^R*O(OIoPhwR zy8dAev(0teoDrWY%w&v~WCdGcmFLmcCeco`^9FSaJR@p(^N2n|1(cPy>*!Xbuz7>nl1Y*j{y=|epgv}n~=Mq7N7E0|4*(%1r zlI)17KCXWQyo#(+iJL7M$+1)#pNys+9)R?Vr%~t2b{dkhwaxI4ddqov@2PAeq_Pv! zdcVe~*41du>8ke*F;J4QVna@N7f%NTnLD$j9fdWoPa10&Z(2uXNB3124f2!3u9-FM zw#U^v_&Yn?&cl7y{fi7aSSsA5%rk zi>N1V)-539$KF>5P7o41-NKRX5+Qz(u?40{Op`d=;xLBV*imS%U@lIGYf)pI=R=D8 zhHy>^p$(uVd&B*A@>Sr46?nhNp9Kd1ApYOUH#4xY zwx=~Sv^1izcQT1kZmZoDhWkjU(bNE?iP!|zM6qtcV!HveWEF)OCosDnzh)?~1s?jD z-a2|P9RqBF`K(iUF=ZR&qWK#>nYSQkVdLoDA~i;PYPfM&g3IY}kSe&dYsIo%>RgQZ zE)NgfW8QXHz55We1)IaO7d%W9fsJUQMVK+>U>lCuzT4inRTDzmp4->s+yklD;9!X-{U`>WB={(ARL5J}G~&H)DTal^13!eTzzrfArf)QD ziN%4SZex>7`?N$6G(mh^kS#p#AhF-F#dAtR&X&-SjT1w)lIXy7HKfCrgj>yif$jCe zNvt*XPrURt7+~oh@i>fT6#cJr1UYFTa_ICNJ+M|lu9&qQ#r+np_AdQ+eluRJxs?4} zO{`}++Ll9LO<-i=1j;$x>kr8K2Y#AaTPJA=?QRI)Gogb52E>DKONJXQzBplpOMho;G>|K84Rh5jJL_!$(&JqZbWSJJ}C{>k!XYC;S*R&bLsC;?jVlrt_E)h2U%#ijzIc)@4r%H#!UZU;C{J!(7JaPIhYzp|o-o?4Kn zwe~IWE0W9%r7+Z*wd9O$E~kwMP5m0%s^V$$?Hk^9;Em}$XX>xbZSFSF)S~q zC!LNK%PzP9d5S&>1tr3->Mh~AId&^ew`PK*^>Hy4r_L~agr~)FaIpA_6oGmX)E*`CDxZO~@N!^H$kH)p9gpeW(upshf zK;-XDMZ$hcxO~F?JAznV(?_P?2C0lSY??G`BSIAIc_96|!C=6wK(@RHoe@oZ$a3Mu3BU^2 z=5!xFQY{D#PwjP{9i%dDPI{#YAY&Sw9w1kwaXb)7C*v!`+!dBx;a(-fQU6wzfWM69cj4FY z@#^9xrmwX%X3kL*;RHr;f8*_!83wGkKRH7^_(-ER z>Yv8!7YZGu)ll}L*jHKijH2nOflDP}+cg1Jac#Z3(q?``;D$K|(_VOHy^jzIYCq7I z?`_%WXX4jFIbfn`R_@X$?HUB7#)B}F)zPO?g|kL0SQ9&1J*&8|%rn_^^Ufr%5rq>z zjLZlXFO`0ki#G59L*0vCh1hWXb$HUNh-0 zh{!T9e!p>gv9|ia+L@Kto5fKRYbgiA+vlw7Jk_I5e?%gy}T9l6z4b; zl%Rr^(w2(1mSpLL-+5SjdEuh`*~9XkQ(Hz`m}Uuz_VHV1Fb2vBV$)m3p6g2Enr z^mseQbMev7F+!Tay^ZRJOJ1=Eu)?C?pG&s3QA#x_o z>>U6lFgKE?y_R8ndl&CMvOszN)&j;cdLvTq@d%q2fGVHR*wKhfx@Sk=gd6_KS=--# zg^BK~Qno5(6y%7nGOyDdUmJj#@k=w8q&;Vq7Qd?Z!wt;ns6*=QahE;N zmiN=F{N7=_Jq)g;w+~~%jp-bo|A3DoIHZ$LhFYDwFq7?XIGRM2ph{f2Ay14Nz72t5 zGl@R3pEr!HdW)|1HkgLtkLFFRu&+6#@5p>Jks&iFQ zMN1J8XHB)M6J})_{-K`lUD-+c0fi`4bt|9)ix=Iq#R?Mj4&h;Z8JzuW?L*7su{r7M z74XVL=no5(I|@;+m$~k?6ZPDxC|?&WGVD7W1B`X%*ZfYnbB`H~XP5g3VS(-gIhWV- zmY{}yYZjpPvdBu&R`SE9_-P`$_x85$I|~MH8Dxv{X4;Mb%oiBzK8Vef^Iz8>7PK!u1X4uLxhcni43NL5CsZYWN`8^?HFGQ?TrTR}f_LF?tznmRr`Jw(li zKNw3pxhfxrG|(9{-?m>kommsAl(wM0c3$NK39$e*b`5XzW55ZkitbJmhz&BQ5;Gu! zmXf9P^dqoYw;-e&dqA%AzalHrK)-w*9eH=X^Lf2L9K0>rEZ!}H$Mx9TKTmB<7n4C20v1i4%?V#E)&>Dt1 zMWqR(+7>^Gl$Z~YAY;-h%gbU9&n66?T{t5GtiZkbl)IEbjgLLWr_t0BkF{^$)I4a8 z#Zka=N@(X8F!l{>iK`P%6>Djqlg{YeqSfP3K=UX0Ht4fWdiT?ornDQc>Y3RR&HK?Ff%HC&@Uk{VIh^%!cx6Mn^HQyeA}ps2p{DKAwO3uW z_ctqU(?Dm$s@5`O?O|(!)y_GGf3j>P>-+_78g;Out$Ts)4{-(16L#PD!&)z*NFMj# zrf-F233kSgUiZcyEOY!zp@QEE{W*mE3o2H+e5?15;0Mf@KfV#BG5ohecjG@&hAiQx zq=8zIOVlLGKh=Dk(GfaRRmCgA=5I5HAQ1#?eZW{-o}T$^B9sD*q2TTUQ&jOokob#`8O{#}t_kwjbE-vrKo6G---2())JFfg*W|HqJHwX1)7Wr%zw0{P!w3EiZr zW)uf|Z5NG!Mrbztw^w?is2Ltrl(kQhKwW*%K04 z1ZJv287LD@tywo+ve*@*y60TI;h3u$66(H{txJ^y>IDQ6{iKdH)XYoKCWSDyf+uE7 zRwWQ|tOO}S&*EpP@d8eO66pN8TZ-cOBH`io{QSP2apr0nFtXk9@_b=&=Xs8OU|{w5 z%zqAC;078U={Ku2zv@6S_t8V9eVn4m_FX82=tFxKA_XlQ+{d6|>we$*b1JMYj3Sa^rBqlCuElf$>Ma3`$x;0_TRL2`cA_@SgY|bJ5DCjLTmBp zM!qo_cGUXIZX_b-vG$v|43sx5NfdaBEun6vDbAE;YFRb4();meS9`0aH^PfJMIV$xqQO=3*5KV7 zij|gIGnUFal_-0S7c3#d!!Ih>pm?ifq3=2wqVZ^gQ^&{20dcIHZ%kkF`nOIJc`(<+ zYoeAMa77^#1#SO;wROC@3Eade1D`;_JA`7a;YXsJ52co;{;rb`!Q7SZSE-hc9GUZ_ z;2QUtnae&!-+pX5ZWzGLCzHOFAi1^Wv7^ z4(fa}$o(BtN&XXqMs{{Kc7F-XP#*i6Q9lwQ^J4Jq-e#QscFOONdJ9-*dH%z#dLzHh z71T9G=@yJ}jBG`OaDw3NeN3>`JI36gTN9d34KK}3DMLZ2NDJrW0ov-WeA>Xodfe{# zEp1|OO)yiDF$;um+dtTU(a)wcZmnic1WC?Me>a=R0He2AUY<>KBs>Z`#-CBaZzo*J z<-4sPQK0_lcz+*D(&6dwc)mM$iaO5vZk8{5b$6T$y@u~CPA_j&P*pMBF6^_$V5qt^ zL#niFGQoN$6J=bifz(Vb_8?-7Heb_h6I>?Y((-)tC>}ij{Z>bnFG*_eALATk-?YTCzO$ z@6V^ct$}VyAcGpdd}7Mps11}QmKY-=xyIC_FNt)6PNist#lPyI1h3RRPm=XlBpWEm9*ncp;J zN&P!E&R-K{Nf#;vI^ssEdZTvxxQ}b5PU}SxNjT-h#Ekawkr}w60lAy0hdTgs%#P74 z;xxK@UK9GOQAHxan}6oVhor9zed@*$8?iXVD?{gRvP42jfArA6>V;fw1Ff7EgZCb4 z{+)N==qRs4u?~?&P6)AL}9M_we}F z==`@hkZ=RYgx<>Tnh13S_NAXaA`+sD9asd5+RtAQi>i-x$N#fQHZLf;{rN_;@f%Uf z|Agq@?UmZz)X38EpXsNqa*qY>134%R^qhA-E%u^;#!u1g7Eogr!OEUG1412f{TOjXs`l(}%XNWGyfuZmn$S1E* z57P5nlul-s*Za{~)fBEb@RW|1_vbySM=C|3-K&G$%h_Gv(!ANGbOHM@uGH?IXibGw zsrFIrMS9c0x;{Aw`B64h&|N=jRW^wC2OJQW4sQ{0Ww-$sHa1YSN<3m$cJT8Uke|y4 z^sEQiKlxfyYLF(5r@}*hvGvCqEW@d7z*X5yh!yUkrC=4EvQ0dAQtCK=g*mH)%d1mR zRGy66DOX}}FaNB1ktHD4C*H+e(7T=s5|0Db9JZB?6qlby1tCyL$hZwpfU@Q5IFP@M z)qYtQE8#NJfE;DX9Z^Wyd5D)=v5PBI92Kec-VPou+U=B@j1+cDzecD{Ts6FDTol5*7BJI zK&xOw4zI4CFUtBJF_p_(Wb+l3m>pB!tSC%NeL8hS;II&mU*OJF3lky2>y5-#^h*%X z248a*qya$=YsI!Pc_NSY;Mis6nB^p*iL0-*whesv0`3)D@UIgyJ1YthJPN9ge;KAn z)IAUQNYnMKWdGuk8e#YKvl%__efO=qwX{J?_Je*0N12qtj$3HEBJ27Ay300Ls^_KcLpJ=0XkP%0{54CkO}qC%rMMHVX{Q+ z;e8ibbHOj1y=22LjfqMG1L_pH-im~2hjgK%86AF^FQ9*SF72wDLhU!uf{*|JwEqd{ z|5|eYJJe&9Yk#3mSgN(agX(2qV>}v9*)dSJM%B$S5kl`PkUtCAJ9px6KU(9b!Gbk7H-!Rk zOuy)ae?!yv>0t#SgyxH#k#Fj=+i8+K)b#cKdVZeD z;_>=Cd7A2ahvxoxSS!-u=`JeSr7(~ojqBoZDle3-W2au=`!XdE2^eKIIO(rAH@fgQ zCg8$K$QNQmWf0-N1*IkFY}y3QDu1&1?uq8lQqn&;C(uMq6QMoa6@kDkOR&;+e+J{N zjJx<5pMy>AcmMnoNSy{Zh7F`W?%IRQAy5ZQsWHu1lQr&|m=e{b5{{ruMpAV$&Y)e6 z!QKC}@`X;Fp&yEfrF39h&0#ub2MY_C$z%$=UQ@}5Kwg`QH~2MB0w2=J$euL6(J)C- zwlzj7ogJo$G-8zImss!UZOrl2Ku&E+JU@4T;HZR{9fiseWyFLfVa<}tzqm&I=DKJ_ z^3Ots(`m%VLn!v4{E@7mvI5SH#n`~}!{5(9RfLhP2jk4pGs6tUar#p1smUD(=NzRRV7$VY6W8p(gF&|P#^GT-m!7E+F32Tc3;#* zg`kv_6pC853S)^Ok2#z`dhmlP!FnQ#%+dQ~r>M|%sFa}nMRn>|hl9H{S?E0ugl0Uj zc#q6l3(X;B(GGEb-~fUIh(@i$__R4ZL}*cvE5C+%2@RdFMr4vsLk#lB98XLkv_|G2 zr4d|6cA>VjPW^(GePM|>X7$jM>XUu&Kif%CXw>Xqoe?^xF{8u;=V+Dd3IeLm zj;#FG2gsHe3mj334rnOr{Bzq$?*yng5MH%l{Al|FsPNcl;&3@kiQ#?8EcXN`95iCE{2Vn2{sZ;ti@7Tni_j zDLB@F0R(J~j07?SMz7zyBo<1Nlb?+{FnL+v{o!{qHs(Ib^>oWf`$TCwUY8rbqh4|` z-Uu7)?|4mxufpPlHMEEWch1J~P83P+-~(I;_y-F-HayWSMg2~{59)$QXi$IHo-03A zA$|sQO#hiNj!k7dOUC>C?Zo-|qKbBl+dELken4q3b9JQa%kt>R$sjA~G~b))89m2t zOqJc9(n8BUFHls*gxE}uCU86^+G8LrCAdjsJLcksi`qpR8Fipst9vfoH^6vgxGooA zSy$$uID!Mm->_*LLcMy6HI8tM6wyQ;U84dbH%8hDh}mTvTgAo8deI^$xL|DmXdP8{8pOn(0{<+ zyAv0^7@;Z2b|6h-b;P*j#Yj-Pj15RQAe`X|RWPC_5A^s0m5>y>uV_8_>#xx1aVTD@ z`=>fQWa<`EA!=L;GJY6*HR}}>%(KO3FU6Z2ZS*^)J0~TkF64z%JnHR-KV-_gvGQAw zDjUd}%d*%xnkesY66~wH5js#-p{PenkSsD0?3w#~_u!zIs0Y*yx-x$cG&9>Q=w)hD z*r0_D*62ff{pLgGgn9@bh+!Y+>sd9@vlGK$U8yc^nyQ1e>8g^|rTPaP-i5y_u@-%G z$$$j&TXw6Xd-d?yf_znaGTa`8#~;Sdq2+U80k(DxLai7&jK51%XwYH1`#;=Io>}2M z&bEb_JeKzK?Q#0N_fl^pL@89Yc4S}U3#u*?E`kZ)r-x=w`>^oOiY=KCDr~XgA|7U6b@3>1f5 zIx-P$63FJ*Z^lfs%kw75 z{>m#}y0f-by%wy-BT>(uvCnh~HB8?05@Cy;6XdOHevbgBZWEU`UHRpJFVhynoy>b> zEXz&>IIN4vRfc1&ue1~R;{Dm(?P?Chhv-6QVa5|c>E+%R2p4MKuLhSYV_d~1YgiP} z4)xQFL@$?xjGj!Wxx*2nvdn8K9A<6Ffg~+#s39G^UF$zc%vL$(ph40MW4QP?%4#G zUl?xaat_F5^3n=p;`?|6`e+)Pgfid42yUMR&Unr+#U_#HVV*iI!x$eMY!gyyEkz)6 zh1NnFUgr4 z?1)A4se2WVh9tZUdj>Yn?Y1EsN+?ULY?_za5P6pcnKf=MWFvU|!HSv54({aHM}J}f zgQNQ_Q%zfPuAs^pzLT84#nwOSnP$oJYN?id>R&9m(OuC^n6N_vc^_A!1T6Hzl6^@Obtt!Y+7k+)DY7;AjBs1_B zh{jgH?7?>f5(gajC9pHpL6S7hoqa`lHNc5oqG3ZOCh}kuW^*Mq6-{4=SzlbG2+cWU z-9JRuS6W$0efy=wY5HhR^s2U9DlCpqZ$9VT8+-#_t3MhcrE#coI96b1C zWR@_gPee?HaE0v)th#uwL(jxh{}QUm`R%8=YktiU?7DmLu%TSRKZMFmw`;eThw0t8 z8WC7!!F1F_o@K~k>J*8qkbI~8kJ$cr6jEcw{E26h0wGwz`57hZQaWa&v46{nbMlD7 z#ieeYPjypJ>@l~+WWTRx$0xvB7+Y0c>Q+`Y)b=!NI2y)49(l!5E4Sp*=&(j>A&zWU?&!H2z6*mEl6>T^m0Pw@An9d{>9k9~KQ!&b z@eL#DK2+e@+e^-i8a|Z83nnk|k5MX$B6+Il$Wly9ai4zp)xh%8Rid8qrq9BSQdnET zOT$TTSM4nZ8m|a0eXHE_AA?%%s?Ai*L}RPYucG#esX0H$&r}i1xRVCia^gdd3fkiC zY_}E*eO6cBUR>iktYm!qSf8P~g#xMC=>z z>j?{!23o?lQWgtgdR9QW4Ba;4oNK&Qwct@$l=W6St{SB)bpV94mkvktwwYdylvlO0 zU?tkhPp}XIm)7T@MBZhNwmh%yMobro$1MtL@|+-5FQSp!lhbC$~u$fT=F4!q%PmLp1ihPTil}- z+obWpR~huev4KOuZs@86)3p=Qy(PPg2_nO!WFG5c4h6NM5=M27wHL{k68%>^NZUPm zQk(QBDR<=L)Nm3sL;NJ+gl&c@YfWEKr#(H7d~#ey$oLdjFc#l*Myu&IxFQWbj=UN2 z`*z6ISIK$L%|0Fnuct_5lr$U06_o@YSd;Y3o(jkke5W~9DXx-$A5Caz=+}u_vmBHy zw*1gA(2@7lQ8RBQ>a6>WtofAOpH<=P-r=9fHRXujAdFy+?mE_lbewJA5z0o7NDbLa zNU(eR@9jaTCvsJCQKc;#diMRPwCRB7C5>Y8c>&Dbc22zD^It=@7;4BCOAXmZ2A>3Q zUvCPOVeS0_k5rt@k6rMKa8L0mu7Q_9ds--?pZbG!gN(WhvhsbQxerG%VD)2+4^RZN zQb?|LIpggwXJmac9gwVevPj!9w_Pcgb&Tv7Pbs(%|d z!4V$#Ip*(27`WN7S>scx_$)s-PI{3vuf1lqYD1o3a(OXsfYmASjxXXIOCLOtM64g3 za94cO?pMQ&?9eppe&y6wo@0NDVfBW9hR4lE5hAZYn#$PmIyc9CBq2$1Hy1SUDS^x_ z7dh&El_SST>9#U9GyT{Gxkh*`2nG{fe~+RgJjTUPkh;a4+6!u`XBwRroKMI^VdTlA z?_RuG4#Q4R?&I5)CCo^qldQD$>uHA4Ek?Wybf1N!7z#YJz}yKMjE_|gF$8}ly<24E0heo=w?o<&8tm9R7BKj}X&aDS z&w9OowyYJXJ1Y4U#Z$LMTJ$_cTW)SnvBl?k&*Fw!9XB4m@R%|wWUNomz{&XQOT?&> z-+pCM?}zz6t`q~}fPaek3ZtXT48~AzD*MF^Wq!{Vuf#9#dGuP6N;YWy1(xRyi18XVdIOIy+bo)x%hK!dhX z4cY%Z>koSeKq_!{N~0o=>_Gi{k_u!3hdngrXN4V@KaYNZU|{G>12-t|0RH|I(8?YV z4ve&Da1vq%`1fbw*KnYHroqcj>;MOvY2dkmtvn4KrnLk5AB{bb4QwlDY%`r5*nlR( WSRcG=>)UM9cO8h1?xexir~d-E47SVw literal 0 HcmV?d00001 diff --git a/include/cst_hermit_glyph.h b/include/cst_hermit_glyph.h new file mode 100644 index 0000000..2e6a18b --- /dev/null +++ b/include/cst_hermit_glyph.h @@ -0,0 +1,63 @@ +#ifndef CST_HERMIT_GLYPH_H +#define CST_HERMIT_GLYPH_H + +#include +#include + +#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_ then CST_. 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 diff --git a/include/cst_version.h b/include/cst_version.h index 6c54555..217835a 100644 --- a/include/cst_version.h +++ b/include/cst_version.h @@ -2,8 +2,8 @@ #define CST_VERSION_H #define CST_VERSION_MAJOR 0 -#define CST_VERSION_MINOR 9 -#define CST_VERSION_PATCH 34 +#define CST_VERSION_MINOR 10 +#define CST_VERSION_PATCH 0 #define CST_VERSION_STR_(x) #x #define CST_VERSION_STR(x) CST_VERSION_STR_(x) diff --git a/scripts/bootstrap_linux.sh b/scripts/bootstrap_linux.sh index 8ea74eb..f485520 100755 --- a/scripts/bootstrap_linux.sh +++ b/scripts/bootstrap_linux.sh @@ -44,7 +44,9 @@ run_make() { install_bin() { local prefix="$1" bin="$2" 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() { @@ -60,7 +62,7 @@ EOF #!$(command -v bash) # Manage the parliament seat service (foreground under systemd, or --service). set -euo pipefail -BIN="$prefix/bin/cst-agent" +BIN="$prefix/bin/hermit" cmd="\${1:-status}" case "\$cmd" in status) @@ -102,19 +104,24 @@ install_from_release() { if [[ -z "$ver" ]]; then ver="$(cst_version)" 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 tmp="$(mktemp -d /tmp/cst-rel.XXXXXX)" # shellcheck disable=SC2046 + local legacy="cst-agent-$ver-linux-$arch.tar.gz" if ! cst_fetch "$tmp/$name" $(cst_release_asset_urls "$ver" "$name"); then - echo "release download failed for $name; falling back to source build" >&2 - rm -rf "$tmp" - return 1 + 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" + return 1 + fi + name="$legacy" fi tar -C "$tmp" -xzf "$tmp/$name" local bin - bin="$(find "$tmp" -type f -name cst-agent | head -1)" - [[ -x "$bin" ]] || { echo "tarball missing cst-agent" >&2; rm -rf "$tmp"; return 1; } + bin="$(find "$tmp" -type f \( -name hermit -o -name cst-agent \) | head -1)" + [[ -x "$bin" ]] || { echo "tarball missing hermit/cst-agent" >&2; rm -rf "$tmp"; return 1; } install_bin "$prefix" "$bin" rm -rf "$tmp" echo "==> installed release $ver ($arch) -> $prefix/bin/cst-agent" @@ -177,7 +184,7 @@ main() { if install_from_release "$prefix" "$dir"; then install_helpers "$prefix" "$dir" echo - echo "OK: $("$prefix/bin/cst-agent" --version) (release install)" + echo "OK: $("$prefix/bin/hermit" --version) (release install)" case ":$PATH:" in *":$prefix/bin:"*) ;; *) echo "Add to PATH: export PATH=\"$prefix/bin:\$PATH\"" ;; @@ -209,14 +216,14 @@ main() { install_helpers "$prefix" "$dir" echo - echo "OK: $("$prefix/bin/cst-agent" --version)" + echo "OK: $("$prefix/bin/hermit" --version)" case ":$PATH:" in *":$prefix/bin:"*) ;; *) echo "Add to PATH: export PATH=\"$prefix/bin:\$PATH\"" ;; 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 "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." } diff --git a/scripts/build_hermit_font.py b/scripts/build_hermit_font.py new file mode 100755 index 0000000..82483e1 --- /dev/null +++ b/scripts/build_hermit_font.py @@ -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()) diff --git a/scripts/cst-agent-parliament.service.in b/scripts/cst-agent-parliament.service.in index e4b86c2..de80176 100644 --- a/scripts/cst-agent-parliament.service.in +++ b/scripts/cst-agent-parliament.service.in @@ -1,5 +1,5 @@ [Unit] -Description=CST-Agent parliament seat service +Description=Hermit parliament seat service Documentation=file://%h/CST-agent/docs/parliament.md After=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 # [Service] # 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 [Install] diff --git a/scripts/package_linux.sh b/scripts/package_linux.sh index 1fe03ce..71975bc 100755 --- a/scripts/package_linux.sh +++ b/scripts/package_linux.sh @@ -29,7 +29,7 @@ mk=(make CC="${T}gcc" AR="${T}ar" STATIC=1 stamp=".gen/release-linux-$ARCH.stamp" 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 echo "==> clean rebuild ($ARCH)" make clean >/dev/null @@ -37,12 +37,12 @@ if [[ "${CST_FORCE_REBUILD:-0}" == 1 || ! -f cst-agent || ! -f "$stamp" || else echo "==> incremental rebuild ($ARCH, stamp match)" fi -"${mk[@]}" -j"$(nproc)" cst-agent +"${mk[@]}" -j"$(nproc)" hermit mkdir -p .gen printf '%s\n' "$want" >"$stamp" -if "${T}readelf" -l cst-agent | grep -q 'Requesting program interpreter'; then - echo "cst-agent is dynamically linked" >&2 +if "${T}readelf" -l hermit | grep -q 'Requesting program interpreter'; then + echo "hermit is dynamically linked" >&2 exit 1 fi if [[ "${CST_LINUX_CHECK:-0}" == 1 ]]; then @@ -50,14 +50,18 @@ if [[ "${CST_LINUX_CHECK:-0}" == 1 ]]; then fi ver="$(cst_version)" -name="cst-agent-$ver-linux-$ARCH" +name="hermit-$ver-linux-$ARCH" rel="dist/$name" rm -rf "$rel" mkdir -p "$rel" -cp -f cst-agent "$rel/cst-agent" -"${T}strip" "$rel/cst-agent" +cp -f hermit "$rel/hermit" +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" echo "$ver" >"$rel/VERSION" 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" echo "$ROOT/$tgz" diff --git a/scripts/release.sh b/scripts/release.sh index cb817e5..a77f496 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -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" "$@" } -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." diff --git a/src/hermit_glyph.c b/src/hermit_glyph.c new file mode 100644 index 0000000..9c7df97 --- /dev/null +++ b/src/hermit_glyph.c @@ -0,0 +1,167 @@ +#define _POSIX_C_SOURCE 200809L +#include "cst_hermit_glyph.h" + +#include +#include +#include + +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 */ +} diff --git a/src/main.c b/src/main.c index 93ccfc5..ab47b6b 100644 --- a/src/main.c +++ b/src/main.c @@ -671,7 +671,7 @@ int main(int argc, char **argv) { do_migrate = 1; break; case 'V': - printf("cst-agent %s\n", CST_VERSION); + printf("hermit %s\n", CST_VERSION); cst_curl_cleanup(); return 0; default: diff --git a/src/os_compat.c b/src/os_compat.c index b74759b..c68b17e 100644 --- a/src/os_compat.c +++ b/src/os_compat.c @@ -230,6 +230,7 @@ int os_rename_replace(const char *from, const char *to) { int os_config_dir(char *out, size_t out_sz) { if (!out || out_sz < 8) return -1; + char neu[OS_PATH_MAX], old[OS_PATH_MAX]; #ifdef _WIN32 const char *base = getenv("XDG_CONFIG_HOME"); if (!base || !os_path_is_abs(base)) @@ -238,20 +239,42 @@ int os_config_dir(char *out, size_t out_sz) { base = getenv("LOCALAPPDATA"); if (!base || !base[0]) return -1; - int n = snprintf(out, out_sz, "%s/cst-agent", base); - os_path_norm_slashes(out); + int n = snprintf(neu, sizeof(neu), "%s/hermit", base); + 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 const char *xdg = getenv("XDG_CONFIG_HOME"); const char *home = getenv("HOME"); - int n; - if (xdg && xdg[0] == '/') - n = snprintf(out, out_sz, "%s/cst-agent", xdg); - else if (home && home[0]) - n = snprintf(out, out_sz, "%s/.config/cst-agent", home); - else + int n, o; + if (xdg && xdg[0] == '/') { + n = snprintf(neu, sizeof(neu), "%s/hermit", xdg); + o = snprintf(old, sizeof(old), "%s/cst-agent", xdg); + } else if (home && home[0]) { + 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; #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 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, "/tmp"); snprintf(cand[nc++], OS_PATH_MAX, "/var/tmp"); - if (cache && cache[0] == '/') - snprintf(cand[nc++], OS_PATH_MAX, "%s/cst-agent/tmp", cache); - else if (home && home[0] == '/') - snprintf(cand[nc++], OS_PATH_MAX, "%s/.cache/cst-agent/tmp", home); + 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); + } 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); + } for (int i = 0; i < nc; i++) { tmp_trim(cand[i]); if (i == nc - 1) diff --git a/src/tui.c b/src/tui.c index 4214311..32afa40 100644 --- a/src/tui.c +++ b/src/tui.c @@ -24,6 +24,7 @@ #include "cst_tools.h" #include "cst_util.h" #include "cst_version.h" +#include "cst_hermit_glyph.h" #include "os_compat.h" #include "sashidown.h" @@ -496,18 +497,18 @@ static char *xstrndup(const char *s, size_t n) { /* --- 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) { const char *nc = getenv("NO_COLOR"); g_nocolor = nc && nc[0]; - const char *asc = getenv("CST_ASCII"); + const char *asc = hermit_getenv("ASCII"); #if defined(__ANDROID__) g_utf8 = 1; /* bionic is UTF-8 only; nl_langinfo needs API 26 */ #elif !defined(_WIN32) const char *cs = nl_langinfo(CODESET); g_utf8 = cs && (strcasecmp(cs, "UTF-8") == 0 || strcasecmp(cs, "utf8") == 0); #endif - g_ascii = asc && asc[0] ? asc[0] != '0' : !g_utf8; + g_ascii = asc[0] ? asc[0] != '0' : !g_utf8; if (!HAS_COLOR()) return; start_color(); @@ -515,54 +516,62 @@ static void theme_init(void) { short primary = COLOR_CYAN; short panel = COLOR_BLACK; - short muted = COLOR_CYAN; + short muted = COLOR_WHITE; short success = COLOR_GREEN; short err = COLOR_RED; - short assist = COLOR_CYAN; + short assist = COLOR_MAGENTA; short text = COLOR_WHITE; 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) { box = 235; side = 234; - accent = 75; /* #5c9cf5 steel blue */ + accent = 75; /* #277F94 brand teal */ bright = 255; logo_dim = 242; - primary = 75; /* brand/user accent: blue, not peach */ - panel = 233; /* #121212 */ + primary = 75; + panel = 233; muted = 244; success = 114; err = 167; - assist = 74; /* soft cyan assistant */ - text = 252; /* brighter body for readability */ + assist = 177; /* #9957AD eye purple */ + tool = 216; /* #F68042 crab orange */ + prompt_c = 152; /* #A9D9D7 prompt cyan */ + text = 252; if (can_change_color()) { - init_color(75, 430, 660, 996); /* #6ea8fe brand */ - init_color(233, 55, 58, 70); /* #0e0f12 deeper panel */ - init_color(244, 502, 520, 560); /* cool muted */ - init_color(252, 820, 830, 850); /* body */ + init_color(75, 153, 498, 580); /* #277F94 brand */ + init_color(152, 663, 851, 843); /* #A9D9D7 prompt */ + init_color(216, 965, 502, 259); /* #F68042 crab */ + 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(167, 850, 320, 320); - init_color(74, 380, 720, 780); - init_color(235, 100, 105, 120); /* raised box */ - init_color(234, 78, 82, 96); /* sidebar */ - init_color(216, 980, 720, 420); /* warm tip only */ + init_color(235, 100, 105, 120); + init_color(234, 78, 82, 96); } } else if (can_change_color() && COLORS >= 8) { 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_GREEN, 400, 750, 450); init_color(COLOR_RED, 850, 320, 320); - init_color(COLOR_BLUE, 380, 720, 780); - init_color(COLOR_YELLOW, 980, 720, 420); + init_color(COLOR_MAGENTA, 600, 341, 678); + init_color(COLOR_YELLOW, 965, 502, 259); panel = COLOR_BLACK; primary = COLOR_CYAN; muted = COLOR_WHITE; text = COLOR_WHITE; success = COLOR_GREEN; 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). */ @@ -574,11 +583,11 @@ static void theme_init(void) { init_pair(CP_MUTED, muted, panel); init_pair(CP_SUCCESS, success, 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_SGR_GREEN, success, panel); - init_pair(CP_SGR_YELLOW, primary, panel); - init_pair(CP_SGR_CYAN, assist, panel); + init_pair(CP_SGR_YELLOW, tool, panel); + init_pair(CP_SGR_CYAN, prompt_c, panel); if (COLORS >= 256) init_pair(CP_SGR_MAGENTA, 176, panel); else @@ -2687,20 +2696,87 @@ static void draw_todo(tui_t *t) { /* --- start screen ---------------------------------------------------------- */ -/* 3-row block font for "cstagent"; each glyph is 4 columns wide. */ -static const char *const LOGO[8][3] = { - {"█▀▀▀", "█ ", "▀▀▀▀"}, /* c */ - {"█▀▀▀", "▀▀▀█", "▀▀▀▀"}, /* s */ - {"▀█▀▀", " █ ", " ▀▀▀"}, /* t */ - {"█▀▀█", "█▀▀█", "▀ ▀"}, /* a */ - {"█▀▀▀", "█ ▀█", "▀▀▀▀"}, /* g */ - {"█▀▀▀", "█▀▀ ", "▀▀▀▀"}, /* e */ - {"█▀▀▄", "█ █", "▀ ▀"}, /* n */ - {"▀█▀▀", " █ ", " ▀▀▀"}, /* t */ +/* 3-row block font for "HERMIT"; each glyph is 4 columns wide. */ +static const char *const LOGO[6][3] = { + {"█ █", "█▀▀█", "█ █"}, /* H */ + {"█▀▀▀", "█▀▀ ", "▀▀▀▀"}, /* E */ + {"█▀▀█", "█▀▀█", "▀ ▀"}, /* R */ + {"█▀▄█", "█ █", "▀ ▀"}, /* M */ + {"▀█▀▀", " █ ", " ▀▀▀"}, /* I */ + {"▀█▀▀", " █ ", " ▀▀▀"}, /* T */ }; -#define LOGO_W (8 * 5 - 1) +#define LOGO_N 6 +#define LOGO_W (LOGO_N * 5 - 1) #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[] = { "Tab switches between Build and Plan (read-only) mode", "Shift+Tab cycles approval: ask, auto, yolo", @@ -2720,40 +2796,51 @@ static void draw_home(tui_t *t) { getmaxyx(t->logw, h, w); int box_top = t->in_y - t->log_y; 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; if (brand_y < 0) brand_y = 0; int want_block = 0; { - const char *lb = getenv("CST_LOGO"); - want_block = lb && !strcmp(lb, "block"); + const char *lb = hermit_getenv("LOGO"); + 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) && box_top - LOGO_H - 2 >= 0 && w >= LOGO_W) { - int x0 = (w - LOGO_W) / 2, y0 = box_top - LOGO_H - 2; - for (int g = 0; g < 8; g++) + int x0 = show_crab ? (crab_x - LOGO_W) / 2 : (w - LOGO_W) / 2; + 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++) - draw_ansi_at(t->logw, y0 + r, x0 + g * 5, 4, LOGO[g][r], - g < 3 ? CP_ACCENT : CP_BRIGHT, 0, 0); + draw_ansi_at(t->logw, y0 + r, x0 + g * 5, 4, LOGO[g][r], CP_ACCENT, 0, + 0); brand_y = y0 + LOGO_H; } else { - const char *mark = "CST-agent"; + const char *mark = "HERMIT"; int mw = (int)strlen(mark); - int x = (w - mw) / 2; + int x = show_crab ? (crab_x - mw) / 2 : (w - mw) / 2; if (x < 0) x = 0; if (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 + 3, mw - 3, "-agent", CP_BRIGHT, 1, 0); + draw_ansi_at(t->logw, brand_y, x, mw, mark, CP_ACCENT, 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; if (ty < box_top) { const char *tag = g_ascii ? "code . plan . parliament" : "code · plan · parliament"; int tw = (int)strlen(tag); - int x = (w - tw) / 2; + int x = show_crab ? (crab_x - tw) / 2 : (w - tw) / 2; if (x < 0) x = 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; if (box_bot < h) { - char hint[96]; + char hint[128]; 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); if (right - hw >= 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); if (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(); if (ext[0] && box_bot + 3 < h) { @@ -3018,7 +3106,7 @@ static void draw_side(tui_t *t) { if (h >= 4) { 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); } wrefresh(s); @@ -3183,9 +3271,11 @@ static void draw_topbar(tui_t *t) { if (hexish) 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; - 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); int model_room = 0; char mid[160] = "";