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:
+235
-100
@@ -5,10 +5,13 @@
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#define CTL_MAX_TAG 32
|
||||
|
||||
typedef enum {
|
||||
ST_TEXT = 0, /* outside a call; accumulate text lines */
|
||||
ST_LINE, /* collecting a line (call/arg/end or text) */
|
||||
ST_ARG_BODY, /* reading nbytes of arg body */
|
||||
ST_TEXT = 0, /* outside a call; accumulate text lines */
|
||||
ST_LINE, /* collecting a line (call/arg/end or text) */
|
||||
ST_ARG_BODY, /* reading nbytes of arg body */
|
||||
ST_HEREDOC, /* reading heredoc lines until the tag line */
|
||||
ST_ERROR
|
||||
} ctl_state_t;
|
||||
|
||||
@@ -26,15 +29,43 @@ struct ctl_parser {
|
||||
size_t arg_cap;
|
||||
size_t arg_got;
|
||||
size_t arg_need;
|
||||
char tag[CTL_MAX_TAG + 1];
|
||||
size_t pending;
|
||||
size_t pending_peak;
|
||||
ctl_parser_stats_t st;
|
||||
char errbuf[256];
|
||||
/* last length-prefixed arg, for actionable contract-error messages */
|
||||
char last_nkey[CTL_MAX_KEY];
|
||||
size_t last_nbytes;
|
||||
};
|
||||
|
||||
const char *ctl_error_kind_name(ctl_error_kind_t k) {
|
||||
switch (k) {
|
||||
case CTL_EK_NONE:
|
||||
return "none";
|
||||
case CTL_EK_BAD_CALL:
|
||||
return "bad_call";
|
||||
case CTL_EK_BAD_ARG:
|
||||
return "bad_arg";
|
||||
case CTL_EK_UNEXPECTED_LINE:
|
||||
return "unexpected_line";
|
||||
case CTL_EK_BODY_EOF:
|
||||
return "body_eof";
|
||||
case CTL_EK_HEREDOC_EOF:
|
||||
return "heredoc_eof";
|
||||
case CTL_EK_LINE_TOO_LONG:
|
||||
return "line_too_long";
|
||||
case CTL_EK_LIMIT:
|
||||
return "limit";
|
||||
case CTL_EK_OOM:
|
||||
return "oom";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
static void bump_pending(ctl_parser_t *p, size_t n) {
|
||||
p->pending = n;
|
||||
if (n > p->pending_peak)
|
||||
p->pending_peak = n;
|
||||
if (n > p->st.pending_peak)
|
||||
p->st.pending_peak = n;
|
||||
}
|
||||
|
||||
static void emit(ctl_parser_t *p, ctl_event_t *ev) {
|
||||
@@ -42,9 +73,10 @@ static void emit(ctl_parser_t *p, ctl_event_t *ev) {
|
||||
p->on_event(ev, p->userdata);
|
||||
}
|
||||
|
||||
static void fail(ctl_parser_t *p, const char *msg) {
|
||||
static void fail(ctl_parser_t *p, ctl_error_kind_t kind, const char *msg) {
|
||||
snprintf(p->errbuf, sizeof(p->errbuf), "%s", msg);
|
||||
p->state = ST_ERROR;
|
||||
p->st.error_kind = kind;
|
||||
ctl_event_t ev = {0};
|
||||
ev.type = CTL_EV_ERROR;
|
||||
ev.error = p->errbuf;
|
||||
@@ -70,6 +102,7 @@ static void reset_arg_body(ctl_parser_t *p) {
|
||||
p->arg_got = 0;
|
||||
p->arg_need = 0;
|
||||
p->arg_key[0] = 0;
|
||||
p->tag[0] = 0;
|
||||
}
|
||||
|
||||
ctl_parser_t *ctl_parser_create(ctl_on_event_fn on_event, void *userdata) {
|
||||
@@ -99,15 +132,25 @@ void ctl_parser_reset(ctl_parser_t *p) {
|
||||
p->line_len = 0;
|
||||
p->in_call = 0;
|
||||
p->pending = 0;
|
||||
/* keep pending_peak for experiment */
|
||||
p->st.error_kind = CTL_EK_NONE;
|
||||
p->last_nkey[0] = 0;
|
||||
/* keep counters / pending_peak for experiments */
|
||||
}
|
||||
|
||||
size_t ctl_parser_pending(const ctl_parser_t *p) {
|
||||
return p ? p->pending : 0;
|
||||
}
|
||||
size_t ctl_parser_pending(const ctl_parser_t *p) { return p ? p->pending : 0; }
|
||||
|
||||
size_t ctl_parser_pending_peak(const ctl_parser_t *p) {
|
||||
return p ? p->pending_peak : 0;
|
||||
return p ? p->st.pending_peak : 0;
|
||||
}
|
||||
|
||||
void ctl_parser_stats(const ctl_parser_t *p, ctl_parser_stats_t *out) {
|
||||
if (!out)
|
||||
return;
|
||||
if (!p) {
|
||||
memset(out, 0, sizeof(*out));
|
||||
return;
|
||||
}
|
||||
*out = p->st;
|
||||
}
|
||||
|
||||
static int starts_with(const char *s, const char *pfx) {
|
||||
@@ -132,6 +175,8 @@ static int parse_call_line(ctl_parser_t *p, const char *line) {
|
||||
if (!*sp)
|
||||
return -1;
|
||||
size_t nlen = strlen(sp);
|
||||
while (nlen > 0 && (sp[nlen - 1] == ' ' || sp[nlen - 1] == '\t'))
|
||||
nlen--;
|
||||
if (nlen == 0 || nlen >= CTL_MAX_NAME)
|
||||
return -1;
|
||||
for (size_t i = 0; i < nlen; i++) {
|
||||
@@ -144,6 +189,7 @@ static int parse_call_line(ctl_parser_t *p, const char *line) {
|
||||
memcpy(p->call.name, sp, nlen);
|
||||
p->call.name[nlen] = 0;
|
||||
p->in_call = 1;
|
||||
p->st.calls_open++;
|
||||
ctl_event_t ev = {0};
|
||||
ev.type = CTL_EV_CALL_OPEN;
|
||||
ev.call = &p->call;
|
||||
@@ -151,12 +197,49 @@ static int parse_call_line(ctl_parser_t *p, const char *line) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int finish_arg(ctl_parser_t *p);
|
||||
static int finish_arg(ctl_parser_t *p) {
|
||||
int i = p->call.nargs;
|
||||
snprintf(p->call.args[i].key, CTL_MAX_KEY, "%s", p->arg_key);
|
||||
if (!p->arg_buf) {
|
||||
p->arg_buf = malloc(1);
|
||||
if (!p->arg_buf)
|
||||
return -1;
|
||||
}
|
||||
p->arg_buf[p->arg_got] = 0;
|
||||
p->call.args[i].value = p->arg_buf;
|
||||
p->call.args[i].value_len = p->arg_got;
|
||||
p->arg_buf = NULL; /* ownership moved */
|
||||
p->arg_cap = 0;
|
||||
p->call.nargs++;
|
||||
p->arg_need = 0;
|
||||
p->arg_got = 0;
|
||||
p->arg_key[0] = 0;
|
||||
p->tag[0] = 0;
|
||||
p->state = ST_LINE;
|
||||
bump_pending(p, 0);
|
||||
ctl_event_t ev = {0};
|
||||
ev.type = CTL_EV_ARG;
|
||||
ev.call = &p->call;
|
||||
emit(p, &ev);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int valid_tag(const char *t, size_t n) {
|
||||
if (n == 0 || n > CTL_MAX_TAG)
|
||||
return 0;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
if (!(isalnum((unsigned char)t[i]) || t[i] == '_'))
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Returns 0 ok, -1 contract error, -2 oom, -3 limit. */
|
||||
static int parse_arg_header(ctl_parser_t *p, const char *line) {
|
||||
/* Forms:
|
||||
* arg <key> <nbytes> → length-prefixed body follows
|
||||
* arg <key> <value...> → value is rest of line (line-mode, confirmed now)
|
||||
* arg <key> <<TAG → heredoc body until a line equal to TAG
|
||||
* arg <key> <value...> → value is rest of line (confirmed now)
|
||||
*/
|
||||
const char *s = line + 4;
|
||||
while (*s == ' ' || *s == '\t')
|
||||
@@ -174,78 +257,81 @@ static int parse_arg_header(ctl_parser_t *p, const char *line) {
|
||||
if (!*sp)
|
||||
return -1;
|
||||
if (p->call.nargs >= CTL_MAX_ARGS)
|
||||
return -1;
|
||||
|
||||
char *end = NULL;
|
||||
unsigned long nbytes = strtoul(sp, &end, 10);
|
||||
int length_prefixed = (end != sp && (*end == '\0' || *end == ' ' || *end == '\t'));
|
||||
if (length_prefixed && end && *end != '\0') {
|
||||
while (*end == ' ' || *end == '\t')
|
||||
end++;
|
||||
if (*end != '\0')
|
||||
length_prefixed = 0; /* e.g. "9foo" or value starting with digits+text */
|
||||
}
|
||||
return -3;
|
||||
|
||||
memcpy(p->arg_key, s, klen);
|
||||
p->arg_key[klen] = 0;
|
||||
free(p->arg_buf);
|
||||
p->arg_buf = NULL;
|
||||
p->arg_cap = 0;
|
||||
p->arg_got = 0;
|
||||
|
||||
if (sp[0] == '<' && sp[1] == '<') {
|
||||
const char *t = sp + 2;
|
||||
while (*t == ' ')
|
||||
t++;
|
||||
size_t tl = strlen(t);
|
||||
while (tl > 0 && (t[tl - 1] == ' ' || t[tl - 1] == '\t'))
|
||||
tl--;
|
||||
if (tl >= 2 && (t[0] == '\'' || t[0] == '"') && t[tl - 1] == t[0]) {
|
||||
t++;
|
||||
tl -= 2;
|
||||
}
|
||||
if (!valid_tag(t, tl))
|
||||
return -1;
|
||||
memcpy(p->tag, t, tl);
|
||||
p->tag[tl] = 0;
|
||||
p->state = ST_HEREDOC;
|
||||
p->st.args_heredoc++;
|
||||
bump_pending(p, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
char *end = NULL;
|
||||
unsigned long nbytes = strtoul(sp, &end, 10);
|
||||
int length_prefixed =
|
||||
(end != sp && isdigit((unsigned char)sp[0]) &&
|
||||
(*end == '\0' || *end == ' ' || *end == '\t'));
|
||||
if (length_prefixed && *end != '\0') {
|
||||
while (*end == ' ' || *end == '\t')
|
||||
end++;
|
||||
if (*end != '\0')
|
||||
length_prefixed = 0; /* "9 apples" is an inline value */
|
||||
}
|
||||
|
||||
if (!length_prefixed) {
|
||||
/* line-mode: entire remainder is the value */
|
||||
size_t vlen = strlen(sp);
|
||||
if (vlen > CTL_MAX_ARG_BYTES)
|
||||
return -1;
|
||||
free(p->arg_buf);
|
||||
return -3;
|
||||
p->arg_buf = malloc(vlen + 1);
|
||||
if (!p->arg_buf)
|
||||
return -2;
|
||||
memcpy(p->arg_buf, sp, vlen);
|
||||
p->arg_buf[vlen] = 0;
|
||||
p->arg_need = vlen;
|
||||
p->arg_got = vlen;
|
||||
p->arg_cap = vlen;
|
||||
finish_arg(p);
|
||||
return 0;
|
||||
p->st.args_inline++;
|
||||
return finish_arg(p) == 0 ? 0 : -2;
|
||||
}
|
||||
|
||||
if (nbytes > CTL_MAX_ARG_BYTES)
|
||||
return -1;
|
||||
return -3;
|
||||
p->arg_need = (size_t)nbytes;
|
||||
p->arg_got = 0;
|
||||
free(p->arg_buf);
|
||||
p->arg_buf = NULL;
|
||||
p->arg_cap = 0;
|
||||
p->st.args_nbytes++;
|
||||
snprintf(p->last_nkey, sizeof(p->last_nkey), "%s", p->arg_key);
|
||||
p->last_nbytes = p->arg_need;
|
||||
if (p->arg_need > 0) {
|
||||
p->arg_buf = malloc(p->arg_need + 1);
|
||||
if (!p->arg_buf)
|
||||
return -2;
|
||||
p->arg_cap = p->arg_need;
|
||||
}
|
||||
if (p->arg_need == 0)
|
||||
return finish_arg(p) == 0 ? 0 : -2;
|
||||
p->state = ST_ARG_BODY;
|
||||
bump_pending(p, p->arg_need);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int finish_arg(ctl_parser_t *p) {
|
||||
int i = p->call.nargs;
|
||||
snprintf(p->call.args[i].key, CTL_MAX_KEY, "%s", p->arg_key);
|
||||
p->call.args[i].value = p->arg_buf;
|
||||
p->call.args[i].value_len = p->arg_got;
|
||||
if (p->arg_buf)
|
||||
p->arg_buf[p->arg_got] = 0;
|
||||
p->arg_buf = NULL; /* ownership moved */
|
||||
p->call.nargs++;
|
||||
p->arg_need = 0;
|
||||
p->arg_got = 0;
|
||||
p->arg_key[0] = 0;
|
||||
p->state = ST_LINE;
|
||||
bump_pending(p, 0);
|
||||
ctl_event_t ev = {0};
|
||||
ev.type = CTL_EV_ARG;
|
||||
ev.call = &p->call;
|
||||
emit(p, &ev);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void emit_text_line(ctl_parser_t *p, const char *line, size_t len) {
|
||||
/* include newline as confirmed text */
|
||||
char tmp[CTL_MAX_LINE + 1];
|
||||
@@ -262,21 +348,55 @@ static void emit_text_line(ctl_parser_t *p, const char *line, size_t len) {
|
||||
}
|
||||
|
||||
static int is_fence_line(const char *line) {
|
||||
if (strncmp(line, "```", 3) == 0)
|
||||
return 1;
|
||||
if (strncmp(line, "~~~", 3) == 0)
|
||||
return 1;
|
||||
return strncmp(line, "```", 3) == 0 || strncmp(line, "~~~", 3) == 0;
|
||||
}
|
||||
|
||||
static void emit_ready(ctl_parser_t *p) {
|
||||
ctl_event_t ev = {0};
|
||||
ev.type = CTL_EV_CALL_READY;
|
||||
ev.call = &p->call;
|
||||
p->st.calls_ready++;
|
||||
emit(p, &ev);
|
||||
clear_call_args(&p->call);
|
||||
p->in_call = 0;
|
||||
bump_pending(p, 0);
|
||||
p->state = ST_LINE;
|
||||
}
|
||||
|
||||
static int heredoc_line(ctl_parser_t *p, const char *line, size_t len) {
|
||||
if (strcmp(line, p->tag) == 0)
|
||||
return finish_arg(p) == 0 ? 0 : -1;
|
||||
size_t need = p->arg_got + len + 1;
|
||||
if (need > CTL_MAX_ARG_BYTES) {
|
||||
fail(p, CTL_EK_LIMIT, "heredoc body too large");
|
||||
return -1;
|
||||
}
|
||||
if (need + 1 > p->arg_cap) {
|
||||
size_t nc = p->arg_cap ? p->arg_cap * 2 : 256;
|
||||
while (nc < need + 1)
|
||||
nc *= 2;
|
||||
char *nb = realloc(p->arg_buf, nc);
|
||||
if (!nb) {
|
||||
fail(p, CTL_EK_OOM, "oom");
|
||||
return -1;
|
||||
}
|
||||
p->arg_buf = nb;
|
||||
p->arg_cap = nc;
|
||||
}
|
||||
memcpy(p->arg_buf + p->arg_got, line, len);
|
||||
p->arg_got += len;
|
||||
p->arg_buf[p->arg_got++] = '\n';
|
||||
bump_pending(p, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int handle_line(ctl_parser_t *p, const char *line) {
|
||||
/* Ignore markdown fences so models can wrap CTL in code blocks. */
|
||||
if (is_fence_line(line)) {
|
||||
if (is_fence_line(line) && !p->in_call) {
|
||||
bump_pending(p, 0);
|
||||
return 0;
|
||||
}
|
||||
/* Ignore blank lines between CTL frames. */
|
||||
if (line[0] == '\0') {
|
||||
if (line[0] == '\0' || (p->in_call && is_fence_line(line))) {
|
||||
bump_pending(p, 0);
|
||||
return 0;
|
||||
}
|
||||
@@ -284,13 +404,13 @@ static int handle_line(ctl_parser_t *p, const char *line) {
|
||||
if (!p->in_call) {
|
||||
if (starts_with(line, "call ")) {
|
||||
if (parse_call_line(p, line) != 0) {
|
||||
fail(p, "invalid call line");
|
||||
fail(p, CTL_EK_BAD_CALL, "invalid call line");
|
||||
return -1;
|
||||
}
|
||||
p->last_nkey[0] = 0;
|
||||
bump_pending(p, 0);
|
||||
return 0;
|
||||
}
|
||||
/* confirmed text line */
|
||||
emit_text_line(p, line, strlen(line));
|
||||
bump_pending(p, 0);
|
||||
return 0;
|
||||
@@ -300,29 +420,33 @@ static int handle_line(ctl_parser_t *p, const char *line) {
|
||||
if (starts_with(line, "arg ")) {
|
||||
int rc = parse_arg_header(p, line);
|
||||
if (rc == -2) {
|
||||
fail(p, "oom");
|
||||
fail(p, CTL_EK_OOM, "oom");
|
||||
return -1;
|
||||
}
|
||||
if (rc == -3) {
|
||||
fail(p, CTL_EK_LIMIT, "too many args or arg too large");
|
||||
return -1;
|
||||
}
|
||||
if (rc != 0) {
|
||||
fail(p, "invalid arg header");
|
||||
fail(p, CTL_EK_BAD_ARG, "invalid arg header");
|
||||
return -1;
|
||||
}
|
||||
if (p->arg_need == 0)
|
||||
finish_arg(p);
|
||||
return 0;
|
||||
}
|
||||
if (strcmp(line, "end") == 0) {
|
||||
ctl_event_t ev = {0};
|
||||
ev.type = CTL_EV_CALL_READY;
|
||||
ev.call = &p->call;
|
||||
emit(p, &ev);
|
||||
clear_call_args(&p->call);
|
||||
p->in_call = 0;
|
||||
bump_pending(p, 0);
|
||||
p->state = ST_LINE;
|
||||
emit_ready(p);
|
||||
return 0;
|
||||
}
|
||||
fail(p, "unexpected line inside call");
|
||||
char msg[256];
|
||||
if (p->last_nkey[0])
|
||||
snprintf(msg, sizeof(msg),
|
||||
"unexpected line \"%.40s\" inside call; arg %s declared %zu "
|
||||
"bytes, so its real body was longer (byte count too small)",
|
||||
line, p->last_nkey, p->last_nbytes);
|
||||
else
|
||||
snprintf(msg, sizeof(msg), "unexpected line \"%.40s\" inside call",
|
||||
line);
|
||||
fail(p, CTL_EK_UNEXPECTED_LINE, msg);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -345,30 +469,30 @@ ctl_status_t ctl_parser_feed(ctl_parser_t *p, const char *data, size_t len) {
|
||||
i += take;
|
||||
bump_pending(p, p->arg_need - p->arg_got);
|
||||
}
|
||||
if (p->arg_got == p->arg_need) {
|
||||
finish_arg(p);
|
||||
/* continue with remaining bytes in LINE mode */
|
||||
if (p->arg_got == p->arg_need && finish_arg(p) != 0) {
|
||||
fail(p, CTL_EK_OOM, "oom");
|
||||
return CTL_ERR_NOMEM;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
/* line accumulation */
|
||||
p->state = ST_LINE;
|
||||
if (p->state != ST_HEREDOC)
|
||||
p->state = ST_LINE;
|
||||
unsigned char c = (unsigned char)data[i++];
|
||||
if (c == '\r')
|
||||
continue; /* normalize CRLF */
|
||||
if (c == '\n') {
|
||||
p->line[p->line_len] = 0;
|
||||
if (handle_line(p, p->line) != 0)
|
||||
return CTL_ERR_CONTRACT;
|
||||
int rc = p->state == ST_HEREDOC ? heredoc_line(p, p->line, p->line_len)
|
||||
: handle_line(p, p->line);
|
||||
p->line_len = 0;
|
||||
if (p->state == ST_ARG_BODY && p->arg_need == 0) {
|
||||
/* already finished empty arg */
|
||||
}
|
||||
if (rc != 0)
|
||||
return p->st.error_kind == CTL_EK_LIMIT ? CTL_ERR_LIMIT
|
||||
: CTL_ERR_CONTRACT;
|
||||
continue;
|
||||
}
|
||||
if (p->line_len + 1 >= CTL_MAX_LINE) {
|
||||
fail(p, "line too long");
|
||||
fail(p, CTL_EK_LINE_TOO_LONG, "line too long");
|
||||
return CTL_ERR_LIMIT;
|
||||
}
|
||||
p->line[p->line_len++] = (char)c;
|
||||
@@ -384,9 +508,25 @@ ctl_status_t ctl_parser_finish(ctl_parser_t *p) {
|
||||
return CTL_ERR_CONTRACT;
|
||||
/* Mid-argument body cannot be confirmed at EOF. */
|
||||
if (p->state == ST_ARG_BODY) {
|
||||
fail(p, "incomplete arg body at EOF");
|
||||
char msg[200];
|
||||
snprintf(msg, sizeof(msg),
|
||||
"stream ended inside arg %s: declared %zu bytes but only %zu "
|
||||
"arrived (byte count too large)",
|
||||
p->arg_key, p->arg_need, p->arg_got);
|
||||
fail(p, CTL_EK_BODY_EOF, msg);
|
||||
return CTL_ERR_CONTRACT;
|
||||
}
|
||||
if (p->state == ST_HEREDOC) {
|
||||
p->line[p->line_len] = 0;
|
||||
if (p->line_len > 0 && strcmp(p->line, p->tag) == 0) {
|
||||
p->line_len = 0;
|
||||
if (finish_arg(p) != 0)
|
||||
return CTL_ERR_NOMEM;
|
||||
} else {
|
||||
fail(p, CTL_EK_HEREDOC_EOF, "stream ended before heredoc terminator");
|
||||
return CTL_ERR_CONTRACT;
|
||||
}
|
||||
}
|
||||
/* Trailing text without final newline is still confirmed text. */
|
||||
if (!p->in_call && p->line_len > 0) {
|
||||
p->line[p->line_len] = 0;
|
||||
@@ -398,21 +538,16 @@ ctl_status_t ctl_parser_finish(ctl_parser_t *p) {
|
||||
if (p->in_call) {
|
||||
if (p->line_len > 0) {
|
||||
p->line[p->line_len] = 0;
|
||||
/* leftover line must be end or fence/blank */
|
||||
if (strcmp(p->line, "end") == 0 || is_fence_line(p->line) ||
|
||||
p->line[0] == '\0') {
|
||||
p->line_len = 0;
|
||||
} else {
|
||||
fail(p, "unexpected trailing line in call at EOF");
|
||||
fail(p, CTL_EK_UNEXPECTED_LINE,
|
||||
"unexpected trailing line in call at EOF");
|
||||
return CTL_ERR_CONTRACT;
|
||||
}
|
||||
}
|
||||
ctl_event_t ev = {0};
|
||||
ev.type = CTL_EV_CALL_READY;
|
||||
ev.call = &p->call;
|
||||
emit(p, &ev);
|
||||
clear_call_args(&p->call);
|
||||
p->in_call = 0;
|
||||
emit_ready(p);
|
||||
}
|
||||
bump_pending(p, 0);
|
||||
return CTL_OK;
|
||||
|
||||
Reference in New Issue
Block a user