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:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user