Wire hermit_getenv across the engine, rebrand WebUI/systemd/docs, and
add apps/desktop (Tauri 2 + React) with a DeepSeek-Harness-like UI.
Qt app/ is frozen; full libcstcore FFI is scaffolded behind --features engine.
Co-authored-by: Cursor <cursoragent@cursor.com>
Topbar spans the full width; seat deck starts under it. Compact badge
and a seat chip on the right of the chrome. Hermit 1.0.2.
Co-authored-by: Cursor <cursoragent@cursor.com>
Compact the home parliament chip, animate crab states like the HTML kit,
ship hermit-icons.ttf (sbix) + frames, and install the font from make/
bootstrap/package paths.
Co-authored-by: Cursor <cursoragent@cursor.com>
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>
Password Realm no longer auto-enables parliament (keeps task/worker tools);
finished jobs fan out to agent notes + TUI; raise PAR_MAX/grep caps and tag truncations.
Co-authored-by: Cursor <cursoragent@cursor.com>
Sanitize session titles, stamp CST on the topbar, fill work/sync/keys so
the sidebar never looks unfinished, drop duplicate ctx from the foot, and
tone down the scroll banner plus composer mode line.
Co-authored-by: Cursor <cursoragent@cursor.com>
Seat role already lives in the Seat Deck. Remove the lonely right-edge
chip and paint the session strip on panel ground so it no longer reads
as a raised bar with a hairline seam.
Co-authored-by: Cursor <cursoragent@cursor.com>
Appending the scanning caret after trailing newlines parked a lonely
block at the left margin. Overlay the tip character instead and let the
decode crest sweep through the wake.
Co-authored-by: Cursor <cursoragent@cursor.com>
Replace noisy glyph spam with a layered frontier: bright cyan plasma at
the tip, a traveling braille wake that ghosts the real text, a lock flash
as glyphs settle, and a scanning write-head caret.
Co-authored-by: Cursor <cursoragent@cursor.com>
Network tokens fill md_live immediately; the UI reveals md_shown at a
non-linear rate (slow at the tip, accelerates with backlog) so glitch
and text stay readable without lagging far behind a fast model.
Co-authored-by: Cursor <cursoragent@cursor.com>
leaveok on chrome panes and always redraw input after side/log motion
so the caret no longer sticks on the Seat Deck version line. Live
assistant text briefly scrambles at the tip then settles to plaintext.
Co-authored-by: Cursor <cursoragent@cursor.com>
Replace the OpenCode-style inspector with a seat/fleet/ctx/work telemetry
rail, default-on for wide sessions, driven by 80ms ASCII micro-motion.
Co-authored-by: Cursor <cursoragent@cursor.com>
Cooler brand palette, compact CST-agent home with tagline and framed
composer, session topbar (model + chair chip), sidebar off by default.
Screenshot pipeline in tools/tui_shot.py; docs/screenshots for before/after.
Co-authored-by: Cursor <cursoragent@cursor.com>
Seats publish via parliament_note, sync pushes on write, member join without
stealing the chair, and worker tools are hard-gated when parliament is active.
Co-authored-by: Cursor <cursoragent@cursor.com>
Pidfile records instance+port; login restarts the service when the seat
changes. TUI /realm status|login|stop; prompts discourage filler chatter.
Co-authored-by: Cursor <cursoragent@cursor.com>
Inject shared parliament context into the agent environment, switch
prompts away from in-process workers when parliament is active, clear
login tickets on chair sync/new-project, and surface realm/chair/service
in /status. Tighten empty-reply handling and prefer-tool principles.
Co-authored-by: Cursor <cursoragent@cursor.com>
Working mode: --mode coding|general (CST_MODE, settings.json, /mode,
WebUI run settings). general swaps the default framing for a general
assistant one -- workflow, safety notes, tool wording (run_shell, the
subagents) -- and /status shows it; minimal (the paper profile) stays
byte-frozen. The prompt state carries the mode so a switch leaves a
system note in history.
Clipboard: clipboard_read / clipboard_write tools (16 MiB cap read,
default 64 KiB window; writing is a WRITE permission with a preview) on
pbcopy / wl-copy / xclip / pbpaste / wl-paste / xclip and the Win32
clipboard. In the TUI, /attach paste falls back to clipboard text when
there is no image on it.
PDF: a dependency-free text extractor (src/pdf_text.c): inflate,
Flate/ASCIIHex/ASCII85, PNG/TIFF predictors, layout-independent object
scan, object streams, page tree walk with inherited resources, content
lexer (Tj/TJ/'/" and the Td/TD/T*/Tm spacing heuristics), per-font
ToUnicode CMaps and inline images skipped. Encrypted files and pages
with no text layer are refused with the reason.
Attachments: /attach (a path or files dropped on the terminal) now take
images, PDFs and text. A PDF is stored as its text layer with the mime
still saying application/pdf and its page count; anything else has to be
UTF-8 text. Documents travel inline as text parts, so CST_NO_IMAGES and
text-only models no longer drop them, while images keep their old path.
Tests: test_pdf_text (plain, bare and zlib-framed Flate, inline image,
CID + ToUnicode, object stream, error paths), documents in test_attach,
inlined documents in test_llm_body, mode in test_config.
Sessions: rename, delete and search; /api/retry regenerates the last
answer, editing a prompt resends from there; /api/export downloads the
transcript as Markdown (cst_session_export_md). Session messages carry
note/tool_result markers, so a reload rebuilds the tool steps with
their arguments, output and exit status; subagent progress is shown.
Diff and settings: /api/diff shows the real diff; /api/settings covers
protocol, max_steps, tool switches and confirm; /api/permissions edits
the rules; sandbox status is reported. Session rename no longer reads
past a short title.
Commands: slash commands with completion in the composer (/model,
/provider, /new, /retry, /undo, /compact, /export, /diff, /init,
/skill and more), a Ctrl+P palette, Shift+Tab cycles the approval
mode, F1 or ? shows the keys. "Thinking expanded" is saved in the
settings (show_thinking), "expand all tool output" in the browser.
Extensions: GET /api/extensions lists MCP servers, plugins, skills,
agent roles and subagent runs; POST /api/extensions/reload and
/api/extensions/trust; a workspace switch reloads them. /api/init and
/api/skill run on the server (cst_init_prompt shared with the TUI).
Errors, statuses and [cst] notes are shown in Chinese with a readable
reason instead of rc=; a failed turn's error survives the reload.
Tests: scripts/e2e_webui_sessions.py, e2e_webui_workspace.py and
e2e_webui_ext.py, run from e2e_webui.sh.
Streaming: Chinese reasoning showed up garbled in the WebUI. The agent
loop now feeds visible/thinking text in runs instead of byte by byte,
and the WebUI holds an incomplete UTF-8 tail until the next chunk
(cst_utf8_hold_feed), so every think_delta/assistant_delta is whole.
src/provider_switch.c: model list fetch/cache, key verify-and-save and
provider switching moved out of tui.c and shared by TUI and WebUI.
WebUI API:
- GET /api/providers, GET /api/models[?refresh=1] (401 on a rejected
key, still listing built-ins), POST /api/provider, POST /api/key
(key checked against /v1/models, saved to credentials 0600, never
echoed in replies, events or logs)
- /api/config validates everything before applying, persists model,
effort and approval (not yolo), warns when effort does not apply;
plan_mode no longer forces read-only
- state: turn_usage, api_base, key_set/key_from, model_info,
effort_applies, sees_images, max_steps, tool_profile, perm_timeout;
permission events carry their timeout
Frontend: searchable model picker with refresh and manual entry,
provider picker and key dialog, effort/vision hints, toasts instead of
silent failures, Chinese error text, real protocol in the settings and
usage panels, approval countdown from the server's timeout.
Tests: utf8 hold unit test, scripts/e2e_webui_settings.py (fake
/v1/models, wrong key, switching, persistence, Chinese thinking) run
from e2e_webui.sh, scripts/tui_keys/provider.py.
Build: <fcntl.h> in tui.c, format-truncation in http_srv.c and /web.
WebUI:
- refuse cross-site requests (Origin must be the page itself) in the
default no-token mode: any page could POST approval=yolo and a prompt
- session/workspace/undo/compact/attach/model changes are 409 while a
turn runs (use-after-free, messages landing in the wrong session)
- ask_user returned "dismissed" at once instead of waiting
- cancel and shutdown wake a turn parked on a permission or question;
SIGTERM mid-turn no longer hangs or crashes
- a page opened while a question is pending still gets it;
Last-Event-ID resumes the stream; event seq matches the SSE id
- token mode: page assets load without the token, images via cookie
- --web-open works, --web-port/--web-token are validated
- frontend: gate keys ignored while typing, clear-images button,
reasoning no longer wiped while streaming
HTTP server: auth and size cap before reading the body, strict
Content-Length, NUL in url_decode, 413 on keep-alive, idle timeout,
absolute-form only without a leading '/', case-insensitive Connection,
header deadline, 400 for long request lines, unlink before close,
poll() instead of select(), SO_EXCLUSIVEADDRUSE on Windows.
Markdown sanitizer escapes quotes in attribute values.
/web: passes this session's provider/model/base/effort/approval (key
via the environment), keeps output off the TUI, shows the real URL and
keeps CURRENT on the TUI's session.
Tests: scripts/e2e_webui_ask.py, scripts/e2e_webui_http.py (run from
e2e_webui.sh), sanitizer quote test.
The front end is rebuilt on the design tokens the desktop app ships (surfaces,
color-mix borders, alpha hovers, accent): a 30-icon inline SVG sprite, dark and
light themes, layout switches as deep links (?side=0, ?panel=0, ?theme=light)
resolved before the first paint. The empty page is one line and leaves with the
first turn; the right rail keeps three cards - todo, file changes (+N -M) and
context (tokens, prompt cache hit rate). Both rails animate in and out, and a
collapsed rail widens its grid track to 0 instead of being removed, so the
transcript never slides into a zero-width column.
- src/webui.c: POST /api/workspace - re-root the agent at another directory
(permission rules, metrics, AGENTS.md follow) and start a session there, as
sessions live per directory; state also reports cached/cache_miss tokens
- src/agent_loop.c: cst_agent_set_workspace(), rebuilt prompt and re-measured
budget; the auto-compact threshold is 75% of the window (was 60%)
- src/tui.c: /web forks a WebUI next to the TUI - separate process and a new
session, because cst_webui_run() blocks and two writers corrupt one session
- src/os_compat.c, src/os_win.c: run_shell runs without a time limit unless
CST_SHELL_TIMEOUT asks for one, so builds and long clones finish
- src/main.c, include/cst_webui.h: one CST_WEBUI_DEFAULT_PORT for --help, the
docs and /web; Termux opens the browser through termux-open-url
- README.md, docs/webui.md, docs/env_openai.md: /web, /api/workspace, the new
state fields, the 75% threshold and the layout deep links
tests/e2e_webui.sh green (permission round-trip, token paths, --web-dev);
make check green.
TUI keys: register both CSI and SS3 forms of the cursor/Home/End keys and
the xterm modifier sequences when terminfo lacks them, so an arrow sent in
the "other" form no longer turns into Esc + a typed letter. Up/Down move by
visual (soft-wrapped) row, and the draft survives a trip through history.
scripts/tui_keys drives the TUI in a pty to check all of this.
Images: text inside a multimodal content array is now a {"type":"text"}
part; DeepSeek rejected the bare string with HTTP 422. deepseek-flash
(and its deepseek-chat / deepseek-reasoner aliases) read images, so the
registry marks them vision; deepseek-v4-pro stays text only.
CI: models.c lost _DEFAULT_SOURCE in 0.7.0 and gmtime_r broke the musl
build; tests sleep with poll() instead of usleep for the same reason.
Images
- vision gate: text-only models get a note instead of image parts and no
view_image tool; unknown models retry once without images on HTTP 400
- paste: Ctrl+V and /attach paste read the clipboard (wl-paste, xclip,
pngpaste/osascript, Win32 PNG or CF_DIB converted to PNG); pasting image
file paths attaches them
- MCP image content is stored in the session and sent with the tool result
- attach: rel path validation, base64 decode, /retry keeps images
Model registry
- built-in src/models_builtin.json embedded at build time, merged with
<config>/models.json and cached GET /models metadata (context, vision,
pricing); most specific rule wins per field
- /model labels, /status source, --list-models [provider]
Cache and cost
- stable request prefix (frozen tools, notes as messages), DeepSeek prefix
completion, OpenAI/Anthropic cache hints, /cost and hit rate in the status
Build
* cross build switches to pkgsCross.ucrt64 (shell-win.nix); curl (Schannel),
winpthreads and ncurses 6.6 are linked statically, so the release is
cst-agent.exe + busybox.exe (busybox-w32 Unicode) with no extra DLLs
* windres embeds a manifest with activeCodePage=UTF-8 and a version resource
* ncurses 6.6 Windows console driver patches (mouse button state, A_DIM,
wide-char declarations): scripts/patches/
* scripts/bootstrap_msys2.sh builds the same release natively in MSYS2
UCRT64; PR #1's MINGW64 notes now point there
* vendor/tre-regex (musl's TRE) provides <regex.h> on MinGW
UTF-8
* paths cross the system call boundary as UTF-16 (*W APIs); console code
pages set to 65001 and restored on exit; files are read and written in
binary mode
* child output that is not valid UTF-8 is decoded from the console, OEM or
ANSI system code page
* read_file hides CR; edit_file, apply_patch and write_file keep CRLF line
endings and a UTF-8 BOM
OS layer and processes
* os_dir_* and cst_fnmatch replace the POSIX-only directory walks in
subagent, plugins, skills, undo and fs_tools; realpath / path_under and
symlink rejection work on Windows
* every child goes through CreateProcessW with its own environment block
inside a Job Object, so timeouts and Esc end the whole process tree
Sandbox (auto mode, "auto · lowil")
* run_shell runs with a restricted Low integrity token; the workspace gets
an inheritable Low label once, the sandbox TEMP is labeled Low, and the
credentials file gets a no-read-up label
* drive roots, the profile and system folders are never labeled
* network commands still ask, since Low IL does not block the network;
Access denied and MSYS2's 0xc0000142 offer a rerun outside the sandbox
* /sandbox lists labeled folders, /sandbox reset-labels restores them
TUI
* raster console fonts are switched to Consolas / NSimSun while running
* the home logo falls back to text when the console font lacks U+2580
* mouse drag copy goes to the Windows clipboard
Distribution
* scripts/install_windows.ps1 installs a Gitea release into
%LOCALAPPDATA%\Programs\cst-agent and adds it to the user PATH;
cst-agent-update.cmd reruns it; scripts/release_windows.sh uploads the
zip with the token taken from the environment
* scripts/run_windows.bat is gone; docs/README-windows.md is rewritten
Tests
* new test_lowil, test_win_utf8 and test_os_dir; tests made portable
(cst_test_util.h)
* scripts/win_test.sh and scripts/winvm/ run the suite on English and
Chinese Windows Server VMs as Administrator, a standard user and a
Chinese user name: 29/29 on all six
setlocale(LC_ALL, "") maps to the ANSI code page on Windows -- 936 (GBK)
on a Chinese system -- so wcrtomb() produces GBK where the protocol wants
UTF-8, and mbrtowc() reads UTF-8 as GBK. Typed Chinese therefore reached
the model as mojibake, and once the buffer held correct UTF-8 the input
line rendered as mojibake instead. Windows' C runtime has no ".UTF-8"
locale to switch to, so the conversion has to be explicit.
os_compat.[ch]
* add os_utf8_encode() and os_utf8_decode(), locale-independent, with
range and overlong-form checks
src/tui.c
* input_put_wc(), the command-palette filter and the ask_user buffer use
os_utf8_encode() instead of wcrtomb()
src/textsel.c
* cst_utf8_decode() uses os_utf8_decode() instead of mbrtowc()
- Drag, double-click (word) and triple-click (line) selection in the log,
auto-scroll at the edges, soft-wrapped lines joined on copy.
- sashidown ANSI renderer: delimiter runs with flanking rules, so
intraword underscores (test_apply_patch.c) stay literal; unmatched
* _ and backticks are shown as-is; * and _ do not close each other.
- tests: test_textsel, test_markdown.
Query CSI ? u at startup and push the disambiguate flag on the alternate
screen when the terminal answers; CSI u reports are mapped back to legacy
input (Shift/Ctrl+Enter insert a newline, Ctrl+Backspace deletes a word).
Ctrl/Alt+Left/Right move by word, Ctrl/Alt+Delete delete the next word.
Our define_key codes move above the range ncurses uses for terminfo
extended keys.
- bootstrap_termux.sh installs pkg-config/binutils, builds with make,
runs the tests and installs into Termux's $PREFIX/bin.
- Makefile drops -lpthread on Android (pthreads are in bionic libc).
- TUI assumes UTF-8 on Android: nl_langinfo needs API 26, Termux
targets 24. All sources compile for aarch64-linux-android24 (NDK 27).
Tools
- Table-driven tool registry with permission classes and default/minimal
profiles; new tools: edit_file, list_dir, glob, grep, delete_file,
move_file, todo_write, web_fetch, ask_user (plus read_file paging).
- Built-in Myers diff; write tools return unified diffs.
- Persistent permission rules (.agent/permissions.json), per-turn undo
snapshots (.agent/undo), AGENTS.md project notes, auto-compact at 80%
of the model context, cancellable run_shell with process-group kill.
TUI
- Table-driven slash commands: /provider /key /effort /status /tools
/permissions /yolo /undo /retry /diff /export /copy /rename /delete
/init /max-steps /todo, with / and @ completion.
- Permission modal with diff and prefix rules, ask_user modal, todo
panel, header with model/effort/context, bracketed paste, line editing,
wide-char cursor, NO_COLOR and ASCII fallback; Esc never quits.
Startup and configuration
- API keys are entered in the TUI (hidden) and saved per provider in
<config>/credentials (0600); env variables are only a fallback.
- Last provider, per-provider model and reasoning effort are remembered
in <config>/settings.json; --effort/-e and /effort send
reasoning_effort (low|medium|high|max) to models that accept it.
- DeepSeek preset uses deepseek-flash / deepseek-v4-pro.
- Session flags --new/--continue/--resume/--list-sessions, --yolo,
--confirm.
Compatibility and tests
- ncurses detection, macOS/Homebrew support, make install, XDG config
dir, POSIX cleanups; Gitea CI (gcc/clang/asan/musl/macOS).
- Unit tests for diff, fs tools, registry, undo/perms and config;
e2e fixtures multi_file_refactor and ask_or_todo; vendored cJSON.