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:
+163
@@ -0,0 +1,163 @@
|
||||
#include "cst_util.h"
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
static int buf_reserve(cst_buf_t *b, size_t extra) {
|
||||
if (b->len + extra + 1 <= b->cap)
|
||||
return 0;
|
||||
size_t nc = b->cap ? b->cap : 256;
|
||||
while (nc < b->len + extra + 1)
|
||||
nc *= 2;
|
||||
char *nb = realloc(b->buf, nc);
|
||||
if (!nb)
|
||||
return -1;
|
||||
b->buf = nb;
|
||||
b->cap = nc;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cst_buf_append(cst_buf_t *b, const char *s, size_t n) {
|
||||
if (!b)
|
||||
return -1;
|
||||
if (buf_reserve(b, n) != 0)
|
||||
return -1;
|
||||
if (n)
|
||||
memcpy(b->buf + b->len, s, n);
|
||||
b->len += n;
|
||||
b->buf[b->len] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cst_buf_puts(cst_buf_t *b, const char *s) {
|
||||
return cst_buf_append(b, s ? s : "", s ? strlen(s) : 0);
|
||||
}
|
||||
|
||||
int cst_buf_printf(cst_buf_t *b, const char *fmt, ...) {
|
||||
va_list ap;
|
||||
va_start(ap, fmt);
|
||||
va_list ap2;
|
||||
va_copy(ap2, ap);
|
||||
int n = vsnprintf(NULL, 0, fmt, ap);
|
||||
va_end(ap);
|
||||
if (n < 0 || buf_reserve(b, (size_t)n) != 0) {
|
||||
va_end(ap2);
|
||||
return -1;
|
||||
}
|
||||
vsnprintf(b->buf + b->len, (size_t)n + 1, fmt, ap2);
|
||||
va_end(ap2);
|
||||
b->len += (size_t)n;
|
||||
return 0;
|
||||
}
|
||||
|
||||
void cst_buf_free(cst_buf_t *b) {
|
||||
if (!b)
|
||||
return;
|
||||
free(b->buf);
|
||||
b->buf = NULL;
|
||||
b->len = b->cap = 0;
|
||||
}
|
||||
|
||||
char *cst_buf_steal(cst_buf_t *b) {
|
||||
if (buf_reserve(b, 0) != 0)
|
||||
return NULL;
|
||||
char *s = b->buf;
|
||||
b->buf = NULL;
|
||||
b->len = b->cap = 0;
|
||||
return s;
|
||||
}
|
||||
|
||||
size_t cst_utf8_floor(const char *s, size_t n) {
|
||||
if (!s || n == 0)
|
||||
return 0;
|
||||
size_t i = n;
|
||||
/* walk back over at most 3 continuation bytes to the lead byte */
|
||||
size_t back = 0;
|
||||
while (i > 0 && back < 3 && ((unsigned char)s[i - 1] & 0xC0) == 0x80) {
|
||||
i--;
|
||||
back++;
|
||||
}
|
||||
if (i == 0)
|
||||
return n;
|
||||
unsigned char lead = (unsigned char)s[i - 1];
|
||||
size_t need = 1;
|
||||
if (lead >= 0xF0)
|
||||
need = 4;
|
||||
else if (lead >= 0xE0)
|
||||
need = 3;
|
||||
else if (lead >= 0xC0)
|
||||
need = 2;
|
||||
if (need == 1)
|
||||
return n; /* ASCII lead; any continuation bytes after it are invalid
|
||||
anyway and get sanitized later */
|
||||
if (back + 1 >= need)
|
||||
return n;
|
||||
return i - 1; /* sequence incomplete: cut before lead byte */
|
||||
}
|
||||
|
||||
static size_t utf8_seq_ok(const unsigned char *p, size_t left) {
|
||||
unsigned char c = p[0];
|
||||
size_t need;
|
||||
unsigned cp;
|
||||
if (c < 0x80)
|
||||
return 1;
|
||||
if ((c & 0xE0) == 0xC0) {
|
||||
need = 2;
|
||||
cp = c & 0x1F;
|
||||
} else if ((c & 0xF0) == 0xE0) {
|
||||
need = 3;
|
||||
cp = c & 0x0F;
|
||||
} else if ((c & 0xF8) == 0xF0) {
|
||||
need = 4;
|
||||
cp = c & 0x07;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
if (left < need)
|
||||
return 0;
|
||||
for (size_t i = 1; i < need; i++) {
|
||||
if ((p[i] & 0xC0) != 0x80)
|
||||
return 0;
|
||||
cp = (cp << 6) | (p[i] & 0x3F);
|
||||
}
|
||||
if ((need == 2 && cp < 0x80) || (need == 3 && cp < 0x800) ||
|
||||
(need == 4 && (cp < 0x10000 || cp > 0x10FFFF)) ||
|
||||
(cp >= 0xD800 && cp <= 0xDFFF))
|
||||
return 0;
|
||||
return need;
|
||||
}
|
||||
|
||||
char *cst_utf8_sanitize(const char *s, size_t n, size_t *out_len) {
|
||||
char *out = malloc(n + 1);
|
||||
if (!out)
|
||||
return NULL;
|
||||
size_t o = 0;
|
||||
const unsigned char *p = (const unsigned char *)(s ? s : "");
|
||||
size_t i = 0;
|
||||
while (i < n) {
|
||||
size_t k = utf8_seq_ok(p + i, n - i);
|
||||
if (k == 0) {
|
||||
out[o++] = '?';
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
memcpy(out + o, p + i, k);
|
||||
o += k;
|
||||
i += k;
|
||||
}
|
||||
out[o] = 0;
|
||||
if (out_len)
|
||||
*out_len = o;
|
||||
return out;
|
||||
}
|
||||
|
||||
uint64_t cst_fnv1a64(uint64_t h, const void *data, size_t n) {
|
||||
const unsigned char *p = data;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
h ^= p[i];
|
||||
h *= 1099511628211ULL;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
Reference in New Issue
Block a user