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:
WHC
2026-10-10 13:57:17 +08:00
parent 2d1052fd5f
commit 7e1dbd9afa
17 changed files with 2978 additions and 146 deletions
+12 -7
View File
@@ -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
View File
@@ -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);
+60
View File
@@ -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);
}
+10
View File
@@ -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);
+11 -51
View File
@@ -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
View File
@@ -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);
}