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:
WHC
2026-10-11 11:09:39 +08:00
parent 50abcf1a91
commit e0b0eddf3a
28 changed files with 3291 additions and 111 deletions
+21 -6
View File
@@ -47,15 +47,18 @@ int cst_protocol_is_text(cst_protocol_t p);
/* Eager protocols execute a call during the stream, at confirmation time. */
int cst_protocol_is_eager(cst_protocol_t p);
/* One image carried by a message (see cst_attach.h). The bytes live beside
* the session; this is the metadata plus the name of the stored copy. */
/* One file carried by a message (see cst_attach.h): an image, or a document
* whose stored payload is its text. The payload lives beside the session;
* this is the metadata plus the name of the stored copy. */
typedef struct {
char id[32]; /* unique inside the session ("img-1a2b3c4d") */
char name[256]; /* display name (original file name) */
char mime[32]; /* image/png, image/jpeg, image/gif, image/webp, image/bmp */
char mime[32]; /* image/png, ...; application/pdf for a PDF whose text
* layer the stored file holds; text/plain for text */
char rel[256]; /* file name inside the session's attachment directory */
long long bytes; /* stored size */
long long bytes; /* stored size (the text, for documents) */
int width, height; /* pixels, 0 when the header could not be read */
int pages; /* PDF page count, 0 for anything else */
char detail[8]; /* auto | low | high (wire hint, "" = auto) */
char origin[32]; /* user | view_image | mcp (for display only) */
} cst_attachment_t;
@@ -82,6 +85,7 @@ typedef struct {
char model[128];
char provider[32];
char effort[16]; /* low | medium | high | max */
char mode[16]; /* coding | general (CST_DEFAULT_MODE) */
cst_protocol_t protocol;
int max_steps; /* model calls per user turn */
size_t tool_out_cap; /* bytes of one tool output sent to the model */
@@ -114,6 +118,14 @@ typedef enum {
const char *cst_approval_name(int a);
int cst_approval_parse(const char *s, int *out);
/* Working mode: how the system prompt frames the agent. "coding" (default)
* is code framed; "general" is for tasks that are not about a codebase
* (documents, data, research, everyday files). Orthogonal to plan mode. */
#define CST_DEFAULT_MODE "coding"
int cst_mode_valid(const char *m);
/* Canonical name of m, or the default when m is unknown / empty. */
const char *cst_mode_name(const char *m);
typedef struct cst_session cst_session_t;
typedef struct cst_agent cst_agent_t;
@@ -406,13 +418,14 @@ int cst_creds_resolve(const char *provider, char *out, size_t out_sz,
/* Hidden console prompt (no echo). */
int cst_creds_prompt_console(const char *provider, char *out, size_t out_sz);
/* Last used provider, model per provider and effort, in
/* Last used provider, model per provider, effort and working mode, in
* <config>/settings.json. */
#define CST_SETTINGS_MAX_MODELS 16
#define CST_SETTINGS_MAX_TRUSTED 32
typedef struct {
char provider[32];
char effort[16];
char mode[16]; /* coding | general, "" = not chosen yet */
char approval[8]; /* "" = not chosen yet */
int show_thinking; /* TUI: Thought blocks start expanded */
/* workspaces whose .agent/plugins and .agent/mcp.json may run */
@@ -427,11 +440,13 @@ typedef struct {
int cst_settings_load(cst_settings_t *s);
int cst_settings_save(const cst_settings_t *s);
const char *cst_settings_model(const cst_settings_t *s, const char *provider);
/* Record cfg's provider, model and effort as the last used ones. */
/* Record cfg's provider, model, effort and mode as the last used ones. */
int cst_settings_remember(const cst_agent_config_t *cfg);
/* Record an approval mode the user picked (CST_APPROVAL_*). */
int cst_settings_remember_approval(int approval);
int cst_settings_remember_thinking(int show_thinking);
/* Record the working mode the user picked (coding | general). */
int cst_settings_remember_mode(const char *mode);
/* Workspace trust for project plugins / MCP servers (by real path). */
int cst_settings_trusted(const char *workspace);
int cst_settings_trust(const char *workspace, int on);
+24 -14
View File
@@ -9,16 +9,24 @@
extern "C" {
#endif
/* --- image attachments ---------------------------------------------------
* An attachment is a validated copy of an image file, stored beside the
* session so the bytes a model is shown stay the bytes that were picked.
* Only the metadata travels in the JSONL; the bytes are encoded into the
* request when it is built. */
/* --- attachments ---------------------------------------------------------
* An attachment is a validated copy of a file, stored beside the session so
* what a model is shown stays what was picked. Images are stored as they are
* and encoded into the request when it is built; a PDF is reduced to its text
* layer and a text file is stored as is, both of which travel inline as text
* (so a text-only model still gets them). Only the metadata goes in the
* JSONL. */
/* Supported: PNG, JPEG, GIF, WebP, BMP (static images). */
#define CST_ATTACH_MAX_BYTES (16 * 1024 * 1024) /* one stored image */
/* Supported: PNG, JPEG, GIF, WebP, BMP (static images), PDF, UTF-8 text. */
#define CST_ATTACH_MAX_BYTES (16 * 1024 * 1024) /* one picked file */
#define CST_ATTACH_MAX_TOTAL (32 * 1024 * 1024) /* per request */
#define CST_ATTACH_MAX_COUNT 8 /* per message */
/* Text stored and inlined per document (a PDF also caps its text here). */
#define CST_ATTACH_TEXT_MAX (4 * 1024 * 1024)
/* Images go on the wire as image parts; everything else is inlined as text. */
int cst_attach_is_image(const cst_attachment_t *a);
int cst_attach_is_text(const cst_attachment_t *a);
/* Magic-byte test: 0 and a canonical mime when data holds a supported
* image, -1 otherwise (an extension alone never decides). */
@@ -35,23 +43,25 @@ int cst_attach_dir(const char *session_path, char *out, size_t out_sz);
int cst_attach_full_path(const char *session_path, const cst_attachment_t *a,
char *out, size_t out_sz);
/* Read src_path, verify it is a supported image within limits, and copy it
* into the session's attachment directory. Returns a malloc'd entry the
* caller owns (cst_attach_free), or NULL with err filled. */
/* Read src_path and copy it into the session's attachment directory: an image
* as it is, a PDF as its extracted text, anything else only when it is text.
* Returns a malloc'd entry the caller owns (cst_attach_free), or NULL with
* err filled. */
cst_attachment_t *cst_attach_import_file(const char *session_path,
const char *src_path, char *err,
size_t err_sz);
/* Store already-read bytes (clipboard, downloads); name is display-only. */
/* Store already-read image bytes (clipboard, downloads); name is
* display-only. */
cst_attachment_t *cst_attach_import_bytes(const char *session_path,
const char *name, const char *data,
size_t n, char *err, size_t err_sz);
/* Validate and read back a stored image, or NULL when it is gone. */
/* Validate and read back a stored payload, or NULL when it is gone. */
char *cst_attach_read(const char *session_path, const cst_attachment_t *a,
size_t *out_len, char *err, size_t err_sz);
/* "data:image/png;base64,...." for the wire; NULL when the file is gone. */
/* "data:image/png;base64,...." for the wire; images only, NULL otherwise. */
char *cst_attach_data_url(const char *session_path, const cst_attachment_t *a);
/* "1920x1080 PNG 412 KB" style one-liner for prompts and the UI. */
/* "1920x1080 png 412 KB" / "pdf 1.2 MB, 12 pages" one-liner for prompts. */
void cst_attach_describe(const cst_attachment_t *a, char *out, size_t out_sz);
void cst_attach_free(cst_attachment_t *a);
+14
View File
@@ -13,6 +13,20 @@ extern "C" {
* ("no image on the clipboard", "install wl-clipboard", ...). */
int cst_clipboard_image(char **data, size_t *n, char *err, size_t err_sz);
/* Cap on text moved through the clipboard (read and write). */
#define CST_CLIPBOARD_TEXT_MAX (1024 * 1024)
/* UTF-8 text on the system clipboard: Wayland (wl-paste), X11 (xclip),
* macOS (pbpaste), Windows (CF_UNICODETEXT). Returns 0 with *data malloc'd
* and NUL-terminated, or -1 with err filled in ("no text on the clipboard",
* "reading the clipboard needs wl-clipboard", ...). */
int cst_clipboard_text(char **data, size_t *n, char *err, size_t err_sz);
/* Replace the clipboard with UTF-8 text: wl-copy / xclip / pbcopy, or the
* Windows clipboard API. Returns 0 on success, else -1 with err. */
int cst_clipboard_set_text(const char *text, size_t n, char *err,
size_t err_sz);
/* Windows CF_DIB bytes (BITMAPINFOHEADER + pixels) as an image file: a PNG
* for uncompressed 24/32-bit bitmaps, else a BMP. NULL when malformed. */
char *cst_dib_to_image(const void *dib, size_t sz, size_t *n);
+27
View File
@@ -0,0 +1,27 @@
#ifndef CST_PDF_H
#define CST_PDF_H
#include <stddef.h>
#ifdef __cplusplus
extern "C" {
#endif
/* Text layer of a PDF, in page order, with no external dependency (no
* poppler, no zlib): classic objects and object streams, FlateDecode plus
* ASCIIHex/ASCII85, PNG/TIFF predictors, Tj/TJ/'/" text with per-font
* ToUnicode CMaps and a WinAnsi / Latin-1 fallback. Scanned pages have no
* text layer and are reported as such.
* Returns a malloc'd NUL-terminated UTF-8 string, or NULL with err filled in
* ("PDF is encrypted ...", "no extractable text ..."). *pages (optional) gets
* the page count; the result is truncated at CST_PDF_TEXT_MAX bytes. */
char *cst_pdf_text(const void *data, size_t n, int *pages, char *err,
size_t err_sz);
#define CST_PDF_TEXT_MAX (4u * 1024u * 1024u)
#ifdef __cplusplus
}
#endif
#endif
+1 -1
View File
@@ -3,7 +3,7 @@
#define CST_VERSION_MAJOR 0
#define CST_VERSION_MINOR 8
#define CST_VERSION_PATCH 5
#define CST_VERSION_PATCH 6
#define CST_VERSION_STR_(x) #x
#define CST_VERSION_STR(x) CST_VERSION_STR_(x)