v0.8.6: general mode, clipboard text, PDF text extraction
ci / tcc-check (push) Successful in 59s
ci / linux (aarch64, 0) (push) Successful in 19m11s
ci / linux (x86, 1) (push) Successful in 11m31s
ci / linux (x86_64, 1) (push) Successful in 11m11s
ci / windows (push) Successful in 9m24s

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:
WHC
2026-10-11 11:09:39 +08:00
parent 50abcf1a91
commit e0b0eddf3a
28 changed files with 3291 additions and 111 deletions
+109 -20
View File
@@ -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},