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
+20
View File
@@ -0,0 +1,20 @@
# Task: three small scripts, planned with a task list
Create these files in the workspace root:
1. `greet.sh` — prints `Hello, NAME!` where NAME is the first argument, or
`Hello, world!` when no argument is given.
2. `count.sh` — prints the number of lines in the file given as the first
argument (just the number). Exits with status 2 and prints a usage line to
stderr when no argument is given.
3. `SCRIPTS.md` — one short section per script explaining how to run it.
Both scripts must be executable POSIX shell scripts (`#!/bin/sh`).
Rules:
- Start by calling `todo_write` with one item per file, then keep the list
up to date (mark each item done as you finish it). Every item must be done
at the end.
- Nobody is available to answer questions in this run. If `ask_user` fails,
pick a sensible default and continue.
- Test both scripts with `run_shell` before you finish.
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env bash
# Pass iff the scripts work and the agent kept a finished task list.
set -euo pipefail
cd "$(dirname "$0")"
fail() { echo "check: $*"; exit 1; }
[[ -x greet.sh && -x count.sh ]] || fail "scripts missing or not executable"
[[ "$(./greet.sh)" == "Hello, world!" ]] || fail "greet.sh without argument"
[[ "$(./greet.sh Ada)" == "Hello, Ada!" ]] || fail "greet.sh Ada"
[[ "$(./count.sh notes.txt | tr -d ' ')" == "3" ]] || fail "count.sh notes.txt"
set +e
./count.sh >/dev/null 2>&1
rc=$?
set -e
[[ "$rc" -eq 2 ]] || fail "count.sh without argument should exit 2 (got $rc)"
[[ -s SCRIPTS.md ]] || fail "SCRIPTS.md missing"
grep -q greet.sh SCRIPTS.md && grep -q count.sh SCRIPTS.md ||
fail "SCRIPTS.md does not mention both scripts"
# The session must show todo_write ending with every item done.
python3 - <<'PY' || fail "no finished todo_write list in the session"
import glob, json, re, sys
last = None
for path in glob.glob(".agent/*.jsonl") + glob.glob(".agent/sessions/*.jsonl"):
for line in open(path, encoding="utf-8"):
try:
m = json.loads(line)
except ValueError:
continue
c = m.get("content") or ""
r = re.search(r"todo list updated: (\d+)/(\d+) done", c)
if r:
last = (int(r.group(1)), int(r.group(2)))
sys.exit(0 if last and last[1] >= 3 and last[0] == last[1] else 1)
PY
echo "check: PASSED"
+3
View File
@@ -0,0 +1,3 @@
first
second
third
+5
View File
@@ -0,0 +1,5 @@
.PHONY: check
check:
@test -f lines.txt || { echo "lines.txt missing"; exit 1; }
@seq -f 'line %04g' 1 500 | cmp - lines.txt && echo "check: PASSED ($$(wc -c < lines.txt) bytes)"
+8
View File
@@ -0,0 +1,8 @@
# 任务:写一个大于 4KB 的文件
请用 `write_file` 工具(不要用 shell 的 seq/printf/循环生成)一次性写出 `lines.txt`:
- 共 500 行,第 i 行是 `line ` 加上 4 位补零的 i,例如第 1 行 `line 0001`,第 500 行 `line 0500`;
- 每行以换行结尾,文件总长 5000 字节。
然后运行 `make check` 验证;若失败,读输出修正后再验证。最后用一句话说明结果。
+4
View File
@@ -0,0 +1,4 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
make check
+11
View File
@@ -0,0 +1,11 @@
# 任务:为 hello 项目写 Makefile
工作区里只有 `hello.c` 和 `tests/expect.txt`,还没有 Makefile。请:
1. 新建 `Makefile`(注意:命令行必须用 TAB 缩进),包含以下目标:
- `all`(默认目标):用 `cc -std=c11 -Wall -Wextra -O2 -o hello hello.c` 编译出 `hello`;
- `test`:依赖 `hello`,运行 `./hello > tests/actual.txt`,再用 `diff -u tests/expect.txt tests/actual.txt` 比较;
- `clean`:删除 `hello` 和 `tests/actual.txt`;
- 把 `all test clean` 声明为 `.PHONY`。
2. 运行 `make clean && make test`,确认通过。
3. 用一句话说明结果。
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/env bash
# Pass iff the agent wrote a working Makefile (recipes need real TABs).
set -euo pipefail
cd "$(dirname "$0")"
test -f Makefile
grep -q "$(printf '^\t')" Makefile
make clean >/dev/null
make test
make clean >/dev/null
test ! -e hello
+7
View File
@@ -0,0 +1,7 @@
#include <stdio.h>
int main(void) {
printf("hello, cst\n");
printf("tab\there\n");
return 0;
}
+2
View File
@@ -0,0 +1,2 @@
hello, cst
tab here
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
# Pass iff the task is done (used by scripts/e2e_fixture.sh).
set -euo pipefail
cd "$(dirname "$0")"
make test
+20
View File
@@ -0,0 +1,20 @@
CC ?= gcc
CFLAGS ?= -std=c11 -Wall -Wextra -O2
OBJS = main.o order.o report.o
.PHONY: all clean test
all: shop
shop: $(OBJS)
$(CC) $(CFLAGS) -o $@ $(OBJS)
%.o: %.c order.h report.h
$(CC) $(CFLAGS) -c -o $@ $<
test: shop
./tests/run.sh
clean:
rm -f shop $(OBJS)
+15
View File
@@ -0,0 +1,15 @@
# Task: rename across a small C project
`shop` is a tiny C program split over several files. Do the following, then stop:
1. Rename the function `compute_total` to `order_total` everywhere: the
declaration, the definition, every call site and the comments. Nothing
named `compute_total` may remain anywhere in the project.
2. `report.c` applies the 10% discount a second time, so the printed total is
too low. Fix it so the discount is applied exactly once (`order_total`
already applies it).
3. Run `make test` and make sure it passes.
4. Summarize the change in two or three sentences.
Find the call sites with the search tools instead of guessing. Edit only
what the task needs.
+11
View File
@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# Pass iff the rename is complete and the discount bug is fixed.
set -euo pipefail
cd "$(dirname "$0")"
if grep -rn --include='*.c' --include='*.h' compute_total . ; then
echo "check: compute_total still present"
exit 1
fi
grep -q 'int order_total(const int \*prices, int n);' order.h
make -s clean >/dev/null
make -s test
+19
View File
@@ -0,0 +1,19 @@
#include <stdio.h>
#include <stdlib.h>
#include "order.h"
#include "report.h"
int main(int argc, char **argv) {
int prices[64];
int n = 0;
for (int i = 1; i < argc && n < 64; i++)
prices[n++] = atoi(argv[i]);
if (n == 0) {
fprintf(stderr, "usage: shop PRICE_CENTS...\n");
return 2;
}
print_report(stdout, prices, n);
/* exit status: 0 when compute_total is positive */
return compute_total(prices, n) > 0 ? 0 : 1;
}
+17
View File
@@ -0,0 +1,17 @@
#include "order.h"
int compute_total(const int *prices, int n) {
int sum = 0;
for (int i = 0; i < n; i++)
sum += prices[i];
if (n > 3)
sum -= sum / 10; /* bulk discount */
return sum;
}
int count_expensive(const int *prices, int n, int limit) {
int c = 0;
for (int i = 0; i < n; i++)
c += prices[i] > limit;
return c;
}
+11
View File
@@ -0,0 +1,11 @@
#ifndef ORDER_H
#define ORDER_H
/* Sum of prices in cents, with the 10% bulk discount applied when there
* are more than three items. */
int compute_total(const int *prices, int n);
/* Number of items that cost more than limit cents. */
int count_expensive(const int *prices, int n, int limit);
#endif
+12
View File
@@ -0,0 +1,12 @@
#include "report.h"
#include "order.h"
void print_report(FILE *out, const int *prices, int n) {
/* compute_total gives the discounted sum */
int total = compute_total(prices, n);
if (n > 3)
total -= total / 10;
fprintf(out, "items: %d\n", n);
fprintf(out, "expensive: %d\n", count_expensive(prices, n, 1000));
fprintf(out, "total: %d.%02d\n", total / 100, total % 100);
}
+8
View File
@@ -0,0 +1,8 @@
#ifndef REPORT_H
#define REPORT_H
#include <stdio.h>
void print_report(FILE *out, const int *prices, int n);
#endif
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Exit 0 only when shop prints the expected reports.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
fail=0
expect() {
local want="$1"
shift
local got
got="$("$ROOT/shop" "$@")"
if [[ "$got" != "$want" ]]; then
printf 'FAIL: shop %s\n--- want\n%s\n--- got\n%s\n' "$*" "$want" "$got"
fail=1
else
echo "ok: shop $*"
fi
}
expect $'items: 2\nexpensive: 0\ntotal: 15.00' 500 1000
expect $'items: 4\nexpensive: 2\ntotal: 36.00' 500 1500 2000 0
expect $'items: 5\nexpensive: 0\ntotal: 45.00' 1000 1000 1000 1000 1000
if [[ "$fail" -ne 0 ]]; then
echo "tests: FAILED"
exit 1
fi
echo "tests: PASSED"
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
make snake
test -x snake
+14
View File
@@ -0,0 +1,14 @@
CC ?= cc
CFLAGS ?= -std=c11 -Wall -Wextra -O2
.PHONY: test clean
test_strutil: strutil.c strutil.h tests/test_strutil.c
$(CC) $(CFLAGS) -o $@ strutil.c tests/test_strutil.c
test: test_strutil
@echo run >> .test_runs
./test_strutil
clean:
rm -f test_strutil
+6
View File
@@ -0,0 +1,6 @@
# 任务:先跑测试,再修,再跑测试
1. 先运行 `make test`,看哪些用例失败(此时不要改代码)。
2. 阅读 `strutil.c`,修复导致失败的 bug(不要修改 `tests/` 下的文件)。
3. 再次运行 `make test`,确认全部通过。
4. 用简短中文说明你修了什么。
+12
View File
@@ -0,0 +1,12 @@
#!/usr/bin/env bash
# Pass iff tests pass now AND the agent really ran `make test` at least twice
# (before and after the fix; an over-eager duplicate filter would skip one).
set -euo pipefail
cd "$(dirname "$0")"
runs=$(wc -l < .test_runs 2>/dev/null || echo 0)
echo "agent test runs: $runs"
git_clean_tests=1
grep -q 'check_vowels("Queue", 4);' tests/test_strutil.c || git_clean_tests=0
[ "$git_clean_tests" = 1 ] || { echo "tests were modified"; exit 1; }
[ "$runs" -ge 2 ] || { echo "make test ran fewer than 2 times"; exit 1; }
make test
+23
View File
@@ -0,0 +1,23 @@
#include "strutil.h"
#include <ctype.h>
#include <string.h>
void str_reverse(char *s) {
size_t n = strlen(s);
for (size_t i = 0; i < n / 2; i++) {
char t = s[i];
s[i] = s[n - i];
s[n - i] = t;
}
}
int count_vowels(const char *s) {
int k = 0;
for (; *s; s++) {
char c = (char)tolower((unsigned char)*s);
if (c == 'a' || c == 'e' || c == 'i' || c == 'o')
k++;
}
return k;
}
+7
View File
@@ -0,0 +1,7 @@
#ifndef STRUTIL_H
#define STRUTIL_H
void str_reverse(char *s);
int count_vowels(const char *s);
#endif
@@ -0,0 +1,39 @@
#include "../strutil.h"
#include <stdio.h>
#include <string.h>
static int fails;
static void check_rev(const char *in, const char *want) {
char buf[64];
snprintf(buf, sizeof(buf), "%s", in);
str_reverse(buf);
if (strcmp(buf, want) != 0) {
printf("FAIL str_reverse(\"%s\") = \"%s\", want \"%s\"\n", in, buf, want);
fails++;
}
}
static void check_vowels(const char *in, int want) {
int got = count_vowels(in);
if (got != want) {
printf("FAIL count_vowels(\"%s\") = %d, want %d\n", in, got, want);
fails++;
}
}
int main(void) {
check_rev("abc", "cba");
check_rev("ab", "ba");
check_rev("", "");
check_vowels("education", 5);
check_vowels("RHYTHM", 0);
check_vowels("Queue", 4);
if (fails) {
printf("%d test(s) failed\n", fails);
return 1;
}
printf("all tests passed\n");
return 0;
}