Extensions: skills, plugins, MCP, sub-agents; configurable keys
ci / linux (clang) (push) Canceled after 0s
ci / linux (gcc) (push) Canceled after 0s
ci / sanitizers (push) Canceled after 0s
ci / musl (push) Canceled after 0s
ci / macos (push) Canceled after 0s

- Skills (SKILL.md discovery, skill tool, /skills /skill)
- plugin.json exec tools with permission prompts, timeouts and workspace trust (/plugins)
- MCP stdio client: JSON-RPC initialize/tools/list/tools/call, mcp__server__tool, /mcp and sidebar status
- task tool with general/explore/plan and .agent/agents/*.md roles, parallel read-only runs, cancel propagation, /agents
- Keymap with Ctrl+X leader, keybinds.json overrides and /keys panel
- Collapsed thinking, throttled streaming Markdown, leaner tool output, parallel read-only tools
- Spawn: close-on-exec pipes and a process-wide fork lock
- Tests for keymap, skills, plugins, MCP, sub-agents, shell compaction; docs/plugins.md
This commit is contained in:
WHC
2026-10-05 14:55:31 +08:00
parent c81df42f0b
commit 7d3ec9fcb9
38 changed files with 8356 additions and 399 deletions
+502
View File
@@ -0,0 +1,502 @@
#include "cst_keymap.h"
#include "cJSON.h"
#include "os_compat.h"
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static const cst_key_action_info_t INFO[CST_KA_COUNT] = {
[CST_KA_PALETTE] = {"palette", "General", "command palette", NULL},
[CST_KA_KEYS] = {"keys", "General", "keyboard shortcuts", "/keys"},
[CST_KA_MODE] = {"mode", "General", "switch Build / Plan mode", NULL},
[CST_KA_APPROVAL] = {"approval", "General", "cycle approval ask/auto/yolo",
NULL},
[CST_KA_NEWLINE] = {"newline", "General", "insert a newline", NULL},
[CST_KA_REDRAW] = {"redraw", "General", "redraw the screen", NULL},
[CST_KA_QUIT] = {"quit", "General", "quit", "/quit"},
[CST_KA_TOGGLE_TOOL] = {"toggle_tool", "View",
"expand/collapse the last tool output", NULL},
[CST_KA_TOGGLE_THOUGHT] = {"toggle_thought", "View",
"expand/collapse the last thought", NULL},
[CST_KA_THINKING_ALL] = {"thinking", "View",
"show/hide all thoughts by default", "/thinking"},
[CST_KA_DETAILS] = {"details", "View", "expand/collapse all tool output",
"/details"},
[CST_KA_TODO] = {"todo", "View", "show/hide the task panel", NULL},
[CST_KA_SIDEBAR] = {"sidebar", "View", "show/hide the sidebar",
"/sidebar"},
[CST_KA_SCROLL_UP] = {"scroll_up", "View", "scroll up half a page", NULL},
[CST_KA_SCROLL_DOWN] = {"scroll_down", "View", "scroll down half a page",
NULL},
[CST_KA_LINE_UP] = {"line_up", "View", "scroll up one line", NULL},
[CST_KA_LINE_DOWN] = {"line_down", "View", "scroll down one line", NULL},
[CST_KA_TOP] = {"top", "View", "jump to the first message", NULL},
[CST_KA_BOTTOM] = {"bottom", "View", "jump to the latest message", NULL},
[CST_KA_NEW_SESSION] = {"new_session", "Session", "start a new session",
"/new"},
[CST_KA_SESSIONS] = {"sessions", "Session", "pick a session", "/session"},
[CST_KA_UNDO] = {"undo", "Session", "revert the last turn's file changes",
"/undo"},
[CST_KA_RETRY] = {"retry", "Session", "drop the last reply and ask again",
"/retry"},
[CST_KA_COMPACT] = {"compact", "Session", "summarize the context",
"/compact"},
[CST_KA_COPY] = {"copy", "Session", "copy the last reply", "/copy"},
[CST_KA_EXPORT] = {"export", "Session", "export the transcript",
"/export"},
[CST_KA_MODEL] = {"model", "Model", "pick the model", "/model"},
[CST_KA_PROVIDER] = {"provider", "Model", "switch provider", "/provider"},
[CST_KA_EFFORT] = {"effort", "Model", "reasoning effort", "/effort"},
[CST_KA_AGENTS] = {"agents", "Agents", "sub-agents and roles", "/agents"},
[CST_KA_SKILLS] = {"skills", "Agents", "skills", "/skills"},
[CST_KA_PLUGINS] = {"plugins", "Agents", "plugins and MCP servers",
"/plugins"},
};
static const struct {
int action;
const char *keys;
} DEFAULTS[] = {
{CST_KA_PALETTE, "ctrl+p"},
{CST_KA_KEYS, "<leader> h"},
{CST_KA_KEYS, "f1"},
{CST_KA_MODE, "tab"},
{CST_KA_APPROVAL, "shift+tab"},
{CST_KA_NEWLINE, "ctrl+n"},
{CST_KA_NEWLINE, "alt+enter"},
{CST_KA_REDRAW, "ctrl+l"},
{CST_KA_QUIT, "<leader> q"},
{CST_KA_TOGGLE_TOOL, "ctrl+o"},
{CST_KA_TOGGLE_THOUGHT, "ctrl+r"},
{CST_KA_THINKING_ALL, "<leader> t"},
{CST_KA_DETAILS, "<leader> d"},
{CST_KA_TODO, "ctrl+t"},
{CST_KA_SIDEBAR, "<leader> s"},
{CST_KA_SCROLL_UP, "pgup"},
{CST_KA_SCROLL_DOWN, "pgdn"},
{CST_KA_LINE_UP, "alt+up"},
{CST_KA_LINE_DOWN, "alt+down"},
{CST_KA_TOP, "ctrl+home"},
{CST_KA_BOTTOM, "ctrl+end"},
{CST_KA_NEW_SESSION, "<leader> n"},
{CST_KA_SESSIONS, "<leader> l"},
{CST_KA_UNDO, "<leader> u"},
{CST_KA_RETRY, "<leader> r"},
{CST_KA_COMPACT, "<leader> c"},
{CST_KA_COPY, "<leader> y"},
{CST_KA_EXPORT, "<leader> x"},
{CST_KA_MODEL, "<leader> m"},
{CST_KA_PROVIDER, "<leader> p"},
{CST_KA_EFFORT, "<leader> e"},
{CST_KA_AGENTS, "<leader> a"},
{CST_KA_SKILLS, "<leader> k"},
{CST_KA_PLUGINS, "<leader> g"},
};
static const struct {
const char *name;
int code;
} NAMED[] = {
{"tab", 9},
{"enter", 13},
{"return", 13},
{"esc", 27},
{"escape", 27},
{"space", ' '},
{"backspace", 127},
{"up", CST_K_UP},
{"down", CST_K_DOWN},
{"left", CST_K_LEFT},
{"right", CST_K_RIGHT},
{"home", CST_K_HOME},
{"end", CST_K_END},
{"pgup", CST_K_PGUP},
{"pageup", CST_K_PGUP},
{"pgdn", CST_K_PGDN},
{"pagedown", CST_K_PGDN},
{"delete", CST_K_DEL},
{"del", CST_K_DEL},
{"insert", CST_K_INS},
};
const cst_key_action_info_t *cst_key_action_info(int action) {
return action > CST_KA_NONE && action < CST_KA_COUNT ? &INFO[action] : NULL;
}
int cst_key_action_find(const char *name) {
for (int i = 1; name && i < CST_KA_COUNT; i++)
if (INFO[i].name && strcmp(INFO[i].name, name) == 0)
return i;
return CST_KA_NONE;
}
static int ieq(const char *a, const char *b, size_t n) {
if (strlen(b) != n)
return 0;
for (size_t i = 0; i < n; i++)
if (tolower((unsigned char)a[i]) != b[i])
return 0;
return 1;
}
/* Decode one UTF-8 code point; returns bytes used (0 on error). */
static int utf8_one(const char *s, size_t n, int *cp) {
const unsigned char *u = (const unsigned char *)s;
if (n >= 1 && u[0] < 0x80) {
*cp = u[0];
return 1;
}
if (n >= 2 && (u[0] & 0xE0) == 0xC0) {
*cp = ((u[0] & 0x1F) << 6) | (u[1] & 0x3F);
return 2;
}
if (n >= 3 && (u[0] & 0xF0) == 0xE0) {
*cp = ((u[0] & 0x0F) << 12) | ((u[1] & 0x3F) << 6) | (u[2] & 0x3F);
return 3;
}
if (n >= 4 && (u[0] & 0xF8) == 0xF0) {
*cp = ((u[0] & 0x07) << 18) | ((u[1] & 0x3F) << 12) |
((u[2] & 0x3F) << 6) | (u[3] & 0x3F);
return 4;
}
return 0;
}
static int parse_key_n(const char *s, size_t n) {
int ctrl = 0, alt = 0, shift = 0;
for (;;) {
const char *plus = memchr(s, '+', n);
if (!plus || plus == s + n - 1)
break;
size_t m = (size_t)(plus - s);
if (ieq(s, "ctrl", m) || ieq(s, "c", m))
ctrl = 1;
else if (ieq(s, "alt", m) || ieq(s, "meta", m) || ieq(s, "m", m))
alt = 1;
else if (ieq(s, "shift", m) || ieq(s, "s", m))
shift = 1;
else
return -1;
n -= m + 1;
s = plus + 1;
}
if (n == 0)
return -1;
int code = -1;
for (size_t i = 0; i < sizeof(NAMED) / sizeof(NAMED[0]); i++)
if (ieq(s, NAMED[i].name, n))
code = NAMED[i].code;
if (code < 0 && (s[0] == 'f' || s[0] == 'F') && n >= 2 && n <= 3) {
int f = atoi(s + 1);
if (f >= 1 && f <= 12 && isdigit((unsigned char)s[1]))
code = CST_K_F1 + f - 1;
}
if (code < 0) {
int cp = 0;
if (utf8_one(s, n, &cp) != (int)n)
return -1;
code = cp;
}
if (shift) {
if (code == 9)
code = CST_K_BTAB;
else if (code < 128 && isalpha(code))
code = toupper(code);
else
return -1;
}
if (ctrl) {
if (code < 128 && isalpha(code))
code = tolower(code) - 'a' + 1;
else if (code == CST_K_HOME)
code = CST_K_CTRL_HOME;
else if (code == CST_K_END)
code = CST_K_CTRL_END;
else if (code == CST_K_UP)
code = CST_K_CTRL_UP;
else if (code == CST_K_DOWN)
code = CST_K_CTRL_DOWN;
else
return -1;
}
if (alt) {
if (code == CST_K_UP)
return CST_K_ALT_UP;
if (code == CST_K_DOWN)
return CST_K_ALT_DOWN;
code |= CST_KEY_ALT;
}
return code;
}
int cst_key_parse(const char *s) {
if (!s)
return -1;
while (*s == ' ')
s++;
size_t n = strlen(s);
while (n && s[n - 1] == ' ')
n--;
return parse_key_n(s, n);
}
const char *cst_key_name(int code, char *buf, size_t n) {
const char *alt = (code & CST_KEY_ALT) ? "alt+" : "";
int c = code & ~CST_KEY_ALT;
static const char *const SPECIAL[] = {
"up", "down", "left", "right", "home",
"end", "pgup", "pgdn", "shift+tab", "delete",
"insert", "ctrl+home", "ctrl+end", "ctrl+up", "ctrl+down",
"alt+up", "alt+down"};
if (c == 9)
snprintf(buf, n, "%stab", alt);
else if (c == 13 || c == 10)
snprintf(buf, n, "%senter", alt);
else if (c == 27)
snprintf(buf, n, "%sesc", alt);
else if (c == ' ')
snprintf(buf, n, "%sspace", alt);
else if (c == 127 || c == 8)
snprintf(buf, n, "%sbackspace", alt);
else if (c >= 1 && c <= 26)
snprintf(buf, n, "%sctrl+%c", alt, 'a' + c - 1);
else if (c >= CST_K_F1 && c <= CST_K_LAST)
snprintf(buf, n, "%sf%d", alt, c - CST_K_F1 + 1);
else if (c >= CST_KEY_SPECIAL && c < CST_K_F1)
snprintf(buf, n, "%s%s", alt, SPECIAL[c - CST_KEY_SPECIAL]);
else if (c >= 'A' && c <= 'Z')
snprintf(buf, n, "%sshift+%c", alt, c - 'A' + 'a');
else if (c < 0x80)
snprintf(buf, n, "%s%c", alt, c);
else {
char u[5] = {0};
if (c < 0x800) {
u[0] = (char)(0xC0 | (c >> 6));
u[1] = (char)(0x80 | (c & 0x3F));
} else if (c < 0x10000) {
u[0] = (char)(0xE0 | (c >> 12));
u[1] = (char)(0x80 | ((c >> 6) & 0x3F));
u[2] = (char)(0x80 | (c & 0x3F));
} else {
u[0] = (char)(0xF0 | (c >> 18));
u[1] = (char)(0x80 | ((c >> 12) & 0x3F));
u[2] = (char)(0x80 | ((c >> 6) & 0x3F));
u[3] = (char)(0x80 | (c & 0x3F));
}
snprintf(buf, n, "%s%s", alt, u);
}
return buf;
}
/* "ctrl+x n", "<leader> n" or "f2" → keys; returns the count (0 on error). */
static int parse_seq(const cst_keymap_t *km, const char *s, int keys[2]) {
int n = 0;
while (*s) {
while (*s == ' ')
s++;
if (!*s)
break;
size_t w = strcspn(s, " ");
if (n == 2)
return 0;
int k = ieq(s, "<leader>", w) ? km->leader : parse_key_n(s, w);
if (k < 0)
return 0;
keys[n++] = k;
s += w;
}
return n;
}
static void add(cst_keymap_t *km, int action, const int keys[2], int nkeys,
int user) {
if (km->n >= CST_KEYMAP_MAX || nkeys < 1)
return;
cst_binding_t *b = &km->b[km->n++];
b->action = action;
b->keys[0] = keys[0];
b->keys[1] = nkeys > 1 ? keys[1] : 0;
b->nkeys = nkeys;
b->user = user;
}
static void build_defaults(cst_keymap_t *km) {
km->n = 0;
for (size_t i = 0; i < sizeof(DEFAULTS) / sizeof(DEFAULTS[0]); i++) {
int keys[2];
int n = parse_seq(km, DEFAULTS[i].keys, keys);
add(km, DEFAULTS[i].action, keys, n, 0);
}
}
void cst_keymap_defaults(cst_keymap_t *km) {
memset(km, 0, sizeof(*km));
km->leader = 24; /* ctrl+x */
build_defaults(km);
}
static void drop_action(cst_keymap_t *km, int action) {
int o = 0;
for (int i = 0; i < km->n; i++)
if (km->b[i].action != action)
km->b[o++] = km->b[i];
km->n = o;
}
static void errf(char *err, size_t err_sz, int *bad, const char *what,
const char *name) {
if (err && err_sz && !*bad)
snprintf(err, err_sz, "keybinds.json: %s \"%.60s\"", what, name);
*bad = 1;
}
int cst_keymap_apply_json(cst_keymap_t *km, const char *json, char *err,
size_t err_sz) {
if (err && err_sz)
err[0] = 0;
cJSON *root = cJSON_Parse(json ? json : "");
if (!cJSON_IsObject(root)) {
cJSON_Delete(root);
if (err && err_sz)
snprintf(err, err_sz, "keybinds.json: not a JSON object");
return -1;
}
int bad = 0;
const cJSON *ld = cJSON_GetObjectItemCaseSensitive(root, "leader");
if (cJSON_IsString(ld)) {
int k = cst_key_parse(ld->valuestring);
if (k > 0) {
km->leader = k;
build_defaults(km);
} else {
errf(err, err_sz, &bad, "bad leader key", ld->valuestring);
}
}
const cJSON *it;
cJSON_ArrayForEach(it, root) {
if (!it->string || strcmp(it->string, "leader") == 0)
continue;
int action = cst_key_action_find(it->string);
if (action == CST_KA_NONE) {
errf(err, err_sz, &bad, "unknown action", it->string);
continue;
}
drop_action(km, action);
const cJSON *v;
if (cJSON_IsString(it)) {
if (strcmp(it->valuestring, "none") == 0)
continue;
int keys[2], n = parse_seq(km, it->valuestring, keys);
if (n)
add(km, action, keys, n, 1);
else
errf(err, err_sz, &bad, "bad key", it->valuestring);
} else if (cJSON_IsArray(it)) {
cJSON_ArrayForEach(v, it) {
if (!cJSON_IsString(v))
continue;
int keys[2], n = parse_seq(km, v->valuestring, keys);
if (n)
add(km, action, keys, n, 1);
else
errf(err, err_sz, &bad, "bad key", v->valuestring);
}
} else {
errf(err, err_sz, &bad, "expected a string or list for", it->string);
}
}
cJSON_Delete(root);
return bad ? -1 : 0;
}
int cst_keymap_load(cst_keymap_t *km, char *err, size_t err_sz) {
cst_keymap_defaults(km);
if (err && err_sz)
err[0] = 0;
char dir[OS_PATH_MAX], path[OS_PATH_MAX];
if (os_config_dir(dir, sizeof(dir)) != 0 ||
os_path_join(path, sizeof(path), dir, "keybinds.json") != 0)
return 0;
FILE *fp = fopen(path, "rb");
if (!fp)
return 0;
char buf[16384];
size_t n = fread(buf, 1, sizeof(buf) - 1, fp);
fclose(fp);
buf[n] = 0;
return cst_keymap_apply_json(km, buf, err, err_sz);
}
int cst_keymap_lookup(const cst_keymap_t *km, const int *seq, int n,
int *action) {
int prefix = 0;
for (int i = 0; i < km->n; i++) {
const cst_binding_t *b = &km->b[i];
if (b->keys[0] != seq[0])
continue;
if (b->nkeys == n && (n == 1 || b->keys[1] == seq[1])) {
*action = b->action;
return CST_KM_MATCH;
}
if (n == 1 && b->nkeys == 2)
prefix = 1;
}
return prefix ? CST_KM_PREFIX : CST_KM_NONE;
}
static void seq_name(const cst_binding_t *b, char *buf, size_t n) {
char a[32], c[32];
cst_key_name(b->keys[0], a, sizeof(a));
if (b->nkeys > 1)
snprintf(buf, n, "%s %s", a, cst_key_name(b->keys[1], c, sizeof(c)));
else
snprintf(buf, n, "%s", a);
}
void cst_keymap_describe(const cst_keymap_t *km, int action, char *buf,
size_t n) {
size_t o = 0;
buf[0] = 0;
for (int i = 0; i < km->n && o + 1 < n; i++) {
if (km->b[i].action != action)
continue;
char s[80];
seq_name(&km->b[i], s, sizeof(s));
int w = snprintf(buf + o, n - o, "%s%s", o ? ", " : "", s);
if (w < 0 || (size_t)w >= n - o)
break;
o += (size_t)w;
}
}
int cst_keymap_conflicts(const cst_keymap_t *km, char *buf, size_t n) {
int count = 0;
size_t o = 0;
if (n)
buf[0] = 0;
for (int i = 0; i < km->n; i++) {
const cst_binding_t *a = &km->b[i];
for (int j = i + 1; j < km->n; j++) {
const cst_binding_t *b = &km->b[j];
int same = a->keys[0] == b->keys[0] && a->nkeys == b->nkeys &&
(a->nkeys == 1 || a->keys[1] == b->keys[1]);
int shadow = a->keys[0] == b->keys[0] && a->nkeys != b->nkeys;
if ((!same && !shadow) || a->action == b->action)
continue;
count++;
char s[80];
seq_name(a->nkeys <= b->nkeys ? a : b, s, sizeof(s));
if (o + 1 < n) {
int w = snprintf(buf + o, n - o, "%s%s: %s, %s",
o ? "\n" : "", s, INFO[a->action].name,
INFO[b->action].name);
if (w > 0 && (size_t)w < n - o)
o += (size_t)w;
}
}
}
return count;
}