v0.8.6: general mode, clipboard text, PDF text extraction
Working mode: --mode coding|general (CST_MODE, settings.json, /mode, WebUI run settings). general swaps the default framing for a general assistant one -- workflow, safety notes, tool wording (run_shell, the subagents) -- and /status shows it; minimal (the paper profile) stays byte-frozen. The prompt state carries the mode so a switch leaves a system note in history. Clipboard: clipboard_read / clipboard_write tools (16 MiB cap read, default 64 KiB window; writing is a WRITE permission with a preview) on pbcopy / wl-copy / xclip / pbpaste / wl-paste / xclip and the Win32 clipboard. In the TUI, /attach paste falls back to clipboard text when there is no image on it. PDF: a dependency-free text extractor (src/pdf_text.c): inflate, Flate/ASCIIHex/ASCII85, PNG/TIFF predictors, layout-independent object scan, object streams, page tree walk with inherited resources, content lexer (Tj/TJ/'/" and the Td/TD/T*/Tm spacing heuristics), per-font ToUnicode CMaps and inline images skipped. Encrypted files and pages with no text layer are refused with the reason. Attachments: /attach (a path or files dropped on the terminal) now take images, PDFs and text. A PDF is stored as its text layer with the mime still saying application/pdf and its page count; anything else has to be UTF-8 text. Documents travel inline as text parts, so CST_NO_IMAGES and text-only models no longer drop them, while images keep their old path. Tests: test_pdf_text (plain, bare and zlib-framed Flate, inline image, CID + ToUnicode, object stream, error paths), documents in test_attach, inlined documents in test_llm_body, mode in test_config.
This commit is contained in:
+108
-6
@@ -177,8 +177,62 @@ static int tool_on(const cst_agent_config_t *cfg, const char *name) {
|
||||
return cst_tool_active(cfg, cst_tool_find(name));
|
||||
}
|
||||
|
||||
/* /mode general (cst_mode_name covers "" and unknown values). */
|
||||
static int general_mode(const cst_agent_config_t *cfg) {
|
||||
return cfg && strcmp(cst_mode_name(cfg->mode), "general") == 0;
|
||||
}
|
||||
|
||||
/* Workflow for /mode general: gather, then do the work, then check it. */
|
||||
static void put_workflow_general(cst_buf_t *b, const cst_agent_config_t *cfg) {
|
||||
cst_buf_puts(b, "\nWorkflow:\n1) Gather what you need");
|
||||
const char *explore[] = {"list_dir", "glob", "grep"};
|
||||
int n = 0;
|
||||
for (int i = 0; i < 3; i++) {
|
||||
if (tool_on(cfg, explore[i]))
|
||||
cst_buf_printf(b, "%s%s", n++ ? ", " : " (", explore[i]);
|
||||
}
|
||||
if (n)
|
||||
cst_buf_puts(b, ")");
|
||||
if (tool_on(cfg, "read_file"))
|
||||
cst_buf_puts(b, "; read big files in slices with read_file offset/limit, "
|
||||
"and use the documents the user attached");
|
||||
cst_buf_puts(b, ".\n2) Do the work");
|
||||
n = 0;
|
||||
if (tool_on(cfg, "write_file")) {
|
||||
cst_buf_puts(b, ": write_file for new files");
|
||||
n++;
|
||||
}
|
||||
if (tool_on(cfg, "edit_file"))
|
||||
cst_buf_printf(b, "%sedit_file for small exact replacements",
|
||||
n++ ? ", " : ": ");
|
||||
if (tool_on(cfg, "apply_patch"))
|
||||
cst_buf_printf(b, "%sapply_patch for multi-hunk diffs to existing files",
|
||||
n++ ? ", " : ": ");
|
||||
if (tool_on(cfg, "run_shell"))
|
||||
cst_buf_printf(b, "%srun_shell for commands, computations, data "
|
||||
"processing and other CLIs",
|
||||
n++ ? ", " : ": ");
|
||||
cst_buf_puts(b, ".\n");
|
||||
if (tool_on(cfg, "run_shell"))
|
||||
cst_buf_puts(b, "3) Check the result before saying it works (re-read it, "
|
||||
"run it, or verify the numbers); fix and re-run until it "
|
||||
"holds.\n");
|
||||
if (tool_on(cfg, "todo_write"))
|
||||
cst_buf_puts(b, "- For work with several steps keep a todo list with "
|
||||
"todo_write and update it as you go.\n");
|
||||
if (tool_on(cfg, "ask_user"))
|
||||
cst_buf_puts(b, "- Use ask_user only when a decision is truly the user's; "
|
||||
"otherwise choose sensibly and say so.\n");
|
||||
cst_buf_puts(b, "Finish with a short plain-text summary of the result.\n"
|
||||
"Never invent other tool names.\n");
|
||||
}
|
||||
|
||||
/* Workflow section for the default tool profile; names only active tools. */
|
||||
static void put_workflow(cst_buf_t *b, const cst_agent_config_t *cfg) {
|
||||
if (general_mode(cfg)) {
|
||||
put_workflow_general(b, cfg);
|
||||
return;
|
||||
}
|
||||
cst_buf_puts(b, "\nWorkflow:\n1) Inspect only what you need");
|
||||
const char *explore[] = {"list_dir", "glob", "grep"};
|
||||
int n = 0;
|
||||
@@ -235,6 +289,25 @@ static const char DEF_INTRO[] =
|
||||
"could not verify, say so.\n"
|
||||
"- Keep going until the task is done or truly blocked.\n\n";
|
||||
|
||||
/* Same role, framed for /mode general: the workspace may hold documents,
|
||||
* data or anything else, not just code. */
|
||||
static const char DEF_INTRO_GENERAL[] =
|
||||
"You are CST-Agent, a general-purpose assistant working in the user's "
|
||||
"local workspace. All tool paths are relative to the workspace root and "
|
||||
"shell commands run there; the material you work on may be code, "
|
||||
"documents, data or anything else.\n\n"
|
||||
"Principles:\n"
|
||||
"- Reply in the user's language. Be concise: lead with the result, no "
|
||||
"filler.\n"
|
||||
"- Understand before answering: read the files, documents and sources the "
|
||||
"task refers to instead of guessing; never invent content, numbers or "
|
||||
"tool results.\n"
|
||||
"- Do what was asked, at the scope asked: leave unrelated files, data and "
|
||||
"material alone.\n"
|
||||
"- Check your work when you can (re-read it, run it, cross-check the "
|
||||
"source); if you could not verify something, say so.\n"
|
||||
"- Keep going until the task is done or truly blocked.\n\n";
|
||||
|
||||
static void put_tool_use(cst_buf_t *b, const cst_agent_config_t *cfg) {
|
||||
cst_buf_puts(b, "\nTool use:\n");
|
||||
if (tool_on(cfg, "run_shell") &&
|
||||
@@ -248,15 +321,26 @@ static void put_tool_use(cst_buf_t *b, const cst_agent_config_t *cfg) {
|
||||
cst_buf_puts(b, "- Read a file before editing it and again after a failed "
|
||||
"edit. The `N|` prefixes in read_file output are not part "
|
||||
"of the file.\n");
|
||||
if (general_mode(cfg) && tool_on(cfg, "web_fetch"))
|
||||
cst_buf_puts(b, "- Use web_fetch for pages the user points you to; say so "
|
||||
"when a page could not be fetched instead of guessing its "
|
||||
"content.\n");
|
||||
cst_buf_puts(b, "- When a call fails, read the error and change approach "
|
||||
"instead of repeating the same call.\n");
|
||||
}
|
||||
|
||||
static void put_safety(cst_buf_t *b, const cst_agent_config_t *cfg) {
|
||||
cst_buf_puts(b, "\nSafety:\n"
|
||||
"- Do not commit, push or rewrite git history unless asked. "
|
||||
"Do not delete or overwrite files you did not create unless "
|
||||
"the task requires it. Never print secrets or API keys.\n");
|
||||
if (general_mode(cfg))
|
||||
cst_buf_puts(b, "\nSafety:\n"
|
||||
"- Do not delete or overwrite files or data you did not "
|
||||
"create unless the task requires it, and do not send the "
|
||||
"user's content anywhere the task did not ask for. Never "
|
||||
"print secrets or API keys.\n");
|
||||
else
|
||||
cst_buf_puts(b, "\nSafety:\n"
|
||||
"- Do not commit, push or rewrite git history unless asked. "
|
||||
"Do not delete or overwrite files you did not create unless "
|
||||
"the task requires it. Never print secrets or API keys.\n");
|
||||
if (tool_on(cfg, "run_shell"))
|
||||
cst_buf_puts(b, "- run_shell may run in a sandbox (writes only inside the "
|
||||
"workspace and temp dir; the network works). If a command is "
|
||||
@@ -365,7 +449,9 @@ char *cst_agent_build_prompt(const cst_agent_config_t *cfg, cst_protocol_t p,
|
||||
const char *project_notes) {
|
||||
int minimal = !cfg || cfg->tool_profile == CST_TOOLS_MINIMAL;
|
||||
cst_buf_t b = {0};
|
||||
cst_buf_puts(&b, minimal ? PROMPT_BASE : DEF_INTRO);
|
||||
cst_buf_puts(&b, minimal ? PROMPT_BASE
|
||||
: general_mode(cfg) ? DEF_INTRO_GENERAL
|
||||
: DEF_INTRO);
|
||||
if (cst_protocol_is_native(p))
|
||||
cst_buf_puts(&b, minimal ? SEC_NATIVE
|
||||
: "Call tools through the native "
|
||||
@@ -2241,11 +2327,12 @@ int cst_agent_undo_available(const cst_agent_t *a) {
|
||||
* later changes reach the model as persisted notes appended to the history,
|
||||
* which keeps the cached prefix intact. */
|
||||
|
||||
#define PROMPT_VERSION 2
|
||||
#define PROMPT_VERSION 3
|
||||
|
||||
typedef struct {
|
||||
int plan;
|
||||
char approval[96];
|
||||
char mode[16];
|
||||
char branch[128];
|
||||
char day[16];
|
||||
char notes[24]; /* hash of AGENTS.md, "" = none */
|
||||
@@ -2255,6 +2342,7 @@ static void state_now(const cst_agent_t *a, pstate_t *s) {
|
||||
memset(s, 0, sizeof(*s));
|
||||
s->plan = a->cfg.plan_mode;
|
||||
snprintf(s->approval, sizeof(s->approval), "%s", approval_desc(&a->cfg));
|
||||
snprintf(s->mode, sizeof(s->mode), "%s", cst_mode_name(a->cfg.mode));
|
||||
git_branch(a->cfg.workspace_root, s->branch, sizeof(s->branch));
|
||||
today(s->day, sizeof(s->day));
|
||||
if (a->notes && a->notes[0])
|
||||
@@ -2267,6 +2355,7 @@ static cJSON *state_json(const pstate_t *s) {
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(o, "plan", s->plan);
|
||||
cJSON_AddStringToObject(o, "approval", s->approval);
|
||||
cJSON_AddStringToObject(o, "mode", s->mode);
|
||||
cJSON_AddStringToObject(o, "branch", s->branch);
|
||||
cJSON_AddStringToObject(o, "day", s->day);
|
||||
cJSON_AddStringToObject(o, "notes", s->notes);
|
||||
@@ -2282,6 +2371,7 @@ static void state_parse(const cJSON *o, pstate_t *s) {
|
||||
if (cJSON_IsString(v)) \
|
||||
snprintf(s->field, sizeof(s->field), "%s", v->valuestring);
|
||||
STATE_STR("approval", approval)
|
||||
STATE_STR("mode", mode)
|
||||
STATE_STR("branch", branch)
|
||||
STATE_STR("day", day)
|
||||
STATE_STR("notes", notes)
|
||||
@@ -2293,6 +2383,7 @@ static char *system_meta(const cst_agent_t *a, const pstate_t *s) {
|
||||
cJSON_AddStringToObject(o, "protocol", cst_protocol_name(a->cfg.protocol));
|
||||
cJSON_AddNumberToObject(o, "pv", PROMPT_VERSION);
|
||||
cJSON_AddNumberToObject(o, "tools", a->cfg.tool_profile);
|
||||
cJSON_AddStringToObject(o, "mode", cst_mode_name(a->cfg.mode));
|
||||
cJSON_AddItemToObject(o, "state", state_json(s));
|
||||
char *out = cJSON_PrintUnformatted(o);
|
||||
cJSON_Delete(o);
|
||||
@@ -2312,10 +2403,13 @@ static int system_current(const cst_message_t *m, const cst_agent_config_t *c) {
|
||||
const cJSON *pv = cJSON_GetObjectItemCaseSensitive(meta, "pv");
|
||||
const cJSON *pr = cJSON_GetObjectItemCaseSensitive(meta, "protocol");
|
||||
const cJSON *tp = cJSON_GetObjectItemCaseSensitive(meta, "tools");
|
||||
const cJSON *md = cJSON_GetObjectItemCaseSensitive(meta, "mode");
|
||||
int ok = cJSON_IsNumber(pv) && pv->valueint == PROMPT_VERSION &&
|
||||
cJSON_IsString(pr) &&
|
||||
strcmp(pr->valuestring, cst_protocol_name(c->protocol)) == 0 &&
|
||||
cJSON_IsNumber(tp) && tp->valueint == c->tool_profile;
|
||||
ok = ok && cJSON_IsString(md) &&
|
||||
strcmp(md->valuestring, cst_mode_name(c->mode)) == 0;
|
||||
cJSON_Delete(meta);
|
||||
return ok;
|
||||
}
|
||||
@@ -2393,6 +2487,14 @@ static void sync_notes(cst_agent_t *a) {
|
||||
"is available again.\n");
|
||||
if (strcmp(now.approval, was.approval) != 0)
|
||||
cst_buf_printf(&b, "[cst] Approval mode is now %s.\n", now.approval);
|
||||
if (strcmp(now.mode, was.mode) != 0)
|
||||
cst_buf_printf(&b,
|
||||
"[cst] Working mode is now %s%s. Earlier instructions about "
|
||||
"how to frame the work no longer apply.\n",
|
||||
now.mode,
|
||||
strcmp(now.mode, "general") == 0
|
||||
? " (general: documents, data, research)"
|
||||
: " (coding: repo, build, tests)");
|
||||
if (strcmp(now.branch, was.branch) != 0)
|
||||
cst_buf_printf(&b, "[cst] The git branch is now %s.\n",
|
||||
now.branch[0] ? now.branch : "(none)");
|
||||
|
||||
+129
-24
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "cJSON.h"
|
||||
#include "cst_attach.h"
|
||||
#include "cst_pdf.h"
|
||||
#include "cst_util.h"
|
||||
#include "os_compat.h"
|
||||
|
||||
@@ -102,6 +103,9 @@ static int le16(const unsigned char *p) {
|
||||
}
|
||||
|
||||
static const char *mime_of_ext(const char *mime) {
|
||||
if (strcmp(mime, "application/pdf") == 0 ||
|
||||
strcmp(mime, "text/plain") == 0)
|
||||
return "txt"; /* a document is stored as the text it was reduced to */
|
||||
if (strcmp(mime, "image/png") == 0)
|
||||
return "png";
|
||||
if (strcmp(mime, "image/jpeg") == 0)
|
||||
@@ -352,23 +356,73 @@ static void make_id(const char *name, const char *data, size_t n, char *out,
|
||||
snprintf(out, out_sz, "img-%08x", (unsigned)(h & 0xFFFFFFFFu));
|
||||
}
|
||||
|
||||
/* n bytes, NUL-terminated: a text payload is handed around as a C string. */
|
||||
static char *memdup_z(const char *data, size_t n) {
|
||||
char *copy = malloc(n + 1);
|
||||
if (!copy)
|
||||
return NULL;
|
||||
memcpy(copy, data, n);
|
||||
copy[n] = 0;
|
||||
return copy;
|
||||
}
|
||||
|
||||
/* What a picked file is, plus the payload to store for it: an image stays as
|
||||
* it is, a PDF is reduced to its text layer (the mime still says PDF, the
|
||||
* stored file holds text), anything else must be UTF-8 text. The caller frees
|
||||
* the result; *out_len is its length. */
|
||||
static char *prepare(const char *data, size_t n, char *mime, size_t mime_sz,
|
||||
int *pages, size_t *out_len, char *err, size_t err_sz) {
|
||||
*pages = 0;
|
||||
*out_len = n;
|
||||
if (cst_attach_sniff(data, n, mime, mime_sz) == 0) {
|
||||
if (n > CST_ATTACH_MAX_BYTES) {
|
||||
snprintf(err, err_sz, "image is %.1f MB; the limit is %d MB",
|
||||
(double)n / (1024 * 1024), (int)(CST_ATTACH_MAX_BYTES >> 20));
|
||||
return NULL;
|
||||
}
|
||||
char *copy = memdup_z(data, n);
|
||||
if (!copy)
|
||||
snprintf(err, err_sz, "out of memory");
|
||||
return copy;
|
||||
}
|
||||
if (n >= 4 && memcmp(data, "%PDF", 4) == 0) {
|
||||
int pg = 0;
|
||||
char perr[256];
|
||||
char *text = cst_pdf_text(data, n, &pg, perr, sizeof(perr));
|
||||
if (!text) {
|
||||
snprintf(err, err_sz, "cannot read the PDF: %.200s", perr);
|
||||
return NULL;
|
||||
}
|
||||
snprintf(mime, mime_sz, "application/pdf");
|
||||
*pages = pg;
|
||||
*out_len = strlen(text);
|
||||
return text;
|
||||
}
|
||||
if (n > CST_ATTACH_TEXT_MAX) {
|
||||
snprintf(err, err_sz, "text is %.1f MB; the limit is %d MB",
|
||||
(double)n / (1024 * 1024), (int)(CST_ATTACH_TEXT_MAX >> 20));
|
||||
return NULL;
|
||||
}
|
||||
if (memchr(data, 0, n) || !cst_utf8_valid(data, n)) {
|
||||
snprintf(err, err_sz,
|
||||
"not an image (PNG, JPEG, GIF, WebP or BMP), a PDF or UTF-8 "
|
||||
"text; the content decides, not the extension");
|
||||
return NULL;
|
||||
}
|
||||
snprintf(mime, mime_sz, "text/plain");
|
||||
char *copy = memdup_z(data, n);
|
||||
if (!copy)
|
||||
snprintf(err, err_sz, "out of memory");
|
||||
return copy;
|
||||
}
|
||||
|
||||
/* Write one payload whose kind is already known. */
|
||||
static cst_attachment_t *store(const char *session_path, const char *name,
|
||||
const char *data, size_t n, const char *origin,
|
||||
char *err, size_t err_sz) {
|
||||
const char *data, size_t n, const char *mime,
|
||||
int pages, const char *origin, char *err,
|
||||
size_t err_sz) {
|
||||
if (!name || !name[0])
|
||||
name = "image";
|
||||
char mime[32];
|
||||
if (cst_attach_sniff(data, n, mime, sizeof(mime)) != 0) {
|
||||
snprintf(err, err_sz,
|
||||
"not a supported image (PNG, JPEG, GIF, WebP or BMP; "
|
||||
"checked by content, not by extension)");
|
||||
return NULL;
|
||||
}
|
||||
if (n > CST_ATTACH_MAX_BYTES) {
|
||||
snprintf(err, err_sz, "image is %.1f MB; the limit is %d MB",
|
||||
(double)n / (1024 * 1024), (int)(CST_ATTACH_MAX_BYTES >> 20));
|
||||
return NULL;
|
||||
}
|
||||
char dir[OS_PATH_MAX];
|
||||
if (cst_attach_dir(session_path, dir, sizeof(dir)) != 0) {
|
||||
snprintf(err, err_sz, "cannot create the attachment directory");
|
||||
@@ -386,7 +440,9 @@ static cst_attachment_t *store(const char *session_path, const char *name,
|
||||
snprintf(a->origin, sizeof(a->origin), "%s", origin ? origin : "user");
|
||||
snprintf(a->detail, sizeof(a->detail), "auto");
|
||||
a->bytes = (long long)n;
|
||||
cst_attach_dimensions(data, n, &a->width, &a->height);
|
||||
a->pages = pages;
|
||||
if (cst_attach_is_image(a))
|
||||
cst_attach_dimensions(data, n, &a->width, &a->height);
|
||||
|
||||
char full[OS_PATH_MAX];
|
||||
if (cst_attach_full_path(session_path, a, full, sizeof(full)) != 0) {
|
||||
@@ -419,14 +475,26 @@ cst_attachment_t *cst_attach_import_bytes(const char *session_path,
|
||||
}
|
||||
err[0] = 0;
|
||||
if (!session_path || !session_path[0]) {
|
||||
snprintf(err, err_sz, "no session to store the image in");
|
||||
snprintf(err, err_sz, "no session to store the file in");
|
||||
return NULL;
|
||||
}
|
||||
if (!data || !n) {
|
||||
snprintf(err, err_sz, "image is empty");
|
||||
snprintf(err, err_sz, "the file is empty");
|
||||
return NULL;
|
||||
}
|
||||
return store(session_path, name, data, n, "user", err, err_sz);
|
||||
char mime[32];
|
||||
if (cst_attach_sniff(data, n, mime, sizeof(mime)) != 0) {
|
||||
snprintf(err, err_sz,
|
||||
"not a supported image (PNG, JPEG, GIF, WebP or BMP; "
|
||||
"checked by content, not by extension)");
|
||||
return NULL;
|
||||
}
|
||||
if (n > CST_ATTACH_MAX_BYTES) {
|
||||
snprintf(err, err_sz, "image is %.1f MB; the limit is %d MB",
|
||||
(double)n / (1024 * 1024), (int)(CST_ATTACH_MAX_BYTES >> 20));
|
||||
return NULL;
|
||||
}
|
||||
return store(session_path, name, data, n, mime, 0, "user", err, err_sz);
|
||||
}
|
||||
|
||||
cst_attachment_t *cst_attach_import_file(const char *session_path,
|
||||
@@ -449,11 +517,30 @@ cst_attachment_t *cst_attach_import_file(const char *session_path,
|
||||
for (const char *p = src_path; *p; p++)
|
||||
if (*p == '/' || *p == '\\')
|
||||
leaf = p + 1;
|
||||
cst_attachment_t *a = store(session_path, leaf, data, n, "user", err, err_sz);
|
||||
char mime[32];
|
||||
int pages = 0;
|
||||
size_t pn = 0;
|
||||
char *payload =
|
||||
prepare(data, n, mime, sizeof(mime), &pages, &pn, err, err_sz);
|
||||
free(data);
|
||||
if (!payload)
|
||||
return NULL;
|
||||
cst_attachment_t *a =
|
||||
store(session_path, leaf, payload, pn, mime, pages, "user", err, err_sz);
|
||||
free(payload);
|
||||
return a;
|
||||
}
|
||||
|
||||
/* Images are opaque and travel as image parts; everything else is text the
|
||||
* store holds and the request inlines. */
|
||||
int cst_attach_is_image(const cst_attachment_t *a) {
|
||||
return a && strncmp(a->mime, "image/", 6) == 0;
|
||||
}
|
||||
|
||||
int cst_attach_is_text(const cst_attachment_t *a) {
|
||||
return a && !cst_attach_is_image(a);
|
||||
}
|
||||
|
||||
char *cst_attach_read(const char *session_path, const cst_attachment_t *a,
|
||||
size_t *out_len, char *err, size_t err_sz) {
|
||||
char dummy[8];
|
||||
@@ -470,11 +557,12 @@ char *cst_attach_read(const char *session_path, const cst_attachment_t *a,
|
||||
char *data = read_whole(full, out_len, CST_ATTACH_MAX_BYTES);
|
||||
if (!data) {
|
||||
snprintf(err, err_sz,
|
||||
"%.200s is missing; the image was deleted from the session "
|
||||
"store",
|
||||
"%.200s is missing; it was deleted from the session store",
|
||||
a->name);
|
||||
return NULL;
|
||||
}
|
||||
if (!cst_attach_is_image(a)) /* a document is text, nothing to verify */
|
||||
return data;
|
||||
char mime[32];
|
||||
if (cst_attach_sniff(data, *out_len, mime, sizeof(mime)) != 0) {
|
||||
snprintf(err, err_sz, "%.200s in the session store is no longer an image",
|
||||
@@ -486,6 +574,8 @@ char *cst_attach_read(const char *session_path, const cst_attachment_t *a,
|
||||
}
|
||||
|
||||
char *cst_attach_data_url(const char *session_path, const cst_attachment_t *a) {
|
||||
if (!cst_attach_is_image(a)) /* a document travels as text, not as a URL */
|
||||
return NULL;
|
||||
char err[256];
|
||||
size_t n = 0;
|
||||
char *data = cst_attach_read(session_path, a, &n, err, sizeof(err));
|
||||
@@ -509,10 +599,21 @@ void cst_attach_describe(const cst_attachment_t *a, char *out, size_t out_sz) {
|
||||
snprintf(out, out_sz, "(none)");
|
||||
return;
|
||||
}
|
||||
const char *kind = strncmp(a->mime, "image/", 6) == 0 ? a->mime + 6 : a->mime;
|
||||
const char *kind = a->mime;
|
||||
char dim[32] = "";
|
||||
if (a->width > 0 && a->height > 0)
|
||||
snprintf(dim, sizeof(dim), "%dx%d ", a->width, a->height);
|
||||
if (cst_attach_is_image(a)) {
|
||||
if (strncmp(a->mime, "image/", 6) == 0)
|
||||
kind = a->mime + 6;
|
||||
if (a->width > 0 && a->height > 0)
|
||||
snprintf(dim, sizeof(dim), "%dx%d ", a->width, a->height);
|
||||
} else if (strcmp(a->mime, "application/pdf") == 0) {
|
||||
kind = "pdf";
|
||||
if (a->pages > 0)
|
||||
snprintf(dim, sizeof(dim), "%d page%s ", a->pages,
|
||||
a->pages == 1 ? "" : "s");
|
||||
} else if (strcmp(a->mime, "text/plain") == 0 || !a->mime[0]) {
|
||||
kind = "text";
|
||||
}
|
||||
out[0] = 0;
|
||||
if (a->bytes >= 1024 * 1024)
|
||||
snprintf(out, out_sz, "%s%s %.1f MB", dim, kind,
|
||||
@@ -580,6 +681,8 @@ char *cst_attachments_to_json(const cst_attachment_t *list, int n) {
|
||||
cJSON_AddNumberToObject(o, "width", a->width);
|
||||
cJSON_AddNumberToObject(o, "height", a->height);
|
||||
}
|
||||
if (a->pages > 0)
|
||||
cJSON_AddNumberToObject(o, "pages", a->pages);
|
||||
if (a->detail[0] && strcmp(a->detail, "auto") != 0)
|
||||
cJSON_AddStringToObject(o, "detail", a->detail);
|
||||
if (a->origin[0])
|
||||
@@ -628,6 +731,8 @@ cst_attachment_t *cst_attachments_from_json(const void *array_json, int *n_out)
|
||||
a->width = cJSON_IsNumber(v) ? v->valueint : 0;
|
||||
v = cJSON_GetObjectItemCaseSensitive(it, "height");
|
||||
a->height = cJSON_IsNumber(v) ? v->valueint : 0;
|
||||
v = cJSON_GetObjectItemCaseSensitive(it, "pages");
|
||||
a->pages = cJSON_IsNumber(v) ? v->valueint : 0;
|
||||
v = cJSON_GetObjectItemCaseSensitive(it, "detail");
|
||||
snprintf(a->detail, sizeof(a->detail), "%s",
|
||||
cJSON_IsString(v) ? v->valuestring : "auto");
|
||||
|
||||
+169
@@ -390,6 +390,175 @@ int cst_clipboard_image(char **data, size_t *n, char *err, size_t err_sz) {
|
||||
|
||||
#endif
|
||||
|
||||
/* --- text clipboard ------------------------------------------------------ */
|
||||
|
||||
#ifdef _WIN32
|
||||
|
||||
int cst_clipboard_text(char **data, size_t *n, char *err, size_t err_sz) {
|
||||
*data = NULL;
|
||||
*n = 0;
|
||||
if (!IsClipboardFormatAvailable(CF_UNICODETEXT) || !OpenClipboard(NULL)) {
|
||||
set_err(err, err_sz, "no text on the clipboard");
|
||||
return -1;
|
||||
}
|
||||
HANDLE h = GetClipboardData(CF_UNICODETEXT);
|
||||
const wchar_t *w = h ? GlobalLock(h) : NULL;
|
||||
if (!w) {
|
||||
if (h)
|
||||
GlobalUnlock(h);
|
||||
CloseClipboard();
|
||||
set_err(err, err_sz, "no text on the clipboard");
|
||||
return -1;
|
||||
}
|
||||
char *utf8 = os_wide_to_utf8(w);
|
||||
size_t len = utf8 ? strlen(utf8) : 0;
|
||||
GlobalUnlock(h);
|
||||
CloseClipboard();
|
||||
if (!utf8 || !len) {
|
||||
free(utf8);
|
||||
set_err(err, err_sz, "no text on the clipboard");
|
||||
return -1;
|
||||
}
|
||||
if (len > CST_CLIPBOARD_TEXT_MAX) {
|
||||
free(utf8);
|
||||
set_err(err, err_sz, "clipboard text is too large");
|
||||
return -1;
|
||||
}
|
||||
*data = utf8;
|
||||
*n = len;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cst_clipboard_set_text(const char *text, size_t n, char *err,
|
||||
size_t err_sz) {
|
||||
if (!text || os_clipboard_set(text, n) != 0) {
|
||||
set_err(err, err_sz, "could not put text on the clipboard");
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
#else
|
||||
|
||||
/* stdout of a clipboard reader, capped and NUL-terminated. */
|
||||
static int clip_read(const char *cmd, const char *tool, char **data, size_t *n,
|
||||
char *err, size_t err_sz) {
|
||||
os_run_result_t r;
|
||||
memset(&r, 0, sizeof(r));
|
||||
if (os_run_shell_c(NULL, cmd, CST_CLIPBOARD_TEXT_MAX + 1, &r) != 0) {
|
||||
os_run_result_free(&r);
|
||||
set_err(err, err_sz, "could not read the clipboard");
|
||||
return -1;
|
||||
}
|
||||
if (r.exit_code != 0 || r.out_len == 0) {
|
||||
int missing = r.exit_code == 127;
|
||||
os_run_result_free(&r);
|
||||
if (missing) {
|
||||
char msg[96];
|
||||
snprintf(msg, sizeof(msg), "reading the clipboard needs %s", tool);
|
||||
set_err(err, err_sz, msg);
|
||||
} else {
|
||||
set_err(err, err_sz, "no text on the clipboard");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
if (r.out_len > CST_CLIPBOARD_TEXT_MAX) {
|
||||
os_run_result_free(&r);
|
||||
set_err(err, err_sz, "clipboard text is too large");
|
||||
return -1;
|
||||
}
|
||||
char *copy = malloc(r.out_len + 1);
|
||||
if (!copy) {
|
||||
os_run_result_free(&r);
|
||||
set_err(err, err_sz, "out of memory");
|
||||
return -1;
|
||||
}
|
||||
memcpy(copy, r.out, r.out_len);
|
||||
copy[r.out_len] = 0;
|
||||
os_run_result_free(&r);
|
||||
*data = copy;
|
||||
*n = r.out_len;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int cst_clipboard_text(char **data, size_t *n, char *err, size_t err_sz) {
|
||||
*data = NULL;
|
||||
*n = 0;
|
||||
#ifdef __APPLE__
|
||||
return clip_read("pbpaste 2>/dev/null", "pbpaste", data, n, err, err_sz);
|
||||
#else
|
||||
const char *wl = getenv("WAYLAND_DISPLAY");
|
||||
if (wl && *wl)
|
||||
return clip_read("wl-paste --type text/plain --no-newline 2>/dev/null",
|
||||
"wl-clipboard (wl-paste)", data, n, err, err_sz);
|
||||
const char *x = getenv("DISPLAY");
|
||||
if (x && *x)
|
||||
return clip_read("xclip -selection clipboard -o 2>/dev/null", "xclip",
|
||||
data, n, err, err_sz);
|
||||
set_err(err, err_sz, "no clipboard here (no Wayland or X11 display)");
|
||||
return -1;
|
||||
#endif
|
||||
}
|
||||
|
||||
int cst_clipboard_set_text(const char *text, size_t n, char *err,
|
||||
size_t err_sz) {
|
||||
if (!text)
|
||||
return -1;
|
||||
if (n > CST_CLIPBOARD_TEXT_MAX) {
|
||||
set_err(err, err_sz, "text is too large for the clipboard");
|
||||
return -1;
|
||||
}
|
||||
const char *fmt = NULL, *tool = NULL;
|
||||
#ifdef __APPLE__
|
||||
fmt = "pbcopy < '%s'";
|
||||
tool = "pbcopy";
|
||||
#else
|
||||
if (getenv("WAYLAND_DISPLAY")) {
|
||||
fmt = "wl-copy < '%s'";
|
||||
tool = "wl-clipboard (wl-copy)";
|
||||
} else if (getenv("DISPLAY")) {
|
||||
fmt = "xclip -selection clipboard -i < '%s'";
|
||||
tool = "xclip";
|
||||
}
|
||||
#endif
|
||||
if (!fmt) {
|
||||
set_err(err, err_sz, "no clipboard here (no Wayland or X11 display)");
|
||||
return -1;
|
||||
}
|
||||
char tmp[OS_PATH_MAX];
|
||||
int fd = os_mkstemp(tmp, sizeof(tmp), "cst-clip");
|
||||
if (fd < 0) {
|
||||
set_err(err, err_sz, "could not create a temporary file");
|
||||
return -1;
|
||||
}
|
||||
FILE *fp = fdopen(fd, "wb");
|
||||
int ok = fp && fwrite(text, 1, n, fp) == n && fclose(fp) == 0;
|
||||
if (!ok) {
|
||||
if (fp)
|
||||
fclose(fp);
|
||||
remove(tmp);
|
||||
set_err(err, err_sz, "could not write the text");
|
||||
return -1;
|
||||
}
|
||||
char cmd[OS_PATH_MAX + 64];
|
||||
snprintf(cmd, sizeof(cmd), fmt, tmp);
|
||||
os_run_result_t r;
|
||||
memset(&r, 0, sizeof(r));
|
||||
int rc = os_run_shell_c(NULL, cmd, 4096, &r);
|
||||
int code = r.exit_code;
|
||||
os_run_result_free(&r);
|
||||
remove(tmp);
|
||||
if (rc != 0 || code != 0) {
|
||||
char msg[96];
|
||||
snprintf(msg, sizeof(msg), "writing the clipboard needs %s", tool);
|
||||
set_err(err, err_sz, msg);
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
static int hexval(int c) {
|
||||
if (c >= '0' && c <= '9')
|
||||
return c - '0';
|
||||
|
||||
+90
-23
@@ -255,28 +255,78 @@ static cJSON *image_part(const cst_llm_request_t *req,
|
||||
return p;
|
||||
}
|
||||
|
||||
/* Append every attachment of m as image parts (or a note saying why not). */
|
||||
static void parts_add_images(const cst_llm_request_t *req, cJSON *parts,
|
||||
const cst_message_t *m, size_t *budget) {
|
||||
/* One stored document as a text part: its text, framed so the model knows
|
||||
* where it came from. Documents are plain text on the wire, so a text-only
|
||||
* model and CST_NO_IMAGES both still get them. */
|
||||
static cJSON *document_part(const cst_llm_request_t *req,
|
||||
const cst_attachment_t *a, size_t *budget,
|
||||
char *why, size_t why_sz) {
|
||||
why[0] = 0;
|
||||
if (!req->session_path || !req->session_path[0]) {
|
||||
snprintf(why, why_sz, "there is no session store to read it from");
|
||||
return NULL;
|
||||
}
|
||||
char err[256];
|
||||
size_t n = 0;
|
||||
char *text = cst_attach_read(req->session_path, a, &n, err, sizeof(err));
|
||||
if (!text) {
|
||||
snprintf(why, why_sz, "%s", err);
|
||||
return NULL;
|
||||
}
|
||||
int cut = 0;
|
||||
if (n > *budget) {
|
||||
n = *budget;
|
||||
cut = 1;
|
||||
}
|
||||
*budget -= n;
|
||||
char desc[128];
|
||||
cst_attach_describe(a, desc, sizeof(desc));
|
||||
cst_buf_t b = {0};
|
||||
cst_buf_printf(&b, "[document \"%.120s\" (%s)%s]\n", a->name, desc,
|
||||
cut ? " -- only the start is included" : "");
|
||||
cst_buf_append(&b, text, n);
|
||||
cst_buf_puts(&b, cut ? "\n[-- document truncated --]"
|
||||
: "\n[-- end of document --]");
|
||||
free(text);
|
||||
cJSON *p = cJSON_CreateObject();
|
||||
cJSON_AddStringToObject(p, "type", "text");
|
||||
cJSON_AddStringToObject(p, "text", b.buf ? b.buf : "");
|
||||
cst_buf_free(&b);
|
||||
return p;
|
||||
}
|
||||
|
||||
/* Append every attachment of m: images as image parts (or a note saying why
|
||||
* not), documents inline as text. */
|
||||
static void parts_add_attachments(const cst_llm_request_t *req, cJSON *parts,
|
||||
const cst_message_t *m, size_t *budget,
|
||||
size_t *doc_budget, const char *img_off) {
|
||||
int n = m->nattach;
|
||||
if (n > CST_ATTACH_MAX_COUNT) {
|
||||
char note[160];
|
||||
snprintf(note, sizeof(note), "[%d of %d images sent]", CST_ATTACH_MAX_COUNT,
|
||||
n);
|
||||
snprintf(note, sizeof(note),
|
||||
"[only the first %d of %d attachments are sent]",
|
||||
CST_ATTACH_MAX_COUNT, n);
|
||||
parts_add_text(parts, cJSON_CreateString(note));
|
||||
n = CST_ATTACH_MAX_COUNT;
|
||||
}
|
||||
for (int i = 0; i < n; i++) {
|
||||
const cst_attachment_t *a = &m->attach[i];
|
||||
int is_img = cst_attach_is_image(a);
|
||||
char why[256];
|
||||
cJSON *p = image_part(req, a, budget, why, sizeof(why));
|
||||
cJSON *p = NULL;
|
||||
if (is_img && img_off)
|
||||
snprintf(why, sizeof(why), "%s", img_off);
|
||||
else if (is_img)
|
||||
p = image_part(req, a, budget, why, sizeof(why));
|
||||
else
|
||||
p = document_part(req, a, doc_budget, why, sizeof(why));
|
||||
if (p) {
|
||||
cJSON_AddItemToArray(parts, p);
|
||||
continue;
|
||||
}
|
||||
char note[512];
|
||||
snprintf(note, sizeof(note), "[image \"%.120s\" not sent: %.200s]",
|
||||
a->name, why);
|
||||
snprintf(note, sizeof(note), "[%s \"%.120s\" not sent: %.200s]",
|
||||
is_img ? "image" : "document", a->name, why);
|
||||
parts_add_text(parts, cJSON_CreateString(note));
|
||||
}
|
||||
}
|
||||
@@ -284,8 +334,10 @@ static void parts_add_images(const cst_llm_request_t *req, cJSON *parts,
|
||||
/* Images a tool result produced are deferred: an assistant tool_call must be
|
||||
* answered by tool messages only, so they ride in one extra user message
|
||||
* after the run of tool messages ends. *pending collects it. */
|
||||
static void defer_images(const cst_llm_request_t *req, const cst_message_t *m,
|
||||
cJSON **pending, size_t *budget) {
|
||||
static void defer_attachments(const cst_llm_request_t *req,
|
||||
const cst_message_t *m, cJSON **pending,
|
||||
size_t *budget, size_t *doc_budget,
|
||||
const char *img_off) {
|
||||
cJSON *p = *pending;
|
||||
if (!p) {
|
||||
p = cJSON_CreateObject();
|
||||
@@ -293,15 +345,18 @@ static void defer_images(const cst_llm_request_t *req, const cst_message_t *m,
|
||||
cJSON_AddArrayToObject(p, "content");
|
||||
*pending = p;
|
||||
}
|
||||
parts_add_images(req, cJSON_GetObjectItemCaseSensitive(p, "content"), m,
|
||||
budget);
|
||||
parts_add_attachments(req, cJSON_GetObjectItemCaseSensitive(p, "content"), m,
|
||||
budget, doc_budget, img_off);
|
||||
}
|
||||
|
||||
/* m's text plus one line per image it would have carried (malloc'd). */
|
||||
/* m's text plus one line per image it would have carried (malloc'd); a
|
||||
* document is not affected (it is text and is sent as such). */
|
||||
static char *image_notes(const cst_message_t *m, const char *why) {
|
||||
cst_buf_t b = {0};
|
||||
cst_buf_puts(&b, m->content ? m->content : "");
|
||||
for (int i = 0; i < m->nattach; i++) {
|
||||
if (!cst_attach_is_image(&m->attach[i]))
|
||||
continue;
|
||||
char d[128];
|
||||
cst_attach_describe(&m->attach[i], d, sizeof(d));
|
||||
cst_buf_printf(&b, "%s[image \"%.120s\" (%s) not sent: %s]",
|
||||
@@ -394,6 +449,7 @@ char *cst_llm_build_body(const cst_llm_request_t *req) {
|
||||
cJSON *prev_user = NULL; /* merge consecutive user messages */
|
||||
cJSON *pending = NULL; /* images from tool results, waiting for a break */
|
||||
size_t img_budget = CST_ATTACH_MAX_TOTAL;
|
||||
size_t doc_budget = CST_ATTACH_MAX_TOTAL;
|
||||
const char *noimg = getenv("CST_NO_IMAGES");
|
||||
const char *img_off =
|
||||
noimg && noimg[0] && noimg[0] != '0'
|
||||
@@ -407,15 +463,26 @@ char *cst_llm_build_body(const cst_llm_request_t *req) {
|
||||
cst_role_t role = m->role;
|
||||
if (role == CST_ROLE_TOOL && !m->tool_call_id[0])
|
||||
role = CST_ROLE_USER;
|
||||
int has_img = m->attach && m->nattach > 0;
|
||||
int n_att = m->attach ? m->nattach : 0;
|
||||
int has_img = 0, has_doc = 0;
|
||||
for (int k = 0; k < n_att; k++) {
|
||||
if (cst_attach_is_image(&m->attach[k]))
|
||||
has_img = 1;
|
||||
else
|
||||
has_doc = 1;
|
||||
}
|
||||
char *noted = NULL;
|
||||
if (has_img && img_off) {
|
||||
/* plain text: text-only APIs may refuse content arrays */
|
||||
noted = image_notes(m, img_off);
|
||||
if (noted)
|
||||
mm.content = noted;
|
||||
has_img = 0;
|
||||
if (!has_doc) {
|
||||
/* plain text: text-only APIs may refuse content arrays */
|
||||
noted = image_notes(m, img_off);
|
||||
if (noted)
|
||||
mm.content = noted;
|
||||
}
|
||||
}
|
||||
/* a document needs a content array even when no image can go out */
|
||||
int att_parts = has_img || has_doc;
|
||||
|
||||
/* Every tool_call must be answered by tool messages, so tool-produced
|
||||
* images only leave the queue when the run of tool results is over. */
|
||||
@@ -431,7 +498,7 @@ char *cst_llm_build_body(const cst_llm_request_t *req) {
|
||||
cJSON *pc = prev_user
|
||||
? cJSON_GetObjectItemCaseSensitive(prev_user, "content")
|
||||
: NULL;
|
||||
if (!has_img && !cJSON_IsArray(pc)) {
|
||||
if (!att_parts && !cJSON_IsArray(pc)) {
|
||||
cJSON *c = clean_string(m->content, 0);
|
||||
if (prev_user) {
|
||||
cst_buf_t b = {0};
|
||||
@@ -463,8 +530,8 @@ char *cst_llm_build_body(const cst_llm_request_t *req) {
|
||||
}
|
||||
cJSON *parts = content_parts(o);
|
||||
parts_add_text(parts, clean_string(m->content, 0));
|
||||
if (has_img)
|
||||
parts_add_images(req, parts, m, &img_budget);
|
||||
if (att_parts)
|
||||
parts_add_attachments(req, parts, m, &img_budget, &doc_budget, img_off);
|
||||
free(noted);
|
||||
continue;
|
||||
}
|
||||
@@ -498,8 +565,8 @@ char *cst_llm_build_body(const cst_llm_request_t *req) {
|
||||
cJSON_AddTrueToObject(o, "prefix");
|
||||
}
|
||||
cJSON_AddItemToArray(msgs, o);
|
||||
if (role == CST_ROLE_TOOL && has_img)
|
||||
defer_images(req, m, &pending, &img_budget);
|
||||
if (role == CST_ROLE_TOOL && n_att)
|
||||
defer_attachments(req, m, &pending, &img_budget, &doc_budget, img_off);
|
||||
free(noted);
|
||||
}
|
||||
if (pending)
|
||||
|
||||
+25
-8
@@ -26,6 +26,8 @@ static void usage(const char *argv0) {
|
||||
" (last used)\n"
|
||||
" -m, --model NAME model (last used for the provider)\n"
|
||||
" -e, --effort LEVEL low | medium | high | max (last used)\n"
|
||||
" -M, --mode NAME coding (default, code framed) | general\n"
|
||||
" (documents, data, research; last used)\n"
|
||||
" -w, --workspace DIR project directory (default: current)\n"
|
||||
" -c, --continue reopen the last session (default)\n"
|
||||
" -N, --new start a new session\n"
|
||||
@@ -59,13 +61,13 @@ static void usage(const char *argv0) {
|
||||
"API keys: the TUI asks for a missing key and saves it per provider\n"
|
||||
" in the config dir (credentials, mode 0600); /key changes it.\n"
|
||||
" Fallbacks: <provider env> (DEEPSEEK_API_KEY, ...), CST_API_KEY.\n"
|
||||
"Settings: provider, models and effort are remembered in\n"
|
||||
"Settings: provider, models, effort and mode are remembered in\n"
|
||||
" settings.json next to the credentials.\n"
|
||||
"Env: CST_PROVIDER CST_MODEL CST_EFFORT CST_PROTOCOL CST_TOOLS\n"
|
||||
" CST_CONFIRM CST_MAX_STEPS CST_TOOL_OUT_CAP CST_CTX_WINDOW\n"
|
||||
" CST_CTX_TOKENS CST_AUTO_COMPACT=0 CST_SHELL_TIMEOUT\n"
|
||||
" CST_METRICS=0 CST_RECORD_SSE CST_REPLAY_SSE CST_MODELS\n"
|
||||
" CST_APPROVAL=ask|auto|yolo CST_AUTO_APPROVE=1 (=yolo)\n"
|
||||
"Env: CST_PROVIDER CST_MODEL CST_EFFORT CST_MODE CST_PROTOCOL\n"
|
||||
" CST_TOOLS CST_CONFIRM CST_MAX_STEPS CST_TOOL_OUT_CAP\n"
|
||||
" CST_CTX_WINDOW CST_CTX_TOKENS CST_AUTO_COMPACT=0\n"
|
||||
" CST_SHELL_TIMEOUT CST_METRICS=0 CST_RECORD_SSE CST_REPLAY_SSE\n"
|
||||
" CST_MODELS CST_APPROVAL=ask|auto|yolo CST_AUTO_APPROVE=1 (=yolo)\n"
|
||||
" CST_SANDBOX=off CST_SHELL CST_NO_PROJECT_NOTES=1 NO_COLOR\n"
|
||||
"TUI: type /help inside for commands and keys\n",
|
||||
argv0);
|
||||
@@ -374,6 +376,7 @@ int main(int argc, char **argv) {
|
||||
{"approval", required_argument, 0, 'A'},
|
||||
{"confirm", required_argument, 0, 'C'},
|
||||
{"effort", required_argument, 0, 'e'},
|
||||
{"mode", required_argument, 0, 'M'},
|
||||
{"help", no_argument, 0, 'h'},
|
||||
{"version", no_argument, 0, 'V'},
|
||||
{"list-models", optional_argument, 0, 0x100},
|
||||
@@ -385,8 +388,8 @@ int main(int argc, char **argv) {
|
||||
{"web-open", no_argument, 0, 0x106},
|
||||
{0, 0, 0, 0}};
|
||||
int c;
|
||||
while ((c = getopt_long(argc, argv, "w:s:b:k:m:P:r:n:T:Hp:dahVNcR:LyA:C:e:", opts, NULL)) !=
|
||||
-1) {
|
||||
while ((c = getopt_long(argc, argv, "w:s:b:k:m:P:r:n:T:Hp:dahVNcR:LyA:C:e:M:",
|
||||
opts, NULL)) != -1) {
|
||||
switch (c) {
|
||||
case 'w':
|
||||
snprintf(cfg.workspace_root, sizeof(cfg.workspace_root), "%s", optarg);
|
||||
@@ -478,6 +481,14 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
snprintf(cfg.effort, sizeof(cfg.effort), "%s", optarg);
|
||||
break;
|
||||
case 'M':
|
||||
if (!cst_mode_valid(optarg)) {
|
||||
fprintf(stderr, "bad --mode %s (coding | general)\n", optarg);
|
||||
curl_global_cleanup();
|
||||
return 1;
|
||||
}
|
||||
snprintf(cfg.mode, sizeof(cfg.mode), "%s", optarg);
|
||||
break;
|
||||
case 'h':
|
||||
usage(argv[0]);
|
||||
curl_global_cleanup();
|
||||
@@ -557,6 +568,12 @@ int main(int argc, char **argv) {
|
||||
snprintf(cfg.effort, sizeof(cfg.effort), "%s",
|
||||
cst_effort_valid(e) ? e : CST_DEFAULT_EFFORT);
|
||||
}
|
||||
if (!cfg.mode[0]) {
|
||||
const char *m = getenv("CST_MODE");
|
||||
if (!cst_mode_valid(m))
|
||||
m = saved.mode;
|
||||
snprintf(cfg.mode, sizeof(cfg.mode), "%s", cst_mode_name(m));
|
||||
}
|
||||
if (!cfg.api_base[0]) {
|
||||
const char *env_base = getenv("CST_API_BASE");
|
||||
if (!env_base || !env_base[0])
|
||||
|
||||
+1901
File diff suppressed because it is too large
Load Diff
+4
-1
@@ -452,7 +452,10 @@ char *cst_session_export_md(cst_session_t *s, const char *title,
|
||||
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);
|
||||
cst_buf_printf(&b, "- %s `%s` (%s)\n",
|
||||
cst_attach_is_image(&m->attach[k]) ? "image"
|
||||
: "document",
|
||||
m->attach[k].name, d);
|
||||
}
|
||||
if (m->nattach)
|
||||
cst_buf_puts(&b, "\n");
|
||||
|
||||
@@ -47,6 +47,8 @@ int cst_settings_load(cst_settings_t *s) {
|
||||
cJSON_GetObjectItemCaseSensitive(root, "provider"));
|
||||
copy_str(s->effort, sizeof(s->effort),
|
||||
cJSON_GetObjectItemCaseSensitive(root, "effort"));
|
||||
copy_str(s->mode, sizeof(s->mode),
|
||||
cJSON_GetObjectItemCaseSensitive(root, "mode"));
|
||||
copy_str(s->approval, sizeof(s->approval),
|
||||
cJSON_GetObjectItemCaseSensitive(root, "approval"));
|
||||
s->show_thinking =
|
||||
@@ -86,6 +88,8 @@ int cst_settings_save(const cst_settings_t *s) {
|
||||
cJSON_AddStringToObject(root, "provider", s->provider);
|
||||
if (s->effort[0])
|
||||
cJSON_AddStringToObject(root, "effort", s->effort);
|
||||
if (s->mode[0])
|
||||
cJSON_AddStringToObject(root, "mode", s->mode);
|
||||
if (s->approval[0])
|
||||
cJSON_AddStringToObject(root, "approval", s->approval);
|
||||
if (s->show_thinking)
|
||||
@@ -135,6 +139,21 @@ int cst_approval_parse(const char *s, int *out) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
static const char *const MODE_NAMES[] = {"coding", "general"};
|
||||
|
||||
int cst_mode_valid(const char *m) {
|
||||
for (size_t i = 0; m && m[0] && i < sizeof(MODE_NAMES) / sizeof(*MODE_NAMES);
|
||||
i++) {
|
||||
if (strcmp(m, MODE_NAMES[i]) == 0)
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const char *cst_mode_name(const char *m) {
|
||||
return cst_mode_valid(m) ? m : CST_DEFAULT_MODE;
|
||||
}
|
||||
|
||||
const char *cst_settings_model(const cst_settings_t *s, const char *provider) {
|
||||
for (int i = 0; provider && i < s->nmodels; i++) {
|
||||
if (strcmp(s->models[i].provider, provider) == 0 && s->models[i].model[0])
|
||||
@@ -148,6 +167,8 @@ int cst_settings_remember(const cst_agent_config_t *cfg) {
|
||||
cst_settings_load(&s);
|
||||
snprintf(s.provider, sizeof(s.provider), "%s", cfg->provider);
|
||||
snprintf(s.effort, sizeof(s.effort), "%s", cfg->effort);
|
||||
if (cfg->mode[0])
|
||||
snprintf(s.mode, sizeof(s.mode), "%s", cfg->mode);
|
||||
int i = 0;
|
||||
while (i < s.nmodels && strcmp(s.models[i].provider, cfg->provider) != 0)
|
||||
i++;
|
||||
@@ -176,6 +197,13 @@ int cst_settings_remember_thinking(int show_thinking) {
|
||||
return cst_settings_save(&s);
|
||||
}
|
||||
|
||||
int cst_settings_remember_mode(const char *mode) {
|
||||
cst_settings_t s;
|
||||
cst_settings_load(&s);
|
||||
snprintf(s.mode, sizeof(s.mode), "%s", cst_mode_name(mode));
|
||||
return cst_settings_save(&s);
|
||||
}
|
||||
|
||||
static void real_ws(const char *ws, char *out, size_t n) {
|
||||
if (!ws || os_realpath(ws, out, n) != 0)
|
||||
snprintf(out, n, "%s", ws ? ws : "");
|
||||
|
||||
+5
-5
@@ -40,7 +40,7 @@ static const struct {
|
||||
"runs commands as needed.",
|
||||
"", 0},
|
||||
{"explore",
|
||||
"Fast read-only explorer: searches and reads the codebase to answer a "
|
||||
"Fast read-only explorer: searches and reads the workspace to answer a "
|
||||
"question (where is X, how does Y work, which files do Z). Several can "
|
||||
"run at once.",
|
||||
"You only read. Search broadly first (grep, glob, list_dir), then read "
|
||||
@@ -49,11 +49,11 @@ static const struct {
|
||||
"you looked for and did not find.",
|
||||
1},
|
||||
{"plan",
|
||||
"Read-only planner: investigates the code involved and returns a "
|
||||
"Read-only planner: investigates the material involved and returns a "
|
||||
"step-by-step implementation plan.",
|
||||
"You only read. Investigate the code involved, then return a concrete "
|
||||
"plan: the files to change and what to change in each, risks and edge "
|
||||
"cases, and how to verify. Sketch code only where it removes ambiguity.",
|
||||
"You only read. Investigate what is involved, then return a concrete "
|
||||
"plan: what to produce or change and where, risks and edge cases, and "
|
||||
"how to check the result. Sketch code only where it removes ambiguity.",
|
||||
1},
|
||||
};
|
||||
#define NBUILTIN ((int)(sizeof(BUILTIN) / sizeof(BUILTIN[0])))
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <string.h>
|
||||
|
||||
#include "cst_tools.h"
|
||||
#include "cst_clipboard.h"
|
||||
#include "cst_util.h"
|
||||
#include "tools_internal.h"
|
||||
|
||||
@@ -106,3 +107,70 @@ int cst_tool_ask_user(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
1);
|
||||
return tool_set_resultf(r, 0, "user answered: %s", ans);
|
||||
}
|
||||
|
||||
/* The system clipboard: what the user copied elsewhere. Reading needs no
|
||||
* permission (READ); writing replaces what the user has copied (WRITE, so
|
||||
* the confirmation frontier applies). */
|
||||
int cst_tool_clipboard_read(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
cst_tool_result_t *r) {
|
||||
(void)ctx;
|
||||
char err[192];
|
||||
char *text = NULL;
|
||||
size_t n = 0;
|
||||
if (cst_clipboard_text(&text, &n, err, sizeof(err)) != 0)
|
||||
return tool_set_result(r, err, 1);
|
||||
long cap = tool_arg_long(c, "max_bytes", 65536);
|
||||
if (cap > 0 && (size_t)cap < n)
|
||||
n = (size_t)cap;
|
||||
cst_buf_t b = {0};
|
||||
cst_buf_printf(&b, "clipboard text (%zu bytes%s):\n", (size_t)n,
|
||||
(size_t)cap < (size_t)n ? ", truncated" : "");
|
||||
cst_buf_append(&b, text, n);
|
||||
if (b.len && b.buf[b.len - 1] != '\n')
|
||||
cst_buf_puts(&b, "\n");
|
||||
free(text);
|
||||
return tool_take_buf(r, &b, 0);
|
||||
}
|
||||
|
||||
int cst_tool_clipboard_write(const cst_tool_ctx_t *ctx,
|
||||
const ctl_tool_call_t *c, cst_tool_result_t *r) {
|
||||
(void)ctx;
|
||||
size_t n = 0;
|
||||
const char *text = tool_arg_n(c, "text", &n);
|
||||
if (!text)
|
||||
return tool_set_result(r, "error: missing text", 1);
|
||||
char err[192];
|
||||
if (cst_clipboard_set_text(text, n, err, sizeof(err)) != 0)
|
||||
return tool_set_result(r, err, 1);
|
||||
return tool_set_resultf(r, 0, "copied %zu bytes to the clipboard", n);
|
||||
}
|
||||
|
||||
/* Confirmation preview: what the clipboard would hold afterwards. Clipboard
|
||||
* text has no diff, so this is the text itself, cut to a readable size. */
|
||||
#define CLIP_PREVIEW_MAX 2000
|
||||
int cst_preview_clipboard_write(const cst_tool_ctx_t *ctx,
|
||||
const ctl_tool_call_t *c, char **out) {
|
||||
(void)ctx;
|
||||
size_t n = 0;
|
||||
const char *text = tool_arg_n(c, "text", &n);
|
||||
if (!text) {
|
||||
*out = strdup("error: missing text");
|
||||
return -1;
|
||||
}
|
||||
int lines = 0;
|
||||
for (size_t i = 0; i < n; i++)
|
||||
lines += text[i] == '\n';
|
||||
size_t cut = n;
|
||||
if (cut > CLIP_PREVIEW_MAX)
|
||||
cut = cst_utf8_floor(text, CLIP_PREVIEW_MAX);
|
||||
cst_buf_t b = {0};
|
||||
cst_buf_printf(&b, "clipboard: replace with %zu bytes, %d line%s\n", n,
|
||||
lines + (n && text[n - 1] != '\n'), lines == 0 ? "" : "s");
|
||||
cst_buf_append(&b, text, cut);
|
||||
if (b.len && b.buf[b.len - 1] != '\n')
|
||||
cst_buf_puts(&b, "\n");
|
||||
if (cut < n)
|
||||
cst_buf_printf(&b, "… (%zu more bytes)\n", n - cut);
|
||||
*out = cst_buf_steal(&b);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -267,6 +267,14 @@ int cst_tool_view_image(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
cst_attachment_t *a = cst_attach_import_file(session, full, err, sizeof(err));
|
||||
if (!a)
|
||||
return tool_set_resultf(r, 1, "error: cannot show %.200s: %s", path, err);
|
||||
if (!cst_attach_is_image(a)) {
|
||||
cst_attach_free(a);
|
||||
return tool_set_resultf(
|
||||
r, 1,
|
||||
"error: %.200s is not an image; a text file or PDF reaches the model "
|
||||
"through /attach <path> (a PDF becomes its text layer)",
|
||||
path);
|
||||
}
|
||||
snprintf(a->origin, sizeof(a->origin), "view_image");
|
||||
const char *d = tool_arg(c, "detail");
|
||||
if (d && (strcmp(d, "low") == 0 || strcmp(d, "high") == 0))
|
||||
|
||||
+16
-2
@@ -66,8 +66,9 @@ static const cst_tool_def_t TOOLS[] = {
|
||||
{"detail", S, 0, "low or high (provider hint; default auto)"}},
|
||||
CST_TP_READ, 0, cst_tool_view_image, NULL, NO_DYN},
|
||||
{"run_shell",
|
||||
"Run a non-interactive shell command in the workspace root for builds, "
|
||||
"tests, git and other CLIs (stdin is empty; long runs are killed). "
|
||||
"Run a non-interactive shell command in the workspace root: builds, "
|
||||
"tests, git, scripts, computations, data processing, other CLIs "
|
||||
"(stdin is empty; long runs are killed). "
|
||||
"Returns stdout+stderr with colors stripped and long output cut to head "
|
||||
"+ tail. Prefer read_file/grep/glob/list_dir over cat/grep/find.",
|
||||
"Run a shell command in the workspace root (non-interactive, stdin is "
|
||||
@@ -158,6 +159,19 @@ static const cst_tool_def_t TOOLS[] = {
|
||||
{{"question", S, 1, "the question"},
|
||||
{"options", S, 0, "suggested answers, one per line"}},
|
||||
CST_TP_ASK, 0, cst_tool_ask_user, NULL, NO_DYN},
|
||||
{"clipboard_read",
|
||||
"Read the text on the system clipboard — what the user copied outside "
|
||||
"the workspace. Returns UTF-8 text.",
|
||||
NULL,
|
||||
{{"max_bytes", I, 0, "cap on the returned text (default 65536)"}},
|
||||
CST_TP_READ, 0, cst_tool_clipboard_read, NULL, NO_DYN},
|
||||
{"clipboard_write",
|
||||
"Replace the system clipboard with text, so the user can paste it into "
|
||||
"another app. Overwrites what they had copied.",
|
||||
NULL,
|
||||
{{"text", S, 1, "text to put on the clipboard"}},
|
||||
CST_TP_WRITE, 0, cst_tool_clipboard_write, cst_preview_clipboard_write,
|
||||
NO_DYN},
|
||||
};
|
||||
#define NTOOLS ((int)(sizeof(TOOLS) / sizeof(TOOLS[0])))
|
||||
|
||||
|
||||
@@ -46,6 +46,13 @@ int cst_tool_todo_write(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
cst_tool_result_t *r);
|
||||
int cst_tool_ask_user(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
cst_tool_result_t *r);
|
||||
/* Clipboard text (system clipboard, not the workspace). */
|
||||
int cst_tool_clipboard_read(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
cst_tool_result_t *r);
|
||||
int cst_tool_clipboard_write(const cst_tool_ctx_t *ctx,
|
||||
const ctl_tool_call_t *c, cst_tool_result_t *r);
|
||||
int cst_preview_clipboard_write(const cst_tool_ctx_t *ctx,
|
||||
const ctl_tool_call_t *c, char **out);
|
||||
int cst_tool_web_fetch(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
cst_tool_result_t *r);
|
||||
int cst_tool_view_image(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||
|
||||
@@ -2354,6 +2354,7 @@ static const char *const TIPS[] = {
|
||||
"Ctrl+P opens the command palette",
|
||||
"Type @ to mention a file in your message",
|
||||
"/effort sets reasoning effort: low, medium, high, max",
|
||||
"/mode general reframes the agent for non-code work",
|
||||
"/model lists the models your API key can use",
|
||||
"/undo reverts the file changes of the last turn",
|
||||
"/provider switches provider; keys are saved per provider",
|
||||
@@ -2832,7 +2833,10 @@ static void reload_hist_from_session(tui_t *t) {
|
||||
for (int k = 0; k < m->nattach; k++) {
|
||||
char d[128];
|
||||
cst_attach_describe(&m->attach[k], d, sizeof(d));
|
||||
cst_buf_printf(&ub, "\n[image %s · %s]", m->attach[k].name, d);
|
||||
cst_buf_printf(&ub, "\n[%s %s · %s]",
|
||||
cst_attach_is_image(&m->attach[k]) ? "image"
|
||||
: "document",
|
||||
m->attach[k].name, d);
|
||||
}
|
||||
block_set_body(b, ub.buf ? ub.buf : "", ub.len);
|
||||
cst_buf_free(&ub);
|
||||
@@ -4361,6 +4365,41 @@ static void cmd_effort(tui_t *t, char *arg, int *quit) {
|
||||
cfg->effort, cfg->provider, cfg->model);
|
||||
}
|
||||
|
||||
static void cmd_mode(tui_t *t, char *arg, int *quit) {
|
||||
(void)quit;
|
||||
cst_agent_config_t *cfg = cst_agent_config_mut(t->agent);
|
||||
static const char *const names[] = {"coding", "general"};
|
||||
static const char *const desc[] = {
|
||||
"code framed: repo, build, tests (default)",
|
||||
"documents, data, research, everyday files"};
|
||||
const char *want = NULL;
|
||||
if (arg && arg[0]) {
|
||||
if (!cst_mode_valid(arg)) {
|
||||
push_system_line(t, "Unknown mode '%s' (coding | general)", arg);
|
||||
return;
|
||||
}
|
||||
want = arg;
|
||||
} else {
|
||||
char items[2][128];
|
||||
int cur_idx = 0;
|
||||
for (int i = 0; i < 2; i++) {
|
||||
snprintf(items[i], 128, "%-8s %s", names[i], desc[i]);
|
||||
if (strcmp(names[i], cst_mode_name(cfg->mode)) == 0)
|
||||
cur_idx = i;
|
||||
}
|
||||
int pick = picker_run(t, "Working mode", items, 2, cur_idx);
|
||||
if (pick < 0)
|
||||
return;
|
||||
want = names[pick];
|
||||
}
|
||||
snprintf(cfg->mode, sizeof(cfg->mode), "%s", want);
|
||||
cst_settings_remember_mode(cfg->mode);
|
||||
push_system_line(t, "(mode → %s%s)", cfg->mode,
|
||||
strcmp(cfg->mode, "general") == 0
|
||||
? "; the system prompt is rewritten on the next turn"
|
||||
: "");
|
||||
}
|
||||
|
||||
static void cmd_provider(tui_t *t, char *arg, int *quit) {
|
||||
(void)quit;
|
||||
cst_agent_config_t *cfg = cst_agent_config_mut(t->agent);
|
||||
@@ -4639,14 +4678,15 @@ static void cmd_status(tui_t *t, char *arg, int *quit) {
|
||||
: !same ? "this run only (--api-key / CST_API_KEY)"
|
||||
: strcmp(from, "saved") == 0 ? "saved (/key to replace)"
|
||||
: from);
|
||||
push_system_line(t, " protocol %s · tools %s · max steps %d",
|
||||
push_system_line(t, " protocol %s · tools %s · mode %s · max steps %d",
|
||||
cst_protocol_name(cfg->protocol),
|
||||
cst_tool_profile_name(cfg->tool_profile), cfg->max_steps);
|
||||
cst_tool_profile_name(cfg->tool_profile),
|
||||
cst_mode_name(cfg->mode), cfg->max_steps);
|
||||
char appr[64];
|
||||
approval_label(t, appr, sizeof(appr));
|
||||
push_system_line(t, " approval %s — %s", appr,
|
||||
approval_help(approval_mode(t)));
|
||||
push_system_line(t, " mode %s",
|
||||
push_system_line(t, " plan %s",
|
||||
cfg->plan_mode ? "Plan (read-only tools)" : "Build");
|
||||
const char *notes = cst_agent_notes(t->agent);
|
||||
push_system_line(t, " notes %s",
|
||||
@@ -4825,19 +4865,39 @@ static void report_attached(tui_t *t, int added) {
|
||||
cst_agent_config(t->agent)->model);
|
||||
}
|
||||
|
||||
/* /attach: stage images for the next user message. The bytes are copied into
|
||||
* the session's attachment directory now, so a picture picked from /tmp keeps
|
||||
* working after the file is gone. */
|
||||
/* /attach paste: the clipboard image if there is one, else its text goes
|
||||
* into the composer (CR dropped, control bytes kept out, INPUT_CAP respected). */
|
||||
static int paste_text_into_input(tui_t *t, const char *text, size_t n) {
|
||||
char *buf = malloc(n + 1);
|
||||
if (!buf)
|
||||
return 0;
|
||||
size_t k = 0;
|
||||
for (size_t i = 0; i < n && t->in_len + k + 1 < INPUT_CAP; i++) {
|
||||
unsigned char c = (unsigned char)text[i];
|
||||
if (c == '\r' || (c < 0x20 && c != '\n' && c != '\t'))
|
||||
continue;
|
||||
buf[k++] = (char)c;
|
||||
}
|
||||
if (k)
|
||||
input_insert(t, buf, k);
|
||||
free(buf);
|
||||
return (int)k;
|
||||
}
|
||||
|
||||
/* /attach: stage files for the next user message -- images, PDFs (reduced to
|
||||
* their text) and text. The payload is copied into the session's attachment
|
||||
* directory now, so a file picked from /tmp keeps working after it is gone. */
|
||||
static void cmd_attach(tui_t *t, char *arg, int *quit) {
|
||||
(void)quit;
|
||||
int n = cst_agent_pending_count(t->agent);
|
||||
if (!arg || !arg[0] || strcmp(arg, "list") == 0) {
|
||||
if (n == 0) {
|
||||
push_system_line(t, "No images picked. Usage: /attach <path>");
|
||||
push_system_line(t, "Nothing attached yet. Usage: /attach <path>, "
|
||||
"/attach paste, /attach list, /attach clear");
|
||||
return;
|
||||
}
|
||||
push_system_line(t, "%d image%s with the next message (sent once):", n,
|
||||
n > 1 ? "s" : "");
|
||||
push_system_line(t, "%d attachment%s with the next message (sent once):",
|
||||
n, n > 1 ? "s" : "");
|
||||
for (int i = 0; i < n; i++) {
|
||||
char line[256];
|
||||
if (cst_agent_pending_describe(t->agent, i, line, sizeof(line)) == 0)
|
||||
@@ -4848,7 +4908,7 @@ static void cmd_attach(tui_t *t, char *arg, int *quit) {
|
||||
}
|
||||
if (strcmp(arg, "clear") == 0 || strcmp(arg, "none") == 0) {
|
||||
cst_agent_pending_clear(t->agent);
|
||||
push_system_line(t, "Dropped the picked images.");
|
||||
push_system_line(t, "Dropped the attachments.");
|
||||
return;
|
||||
}
|
||||
char err[256];
|
||||
@@ -4856,7 +4916,24 @@ static void cmd_attach(tui_t *t, char *arg, int *quit) {
|
||||
char *img = NULL;
|
||||
size_t len = 0;
|
||||
if (cst_clipboard_image(&img, &len, err, sizeof(err)) != 0) {
|
||||
push_system_line(t, "Error: %s", err);
|
||||
/* no image: put the clipboard text in the composer instead */
|
||||
char *text = NULL;
|
||||
size_t tn = 0;
|
||||
char terr[192];
|
||||
if (cst_clipboard_text(&text, &tn, terr, sizeof(terr)) != 0) {
|
||||
push_system_line(t, "Error: %s", terr);
|
||||
return;
|
||||
}
|
||||
int added = paste_text_into_input(t, text, tn);
|
||||
free(text);
|
||||
if (added <= 0) {
|
||||
push_system_line(t, "(no room in the composer for the clipboard "
|
||||
"text)");
|
||||
return;
|
||||
}
|
||||
push_system_line(t, "(pasted %d bytes of clipboard text into the "
|
||||
"composer)", added);
|
||||
layout_maybe_grow_input(t);
|
||||
return;
|
||||
}
|
||||
char mime[32], name[32];
|
||||
@@ -4881,19 +4958,29 @@ static void cmd_attach(tui_t *t, char *arg, int *quit) {
|
||||
report_attached(t, 1);
|
||||
}
|
||||
|
||||
static int file_is_image(const char *path) {
|
||||
/* A file worth attaching: an image, a PDF, or something that reads as text.
|
||||
* Deciding here in the TUI only avoids turning a random dropped binary into
|
||||
* a pasted path; the store checks the content for real. */
|
||||
static int file_is_attachable(const char *path) {
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f)
|
||||
return 0;
|
||||
unsigned char head[16];
|
||||
unsigned char head[64];
|
||||
size_t got = fread(head, 1, sizeof(head), f);
|
||||
fclose(f);
|
||||
char mime[32];
|
||||
return cst_attach_sniff(head, got, mime, sizeof(mime)) == 0;
|
||||
if (cst_attach_sniff(head, got, mime, sizeof(mime)) == 0)
|
||||
return 1;
|
||||
if (got >= 4 && memcmp(head, "%PDF", 4) == 0)
|
||||
return 1;
|
||||
for (size_t i = 0; i < got; i++)
|
||||
if (head[i] == 0 || head[i] < 0x09)
|
||||
return 0;
|
||||
return got > 0;
|
||||
}
|
||||
|
||||
/* A paste that is nothing but image file paths (a file dropped on the
|
||||
* terminal) attaches those files instead of typing the paths. */
|
||||
/* A paste that is nothing but file paths (a file dropped on the terminal)
|
||||
* attaches those files instead of typing the paths. */
|
||||
static void paste_image_paths(tui_t *t) {
|
||||
size_t from = t->paste_at, to = t->cur;
|
||||
if (to <= from || to - from != t->in_len - t->paste_len0 ||
|
||||
@@ -4905,7 +4992,7 @@ static void paste_image_paths(tui_t *t) {
|
||||
if (n <= 0)
|
||||
return;
|
||||
for (int i = 0; i < n; i++)
|
||||
if (!file_is_image(paths[i]))
|
||||
if (!file_is_attachable(paths[i]))
|
||||
return;
|
||||
input_erase(t, from, to);
|
||||
int added = 0;
|
||||
@@ -5441,6 +5528,8 @@ static const slash_cmd_t SLASH[] = {
|
||||
cmd_protocol},
|
||||
{"tools", "", "[on|off NAME|minimal]", "list or toggle tools", 0,
|
||||
cmd_tools},
|
||||
{"mode", "", "[coding|general]", "coding or general (non-code) framing", 1,
|
||||
cmd_mode},
|
||||
{"permissions", "perms", "[reset]", "show saved always-allow rules", 1,
|
||||
cmd_permissions},
|
||||
{"sandbox", "", "[reset-labels]", "shell sandbox and labeled folders", 1,
|
||||
@@ -5464,8 +5553,8 @@ static const slash_cmd_t SLASH[] = {
|
||||
{"export", "", "[file]", "write the transcript as Markdown", 1,
|
||||
cmd_export},
|
||||
{"copy", "", "", "copy the last reply (OSC 52)", 1, cmd_copy},
|
||||
{"attach", "image", "[path|paste|clear]",
|
||||
"send image(s) with the next message",
|
||||
{"attach", "image|file", "[path|paste|clear]",
|
||||
"send files (image, PDF, text) or clipboard text with the next message",
|
||||
1, cmd_attach},
|
||||
{"init", "", "", "have the agent write AGENTS.md", 0, cmd_init},
|
||||
{"details", "", "", "expand/collapse all tool output", 1, cmd_details},
|
||||
|
||||
+16
@@ -263,6 +263,7 @@ static cJSON *wui_state_new(wui_t *w) {
|
||||
cJSON_AddStringToObject(o, "provider", cfg->provider);
|
||||
cJSON_AddStringToObject(o, "model", cfg->model);
|
||||
cJSON_AddStringToObject(o, "effort", cfg->effort);
|
||||
cJSON_AddStringToObject(o, "mode", cst_mode_name(cfg->mode));
|
||||
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);
|
||||
@@ -1670,6 +1671,12 @@ static int h_settings_get(cst_http_conn_t *c, void *ud) {
|
||||
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_AddStringToObject(o, "mode", cst_mode_name(cfg->mode));
|
||||
cJSON *md = cJSON_AddArrayToObject(o, "modes");
|
||||
if (md) {
|
||||
cJSON_AddItemToArray(md, cJSON_CreateString("coding"));
|
||||
cJSON_AddItemToArray(md, cJSON_CreateString("general"));
|
||||
}
|
||||
cJSON *cf = cJSON_AddObjectToObject(o, "confirm");
|
||||
if (cf) {
|
||||
cJSON_AddBoolToObject(cf, "shell", cfg->confirm_shell);
|
||||
@@ -1728,6 +1735,7 @@ static int h_settings_x(cst_http_conn_t *c, void *ud) {
|
||||
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 *mode = cJSON_GetObjectItem(j, "mode");
|
||||
const cJSON *tools = cJSON_GetObjectItem(j, "tools");
|
||||
const cJSON *conf = cJSON_GetObjectItem(j, "confirm");
|
||||
cst_protocol_t p = cfg->protocol;
|
||||
@@ -1742,6 +1750,9 @@ static int h_settings_x(cst_http_conn_t *c, void *ud) {
|
||||
else if (prof && (!cJSON_IsString(prof) ||
|
||||
cst_tool_profile_parse(prof->valuestring, &profile) != 0))
|
||||
snprintf(bad, sizeof(bad), "未知的工具集(default 或 minimal)");
|
||||
else if (mode && (!cJSON_IsString(mode) ||
|
||||
!cst_mode_valid(mode->valuestring)))
|
||||
snprintf(bad, sizeof(bad), "工作模式只能是 coding / general");
|
||||
else if (tools && !cJSON_IsObject(tools))
|
||||
snprintf(bad, sizeof(bad), "tools 要是 {\"名字\": true/false}");
|
||||
else if (conf && !cJSON_IsObject(conf))
|
||||
@@ -1760,6 +1771,11 @@ static int h_settings_x(cst_http_conn_t *c, void *ud) {
|
||||
}
|
||||
cfg->protocol = p;
|
||||
cfg->tool_profile = profile;
|
||||
if (mode) {
|
||||
snprintf(cfg->mode, sizeof(cfg->mode), "%s",
|
||||
cst_mode_name(mode->valuestring));
|
||||
cst_settings_remember_mode(cfg->mode);
|
||||
}
|
||||
if (steps)
|
||||
cfg->max_steps = (int)steps->valuedouble;
|
||||
for (const cJSON *t = tools ? tools->child : NULL; t; t = t->next)
|
||||
|
||||
Reference in New Issue
Block a user