Extensions: skills, plugins, MCP, sub-agents; configurable keys
- 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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user