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,317 @@
|
||||
#include "cst_agent.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
const char *p;
|
||||
size_t n;
|
||||
} line_t;
|
||||
|
||||
typedef struct {
|
||||
line_t *v;
|
||||
size_t n;
|
||||
size_t cap;
|
||||
} lines_t;
|
||||
|
||||
static int lines_push(lines_t *L, const char *p, size_t n) {
|
||||
if (L->n == L->cap) {
|
||||
size_t nc = L->cap ? L->cap * 2 : 64;
|
||||
line_t *nv = realloc(L->v, nc * sizeof(*nv));
|
||||
if (!nv)
|
||||
return -1;
|
||||
L->v = nv;
|
||||
L->cap = nc;
|
||||
}
|
||||
L->v[L->n].p = p;
|
||||
L->v[L->n].n = n;
|
||||
L->n++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Split into lines without '\n'. A trailing newline does not add an empty
|
||||
* final line. */
|
||||
static int split_lines(const char *s, size_t n, lines_t *out) {
|
||||
size_t start = 0;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if (s[i] == '\n') {
|
||||
if (lines_push(out, s + start, i - start) != 0)
|
||||
return -1;
|
||||
start = i + 1;
|
||||
}
|
||||
}
|
||||
if (start < n && lines_push(out, s + start, n - start) != 0)
|
||||
return -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static size_t rtrim_len(const line_t *l) {
|
||||
size_t n = l->n;
|
||||
while (n > 0 && (l->p[n - 1] == ' ' || l->p[n - 1] == '\t' ||
|
||||
l->p[n - 1] == '\r'))
|
||||
n--;
|
||||
return n;
|
||||
}
|
||||
|
||||
static int line_eq(const line_t *a, const line_t *b, int loose) {
|
||||
if (!loose)
|
||||
return a->n == b->n && memcmp(a->p, b->p, a->n) == 0;
|
||||
size_t na = rtrim_len(a), nb = rtrim_len(b);
|
||||
return na == nb && memcmp(a->p, b->p, na) == 0;
|
||||
}
|
||||
|
||||
static int seq_at(const lines_t *file, size_t pos, const lines_t *seq,
|
||||
int loose) {
|
||||
if (pos + seq->n > file->n)
|
||||
return 0;
|
||||
for (size_t i = 0; i < seq->n; i++) {
|
||||
if (!line_eq(&file->v[pos + i], &seq->v[i], loose))
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Find seq in file at or after min_pos. hint<0 means unknown. Returns
|
||||
* position, -1 when absent, -2 when ambiguous without a hint. */
|
||||
static long locate(const lines_t *file, size_t min_pos, const lines_t *seq,
|
||||
long hint, int loose) {
|
||||
if (seq->n > file->n)
|
||||
return -1;
|
||||
size_t last = file->n - seq->n;
|
||||
if (hint >= 0) {
|
||||
long best = -1;
|
||||
long best_d = 0;
|
||||
for (size_t p = min_pos; p <= last; p++) {
|
||||
if (!seq_at(file, p, seq, loose))
|
||||
continue;
|
||||
long d = (long)p - hint;
|
||||
if (d < 0)
|
||||
d = -d;
|
||||
if (best < 0 || d < best_d) {
|
||||
best = (long)p;
|
||||
best_d = d;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
long found = -1;
|
||||
for (size_t p = min_pos; p <= last; p++) {
|
||||
if (seq_at(file, p, seq, loose)) {
|
||||
if (found >= 0)
|
||||
return -2;
|
||||
found = (long)p;
|
||||
}
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
static int parse_hunk_header(const char *p, size_t n, long *a, long *b,
|
||||
long *c, long *d) {
|
||||
/* "@@ -a[,b] +c[,d] @@..." */
|
||||
char tmp[128];
|
||||
if (n >= sizeof(tmp))
|
||||
n = sizeof(tmp) - 1;
|
||||
memcpy(tmp, p, n);
|
||||
tmp[n] = 0;
|
||||
*b = *d = 1;
|
||||
if (sscanf(tmp, "@@ -%ld,%ld +%ld,%ld", a, b, c, d) == 4)
|
||||
return 0;
|
||||
*b = *d = 1;
|
||||
if (sscanf(tmp, "@@ -%ld +%ld,%ld", a, c, d) == 3)
|
||||
return 0;
|
||||
*b = *d = 1;
|
||||
if (sscanf(tmp, "@@ -%ld,%ld +%ld", a, b, c) == 3)
|
||||
return 0;
|
||||
*b = *d = 1;
|
||||
if (sscanf(tmp, "@@ -%ld +%ld", a, c) == 2)
|
||||
return 0;
|
||||
return -1;
|
||||
}
|
||||
|
||||
int cst_patch_apply_buffer(const char *orig, size_t olen, const char *hunks,
|
||||
size_t hlen, char **out, size_t *out_len,
|
||||
int *nhunks, char *err, size_t err_sz) {
|
||||
lines_t file = {0}, hl = {0};
|
||||
lines_t old_seq = {0}, new_seq = {0};
|
||||
int rc = -1;
|
||||
int count = 0;
|
||||
long delta = 0;
|
||||
size_t min_pos = 0;
|
||||
int orig_final_nl = olen == 0 || orig[olen - 1] == '\n';
|
||||
int final_nl = orig_final_nl;
|
||||
if (err && err_sz)
|
||||
err[0] = 0;
|
||||
if (out)
|
||||
*out = NULL;
|
||||
|
||||
if (split_lines(orig ? orig : "", olen, &file) != 0 ||
|
||||
split_lines(hunks ? hunks : "", hlen, &hl) != 0) {
|
||||
snprintf(err, err_sz, "oom");
|
||||
goto done;
|
||||
}
|
||||
|
||||
size_t i = 0;
|
||||
while (i < hl.n) {
|
||||
const line_t *h = &hl.v[i];
|
||||
if (!(h->n >= 2 && h->p[0] == '@' && h->p[1] == '@')) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
long a = -1, b = -1, c = -1, d = -1;
|
||||
int have_hint = parse_hunk_header(h->p, h->n, &a, &b, &c, &d) == 0;
|
||||
i++;
|
||||
old_seq.n = new_seq.n = 0;
|
||||
int last_kind = 0; /* ' ', '-', '+' of previous body line */
|
||||
int new_no_nl = 0, old_no_nl = 0;
|
||||
for (; i < hl.n; i++) {
|
||||
const line_t *l = &hl.v[i];
|
||||
if (l->n >= 2 && l->p[0] == '@' && l->p[1] == '@')
|
||||
break;
|
||||
if (l->n == 0) {
|
||||
/* models often drop the leading space of blank context lines; a
|
||||
* blank line at the very end of the patch is just a terminator */
|
||||
if (i + 1 == hl.n)
|
||||
break;
|
||||
lines_push(&old_seq, l->p, 0);
|
||||
lines_push(&new_seq, l->p, 0);
|
||||
last_kind = ' ';
|
||||
continue;
|
||||
}
|
||||
char tag = l->p[0];
|
||||
if (tag == ' ') {
|
||||
lines_push(&old_seq, l->p + 1, l->n - 1);
|
||||
lines_push(&new_seq, l->p + 1, l->n - 1);
|
||||
} else if (tag == '-') {
|
||||
if (l->n >= 4 && memcmp(l->p, "--- ", 4) == 0 && i + 1 < hl.n &&
|
||||
hl.v[i + 1].n >= 4 && memcmp(hl.v[i + 1].p, "+++ ", 4) == 0)
|
||||
break; /* next file header */
|
||||
lines_push(&old_seq, l->p + 1, l->n - 1);
|
||||
} else if (tag == '+') {
|
||||
lines_push(&new_seq, l->p + 1, l->n - 1);
|
||||
} else if (tag == '\\') {
|
||||
if (last_kind == '+')
|
||||
new_no_nl = 1;
|
||||
else if (last_kind == '-')
|
||||
old_no_nl = 1;
|
||||
else if (last_kind == ' ')
|
||||
new_no_nl = old_no_nl = 1;
|
||||
continue;
|
||||
} else {
|
||||
break; /* diff --git, index ..., prose */
|
||||
}
|
||||
last_kind = tag;
|
||||
}
|
||||
|
||||
long hint = -1;
|
||||
if (have_hint) {
|
||||
/* "-a,0" inserts after line a; otherwise a is 1-based start */
|
||||
hint = (b == 0 ? a : a - 1) + delta;
|
||||
if (hint < 0)
|
||||
hint = 0;
|
||||
}
|
||||
|
||||
long pos;
|
||||
if (old_seq.n == 0) {
|
||||
if (hint >= 0)
|
||||
pos = hint > (long)file.n ? (long)file.n : hint;
|
||||
else if (file.n == 0)
|
||||
pos = 0;
|
||||
else {
|
||||
snprintf(err, err_sz,
|
||||
"hunk %d: pure insertion needs @@ -a,0 +c,d @@ line numbers",
|
||||
count + 1);
|
||||
goto done;
|
||||
}
|
||||
} else {
|
||||
pos = locate(&file, min_pos, &old_seq, hint, 0);
|
||||
if (pos == -1)
|
||||
pos = locate(&file, min_pos, &old_seq, hint, 1);
|
||||
if (pos == -1 && min_pos > 0) {
|
||||
pos = locate(&file, 0, &old_seq, hint, 0);
|
||||
if (pos == -1)
|
||||
pos = locate(&file, 0, &old_seq, hint, 1);
|
||||
}
|
||||
if (pos == -2) {
|
||||
snprintf(err, err_sz,
|
||||
"hunk %d: ambiguous, context matches several places; add "
|
||||
"line numbers or more context",
|
||||
count + 1);
|
||||
goto done;
|
||||
}
|
||||
if (pos < 0) {
|
||||
snprintf(err, err_sz,
|
||||
"hunk %d: context/removed lines not found in file (first: "
|
||||
"\"%.*s\")",
|
||||
count + 1, (int)(old_seq.v[0].n > 80 ? 80 : old_seq.v[0].n),
|
||||
old_seq.v[0].p);
|
||||
goto done;
|
||||
}
|
||||
}
|
||||
|
||||
/* splice: file[pos .. pos+old) -> new_seq */
|
||||
size_t old_n = old_seq.n, new_n = new_seq.n;
|
||||
size_t need = file.n - old_n + new_n;
|
||||
if (need > file.cap) {
|
||||
line_t *nv = realloc(file.v, (need + 16) * sizeof(line_t));
|
||||
if (!nv) {
|
||||
snprintf(err, err_sz, "oom");
|
||||
goto done;
|
||||
}
|
||||
file.v = nv;
|
||||
file.cap = need + 16;
|
||||
}
|
||||
memmove(&file.v[pos + new_n], &file.v[pos + old_n],
|
||||
(file.n - (size_t)pos - old_n) * sizeof(line_t));
|
||||
for (size_t k = 0; k < new_n; k++)
|
||||
file.v[pos + k] = new_seq.v[k];
|
||||
int touches_end = (size_t)pos + new_n == need;
|
||||
file.n = need;
|
||||
delta += (long)new_n - (long)old_n;
|
||||
min_pos = (size_t)pos + new_n;
|
||||
if (touches_end) {
|
||||
if (new_no_nl)
|
||||
final_nl = 0;
|
||||
else if (old_no_nl)
|
||||
final_nl = 1;
|
||||
}
|
||||
count++;
|
||||
}
|
||||
|
||||
if (count == 0) {
|
||||
snprintf(err, err_sz, "no @@ hunk found");
|
||||
goto done;
|
||||
}
|
||||
|
||||
{
|
||||
size_t total = 1;
|
||||
for (size_t k = 0; k < file.n; k++)
|
||||
total += file.v[k].n + 1;
|
||||
char *buf = malloc(total);
|
||||
if (!buf) {
|
||||
snprintf(err, err_sz, "oom");
|
||||
goto done;
|
||||
}
|
||||
size_t o = 0;
|
||||
for (size_t k = 0; k < file.n; k++) {
|
||||
memcpy(buf + o, file.v[k].p, file.v[k].n);
|
||||
o += file.v[k].n;
|
||||
if (k + 1 < file.n || final_nl)
|
||||
buf[o++] = '\n';
|
||||
}
|
||||
buf[o] = 0;
|
||||
*out = buf;
|
||||
if (out_len)
|
||||
*out_len = o;
|
||||
}
|
||||
if (nhunks)
|
||||
*nhunks = count;
|
||||
rc = 0;
|
||||
|
||||
done:
|
||||
free(file.v);
|
||||
free(hl.v);
|
||||
free(old_seq.v);
|
||||
free(new_seq.v);
|
||||
return rc;
|
||||
}
|
||||
Reference in New Issue
Block a user