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
+164
View File
@@ -323,6 +323,170 @@ int cst_tool_run_shell_ex(const char *workspace_root,
return 0;
}
#define SHC_LINE_MAX 2000
#define SHC_HEAD 80
#define SHC_TAIL 120
#define SHC_BYTES_MAX (40 * 1024)
/* One output line: escapes removed and only what follows the last lone
* `\r` kept (what a terminal would finally show). */
static void shc_clean_line(const char *s, size_t n, cst_buf_t *out) {
size_t start = out->len;
for (size_t i = 0; i < n; i++) {
unsigned char ch = (unsigned char)s[i];
if (ch == 0x1b && i + 1 < n) {
unsigned char k = (unsigned char)s[i + 1];
if (k == '[') {
i += 2;
while (i < n && ((unsigned char)s[i] < 0x40 || (unsigned char)s[i] > 0x7e))
i++;
} else if (k == ']') {
i += 2;
while (i < n && s[i] != 0x07 &&
!(s[i] == 0x1b && i + 1 < n && s[i + 1] == '\\'))
i++;
if (i < n && s[i] == 0x1b)
i++;
} else {
i++;
}
continue;
}
if (ch == '\r') {
if (i + 1 < n)
out->len = start;
continue;
}
if (ch == '\b') {
if (out->len > start)
out->len--;
continue;
}
if (ch < 0x20 && ch != '\t')
continue;
cst_buf_append(out, (const char *)&s[i], 1);
}
}
typedef struct {
size_t off, len;
} shc_line_t;
static void shc_put(cst_buf_t *b, const char *s, size_t n) {
if (n > SHC_LINE_MAX) {
size_t keep = cst_utf8_floor(s, SHC_LINE_MAX);
cst_buf_append(b, s, keep);
cst_buf_printf(b, " … [+%zu bytes]\n", n - keep);
return;
}
cst_buf_append(b, s, n);
cst_buf_puts(b, "\n");
}
char *cst_shell_compact(const char *s, size_t n, size_t *out_len) {
cst_buf_t clean = {0};
shc_line_t *lines = NULL;
size_t nl = 0, cap = 0;
for (size_t i = 0; i < n;) {
const char *e = memchr(s + i, '\n', n - i);
size_t end = e ? (size_t)(e - s) : n;
size_t ln = end - i;
if (ln && s[i + ln - 1] == '\r')
ln--;
if (nl == cap) {
cap = cap ? cap * 2 : 256;
shc_line_t *nv = realloc(lines, cap * sizeof(*nv));
if (!nv)
break;
lines = nv;
}
size_t off = clean.len;
shc_clean_line(s + i, ln, &clean);
lines[nl].off = off;
lines[nl].len = clean.len - off;
nl++;
i = e ? end + 1 : n;
}
/* Collapse runs of identical lines: keep one plus a count. */
size_t m = 0;
int *rep = calloc(nl ? nl : 1, sizeof(int));
for (size_t i = 0; rep && i < nl;) {
size_t j = i + 1;
while (j < nl && lines[j].len == lines[i].len &&
memcmp(clean.buf + lines[j].off, clean.buf + lines[i].off,
lines[i].len) == 0)
j++;
lines[m] = lines[i];
rep[m] = (int)(j - i);
m++;
if (j - i == 2) {
lines[m] = lines[i];
rep[m - 1] = rep[m] = 1;
m++;
}
i = j;
}
if (!rep)
m = nl;
size_t total = 0;
for (size_t i = 0; i < m; i++)
total += (lines[i].len < SHC_LINE_MAX ? lines[i].len : SHC_LINE_MAX) + 1;
int cut = m > SHC_HEAD + SHC_TAIL + 20 || total > SHC_BYTES_MAX;
size_t head = m, tail_from = m;
if (cut) {
head = m < SHC_HEAD ? m : SHC_HEAD;
tail_from = m > SHC_TAIL ? m - SHC_TAIL : 0;
if (tail_from < head)
tail_from = head;
/* byte budget: shrink the head first, keep the tail (errors live there) */
size_t tb = 0;
for (size_t i = tail_from; i < m; i++)
tb += (lines[i].len < SHC_LINE_MAX ? lines[i].len : SHC_LINE_MAX) + 1;
while (tb > SHC_BYTES_MAX * 3 / 4 && tail_from + 1 < m) {
tb -= (lines[tail_from].len < SHC_LINE_MAX ? lines[tail_from].len
: SHC_LINE_MAX) + 1;
tail_from++;
}
size_t hb = 0, h = 0;
while (h < head) {
size_t l = (lines[h].len < SHC_LINE_MAX ? lines[h].len : SHC_LINE_MAX) + 1;
if (hb + l + tb > SHC_BYTES_MAX)
break;
hb += l;
h++;
}
head = h;
}
cst_buf_t b = {0};
for (size_t i = 0; i < m; i++) {
if (cut && i == head && head < tail_from) {
size_t omitted = 0;
for (size_t k = head; k < tail_from; k++)
omitted += rep ? (size_t)rep[k] : 1;
cst_buf_printf(&b, "… %zu lines omitted …\n", omitted);
i = tail_from;
if (i >= m)
break;
}
shc_put(&b, clean.buf ? clean.buf + lines[i].off : "", lines[i].len);
if (rep && rep[i] > 2)
cst_buf_printf(&b, "… (previous line repeated %d more times)\n",
rep[i] - 1);
}
/* Keep "no trailing newline" outputs that way. */
if (n && s[n - 1] != '\n' && b.len && b.buf[b.len - 1] == '\n')
b.len--;
if (!b.buf)
cst_buf_puts(&b, "");
else
b.buf[b.len] = 0;
free(rep);
free(lines);
cst_buf_free(&clean);
*out_len = b.len;
return cst_buf_steal(&b);
}
/* --- apply_patch --------------------------------------------------------- */
typedef struct {