Two strands land together: the window draws pictures now, and the whole front
end builds, installs and runs on the phone with the engine linked in-process.
Images (the "make a picture actually show up" part)
- Engine, additively: cst_agent_hooks_t.on_tool_images(call, list, n, ud) fires
when a call left attachments behind, and cst_agent_pending_rel() /
cst_agent_pending_is_image() read that list without the app touching the
store. tests/test_attach.c::test_agent_pending pins the three cases (an
image, a stored name that passes cst_attach_rel_ok, out-of-range NULL).
- App: image://cstattach/<stored name> (app/src/ui/SessionImages.cpp) serves
only the current session's attachment directory, checked with
cst_attach_rel_ok, decodes down to the requested sourceSize and never
upscales, and holds a mutex because requestImage() runs on the render
thread while the directory can change underneath it. AgentCore gained
attachmentDir(), pendingImages(), the toolImages()/promptImages() signals
and hookToolImages(); history() gives replayed rows their images too.
- QML: ImageStrip.qml (a thumb strip, <=150 px tall, shaped by the picture,
tooltip with name/size/origin, the file's name when it went missing),
wired into MessageCard and ToolCard. Qt 6's ListModel cannot hold a
list-valued role (append() reads back undefined, setProperty drops it), so
a row carries one image per line as <stored>|<name>|<origin>|<w>x<h> and
ImageStrip decodes that.
Android (the engine is already in the APK; this is the front end)
- app/src/main.cpp: on Android the workspace, config, tmp and HOME go under
the app's own data directory before the engine opens anything, because
QDir::currentPath() is "/" there and /tmp is not writable.
- app/android/: the manifest (QtApplication/QtActivity, INTERNET and
POST_NOTIFICATIONS, adjustResize, no storage permission -- the picker will
be the system one), strings, a vector icon; minSdk 24 and targetSdk 35 come
from app/CMakeLists.txt through QT_ANDROID_MIN/TARGET_SDK_VERSION.
- app/src/platform/android/AndroidTools.cpp: the Android file behind
PlatformInit(), owner "app:android" (notification, clipboard, share sheet,
file picker are stubs for now, so the tool list does not lie about them).
- app/tools/selftest.c decides its scratch workspace with os_temp_dir(), so
the C smoke test runs on the device too.
- scripts/build_apk_android.sh: sources to installed APK in one command
(environment checks, qt-cmake configure, --target apk, adb install), and
the host-side plumbing the NixOS + qt.io combination needs -- the NDK
sysroot include dirs for Qt's FindGLESv2/FindEGL (they only look under
<sysroot>/include), -L$PREFIX/lib on the shared/module link lines as well
as the executables', the Android debug keystore created on first use with
QT_ANDROID_SIGN_APK + QT_ANDROID_KEYSTORE_* (androiddeployqt copies the
unsigned gradle output and signs it itself), and the #!/bin/bash shebang of
the build-tools wrappers, which NixOS does not have.
- scripts/gradle-android-init.gradle (installed into ~/.gradle/init.d/): the
release lint task is switched off -- lintVitalAnalyzeRelease resolves its
own tool jars straight from dl.google.com, bypassing every repository the
build configures, and fails the build after all the real work is done.
- Icons: app/icons/*.svg (Breeze's 24@3x files, the 19 names the window asks
for) are compiled in as :/cst-icons and ThemeIcons.cpp falls back to them
when the platform has no icon theme. On the phone the Qt theme lookup found
nothing and the QML glyph fallback was an empty box in the phone's fonts.
- docs/android.md: "The APK (Qt front end)" -- the SDK assembled without
sdkmanager, the host Qt's missing libraries on NixOS, the Gradle mirrors,
lint, signing -- and a Status that says what runs where. app/README.md
gains the images and APK lines, and stops claiming Android platform tools
that are still stubs.
Verified: engine `make check` green; desktop `cst-app-selftest: OK` and
--uitest all checks pass; phone (NX789J, Android 16, arm64-v8a) -- the APK
installs, starts on the workspace under /data/user/0/xyz.whcself.cstagent/
files/, a turn reaches a configured provider over HTTPS from inside the ART
process and renders the reply, rotation keeps the session and docks the
sidebar, and the toolbar/card icons draw.
Version 0.9.0.
mode = coding
The engine can be embedded now instead of only driven from a terminal: the
same objects link into a Qt front end (app/) that owns a cst_agent_t inside
its own process -- no port, no token, no second process -- and that front end
now runs on the desktop and, engine-side, on the phone.
Capability metadata (include/cst_tools.h, src/tools/registry.c):
- cst_tool_def_t gains perm_name, platforms and available (a probe that
returns a reason), so a permission is addressable by name and a tool can
say why it cannot run here.
- cst_platform_name(), cst_tool_platform_ok(), cst_tool_available(),
cst_tool_meta_reload(), cst_tool_perm_name() and CST_PLAT_DESKTOP[].
- cst_tool_register() may take over a built-in name that is not runnable on
this platform, so a front end replaces it with its own implementation.
- the registry entries carry their class (delete_file -> "delete"), the
desktop clipboard tools carry a clipboard probe; find/active/run gate on it.
- /api/settings.tools[] exposes perm_name, platforms, available, why and the
declared parameters.
Engine as a library (Makefile, CMakeLists.txt):
- libcstcore.a from the CORE_OBJ set, and a CMake cstcore target shared by
the CLI and by app/ (-DCST_BUILD_APP=ON): the engine is compiled once.
- Android branch for the vendored sashidown: a per-ABI archive built with the
NDK's aarch64-linux-android24-clang wrapper, because CMake's compiler is
plain clang and keeps --target/--sysroot in its own variables.
Qt front end (app/, new):
- AgentCore wraps one cst_agent_t: turns run on a worker thread, every
cst_agent_hooks_t callback becomes a Qt signal, and the two hooks that need
an answer (on_permission, on_ask_user) block the worker until the GUI
replies; mode, approval and plan mode go through the engine's config.
- ToolProvider hands the front end's tools to the engine (owner "app:*"); the
Linux one is notification_send with a probe for notify-send/D-Bus.
- Main.qml: transcript, tool blocks, consent dialog (Deny/Once/Always ->
CST_PERM_*), ask_user, mode switch, Esc to cancel; --selftest stays
headless and runs the embedding checks without QML.
Android (scripts/, docs/android.md, new):
- scripts/build_third_party_android.sh builds static mbedTLS + libcurl for an
ABI (CA path /system/etc/security/cacerts); build_android.sh builds
cst-agent, libcstcore.a and the test binaries for the phone, can push them
and run the whole suite there.
- sandbox_exec.c keeps the plain exec path on Android: the seccomp supervisor
compiled in and the probe reported "seccomp", but writes outside the
workspace were not denied on the device -- claiming a sandbox that does not
hold is worse than reporting none.
- tests/test_diff.c compares layouts only against real GNU diffutils: toybox's
diff prints the same hunk header with its own hunk/context rules.
Tests: test_tool_meta (metadata, platform gate, probe reasons, takeover of a
built-in name) and test_embed (in-process embedding contract) are new; the
existing registry/llm_body assertions now follow availability. 38 test
binaries pass on the host, 36/36 on arm64-v8a Android 16, cst-app-selftest
passes in both the standalone app/ build and the repo build.
Version 0.8.7.
Process timeline: a run of consecutive thinking and tool calls is one
block that folds to a single line ("思考 4 次 · 工具 6 次"; while running
it shows the current action) and opens into a compact timeline, one row
per thought or call. Visible text, a note or the next prompt closes the
block; history and the live stream follow the same rule. show_thinking
now also applies while streaming instead of forcing thoughts open.
Layout: the avatar gutter and the flex-wrap that dropped it under long
answers are gone, so answer, process blocks, notes and composer share one
left edge. The text column and composer are both 720px (--content); the
rail and run panel grow with clamp() on wide screens. A rebuild after
each turn no longer replays every entrance animation, and the turn time
survives it. Message actions are icon buttons and only appear under text.
Images: a prompt's images sit above the bubble, right-aligned (one keeps
its shape, several are 152px rounded tiles) and open in a lightbox (Esc,
arrow keys). Images uploaded from the page show as thumbnails over the
composer and appear on the prompt as soon as it is sent.
Also: ?theme= is no longer overridden by app.js, the user bubble is
visible in the light theme, and the narrow top bar no longer wraps.
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.
A second implementation of cst_agent_hooks_t next to the TUI: the turn runs on
a worker thread, the hooks publish events into a ring buffer that SSE clients
read, so the engine itself needs no changes.
- src/net.c, include/cst_net.h: POSIX/Winsock sockets, non-blocking connect
- src/http_srv.c, include/cst_http.h: HTTP/1.1, keep-alive, chunked streaming,
body caps (413), Expect: 100-continue, connection cap, exact routing
- src/web_md.c, include/cst_web_md.h: streaming markdown -> sanitized HTML
(tag/attribute allowlist, javascript: URLs and off-site images dropped)
- src/webui.c, include/cst_webui.h: event ring + SSE broadcast, all routes,
token (Bearer/Cookie/query) and loopback Host checks, 3 s auto-deny when no
client is attached
- src/webui_stub.c, CST_WEBUI=0 / CST_BUILD_WEBUI=OFF: build without the WebUI
- web/: no-build front end (transcript, approvals, todo, sessions, image
upload, status bar)
- docs/webui.md: engine API v1 - events, requests, security model
- tests/test_webui.c, scripts/e2e_webui.sh: unit tests plus an end-to-end run
driven by a recorded SSE stream (no API key needed)
- src/main.c: --web --web-port --web-host --web-token --web-dev --web-open
and CST_WEB_PORT/HOST/TOKEN/ROOT
An empty POST body is now accepted without Content-Length (RFC 9112 6.3) so
curl-driven clients can reach cancel/undo/compact/quit; CMakeList's core tests
learned about sashidown and test_webui, and CST_WEBUI=0 defines CST_NO_WEBUI.
Landlock no longer handles TCP, the Seatbelt profile drops (deny network*),
and the seccomp filter stops refusing IPv4/IPv6 sockets. Sandboxed commands
run without asking in auto mode on every backend (Low-IL no longer asks for
network commands), and connection errors are not treated as sandbox blocks.
After a successful windows-cross build on main or v* tags, upload the zip
and .sha256 via scripts/release_windows.sh (creates/updates v$VERSION).
Prefer secrets.RELEASE_TOKEN; fall back to GITHUB_TOKEN. Skip on PRs.
With Yama ptrace_scope=2 and no CAP_SYS_PTRACE, process_vm_readv and
/proc/<pid>/mem both fail, so sc_decide cannot read path strings and every
workspace write gets EACCES. The old probe only checked the USER_NOTIF
round trip, so CST_SANDBOX=seccomp and the forced-seccomp test pass looked
available and then failed on the first write.
sc_can_peek() forks a child, places a marker on its stack, and retries the
same sc_peek path. sc_probe() now needs that too, so those hosts fall back
to none (or keep landlock) instead of a silent deny-all sandbox.
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
- 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.
- Child installs a seccomp filter with a listener; a supervisor thread
resolves the path of write-type syscalls (cwd, dirfd, .., symlinks,
/proc/self, /dev/stdout, both ends of rename/link) and answers
CONTINUE or EACCES; IPv4/IPv6 sockets are refused
- Probe order Landlock -> seccomp -> none, checked in a throwaway child;
CST_SANDBOX=landlock|seccomp|off
- curl connect failures count as sandbox refusals
- test_sandbox runs the detected sandbox and forced seccomp, with escape,
symlink, rename, link, chmod, stdout, ENOENT, background and network cases
- Docs: platform matrix, limits (not a security boundary, Yama and
background jobs)
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.