v0.8.4: WebUI sessions, diff, settings, slash commands and extensions
Sessions: rename, delete and search; /api/retry regenerates the last answer, editing a prompt resends from there; /api/export downloads the transcript as Markdown (cst_session_export_md). Session messages carry note/tool_result markers, so a reload rebuilds the tool steps with their arguments, output and exit status; subagent progress is shown. Diff and settings: /api/diff shows the real diff; /api/settings covers protocol, max_steps, tool switches and confirm; /api/permissions edits the rules; sandbox status is reported. Session rename no longer reads past a short title. Commands: slash commands with completion in the composer (/model, /provider, /new, /retry, /undo, /compact, /export, /diff, /init, /skill and more), a Ctrl+P palette, Shift+Tab cycles the approval mode, F1 or ? shows the keys. "Thinking expanded" is saved in the settings (show_thinking), "expand all tool output" in the browser. Extensions: GET /api/extensions lists MCP servers, plugins, skills, agent roles and subagent runs; POST /api/extensions/reload and /api/extensions/trust; a workspace switch reloads them. /api/init and /api/skill run on the server (cst_init_prompt shared with the TUI). Errors, statuses and [cst] notes are shown in Chinese with a readable reason instead of rc=; a failed turn's error survives the reload. Tests: scripts/e2e_webui_sessions.py, e2e_webui_workspace.py and e2e_webui_ext.py, run from e2e_webui.sh.
This commit is contained in:
+12
-7
@@ -2948,13 +2948,18 @@ int cst_agent_new_session(cst_agent_t *a, char *id_out, size_t id_sz) {
|
||||
return -1;
|
||||
if (cst_agent_store_ensure(a->cfg.workspace_root) != 0)
|
||||
return -1;
|
||||
char id[128];
|
||||
if (cst_agent_store_new_id(id, sizeof(id)) != 0)
|
||||
return -1;
|
||||
char path[4096];
|
||||
if (cst_agent_store_path_for_id(a->cfg.workspace_root, id, path,
|
||||
sizeof(path)) != 0)
|
||||
return -1;
|
||||
char id[128], path[4096];
|
||||
/* ids are per second: never reopen (and append to) one that exists */
|
||||
for (int tries = 0;; tries++) {
|
||||
if (tries == 16 || cst_agent_store_new_id(id, sizeof(id)) != 0 ||
|
||||
cst_agent_store_path_for_id(a->cfg.workspace_root, id, path,
|
||||
sizeof(path)) != 0)
|
||||
return -1;
|
||||
FILE *fp = fopen(path, "rb");
|
||||
if (!fp)
|
||||
break;
|
||||
fclose(fp);
|
||||
}
|
||||
if (agent_reopen_session(a, path) != 0)
|
||||
return -1;
|
||||
cst_agent_store_set_current(a->cfg.workspace_root, id);
|
||||
|
||||
+2
-1
@@ -57,7 +57,8 @@ static void make_id(char *out, size_t n) {
|
||||
time_t now = time(NULL);
|
||||
struct tm tm;
|
||||
os_localtime(&now, &tm);
|
||||
unsigned r = (unsigned)(os_getpid() ^ (unsigned)now);
|
||||
static unsigned seq;
|
||||
unsigned r = (unsigned)(os_getpid() ^ (unsigned)now) + 0x9e37u * seq++;
|
||||
snprintf(out, n, "%04d%02d%02d-%02d%02d%02d-%04x", tm.tm_year + 1900,
|
||||
tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec,
|
||||
r & 0xffff);
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "cJSON.h"
|
||||
#include "cst_agent.h"
|
||||
#include "cst_attach.h"
|
||||
#include "cst_util.h"
|
||||
|
||||
#include <pthread.h>
|
||||
#include <stdio.h>
|
||||
@@ -409,3 +410,62 @@ int cst_session_rewrite(cst_session_t *s, const cst_message_t *msgs, int n) {
|
||||
free(copy);
|
||||
return rc;
|
||||
}
|
||||
|
||||
int cst_message_is_tool_result(const cst_message_t *m) {
|
||||
return m && (m->role == CST_ROLE_TOOL ||
|
||||
(m->role == CST_ROLE_USER && m->tool_call_id[0]));
|
||||
}
|
||||
|
||||
static void md_fence(cst_buf_t *b, const char *s, size_t max) {
|
||||
size_t n = strlen(s);
|
||||
if (n > max)
|
||||
n = cst_utf8_floor(s, max);
|
||||
cst_buf_puts(b, "````\n");
|
||||
cst_buf_append(b, s, n);
|
||||
cst_buf_puts(b, n < strlen(s) ? "\n…(truncated)\n````\n\n" : "\n````\n\n");
|
||||
}
|
||||
|
||||
char *cst_session_export_md(cst_session_t *s, const char *title,
|
||||
const char *sid, const char *model) {
|
||||
if (!s)
|
||||
return NULL;
|
||||
cst_buf_t b = {0};
|
||||
cst_buf_printf(&b, "# %s\n\nsession `%s` · model `%s`\n\n",
|
||||
title ? title : "", sid ? sid : "", model ? model : "");
|
||||
cst_session_lock(s);
|
||||
for (int i = 0; i < cst_session_count(s); i++) {
|
||||
const cst_message_t *m = cst_session_get(s, i);
|
||||
const char *c = m->content ? m->content : "";
|
||||
if (m->role == CST_ROLE_SYSTEM)
|
||||
continue;
|
||||
if (cst_message_is_tool_result(m)) {
|
||||
cst_buf_printf(&b, "**Tool result** `%s`\n\n", m->tool_name);
|
||||
md_fence(&b, c, 4000);
|
||||
} else if (cst_message_is_note(m)) {
|
||||
cst_buf_puts(&b, "> ");
|
||||
cst_buf_append(&b, c, strcspn(c, "\n"));
|
||||
cst_buf_puts(&b, "\n\n");
|
||||
} else if (m->role == CST_ROLE_USER) {
|
||||
cst_buf_puts(&b, "## User\n\n");
|
||||
cst_buf_puts(&b, c);
|
||||
cst_buf_puts(&b, "\n\n");
|
||||
for (int k = 0; k < m->nattach; k++) {
|
||||
char d[128];
|
||||
cst_attach_describe(&m->attach[k], d, sizeof(d));
|
||||
cst_buf_printf(&b, "- image `%s` (%s)\n", m->attach[k].name, d);
|
||||
}
|
||||
if (m->nattach)
|
||||
cst_buf_puts(&b, "\n");
|
||||
} else {
|
||||
cst_buf_puts(&b, "## Assistant\n\n");
|
||||
cst_buf_puts(&b, c);
|
||||
cst_buf_puts(&b, "\n\n");
|
||||
if (m->tool_calls_json && m->tool_calls_json[0]) {
|
||||
cst_buf_puts(&b, "**Tool calls**\n\n");
|
||||
md_fence(&b, m->tool_calls_json, 4000);
|
||||
}
|
||||
}
|
||||
}
|
||||
cst_session_unlock(s);
|
||||
return cst_buf_steal(&b);
|
||||
}
|
||||
|
||||
@@ -488,6 +488,16 @@ char *cst_skill_read(const char *ws, const char *name, const char *file,
|
||||
return cst_buf_steal(&b);
|
||||
}
|
||||
|
||||
const char *cst_init_prompt(void) {
|
||||
return "Analyze this repository and write an AGENTS.md file at the "
|
||||
"workspace root for future coding agents. Cover: what the project "
|
||||
"is, how to build, test and run it (exact commands), the directory "
|
||||
"layout, coding conventions you can infer from the code, and "
|
||||
"anything surprising. Explore with list_dir/glob/grep/read_file "
|
||||
"before writing. Keep it under 150 lines. If AGENTS.md already "
|
||||
"exists, improve it rather than starting over.";
|
||||
}
|
||||
|
||||
char *cst_skill_prompt(const char *ws, const char *name, const char *task) {
|
||||
int code = 0;
|
||||
char *body = cst_skill_read(ws, name, NULL, &code);
|
||||
|
||||
@@ -1042,10 +1042,7 @@ static int find_tool_block(tui_t *t, const char *id) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int msg_is_tool_result(const cst_message_t *m) {
|
||||
return m && (m->role == CST_ROLE_TOOL ||
|
||||
(m->role == CST_ROLE_USER && m->tool_call_id[0]));
|
||||
}
|
||||
#define msg_is_tool_result cst_message_is_tool_result
|
||||
|
||||
/* Strip the "[tool …]" / "result <id> exit=N <n>" header and "end" trailer. */
|
||||
static void tool_result_view(const char *content, const char **body,
|
||||
@@ -4774,12 +4771,6 @@ static void cmd_diff(tui_t *t, char *arg, int *quit) {
|
||||
free(df.out);
|
||||
}
|
||||
|
||||
static void md_fence(FILE *fp, const char *s, size_t max) {
|
||||
size_t n = strlen(s);
|
||||
fprintf(fp, "````\n%.*s%s\n````\n\n", (int)(n > max ? max : n), s,
|
||||
n > max ? "\n…(truncated)" : "");
|
||||
}
|
||||
|
||||
static void cmd_export(tui_t *t, char *arg, int *quit) {
|
||||
(void)quit;
|
||||
const cst_agent_config_t *cfg = cst_agent_config(t->agent);
|
||||
@@ -4799,44 +4790,20 @@ static void cmd_export(tui_t *t, char *arg, int *quit) {
|
||||
*sl = 0;
|
||||
os_mkdir_p(dir, 0755);
|
||||
}
|
||||
FILE *fp = fopen(path, "wb");
|
||||
char *md = cst_session_export_md(cst_agent_session(t->agent), t->title, sid,
|
||||
cfg->model);
|
||||
FILE *fp = md ? fopen(path, "wb") : NULL;
|
||||
if (!fp) {
|
||||
free(md);
|
||||
push_system_line(t, "Error: cannot write %s", path);
|
||||
return;
|
||||
}
|
||||
fprintf(fp, "# %s\n\nsession `%s` · model `%s`\n\n", t->title, sid,
|
||||
cfg->model);
|
||||
cst_session_t *s = cst_agent_session(t->agent);
|
||||
cst_session_lock(s);
|
||||
for (int i = 0; i < cst_session_count(s); i++) {
|
||||
const cst_message_t *m = cst_session_get(s, i);
|
||||
const char *c = m->content ? m->content : "";
|
||||
if (m->role == CST_ROLE_SYSTEM)
|
||||
continue;
|
||||
if (msg_is_tool_result(m)) {
|
||||
fprintf(fp, "**Tool result** `%s`\n\n", m->tool_name);
|
||||
md_fence(fp, c, 4000);
|
||||
} else if (cst_message_is_note(m)) {
|
||||
fprintf(fp, "> %.*s\n\n", (int)strcspn(c, "\n"), c);
|
||||
} else if (m->role == CST_ROLE_USER) {
|
||||
fprintf(fp, "## User\n\n%s\n\n", c);
|
||||
for (int k = 0; k < m->nattach; k++) {
|
||||
char d[128];
|
||||
cst_attach_describe(&m->attach[k], d, sizeof(d));
|
||||
fprintf(fp, "- image `%s` (%s)\n", m->attach[k].name, d);
|
||||
}
|
||||
if (m->nattach)
|
||||
fputc('\n', fp);
|
||||
} else {
|
||||
fprintf(fp, "## Assistant\n\n%s\n\n", c);
|
||||
if (m->tool_calls_json && m->tool_calls_json[0]) {
|
||||
fprintf(fp, "**Tool calls**\n\n");
|
||||
md_fence(fp, m->tool_calls_json, 4000);
|
||||
}
|
||||
}
|
||||
fputs(md, fp);
|
||||
free(md);
|
||||
if (fclose(fp) != 0) {
|
||||
push_system_line(t, "Error: cannot write %s", path);
|
||||
return;
|
||||
}
|
||||
cst_session_unlock(s);
|
||||
fclose(fp);
|
||||
push_system_line(t, "(exported to %s)", path);
|
||||
}
|
||||
|
||||
@@ -5013,14 +4980,7 @@ static void cmd_init(tui_t *t, char *arg, int *quit) {
|
||||
(void)arg;
|
||||
(void)quit;
|
||||
free(t->pending_prompt);
|
||||
t->pending_prompt = xstrdup(
|
||||
"Analyze this repository and write an AGENTS.md file at the workspace "
|
||||
"root for future coding agents. Cover: what the project is, how to "
|
||||
"build, test and run it (exact commands), the directory layout, coding "
|
||||
"conventions you can infer from the code, and anything surprising. "
|
||||
"Explore with list_dir/glob/grep/read_file before writing. Keep it "
|
||||
"under 150 lines. If AGENTS.md already exists, improve it rather than "
|
||||
"starting over.");
|
||||
t->pending_prompt = xstrdup(cst_init_prompt());
|
||||
}
|
||||
|
||||
static void cmd_compact(tui_t *t, char *arg, int *quit) {
|
||||
|
||||
+822
-6
@@ -14,14 +14,22 @@
|
||||
|
||||
#include "cst_agent.h"
|
||||
#include "cst_attach.h"
|
||||
#include "cst_ext.h"
|
||||
#include "cst_http.h"
|
||||
#include "cst_mcp.h"
|
||||
#include "cst_models.h"
|
||||
#include "cst_perms.h"
|
||||
#include "cst_plugins.h"
|
||||
#include "cst_provider_switch.h"
|
||||
#include "cst_sandbox.h"
|
||||
#include "cst_skills.h"
|
||||
#include "cst_subagent.h"
|
||||
#include "cst_tools.h"
|
||||
#include "cst_util.h"
|
||||
#include "cst_web_md.h"
|
||||
#include "cst_webui.h"
|
||||
#include "cJSON.h"
|
||||
#include "mm_tool.h"
|
||||
#include "os_compat.h"
|
||||
|
||||
#include <pthread.h>
|
||||
@@ -62,6 +70,7 @@ typedef struct wui {
|
||||
int need_auth;
|
||||
const char *root; /* --web-dev: serve assets from disk */
|
||||
int perm_timeout_s;
|
||||
int show_thinking; /* settings.json: reasoning starts expanded */
|
||||
|
||||
pthread_mutex_t mu;
|
||||
pthread_cond_t cv; /* permission / ask_user answers */
|
||||
@@ -257,6 +266,7 @@ static cJSON *wui_state_new(wui_t *w) {
|
||||
cJSON_AddStringToObject(o, "approval", cst_approval_name(cfg->approval));
|
||||
cJSON_AddStringToObject(o, "protocol_tools", cst_protocol_name(cfg->protocol));
|
||||
cJSON_AddBoolToObject(o, "plan_mode", cfg->plan_mode);
|
||||
cJSON_AddBoolToObject(o, "show_thinking", w->show_thinking);
|
||||
cJSON_AddBoolToObject(o, "status_busy", w->busy);
|
||||
cJSON_AddStringToObject(o, "todo", cst_agent_todo(w->agent)
|
||||
? cst_agent_todo(w->agent)
|
||||
@@ -307,6 +317,126 @@ static void wui_emit_state(wui_t *w) {
|
||||
|
||||
/* --- session -------------------------------------------------------------- */
|
||||
|
||||
static cJSON *call_json(const ctl_tool_call_t *call);
|
||||
|
||||
/* A text-protocol reply keeps its call frames in content: the page gets the
|
||||
* prose and the calls apart. */
|
||||
typedef struct {
|
||||
cst_buf_t text;
|
||||
cJSON *calls;
|
||||
int bad;
|
||||
} wui_split_t;
|
||||
|
||||
static void split_ev(const ctl_event_t *ev, void *ud) {
|
||||
wui_split_t *s = ud;
|
||||
if (ev->type == CTL_EV_TEXT)
|
||||
cst_buf_append(&s->text, ev->text, ev->text_len);
|
||||
else if (ev->type == CTL_EV_CALL_READY && ev->call)
|
||||
cJSON_AddItemToArray(s->calls, call_json(ev->call));
|
||||
else if (ev->type == CTL_EV_ERROR)
|
||||
s->bad = 1;
|
||||
}
|
||||
|
||||
static int msg_protocol(const cst_message_t *m, cst_protocol_t *p) {
|
||||
cJSON *o = m->meta_json ? cJSON_Parse(m->meta_json) : NULL;
|
||||
const cJSON *pr = cJSON_GetObjectItemCaseSensitive(o, "protocol");
|
||||
int rc = cJSON_IsString(pr) ? cst_protocol_parse(pr->valuestring, p) : -1;
|
||||
cJSON_Delete(o);
|
||||
return rc;
|
||||
}
|
||||
|
||||
/* Visible text (malloc'd) and calls of a text-protocol reply; NULL when the
|
||||
* frames do not parse, and the raw text is shown as it is. */
|
||||
static char *split_reply(const char *s, cst_protocol_t p, cJSON **calls) {
|
||||
wui_split_t sp;
|
||||
memset(&sp, 0, sizeof(sp));
|
||||
sp.calls = cJSON_CreateArray();
|
||||
if (!sp.calls)
|
||||
return NULL;
|
||||
size_t n = strlen(s);
|
||||
if (p == CST_PROTO_XML) {
|
||||
mm_scan_t *sc = mm_scan_create(split_ev, &sp);
|
||||
sp.bad = !sc;
|
||||
if (sc) {
|
||||
mm_scan_feed(sc, s, n);
|
||||
mm_scan_finish(sc);
|
||||
mm_scan_destroy(sc);
|
||||
}
|
||||
} else {
|
||||
ctl_parser_t *cp = ctl_parser_create(split_ev, &sp);
|
||||
sp.bad = !cp;
|
||||
if (cp) {
|
||||
ctl_parser_feed(cp, s, n);
|
||||
ctl_parser_finish(cp);
|
||||
ctl_parser_destroy(cp);
|
||||
}
|
||||
}
|
||||
if (sp.bad || !cJSON_GetArraySize(sp.calls)) {
|
||||
cst_buf_free(&sp.text);
|
||||
cJSON_Delete(sp.calls);
|
||||
return NULL;
|
||||
}
|
||||
*calls = sp.calls;
|
||||
char *t = cst_buf_steal(&sp.text);
|
||||
return t ? t : strdup("");
|
||||
}
|
||||
|
||||
/* Native tool_calls as [{id, name, args}], args parsed when they are JSON. */
|
||||
static cJSON *native_calls(const char *tool_calls_json) {
|
||||
cJSON *in = cJSON_Parse(tool_calls_json);
|
||||
cJSON *out = cJSON_IsArray(in) ? cJSON_CreateArray() : NULL;
|
||||
const cJSON *src = out ? in : NULL;
|
||||
const cJSON *it;
|
||||
cJSON_ArrayForEach(it, src) {
|
||||
const cJSON *fn = cJSON_GetObjectItemCaseSensitive(it, "function");
|
||||
const cJSON *id = cJSON_GetObjectItemCaseSensitive(it, "id");
|
||||
const cJSON *nm = cJSON_GetObjectItemCaseSensitive(fn, "name");
|
||||
const cJSON *ar = cJSON_GetObjectItemCaseSensitive(fn, "arguments");
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
if (!o)
|
||||
continue;
|
||||
cJSON_AddStringToObject(o, "id", cJSON_IsString(id) ? id->valuestring : "");
|
||||
cJSON_AddStringToObject(o, "name", cJSON_IsString(nm) ? nm->valuestring : "");
|
||||
cJSON *args = cJSON_IsString(ar) ? cJSON_Parse(ar->valuestring) : NULL;
|
||||
if (cJSON_IsObject(args))
|
||||
cJSON_AddItemToObject(o, "args", args);
|
||||
else {
|
||||
cJSON_Delete(args);
|
||||
cJSON_AddItemToObject(o, "args", cJSON_CreateObject());
|
||||
}
|
||||
cJSON_AddItemToArray(out, o);
|
||||
}
|
||||
cJSON_Delete(in);
|
||||
return out;
|
||||
}
|
||||
|
||||
/* Body and exit code of a stored tool result: "[tool NAME id=ID exit=N]\n"
|
||||
* (native) or "result ID exit=N LEN\n...\nend" (text protocols). */
|
||||
static void tool_out_view(const char *s, const char **body, size_t *len,
|
||||
int *exit_code) {
|
||||
size_t n = strlen(s);
|
||||
*exit_code = 0;
|
||||
int framed = strncmp(s, "result ", 7) == 0;
|
||||
if (framed || strncmp(s, "[tool ", 6) == 0) {
|
||||
const char *nl = strchr(s, '\n');
|
||||
const char *ex = strstr(s, " exit=");
|
||||
if (ex && (!nl || ex < nl))
|
||||
*exit_code = atoi(ex + 6);
|
||||
if (nl) {
|
||||
n -= (size_t)(nl + 1 - s);
|
||||
s = nl + 1;
|
||||
} else {
|
||||
n = 0;
|
||||
}
|
||||
if (framed && n >= 4 && strcmp(s + n - 4, "\nend") == 0)
|
||||
n -= 4;
|
||||
else if (framed && n == 3 && strcmp(s, "end") == 0)
|
||||
n = 0;
|
||||
}
|
||||
*body = s;
|
||||
*len = n;
|
||||
}
|
||||
|
||||
static cJSON *msg_json(const cst_message_t *m) {
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
if (!o)
|
||||
@@ -320,15 +450,50 @@ static cJSON *msg_json(const cst_message_t *m) {
|
||||
}
|
||||
cJSON_AddStringToObject(o, "role", role);
|
||||
cJSON_AddStringToObject(o, "content", m->content ? m->content : "");
|
||||
if (m->role == CST_ROLE_ASSISTANT && m->content && m->content[0]) {
|
||||
char *html = cst_web_md_html(m->content, strlen(m->content));
|
||||
cJSON *calls = NULL;
|
||||
char *prose = NULL;
|
||||
cst_protocol_t proto;
|
||||
if (m->role == CST_ROLE_ASSISTANT && m->tool_calls_json &&
|
||||
m->tool_calls_json[0])
|
||||
calls = native_calls(m->tool_calls_json);
|
||||
else if (m->role == CST_ROLE_ASSISTANT && m->content && m->content[0] &&
|
||||
msg_protocol(m, &proto) == 0 && cst_protocol_is_text(proto))
|
||||
prose = split_reply(m->content, proto, &calls);
|
||||
if (prose)
|
||||
cJSON_AddStringToObject(o, "text", prose);
|
||||
if (calls)
|
||||
cJSON_AddItemToObject(o, "calls", calls);
|
||||
const char *shown = prose ? prose : m->content;
|
||||
if (m->role == CST_ROLE_ASSISTANT && shown && shown[0]) {
|
||||
char *html = cst_web_md_html(shown, strlen(shown));
|
||||
if (html) {
|
||||
cJSON_AddStringToObject(o, "html", html);
|
||||
free(html);
|
||||
}
|
||||
}
|
||||
free(prose);
|
||||
if (cst_message_is_tool_result(m)) {
|
||||
const char *body;
|
||||
size_t blen;
|
||||
int ex;
|
||||
tool_out_view(m->content ? m->content : "", &body, &blen, &ex);
|
||||
size_t cap = blen > 32 * 1024 ? cst_utf8_floor(body, 32 * 1024) : blen;
|
||||
char *out = malloc(cap + 1);
|
||||
if (out) {
|
||||
memcpy(out, body, cap);
|
||||
out[cap] = 0;
|
||||
cJSON_AddStringToObject(o, "out", out);
|
||||
free(out);
|
||||
}
|
||||
cJSON_AddBoolToObject(o, "truncated", cap < blen);
|
||||
cJSON_AddNumberToObject(o, "exit", ex);
|
||||
}
|
||||
if (m->reasoning && m->reasoning[0])
|
||||
cJSON_AddStringToObject(o, "reasoning", m->reasoning);
|
||||
if (cst_message_is_note(m))
|
||||
cJSON_AddBoolToObject(o, "note", 1);
|
||||
if (cst_message_is_tool_result(m))
|
||||
cJSON_AddBoolToObject(o, "tool_result", 1);
|
||||
if (m->tool_call_id[0])
|
||||
cJSON_AddStringToObject(o, "tool_call_id", m->tool_call_id);
|
||||
if (m->tool_name[0])
|
||||
@@ -870,9 +1035,14 @@ static void *wui_turn_main(void *arg) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int wui_start_turn(wui_t *w, const char *text, char *err, size_t err_sz) {
|
||||
/* claimed: the caller holds op_busy (wui_claim); it passes to the turn, and is
|
||||
* released either way. */
|
||||
static int wui_start_turn(wui_t *w, const char *text, int claimed, char *err,
|
||||
size_t err_sz) {
|
||||
pthread_mutex_lock(&w->mu);
|
||||
if (w->busy || w->op_busy) {
|
||||
if (claimed)
|
||||
w->op_busy = 0;
|
||||
else if (w->busy || w->op_busy) {
|
||||
pthread_mutex_unlock(&w->mu);
|
||||
snprintf(err, err_sz, w->busy ? "a turn is already running"
|
||||
: "another change is running");
|
||||
@@ -931,7 +1101,7 @@ static int h_turn(cst_http_conn_t *c, void *ud) {
|
||||
return cst_http_send_err(c, 400, "expected {\"text\": \"...\"}");
|
||||
}
|
||||
char err[256] = "";
|
||||
int rc = wui_start_turn(w, text->valuestring, err, sizeof(err));
|
||||
int rc = wui_start_turn(w, text->valuestring, 0, err, sizeof(err));
|
||||
cJSON_Delete(body);
|
||||
if (rc != 0)
|
||||
return cst_http_send_err(c, 409, err);
|
||||
@@ -1146,6 +1316,9 @@ static int h_workspace_x(cst_http_conn_t *c, void *ud) {
|
||||
cJSON_Delete(j);
|
||||
if (rc != 0)
|
||||
return cst_http_send_err(c, 400, err[0] ? err : "cannot switch workspace");
|
||||
/* MCP servers, plugins and the task tool are bound to a workspace */
|
||||
cst_extensions_unload();
|
||||
cst_extensions_load(cst_agent_config(w->agent), NULL, 0);
|
||||
char id[128] = "";
|
||||
if (cst_agent_new_session(w->agent, id, sizeof(id)) != 0)
|
||||
return cst_http_send_err(c, 500, "cannot start a session there");
|
||||
@@ -1178,6 +1351,151 @@ static int h_session_switch_x(cst_http_conn_t *c, void *ud) {
|
||||
"{\"ok\":true}");
|
||||
}
|
||||
|
||||
/* Like wui_claim but a running turn is fine: for work that reads the session
|
||||
* object (a turn only appends to it, under its lock) or the store. */
|
||||
static int wui_claim_read(wui_t *w) {
|
||||
pthread_mutex_lock(&w->mu);
|
||||
int ok = !w->op_busy;
|
||||
if (ok)
|
||||
w->op_busy = 1;
|
||||
pthread_mutex_unlock(&w->mu);
|
||||
return ok ? 0 : -1;
|
||||
}
|
||||
|
||||
/* POST /api/sessions/rename {"id"?: "...", "title": "..."}; "" clears it. */
|
||||
static int h_session_rename(cst_http_conn_t *c, void *ud) {
|
||||
wui_t *w = ud;
|
||||
cJSON *j = wui_body(c);
|
||||
const cJSON *id = j ? cJSON_GetObjectItem(j, "id") : NULL;
|
||||
const cJSON *title = j ? cJSON_GetObjectItem(j, "title") : NULL;
|
||||
if (!cJSON_IsString(title) || (id && !cJSON_IsString(id))) {
|
||||
cJSON_Delete(j);
|
||||
return cst_http_send_err(c, 400, "需要 {\"title\": \"…\", \"id\": 可选}");
|
||||
}
|
||||
if (wui_claim_read(w) != 0) {
|
||||
cJSON_Delete(j);
|
||||
return cst_http_send_err(c, 409, "another change is running");
|
||||
}
|
||||
const cst_agent_config_t *cfg = cst_agent_config(w->agent);
|
||||
char sid[128], t[121];
|
||||
if (id && id->valuestring[0])
|
||||
snprintf(sid, sizeof(sid), "%s", id->valuestring);
|
||||
else
|
||||
cst_agent_session_id(w->agent, sid, sizeof(sid));
|
||||
size_t tn = strlen(title->valuestring);
|
||||
if (tn > sizeof(t) - 1)
|
||||
tn = cst_utf8_floor(title->valuestring, sizeof(t) - 1);
|
||||
memcpy(t, title->valuestring, tn);
|
||||
t[tn] = 0;
|
||||
cJSON_Delete(j);
|
||||
cst_session_info_t in;
|
||||
int rc = cst_agent_store_info(cfg->workspace_root, sid, &in) != 0 ? 404
|
||||
: cst_agent_store_title_set(cfg->workspace_root, sid, t) != 0 ? 500
|
||||
: 200;
|
||||
wui_release(w);
|
||||
if (rc == 404)
|
||||
return cst_http_send_err(c, 404, "no such session");
|
||||
if (rc == 500)
|
||||
return cst_http_send_err(c, 500, "cannot set the title");
|
||||
return cst_http_send_str(c, 200, "application/json; charset=utf-8",
|
||||
"{\"ok\":true}");
|
||||
}
|
||||
|
||||
/* POST /api/sessions/delete {"id": "..."}. Deleting the open session first
|
||||
* moves to a new one. */
|
||||
static int h_session_delete_x(cst_http_conn_t *c, void *ud) {
|
||||
wui_t *w = ud;
|
||||
cJSON *j = wui_body(c);
|
||||
const cJSON *id = j ? cJSON_GetObjectItem(j, "id") : NULL;
|
||||
if (!cJSON_IsString(id) || !id->valuestring[0]) {
|
||||
cJSON_Delete(j);
|
||||
return cst_http_send_err(c, 400, "需要 {\"id\": \"…\"}");
|
||||
}
|
||||
char victim[128];
|
||||
snprintf(victim, sizeof(victim), "%s", id->valuestring);
|
||||
cJSON_Delete(j);
|
||||
const cst_agent_config_t *cfg = cst_agent_config(w->agent);
|
||||
cst_session_info_t in;
|
||||
if (cst_agent_store_info(cfg->workspace_root, victim, &in) != 0)
|
||||
return cst_http_send_err(c, 404, "no such session");
|
||||
char cur[128];
|
||||
cst_agent_session_id(w->agent, cur, sizeof(cur));
|
||||
int was_current = strcmp(cur, victim) == 0;
|
||||
if (was_current) {
|
||||
if (cst_agent_new_session(w->agent, cur, sizeof(cur)) != 0)
|
||||
return cst_http_send_err(c, 500, "cannot start a session");
|
||||
wui_emit_str(w, "session", "id", cur);
|
||||
wui_emit_state(w);
|
||||
}
|
||||
if (cst_agent_store_delete(cfg->workspace_root, victim) != 0)
|
||||
return cst_http_send_err(c, 500, "cannot delete the session");
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
if (o) {
|
||||
cJSON_AddBoolToObject(o, "ok", 1);
|
||||
cJSON_AddStringToObject(o, "current", cur);
|
||||
}
|
||||
return wui_send_obj(c, 200, o);
|
||||
}
|
||||
|
||||
/* POST /api/retry {"text"?: "..."}: drop the last prompt and everything after
|
||||
* it, then run it again, or text in its place. File changes are kept. */
|
||||
static int h_retry(cst_http_conn_t *c, void *ud) {
|
||||
wui_t *w = ud;
|
||||
cJSON *j = wui_body(c);
|
||||
const cJSON *text = j ? cJSON_GetObjectItem(j, "text") : NULL;
|
||||
if (text && (!cJSON_IsString(text) || !text->valuestring[0])) {
|
||||
cJSON_Delete(j);
|
||||
return cst_http_send_err(c, 400, "text 不能为空");
|
||||
}
|
||||
if (wui_claim(w) != 0) {
|
||||
cJSON_Delete(j);
|
||||
return cst_http_send_err(c, 409, "busy: wait for the running turn");
|
||||
}
|
||||
char *prompt = cst_agent_rewind_last_turn(w->agent);
|
||||
if (!prompt) {
|
||||
wui_release(w);
|
||||
cJSON_Delete(j);
|
||||
return cst_http_send_err(c, 409, "nothing to retry");
|
||||
}
|
||||
const char *use = text ? text->valuestring : prompt;
|
||||
char err[256] = "";
|
||||
int rc = wui_start_turn(w, use, 1, err, sizeof(err));
|
||||
cJSON *o = rc == 0 ? cJSON_CreateObject() : NULL;
|
||||
if (o) {
|
||||
cJSON_AddBoolToObject(o, "ok", 1);
|
||||
cJSON_AddStringToObject(o, "text", use);
|
||||
}
|
||||
free(prompt);
|
||||
cJSON_Delete(j);
|
||||
if (rc != 0) {
|
||||
cJSON_Delete(o);
|
||||
return cst_http_send_err(c, 500, err);
|
||||
}
|
||||
return wui_send_obj(c, 202, o);
|
||||
}
|
||||
|
||||
/* GET /api/export: the open session as Markdown. */
|
||||
static int h_export(cst_http_conn_t *c, void *ud) {
|
||||
wui_t *w = ud;
|
||||
if (wui_claim_read(w) != 0)
|
||||
return cst_http_send_err(c, 409, "another change is running");
|
||||
const cst_agent_config_t *cfg = cst_agent_config(w->agent);
|
||||
char sid[128];
|
||||
cst_agent_session_id(w->agent, sid, sizeof(sid));
|
||||
cst_session_info_t in;
|
||||
const char *title = sid;
|
||||
if (cst_agent_store_info(cfg->workspace_root, sid, &in) == 0)
|
||||
title = in.title[0] ? in.title : in.first_user[0] ? in.first_user : sid;
|
||||
char *md = cst_session_export_md(cst_agent_session(w->agent), title, sid,
|
||||
cfg->model);
|
||||
wui_release(w);
|
||||
if (!md)
|
||||
return cst_http_send_err(c, 500, "out of memory");
|
||||
int rc = cst_http_send_str(c, 200, "text/markdown; charset=utf-8", md);
|
||||
free(md);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static int h_config(cst_http_conn_t *c, void *ud) {
|
||||
wui_t *w = ud;
|
||||
cst_agent_config_t *cfg = cst_agent_config_mut(w->agent);
|
||||
@@ -1192,6 +1510,7 @@ static int h_config(cst_http_conn_t *c, void *ud) {
|
||||
const cJSON *effort = cJSON_GetObjectItem(j, "effort");
|
||||
const cJSON *approval = cJSON_GetObjectItem(j, "approval");
|
||||
const cJSON *plan = cJSON_GetObjectItem(j, "plan_mode");
|
||||
const cJSON *think = cJSON_GetObjectItem(j, "show_thinking");
|
||||
const char *bad = NULL;
|
||||
if (cJSON_GetObjectItem(j, "provider"))
|
||||
bad = "切换提供商请用 POST /api/provider";
|
||||
@@ -1206,6 +1525,8 @@ static int h_config(cst_http_conn_t *c, void *ud) {
|
||||
bad = "审批模式只能是 ask / auto / yolo";
|
||||
if (!bad && plan && !cJSON_IsBool(plan))
|
||||
bad = "plan_mode 必须是 true 或 false";
|
||||
if (!bad && think && !cJSON_IsBool(think))
|
||||
bad = "show_thinking 必须是 true 或 false";
|
||||
if (bad) {
|
||||
cJSON_Delete(j);
|
||||
return cst_http_send_err(c, 400, bad);
|
||||
@@ -1231,6 +1552,12 @@ static int h_config(cst_http_conn_t *c, void *ud) {
|
||||
}
|
||||
if (plan)
|
||||
cfg->plan_mode = cJSON_IsTrue(plan) ? 1 : 0;
|
||||
if (think) {
|
||||
pthread_mutex_lock(&w->mu);
|
||||
w->show_thinking = cJSON_IsTrue(think) ? 1 : 0;
|
||||
pthread_mutex_unlock(&w->mu);
|
||||
cst_settings_remember_thinking(cJSON_IsTrue(think));
|
||||
}
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
if (o) {
|
||||
cJSON_AddBoolToObject(o, "ok", 1);
|
||||
@@ -1245,6 +1572,262 @@ static int h_config(cst_http_conn_t *c, void *ud) {
|
||||
return wui_send_obj(c, 200, o);
|
||||
}
|
||||
|
||||
/* GET /api/diff[?path=rel]: git status and the working-tree diff. git runs
|
||||
* without a shell, external diff drivers or textconv, for at most 10 s. */
|
||||
#define WUI_DIFF_CAP (512u * 1024u)
|
||||
static int h_diff(cst_http_conn_t *c, void *ud) {
|
||||
wui_t *w = ud;
|
||||
char path[1024] = "";
|
||||
cst_http_query(cst_http_request(c), "path", path, sizeof(path));
|
||||
char ws[sizeof(((cst_agent_config_t *)0)->workspace_root)];
|
||||
pthread_mutex_lock(&w->mu);
|
||||
int ok = !w->op_busy;
|
||||
if (ok)
|
||||
snprintf(ws, sizeof(ws), "%s", cst_agent_config(w->agent)->workspace_root);
|
||||
pthread_mutex_unlock(&w->mu);
|
||||
if (!ok)
|
||||
return cst_http_send_err(c, 409, "another change is running");
|
||||
os_set_run_timeout(10);
|
||||
os_run_result_t st, df;
|
||||
memset(&st, 0, sizeof(st));
|
||||
memset(&df, 0, sizeof(df));
|
||||
const char *sv[] = {"git", "status", "--short", "--untracked-files=all", NULL};
|
||||
int git = os_run_capture(ws, sv, 64 * 1024, &st) == 0 && st.exit_code == 0;
|
||||
const char *dv[] = {"git", "--no-pager", "diff", "--no-color",
|
||||
"--no-ext-diff", "--no-textconv", "--",
|
||||
path[0] ? path : NULL, NULL};
|
||||
if (git)
|
||||
os_run_capture(ws, dv, WUI_DIFF_CAP, &df);
|
||||
os_set_run_timeout(0);
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
if (o) {
|
||||
cJSON_AddBoolToObject(o, "git", git);
|
||||
cJSON_AddStringToObject(o, "path", path);
|
||||
if (git) {
|
||||
cJSON *files = cJSON_AddArrayToObject(o, "files");
|
||||
const char *s = st.out ? st.out : "";
|
||||
while (files && *s) {
|
||||
size_t n = strcspn(s, "\n");
|
||||
if (n > 3) {
|
||||
char code[3] = {s[0], s[1], 0}, name[1024];
|
||||
snprintf(name, sizeof(name), "%.*s", (int)(n - 3), s + 3);
|
||||
cJSON *f = cJSON_CreateObject();
|
||||
if (f) {
|
||||
cJSON_AddStringToObject(f, "status", code);
|
||||
cJSON_AddStringToObject(f, "path", name);
|
||||
cJSON_AddItemToArray(files, f);
|
||||
}
|
||||
}
|
||||
s += n + (s[n] == '\n');
|
||||
}
|
||||
size_t dl = df.out ? cst_utf8_floor(df.out, df.out_len) : 0;
|
||||
char *d = malloc(dl + 1);
|
||||
if (d) {
|
||||
memcpy(d, df.out ? df.out : "", dl);
|
||||
d[dl] = 0;
|
||||
cJSON_AddStringToObject(o, "diff", d);
|
||||
free(d);
|
||||
}
|
||||
cJSON_AddBoolToObject(o, "truncated", df.out_len >= WUI_DIFF_CAP);
|
||||
if (df.exit_code != 0 && df.exit_code != 1)
|
||||
cJSON_AddNumberToObject(o, "exit", df.exit_code);
|
||||
}
|
||||
}
|
||||
os_run_result_free(&st);
|
||||
os_run_result_free(&df);
|
||||
return wui_send_obj(c, 200, o);
|
||||
}
|
||||
|
||||
static const char *const wui_perm_names[] = {"read", "write", "shell",
|
||||
"net", "ask", "meta"};
|
||||
|
||||
/* GET /api/settings: what POST /api/settings can change, plus the sandbox. */
|
||||
static int h_settings_get(cst_http_conn_t *c, void *ud) {
|
||||
wui_t *w = ud;
|
||||
cst_agent_config_t *cfg = malloc(sizeof(*cfg));
|
||||
if (!cfg)
|
||||
return cst_http_send_err(c, 500, "out of memory");
|
||||
/* held to the end: an extensions reload frees the tools listed below */
|
||||
if (wui_claim_read(w) != 0) {
|
||||
free(cfg);
|
||||
return cst_http_send_err(c, 409, "正在切换设置,稍后再试");
|
||||
}
|
||||
*cfg = *cst_agent_config(w->agent);
|
||||
memset(cfg->api_key, 0, sizeof(cfg->api_key));
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
if (!o) {
|
||||
wui_release(w);
|
||||
free(cfg);
|
||||
return cst_http_send_err(c, 500, "out of memory");
|
||||
}
|
||||
cJSON_AddStringToObject(o, "protocol", cst_protocol_name(cfg->protocol));
|
||||
cJSON *ps = cJSON_AddArrayToObject(o, "protocols");
|
||||
for (int p = CST_PROTO_NATIVE_EOS; ps && p <= CST_PROTO_XML; p++)
|
||||
cJSON_AddItemToArray(ps, cJSON_CreateString(cst_protocol_name(p)));
|
||||
cJSON_AddNumberToObject(o, "max_steps", cfg->max_steps);
|
||||
cJSON_AddStringToObject(o, "tool_profile",
|
||||
cst_tool_profile_name(cfg->tool_profile));
|
||||
cJSON *pr = cJSON_AddArrayToObject(o, "profiles");
|
||||
for (int p = CST_TOOLS_DEFAULT; pr && p <= CST_TOOLS_MINIMAL; p++)
|
||||
cJSON_AddItemToArray(pr, cJSON_CreateString(cst_tool_profile_name(p)));
|
||||
cJSON *cf = cJSON_AddObjectToObject(o, "confirm");
|
||||
if (cf) {
|
||||
cJSON_AddBoolToObject(cf, "shell", cfg->confirm_shell);
|
||||
cJSON_AddBoolToObject(cf, "write", cfg->confirm_write);
|
||||
cJSON_AddBoolToObject(cf, "net", cfg->confirm_net);
|
||||
}
|
||||
cJSON_AddStringToObject(o, "approval", cst_approval_name(cfg->approval));
|
||||
cJSON *tools = cJSON_AddArrayToObject(o, "tools");
|
||||
for (int i = 0; tools && i < cst_tool_count(); i++) {
|
||||
const cst_tool_def_t *d = cst_tool_at(i);
|
||||
cJSON *t = d ? cJSON_CreateObject() : NULL;
|
||||
if (!t)
|
||||
continue;
|
||||
const char *desc = d->desc ? d->desc : "";
|
||||
char first[160];
|
||||
size_t dl = strcspn(desc, ".\n");
|
||||
dl = cst_utf8_floor(desc, dl < sizeof(first) - 1 ? dl : sizeof(first) - 1);
|
||||
memcpy(first, desc, dl);
|
||||
first[dl] = 0;
|
||||
cJSON_AddStringToObject(t, "name", d->name);
|
||||
cJSON_AddStringToObject(t, "perm", (int)d->perm < 6 ? wui_perm_names[d->perm]
|
||||
: "?");
|
||||
cJSON_AddBoolToObject(t, "on", cst_tool_active(cfg, d));
|
||||
cJSON_AddBoolToObject(t, "off", cst_tool_disabled(cfg, d->name));
|
||||
cJSON_AddStringToObject(t, "desc", first);
|
||||
if (d->owner)
|
||||
cJSON_AddStringToObject(t, "owner", d->owner);
|
||||
cJSON_AddItemToArray(tools, t);
|
||||
}
|
||||
wui_release(w);
|
||||
int sk = cst_sandbox_probe();
|
||||
cJSON *sb = cJSON_AddObjectToObject(o, "sandbox");
|
||||
if (sb) {
|
||||
cJSON_AddStringToObject(sb, "kind", cst_sandbox_name(sk));
|
||||
cJSON_AddBoolToObject(sb, "active", sk != CST_SANDBOX_NONE);
|
||||
char *l = cst_sandbox_labels();
|
||||
if (l)
|
||||
cJSON_AddStringToObject(sb, "labels", l);
|
||||
free(l);
|
||||
}
|
||||
free(cfg);
|
||||
return wui_send_obj(c, 200, o);
|
||||
}
|
||||
|
||||
/* POST /api/settings {protocol?, max_steps?, tool_profile?,
|
||||
* tools?: {name: on}, confirm?: {shell?, write?, net?}}. All checked before
|
||||
* any of it applies; this run only, like the TUI commands. */
|
||||
static int h_settings_x(cst_http_conn_t *c, void *ud) {
|
||||
wui_t *w = ud;
|
||||
cst_agent_config_t *cfg = cst_agent_config_mut(w->agent);
|
||||
cJSON *j = wui_body(c);
|
||||
if (!cJSON_IsObject(j)) {
|
||||
cJSON_Delete(j);
|
||||
return cst_http_send_err(c, 400, "expected a JSON object");
|
||||
}
|
||||
const cJSON *proto = cJSON_GetObjectItem(j, "protocol");
|
||||
const cJSON *steps = cJSON_GetObjectItem(j, "max_steps");
|
||||
const cJSON *prof = cJSON_GetObjectItem(j, "tool_profile");
|
||||
const cJSON *tools = cJSON_GetObjectItem(j, "tools");
|
||||
const cJSON *conf = cJSON_GetObjectItem(j, "confirm");
|
||||
cst_protocol_t p = cfg->protocol;
|
||||
int profile = cfg->tool_profile;
|
||||
char bad[200] = "";
|
||||
if (proto && (!cJSON_IsString(proto) ||
|
||||
cst_protocol_parse(proto->valuestring, &p) != 0))
|
||||
snprintf(bad, sizeof(bad), "未知的工具协议");
|
||||
else if (steps && (!cJSON_IsNumber(steps) || steps->valuedouble < 1 ||
|
||||
steps->valuedouble > 1000))
|
||||
snprintf(bad, sizeof(bad), "max_steps 要在 1 到 1000 之间");
|
||||
else if (prof && (!cJSON_IsString(prof) ||
|
||||
cst_tool_profile_parse(prof->valuestring, &profile) != 0))
|
||||
snprintf(bad, sizeof(bad), "未知的工具集(default 或 minimal)");
|
||||
else if (tools && !cJSON_IsObject(tools))
|
||||
snprintf(bad, sizeof(bad), "tools 要是 {\"名字\": true/false}");
|
||||
else if (conf && !cJSON_IsObject(conf))
|
||||
snprintf(bad, sizeof(bad), "confirm 要是 {shell, write, net}");
|
||||
for (const cJSON *t = tools ? tools->child : NULL; !bad[0] && t; t = t->next)
|
||||
if (!cJSON_IsBool(t) || cst_tool_index(t->string) < 0)
|
||||
snprintf(bad, sizeof(bad), "未知的工具或取值:%.80s", t->string);
|
||||
for (const cJSON *t = conf ? conf->child : NULL; !bad[0] && t; t = t->next)
|
||||
if (!cJSON_IsBool(t) || (strcmp(t->string, "shell") != 0 &&
|
||||
strcmp(t->string, "write") != 0 &&
|
||||
strcmp(t->string, "net") != 0))
|
||||
snprintf(bad, sizeof(bad), "未知的确认项:%.80s", t->string);
|
||||
if (bad[0]) {
|
||||
cJSON_Delete(j);
|
||||
return cst_http_send_err(c, 400, bad);
|
||||
}
|
||||
cfg->protocol = p;
|
||||
cfg->tool_profile = profile;
|
||||
if (steps)
|
||||
cfg->max_steps = (int)steps->valuedouble;
|
||||
for (const cJSON *t = tools ? tools->child : NULL; t; t = t->next)
|
||||
cst_tool_set_disabled(cfg, t->string, cJSON_IsFalse(t));
|
||||
for (const cJSON *t = conf ? conf->child : NULL; t; t = t->next) {
|
||||
int on = cJSON_IsTrue(t);
|
||||
if (strcmp(t->string, "shell") == 0)
|
||||
cfg->confirm_shell = on;
|
||||
else if (strcmp(t->string, "write") == 0)
|
||||
cfg->confirm_write = on;
|
||||
else
|
||||
cfg->confirm_net = on;
|
||||
}
|
||||
cJSON_Delete(j);
|
||||
wui_emit_state(w);
|
||||
return cst_http_send_str(c, 200, "application/json; charset=utf-8",
|
||||
"{\"ok\":true}");
|
||||
}
|
||||
|
||||
/* GET /api/permissions: the saved always-allow rules, read from disk (the
|
||||
* agent's copy is the turn's to change). */
|
||||
static int h_perms_get(cst_http_conn_t *c, void *ud) {
|
||||
wui_t *w = ud;
|
||||
char ws[sizeof(((cst_agent_config_t *)0)->workspace_root)];
|
||||
pthread_mutex_lock(&w->mu);
|
||||
int ok = !w->op_busy;
|
||||
if (ok)
|
||||
snprintf(ws, sizeof(ws), "%s", cst_agent_config(w->agent)->workspace_root);
|
||||
pthread_mutex_unlock(&w->mu);
|
||||
if (!ok)
|
||||
return cst_http_send_err(c, 409, "another change is running");
|
||||
cst_perms_t *p = cst_perms_open(ws);
|
||||
char *d = p ? cst_perms_describe(p) : NULL;
|
||||
cst_perms_close(p);
|
||||
char path[sizeof(ws) + 32], *raw = NULL;
|
||||
size_t rn = 0;
|
||||
snprintf(path, sizeof(path), "%s/.agent/permissions.json", ws);
|
||||
cJSON *saved = read_file_all(path, &raw, &rn) == 0
|
||||
? cJSON_ParseWithLength(raw, rn)
|
||||
: NULL;
|
||||
free(raw);
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
if (o) {
|
||||
cJSON_AddStringToObject(o, "rules", d ? d : "");
|
||||
cJSON_AddStringToObject(o, "file", ".agent/permissions.json");
|
||||
static const char *const keys[] = {"tools", "shell_prefixes"};
|
||||
for (int k = 0; k < 2; k++) {
|
||||
cJSON *arr = cJSON_AddArrayToObject(o, keys[k]);
|
||||
const cJSON *it;
|
||||
cJSON_ArrayForEach(it, cJSON_GetObjectItemCaseSensitive(saved, keys[k]))
|
||||
if (arr && cJSON_IsString(it))
|
||||
cJSON_AddItemToArray(arr, cJSON_CreateString(it->valuestring));
|
||||
}
|
||||
}
|
||||
cJSON_Delete(saved);
|
||||
free(d);
|
||||
return wui_send_obj(c, 200, o);
|
||||
}
|
||||
|
||||
/* DELETE /api/permissions: forget every saved rule. */
|
||||
static int h_perms_reset_x(cst_http_conn_t *c, void *ud) {
|
||||
wui_t *w = ud;
|
||||
if (cst_perms_reset(cst_agent_perms(w->agent)) != 0)
|
||||
return cst_http_send_err(c, 500, "cannot reset the rules");
|
||||
return cst_http_send_str(c, 200, "application/json; charset=utf-8",
|
||||
"{\"ok\":true}");
|
||||
}
|
||||
|
||||
/* GET /api/providers: the presets, whether each has a key and from where.
|
||||
* Never the key itself. */
|
||||
static int h_providers(cst_http_conn_t *c, void *ud) {
|
||||
@@ -1483,9 +2066,223 @@ static int h_attach_clear_x(cst_http_conn_t *c, void *ud) {
|
||||
"{\"ok\":true}");
|
||||
}
|
||||
|
||||
/* --- /init, /skill and extensions ------------------------------------------ */
|
||||
|
||||
static int wui_turn_reply(cst_http_conn_t *c, const char *text) {
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
if (o) {
|
||||
cJSON_AddBoolToObject(o, "ok", 1);
|
||||
cJSON_AddStringToObject(o, "text", text);
|
||||
}
|
||||
return wui_send_obj(c, 202, o);
|
||||
}
|
||||
|
||||
/* POST /api/init: a turn that has the agent write AGENTS.md. */
|
||||
static int h_init(cst_http_conn_t *c, void *ud) {
|
||||
wui_t *w = ud;
|
||||
char err[256] = "";
|
||||
if (wui_start_turn(w, cst_init_prompt(), 0, err, sizeof(err)) != 0)
|
||||
return cst_http_send_err(c, 409, err);
|
||||
return wui_turn_reply(c, cst_init_prompt());
|
||||
}
|
||||
|
||||
/* POST /api/skill {"name": "...", "task"?: "..."}: a turn with the skill's
|
||||
* instructions inlined before the task. */
|
||||
static int h_skill(cst_http_conn_t *c, void *ud) {
|
||||
wui_t *w = ud;
|
||||
cJSON *j = wui_body(c);
|
||||
const cJSON *name = j ? cJSON_GetObjectItem(j, "name") : NULL;
|
||||
const cJSON *task = j ? cJSON_GetObjectItem(j, "task") : NULL;
|
||||
if (!cJSON_IsString(name) || !name->valuestring[0] ||
|
||||
strlen(name->valuestring) > 63 || (task && !cJSON_IsString(task))) {
|
||||
cJSON_Delete(j);
|
||||
return cst_http_send_err(c, 400, "需要 {\"name\": \"skill 名\", \"task\": 可选}");
|
||||
}
|
||||
if (wui_claim(w) != 0) {
|
||||
cJSON_Delete(j);
|
||||
return cst_http_send_err(c, 409, "busy: wait for the running turn");
|
||||
}
|
||||
char *p = cst_skill_prompt(cst_agent_config(w->agent)->workspace_root,
|
||||
name->valuestring, task ? task->valuestring : "");
|
||||
cJSON_Delete(j);
|
||||
if (!p) {
|
||||
wui_release(w);
|
||||
return cst_http_send_err(c, 404, "no such skill");
|
||||
}
|
||||
char err[256] = "";
|
||||
int rc = wui_start_turn(w, p, 1, err, sizeof(err));
|
||||
rc = rc == 0 ? wui_turn_reply(c, p) : cst_http_send_err(c, 500, err);
|
||||
free(p);
|
||||
return rc;
|
||||
}
|
||||
|
||||
static const char *wui_mcp_state(int st) {
|
||||
switch (st) {
|
||||
case CST_MCP_STARTING: return "starting";
|
||||
case CST_MCP_READY: return "ready";
|
||||
case CST_MCP_FAILED: return "failed";
|
||||
case CST_MCP_EXITED: return "exited";
|
||||
default: return "off";
|
||||
}
|
||||
}
|
||||
|
||||
#define WUI_RUNS_MAX 64
|
||||
|
||||
/* GET /api/extensions: MCP servers, plugins, skills, sub-agent roles and the
|
||||
* open session's sub-agent runs. */
|
||||
static int h_extensions(cst_http_conn_t *c, void *ud) {
|
||||
wui_t *w = ud;
|
||||
if (wui_claim_read(w) != 0)
|
||||
return cst_http_send_err(c, 409, "another change is running");
|
||||
const cst_agent_config_t *cfg = cst_agent_config(w->agent);
|
||||
const char *ws = cfg->workspace_root;
|
||||
char sid[128];
|
||||
cst_agent_session_id(w->agent, sid, sizeof(sid));
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
cst_mcp_info_t *mi = calloc(CST_MCP_MAX, sizeof(*mi));
|
||||
cst_plugin_info_t *pi = calloc(CST_PLUGINS_MAX, sizeof(*pi));
|
||||
cst_skill_t *sk = calloc(CST_SKILLS_MAX, sizeof(*sk));
|
||||
cst_agent_role_t *ro = calloc(CST_ROLES_MAX, sizeof(*ro));
|
||||
cst_subagent_rec_t *ru = calloc(WUI_RUNS_MAX, sizeof(*ru));
|
||||
if (!o || !mi || !pi || !sk || !ro || !ru) {
|
||||
wui_release(w);
|
||||
cJSON_Delete(o);
|
||||
free(mi);
|
||||
free(pi);
|
||||
free(sk);
|
||||
free(ro);
|
||||
free(ru);
|
||||
return cst_http_send_err(c, 500, "out of memory");
|
||||
}
|
||||
cJSON_AddBoolToObject(o, "enabled", cfg->tool_profile != CST_TOOLS_MINIMAL);
|
||||
cJSON_AddBoolToObject(o, "trusted", cst_settings_trusted(ws));
|
||||
cJSON_AddStringToObject(o, "report", cst_extensions_report());
|
||||
cJSON_AddBoolToObject(o, "task_tool", cst_tool_find("task") != NULL);
|
||||
|
||||
cJSON *a = cJSON_AddArrayToObject(o, "mcp");
|
||||
int n = cst_mcp_info(mi, CST_MCP_MAX);
|
||||
for (int i = 0; a && i < n; i++) {
|
||||
cJSON *e = cJSON_CreateObject();
|
||||
if (!e)
|
||||
continue;
|
||||
cJSON_AddStringToObject(e, "name", mi[i].name);
|
||||
cJSON_AddStringToObject(e, "source", mi[i].source);
|
||||
cJSON_AddStringToObject(e, "command", mi[i].command);
|
||||
cJSON_AddStringToObject(e, "state", wui_mcp_state(mi[i].state));
|
||||
cJSON_AddNumberToObject(e, "tools", mi[i].ntools);
|
||||
cJSON_AddStringToObject(e, "error", mi[i].error);
|
||||
cJSON_AddItemToArray(a, e);
|
||||
}
|
||||
a = cJSON_AddArrayToObject(o, "plugins");
|
||||
n = cst_plugins_info(pi, CST_PLUGINS_MAX);
|
||||
for (int i = 0; a && i < n; i++) {
|
||||
cJSON *e = cJSON_CreateObject();
|
||||
if (!e)
|
||||
continue;
|
||||
cJSON_AddStringToObject(e, "name", pi[i].name);
|
||||
cJSON_AddStringToObject(e, "source", pi[i].source);
|
||||
cJSON_AddStringToObject(e, "desc", pi[i].desc);
|
||||
cJSON_AddNumberToObject(e, "tools", pi[i].ntools);
|
||||
cJSON_AddNumberToObject(e, "mcp", pi[i].nmcp);
|
||||
cJSON_AddBoolToObject(e, "skills", pi[i].nskills_root);
|
||||
cJSON_AddStringToObject(e, "error", pi[i].error);
|
||||
cJSON_AddItemToArray(a, e);
|
||||
}
|
||||
a = cJSON_AddArrayToObject(o, "skills");
|
||||
n = cst_skills_list(ws, sk, CST_SKILLS_MAX);
|
||||
for (int i = 0; a && i < n; i++) {
|
||||
cJSON *e = cJSON_CreateObject();
|
||||
if (!e)
|
||||
continue;
|
||||
cJSON_AddStringToObject(e, "name", sk[i].name);
|
||||
cJSON_AddStringToObject(e, "source", sk[i].source);
|
||||
cJSON_AddStringToObject(e, "desc", sk[i].desc);
|
||||
cJSON_AddItemToArray(a, e);
|
||||
}
|
||||
a = cJSON_AddArrayToObject(o, "roles");
|
||||
n = cst_roles_list(ws, ro, CST_ROLES_MAX);
|
||||
for (int i = 0; a && i < n; i++) {
|
||||
cJSON *e = cJSON_CreateObject();
|
||||
if (!e)
|
||||
continue;
|
||||
cJSON_AddStringToObject(e, "name", ro[i].name);
|
||||
cJSON_AddStringToObject(e, "source", ro[i].source);
|
||||
cJSON_AddStringToObject(e, "desc", ro[i].desc);
|
||||
cJSON_AddStringToObject(e, "model", ro[i].model);
|
||||
cJSON_AddBoolToObject(e, "read_only", ro[i].read_only);
|
||||
cJSON_AddItemToArray(a, e);
|
||||
}
|
||||
cst_roles_free(ro, n);
|
||||
a = cJSON_AddArrayToObject(o, "runs");
|
||||
n = cst_subagent_list(ws, sid, ru, WUI_RUNS_MAX);
|
||||
for (int i = 0; a && i < n; i++) {
|
||||
cJSON *e = cJSON_CreateObject();
|
||||
if (!e)
|
||||
continue;
|
||||
cJSON_AddStringToObject(e, "file", ru[i].file);
|
||||
cJSON_AddStringToObject(e, "role", ru[i].role);
|
||||
cJSON_AddStringToObject(e, "desc", ru[i].desc);
|
||||
cJSON_AddStringToObject(e, "status", ru[i].status);
|
||||
cJSON_AddNumberToObject(e, "steps", ru[i].steps);
|
||||
cJSON_AddNumberToObject(e, "tools", ru[i].tools);
|
||||
cJSON_AddNumberToObject(e, "ms", ru[i].ms);
|
||||
cJSON_AddItemToArray(a, e);
|
||||
}
|
||||
wui_release(w);
|
||||
free(mi);
|
||||
free(pi);
|
||||
free(sk);
|
||||
free(ro);
|
||||
free(ru);
|
||||
return wui_send_obj(c, 200, o);
|
||||
}
|
||||
|
||||
static int wui_ext_reply(wui_t *w, cst_http_conn_t *c) {
|
||||
char rep[256];
|
||||
cst_extensions_unload();
|
||||
cst_extensions_load(cst_agent_config(w->agent), rep, sizeof(rep));
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
if (o) {
|
||||
cJSON_AddBoolToObject(o, "ok", 1);
|
||||
cJSON_AddBoolToObject(
|
||||
o, "trusted",
|
||||
cst_settings_trusted(cst_agent_config(w->agent)->workspace_root));
|
||||
cJSON_AddStringToObject(o, "report", rep);
|
||||
}
|
||||
return wui_send_obj(c, 200, o);
|
||||
}
|
||||
|
||||
/* POST /api/extensions/reload: stop MCP servers and plugins, read them and
|
||||
* the skills again. */
|
||||
static int h_ext_reload_x(cst_http_conn_t *c, void *ud) {
|
||||
return wui_ext_reply(ud, c);
|
||||
}
|
||||
|
||||
/* POST /api/extensions/trust {"on": bool}: whether .agent/plugins and
|
||||
* .agent/mcp.json of this workspace load, then reload. */
|
||||
static int h_ext_trust_x(cst_http_conn_t *c, void *ud) {
|
||||
wui_t *w = ud;
|
||||
cJSON *j = wui_body(c);
|
||||
const cJSON *on = j ? cJSON_GetObjectItem(j, "on") : NULL;
|
||||
if (!cJSON_IsBool(on)) {
|
||||
cJSON_Delete(j);
|
||||
return cst_http_send_err(c, 400, "需要 {\"on\": true 或 false}");
|
||||
}
|
||||
int want = cJSON_IsTrue(on);
|
||||
cJSON_Delete(j);
|
||||
if (cst_settings_trust(cst_agent_config(w->agent)->workspace_root, want) != 0)
|
||||
return cst_http_send_err(c, 500, "cannot save the settings");
|
||||
return wui_ext_reply(w, c);
|
||||
}
|
||||
|
||||
WUI_EXCLUSIVE(h_session_new)
|
||||
WUI_EXCLUSIVE(h_workspace)
|
||||
WUI_EXCLUSIVE(h_session_switch)
|
||||
WUI_EXCLUSIVE(h_session_delete)
|
||||
WUI_EXCLUSIVE(h_settings)
|
||||
WUI_EXCLUSIVE(h_perms_reset)
|
||||
WUI_EXCLUSIVE(h_ext_reload)
|
||||
WUI_EXCLUSIVE(h_ext_trust)
|
||||
WUI_EXCLUSIVE(h_undo)
|
||||
WUI_EXCLUSIVE(h_compact)
|
||||
WUI_EXCLUSIVE(h_attach_upload)
|
||||
@@ -1678,6 +2475,11 @@ int cst_webui_run(cst_agent_t *agent, const cst_webui_opts_t *opts) {
|
||||
w->agent = agent;
|
||||
w->perm_timeout_s = opts->perm_timeout_s;
|
||||
w->root = opts->root;
|
||||
{
|
||||
cst_settings_t st;
|
||||
cst_settings_load(&st);
|
||||
w->show_thinking = st.show_thinking;
|
||||
}
|
||||
pthread_cond_init(&w->cv, NULL);
|
||||
snprintf(w->host, sizeof(w->host), "%s",
|
||||
opts->host && opts->host[0] ? opts->host : "127.0.0.1");
|
||||
@@ -1706,6 +2508,20 @@ int cst_webui_run(cst_agent_t *agent, const cst_webui_opts_t *opts) {
|
||||
{"GET", "/api/sessions", h_sessions, NULL, 0, 0},
|
||||
{"POST", "/api/sessions/new", h_session_new, NULL, 0, 0},
|
||||
{"POST", "/api/sessions/switch", h_session_switch, NULL, 0, 0},
|
||||
{"POST", "/api/sessions/rename", h_session_rename, NULL, 0, 0},
|
||||
{"POST", "/api/sessions/delete", h_session_delete, NULL, 0, 0},
|
||||
{"POST", "/api/retry", h_retry, NULL, 0, 0},
|
||||
{"GET", "/api/export", h_export, NULL, 0, 0},
|
||||
{"GET", "/api/diff", h_diff, NULL, 0, 0},
|
||||
{"GET", "/api/settings", h_settings_get, NULL, 0, 0},
|
||||
{"POST", "/api/settings", h_settings, NULL, 0, 0},
|
||||
{"GET", "/api/permissions", h_perms_get, NULL, 0, 0},
|
||||
{"DELETE", "/api/permissions", h_perms_reset, NULL, 0, 0},
|
||||
{"POST", "/api/init", h_init, NULL, 0, 0},
|
||||
{"POST", "/api/skill", h_skill, NULL, 0, 0},
|
||||
{"GET", "/api/extensions", h_extensions, NULL, 0, 0},
|
||||
{"POST", "/api/extensions/reload", h_ext_reload, NULL, 0, 0},
|
||||
{"POST", "/api/extensions/trust", h_ext_trust, NULL, 0, 0},
|
||||
{"POST", "/api/workspace", h_workspace, NULL, 0, 0},
|
||||
{"POST", "/api/turn", h_turn, NULL, 0, 0},
|
||||
{"POST", "/api/cancel", h_cancel, NULL, 0, 0},
|
||||
@@ -1773,7 +2589,7 @@ int cst_webui_run(cst_agent_t *agent, const cst_webui_opts_t *opts) {
|
||||
|
||||
if (opts->prompt && opts->prompt[0]) {
|
||||
char err2[256] = "";
|
||||
if (wui_start_turn(w, opts->prompt, err2, sizeof(err2)) != 0)
|
||||
if (wui_start_turn(w, opts->prompt, 0, err2, sizeof(err2)) != 0)
|
||||
fprintf(stderr, "webui: %s\n", err2);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user