Usability pass: tool registry, TUI polish, saved keys and settings

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.
This commit is contained in:
WHC
2026-10-05 01:47:01 +08:00
parent 9877ff48b6
commit 2fde898da0
105 changed files with 19348 additions and 3786 deletions
+108 -23
View File
@@ -1,16 +1,65 @@
CC ?= gcc
CFLAGS ?= -std=c11 -Wall -Wextra -O2 -g
SD_DIR := vendor/sashidown
CPPFLAGS += -Iinclude -I$(SD_DIR)/include -D_XOPEN_SOURCE_EXTENDED
LDFLAGS += -L$(SD_DIR)
LDLIBS += -lsashidown -lm -lcurl -lncursesw -lpthread
CJSON_DIR := vendor/cjson
PKG_CONFIG ?= pkg-config
PREFIX ?= /usr/local
DESTDIR ?=
UNAME_S := $(shell uname -s 2>/dev/null)
SRC = \
# macOS: Homebrew ncurses/curl are keg-only, so point pkg-config at them.
# (macOS ships GNU make 3.81, whose $(shell) ignores exported variables.)
PKG_ENV :=
ifeq ($(UNAME_S),Darwin)
BREW_PREFIX := $(shell brew --prefix 2>/dev/null)
ifneq ($(BREW_PREFIX),)
PKG_ENV := PKG_CONFIG_PATH="$(BREW_PREFIX)/opt/ncurses/lib/pkgconfig:$(BREW_PREFIX)/opt/curl/lib/pkgconfig:$$PKG_CONFIG_PATH"
endif
CPPFLAGS += -D_DARWIN_C_SOURCE
endif
PKGC = $(PKG_ENV) $(PKG_CONFIG)
# Wide-character curses: ncursesw, else ncurses (macOS/BSD build it wide).
CURSES_PC := $(shell for p in ncursesw ncurses; do $(PKGC) --exists $$p 2>/dev/null && { echo $$p; exit 0; }; done)
ifneq ($(CURSES_PC),)
CURSES_CFLAGS := $(shell $(PKGC) --cflags $(CURSES_PC))
CURSES_LIBS := $(shell $(PKGC) --libs $(CURSES_PC))
else ifeq ($(UNAME_S),Darwin)
CURSES_LIBS := -lncurses
else
CURSES_LIBS := -lncursesw
endif
CURL_CFLAGS := $(shell $(PKGC) --cflags libcurl 2>/dev/null)
CURL_LIBS := $(shell $(PKGC) --libs libcurl 2>/dev/null)
ifeq ($(strip $(CURL_LIBS)),)
CURL_LIBS := -lcurl
endif
PKG_CFLAGS := $(CURL_CFLAGS) $(CURSES_CFLAGS)
PKG_LIBS := $(CURL_LIBS) $(CURSES_LIBS)
CPPFLAGS += -Iinclude -I$(SD_DIR)/include -I$(CJSON_DIR) -D_XOPEN_SOURCE_EXTENDED $(PKG_CFLAGS)
LDFLAGS += -L$(SD_DIR)
LDLIBS += -lsashidown -lm $(PKG_LIBS) -lpthread
CORE_SRC = \
$(CJSON_DIR)/cJSON.c \
src/util.c \
src/ctl_parse.c \
src/os_compat.c \
src/sandbox.c \
src/session.c \
src/tools/tools.c \
src/tools/patch.c \
src/tools/registry.c \
src/tools/fs_tools.c \
src/tools/agent_tools.c \
src/tools/web.c \
src/tools/diff.c \
src/undo.c \
src/permissions.c \
src/settings.c \
src/llm_openai.c \
src/agent_loop.c \
src/agent_store.c \
@@ -18,16 +67,36 @@ SRC = \
src/creds.c \
src/mm_tool.c \
src/models.c \
src/tui.c \
src/main.c
src/metrics.c
SRC = $(CORE_SRC) src/tui.c src/main.c
OBJ = $(SRC:.c=.o)
HDRS = $(wildcard include/*.h) $(wildcard src/tools/*.h) $(CJSON_DIR)/cJSON.h
.PHONY: all clean check demo sashidown-lib cmake-gcc cmake-clang
TESTS = \
tests/test_ctl_frontier \
tests/test_ctl_heredoc \
tests/test_json_baseline \
tests/test_replay_starvation \
tests/test_replay_real \
tests/test_mm_tool \
tests/test_llm_body \
tests/test_sse_decode \
tests/test_apply_patch \
tests/test_session_roundtrip \
tests/test_shell \
tests/test_diff \
tests/test_tools_fs \
tests/test_registry \
tests/test_undo \
tests/test_config
TEST_LIBS = -lm $(CURL_LIBS) -lpthread
.PHONY: all clean check demo sashidown-lib cmake-gcc cmake-clang asan install uninstall
all: sashidown-lib cst-agent
# Optional CMake entrypoints (same sources)
cmake-gcc:
cmake -B build-gcc -DCMAKE_C_COMPILER=gcc -DCST_BUILD_TUI=ON
cmake --build build-gcc
@@ -43,35 +112,51 @@ sashidown-lib:
cst-agent: $(OBJ) $(SD_DIR)/libsashidown.a
$(CC) $(CFLAGS) -o $@ $(OBJ) $(LDFLAGS) $(LDLIBS)
src/%.o: src/%.c include/ctl.h include/cst_agent.h
%.o: %.c $(HDRS)
$(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<
src/tools/%.o: src/tools/%.c include/ctl.h include/cst_agent.h
$(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<
src/tui.o: src/tui.c include/cst_agent.h $(SD_DIR)/include/sashidown.h
src/tui.o: src/tui.c $(HDRS) $(SD_DIR)/include/sashidown.h
$(CC) $(CFLAGS) $(CPPFLAGS) -c -o $@ $<
tests/test_ctl_frontier: tests/test_ctl_frontier.c src/ctl_parse.c include/ctl.h
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_ctl_frontier.c src/ctl_parse.c
tests/test_json_baseline: tests/test_json_baseline.c src/json_baseline.c include/cst_agent.h
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_json_baseline.c src/json_baseline.c
tests/test_ctl_heredoc: tests/test_ctl_heredoc.c src/ctl_parse.c include/ctl.h
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_ctl_heredoc.c src/ctl_parse.c
tests/test_replay_starvation: tests/test_replay_starvation.c src/ctl_parse.c src/json_baseline.c
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_replay_starvation.c src/ctl_parse.c src/json_baseline.c
tests/test_json_baseline: tests/test_json_baseline.c src/json_baseline.c $(CJSON_DIR)/cJSON.c $(HDRS)
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_json_baseline.c src/json_baseline.c $(CJSON_DIR)/cJSON.c -lm
tests/test_replay_starvation: tests/test_replay_starvation.c src/ctl_parse.c src/json_baseline.c $(CJSON_DIR)/cJSON.c
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_replay_starvation.c src/ctl_parse.c src/json_baseline.c $(CJSON_DIR)/cJSON.c -lm
tests/test_replay_real: tests/test_replay_real.c src/ctl_parse.c src/json_baseline.c $(CJSON_DIR)/cJSON.c
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_replay_real.c src/ctl_parse.c src/json_baseline.c $(CJSON_DIR)/cJSON.c -lm
tests/test_mm_tool: tests/test_mm_tool.c src/mm_tool.c include/mm_tool.h
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_mm_tool.c src/mm_tool.c
check: tests/test_ctl_frontier tests/test_json_baseline tests/test_replay_starvation tests/test_mm_tool
./tests/test_ctl_frontier
./tests/test_json_baseline
./tests/test_replay_starvation
./tests/test_mm_tool
tests/test_%: tests/test_%.c $(CORE_SRC) $(HDRS)
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ $< $(CORE_SRC) $(TEST_LIBS)
check: $(TESTS)
@set -e; for t in $(TESTS); do echo "== $$t"; ./$$t; done
# AddressSanitizer + UBSan build of the agent and all tests.
asan:
$(MAKE) clean
$(MAKE) sashidown-lib
$(MAKE) CFLAGS="-std=c11 -Wall -Wextra -O1 -g -fsanitize=address,undefined -fno-sanitize-recover=undefined -fno-omit-frame-pointer" cst-agent check
demo: cst-agent
./cst-agent --demo-ctl --workspace .
clean:
rm -f $(OBJ) cst-agent tests/test_ctl_frontier tests/test_json_baseline tests/test_replay_starvation tests/test_mm_tool
rm -f $(OBJ) cst-agent $(TESTS)
install: cst-agent
install -d "$(DESTDIR)$(PREFIX)/bin"
install -m 0755 cst-agent "$(DESTDIR)$(PREFIX)/bin/cst-agent"
uninstall:
rm -f "$(DESTDIR)$(PREFIX)/bin/cst-agent"