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:
@@ -0,0 +1,146 @@
|
||||
#ifndef _DEFAULT_SOURCE
|
||||
#define _DEFAULT_SOURCE
|
||||
#endif
|
||||
|
||||
#include "cst_agent.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define CHECK(c) \
|
||||
do { \
|
||||
if (!(c)) { \
|
||||
fprintf(stderr, "%s:%d: CHECK(%s) failed\n", __FILE__, __LINE__, #c); \
|
||||
exit(1); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
static char g_dir[256];
|
||||
|
||||
static void write_file(const char *name, const char *text) {
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "%s/cst-agent/%s", g_dir, name);
|
||||
FILE *fp = fopen(path, "w");
|
||||
CHECK(fp);
|
||||
fputs(text, fp);
|
||||
fclose(fp);
|
||||
}
|
||||
|
||||
static void test_creds(void) {
|
||||
char key[512];
|
||||
const char *from = NULL;
|
||||
CHECK(cst_creds_resolve("deepseek", key, sizeof(key), &from) != 0);
|
||||
|
||||
/* legacy single-key file follows its api_base */
|
||||
write_file("credentials", "api_key=sk-legacy\n"
|
||||
"api_base=https://api.minimax.cn/v1/\n");
|
||||
CHECK(cst_creds_load("minimax", key, sizeof(key)) == 0);
|
||||
CHECK(strcmp(key, "sk-legacy") == 0);
|
||||
CHECK(cst_creds_load("deepseek", key, sizeof(key)) != 0);
|
||||
write_file("credentials", "api_key=sk-old\n");
|
||||
CHECK(cst_creds_load("minimax", key, sizeof(key)) == 0);
|
||||
CHECK(cst_creds_load("openai", key, sizeof(key)) != 0);
|
||||
|
||||
CHECK(cst_creds_save("deepseek", "sk-ds-1") == 0);
|
||||
CHECK(cst_creds_save("openai", "sk-oa") == 0);
|
||||
CHECK(cst_creds_save("deepseek", "sk-ds-2") == 0);
|
||||
CHECK(cst_creds_load("deepseek", key, sizeof(key)) == 0);
|
||||
CHECK(strcmp(key, "sk-ds-2") == 0);
|
||||
CHECK(cst_creds_load("openai", key, sizeof(key)) == 0);
|
||||
CHECK(strcmp(key, "sk-oa") == 0);
|
||||
CHECK(cst_creds_load("minimax", key, sizeof(key)) == 0); /* legacy kept */
|
||||
CHECK(strcmp(key, "sk-old") == 0);
|
||||
CHECK(cst_creds_save("deepseek", "bad\nkey") != 0);
|
||||
|
||||
char path[512];
|
||||
snprintf(path, sizeof(path), "%s/cst-agent/credentials", g_dir);
|
||||
struct stat st;
|
||||
CHECK(stat(path, &st) == 0);
|
||||
CHECK((st.st_mode & 0077) == 0);
|
||||
|
||||
CHECK(cst_creds_resolve("deepseek", key, sizeof(key), &from) == 0);
|
||||
CHECK(strcmp(from, "saved") == 0);
|
||||
setenv("MINIMAX_API_KEY", "sk-env", 1);
|
||||
unlink(path);
|
||||
CHECK(cst_creds_resolve("minimax", key, sizeof(key), &from) == 0);
|
||||
CHECK(strcmp(key, "sk-env") == 0 && strcmp(from, "MINIMAX_API_KEY") == 0);
|
||||
unsetenv("MINIMAX_API_KEY");
|
||||
}
|
||||
|
||||
static void test_settings(void) {
|
||||
cst_settings_t s;
|
||||
CHECK(cst_settings_load(&s) != 0);
|
||||
CHECK(cst_settings_model(&s, "deepseek") == NULL);
|
||||
|
||||
cst_agent_config_t cfg;
|
||||
memset(&cfg, 0, sizeof(cfg));
|
||||
snprintf(cfg.provider, sizeof(cfg.provider), "deepseek");
|
||||
snprintf(cfg.model, sizeof(cfg.model), "deepseek-v4-pro");
|
||||
snprintf(cfg.effort, sizeof(cfg.effort), "high");
|
||||
CHECK(cst_settings_remember(&cfg) == 0);
|
||||
snprintf(cfg.provider, sizeof(cfg.provider), "openai");
|
||||
snprintf(cfg.model, sizeof(cfg.model), "gpt-5");
|
||||
snprintf(cfg.effort, sizeof(cfg.effort), "max");
|
||||
CHECK(cst_settings_remember(&cfg) == 0);
|
||||
|
||||
CHECK(cst_settings_load(&s) == 0);
|
||||
CHECK(strcmp(s.provider, "openai") == 0);
|
||||
CHECK(strcmp(s.effort, "max") == 0);
|
||||
CHECK(strcmp(cst_settings_model(&s, "deepseek"), "deepseek-v4-pro") == 0);
|
||||
CHECK(strcmp(cst_settings_model(&s, "openai"), "gpt-5") == 0);
|
||||
CHECK(s.nmodels == 2);
|
||||
|
||||
write_file("settings.json", "{not json");
|
||||
CHECK(cst_settings_load(&s) != 0);
|
||||
CHECK(s.provider[0] == 0);
|
||||
}
|
||||
|
||||
static void test_effort(void) {
|
||||
CHECK(cst_effort_valid("low") && cst_effort_valid("max"));
|
||||
CHECK(!cst_effort_valid("ultra") && !cst_effort_valid(NULL));
|
||||
CHECK(strcmp(cst_effort_wire("deepseek", "deepseek-flash", "max"), "max") ==
|
||||
0);
|
||||
CHECK(strcmp(cst_effort_wire("openai", "o3-mini", "max"), "high") == 0);
|
||||
CHECK(cst_effort_wire("openai", "gpt-4.1-mini", "high") == NULL);
|
||||
CHECK(cst_effort_wire("minimax", "MiniMax-M2.7", "high") == NULL);
|
||||
|
||||
cst_message_t m = {0};
|
||||
m.role = CST_ROLE_USER;
|
||||
m.content = "hi";
|
||||
cst_llm_request_t req = {0};
|
||||
req.model = "deepseek-flash";
|
||||
req.msgs = &m;
|
||||
req.nmsgs = 1;
|
||||
char *body = cst_llm_build_body(&req);
|
||||
CHECK(body && !strstr(body, "reasoning_effort"));
|
||||
free(body);
|
||||
req.reasoning_effort = "high";
|
||||
body = cst_llm_build_body(&req);
|
||||
CHECK(body && strstr(body, "\"reasoning_effort\":\"high\""));
|
||||
free(body);
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
snprintf(g_dir, sizeof(g_dir), "/tmp/cst-config-XXXXXX");
|
||||
CHECK(mkdtemp(g_dir));
|
||||
setenv("XDG_CONFIG_HOME", g_dir, 1);
|
||||
unsetenv("DEEPSEEK_API_KEY");
|
||||
unsetenv("MINIMAX_API_KEY");
|
||||
unsetenv("OPENAI_API_KEY");
|
||||
char sub[300];
|
||||
snprintf(sub, sizeof(sub), "%s/cst-agent", g_dir);
|
||||
CHECK(mkdir(sub, 0700) == 0);
|
||||
|
||||
test_creds();
|
||||
test_settings();
|
||||
test_effort();
|
||||
|
||||
char cmd[400];
|
||||
snprintf(cmd, sizeof(cmd), "rm -rf '%s'", g_dir);
|
||||
CHECK(system(cmd) == 0);
|
||||
printf("test_config: OK\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user