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