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
+100
View File
@@ -325,6 +325,100 @@ static void test_import_file(const char *root) {
"missing file refused");
}
/* Documents: text is stored as text, a PDF as its text layer while the mime
* still says PDF, and neither is an image as far as the wire is concerned. */
static void test_documents(const char *root) {
char session[1024], src[1024];
snprintf(session, sizeof(session), "%.900s/sessions/doc.jsonl", root);
char err[256] = "";
const char *text = "# Notes\n\nFive painters, one canvas.\n";
snprintf(src, sizeof(src), "%.900s/notes.md", root);
FILE *f = fopen(src, "wb");
EXPECT(f != NULL, "write the text file");
if (f) {
fwrite(text, 1, strlen(text), f);
fclose(f);
}
cst_attachment_t *a = cst_attach_import_file(session, src, err, sizeof(err));
EXPECT(a != NULL, "text file imported");
if (a) {
EXPECT(strcmp(a->mime, "text/plain") == 0, "text mime");
EXPECT(!cst_attach_is_image(a) && cst_attach_is_text(a), "not an image");
EXPECT(a->pages == 0, "no pages for text");
EXPECT(strstr(a->rel, ".txt") != NULL, "text stored as .txt");
EXPECT(a->bytes == (long long)strlen(text), "text size");
size_t n = 0;
char *back = cst_attach_read(session, a, &n, err, sizeof(err));
EXPECT(back && n == strlen(text) && memcmp(back, text, n) == 0,
"text reads back exactly");
free(back);
EXPECT(cst_attach_data_url(session, a) == NULL,
"a document has no data url");
char desc[128];
cst_attach_describe(a, desc, sizeof(desc));
EXPECT(strstr(desc, "text") != NULL, "describe says text");
cst_attach_free(a);
}
/* a one-page PDF with a text layer */
static const char PDF[] =
"%PDF-1.4\n"
"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n"
"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n"
"3 0 obj\n<< /Type /Page /Parent 2 0 R /Resources << /Font << /F1 4 0 R "
">> >> /Contents 5 0 R >>\nendobj\n"
"4 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\n"
"endobj\n"
"5 0 obj\n<< >>\nstream\n"
"BT /F1 12 Tf 72 720 Td (Attached paper abstract) Tj ET\n"
"endstream\nendobj\n"
"trailer\n<< /Root 1 0 R >>\n%%EOF\n";
snprintf(src, sizeof(src), "%.900s/paper.pdf", root);
f = fopen(src, "wb");
EXPECT(f != NULL, "write the PDF");
if (f) {
fwrite(PDF, 1, strlen(PDF), f);
fclose(f);
}
err[0] = 0;
a = cst_attach_import_file(session, src, err, sizeof(err));
EXPECT(a != NULL, "PDF imported");
if (a) {
EXPECT(strcmp(a->mime, "application/pdf") == 0, "the PDF keeps its mime");
EXPECT(a->pages == 1, "page count comes from the extractor");
EXPECT(strstr(a->rel, ".txt") != NULL, "the text layer is what is stored");
EXPECT(cst_attach_is_text(a), "documents are text on the wire");
char desc[128];
cst_attach_describe(a, desc, sizeof(desc));
EXPECT(strstr(desc, "pdf") != NULL && strstr(desc, "1 page") != NULL,
"describe names the PDF and its pages");
size_t n = 0;
char *back = cst_attach_read(session, a, &n, err, sizeof(err));
EXPECT(back && strstr(back, "Attached paper abstract") != NULL,
"the extracted text reads back");
free(back);
cst_attach_free(a);
}
err[0] = 0;
EXPECT(cst_attach_import_file(session, "/nonexistent/x.pdf", err,
sizeof(err)) == NULL,
"a missing PDF is refused");
/* neither image, PDF nor text: a binary blob */
static const char binary[] = "\x01\x02\x03\x00\x04\x05";
snprintf(src, sizeof(src), "%.900s/blob.bin", root);
f = fopen(src, "wb");
if (f) {
fwrite(binary, 1, sizeof(binary), f);
fclose(f);
}
err[0] = 0;
EXPECT(cst_attach_import_file(session, src, err, sizeof(err)) == NULL,
"a binary file is refused");
EXPECT(strstr(err, "not an image") != NULL, "err says what was expected");
}
static void test_json_roundtrip(void) {
cst_attachment_t list[3];
memset(list, 0, sizeof(list));
@@ -344,6 +438,8 @@ static void test_json_roundtrip(void) {
list[1].bytes = 4096;
snprintf(list[1].detail, sizeof(list[1].detail), "high");
snprintf(list[1].origin, sizeof(list[1].origin), "view_image");
snprintf(list[1].mime, sizeof(list[1].mime), "application/pdf");
list[1].pages = 12;
snprintf(list[2].id, sizeof(list[2].id), "img-3");
snprintf(list[2].name, sizeof(list[2].name), "c.webp");
@@ -365,6 +461,9 @@ static void test_json_roundtrip(void) {
EXPECT(strcmp(back[1].detail, "high") == 0 &&
strcmp(back[1].origin, "view_image") == 0,
"detail and origin survive");
EXPECT(strcmp(back[1].mime, "application/pdf") == 0 && back[1].pages == 12,
"a document keeps its mime and page count");
EXPECT(back[2].pages == 0, "pages default to 0");
EXPECT(strcmp(back[2].mime, "image/webp") == 0 && back[2].width == 0,
"entry without dimensions");
EXPECT(strcmp(back[0].detail, "auto") == 0, "missing detail defaults");
@@ -530,6 +629,7 @@ int main(void) {
test_dib();
test_store(root);
test_import_file(root);
test_documents(root);
test_json_roundtrip();
test_lists();
test_agent_pending(root);