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