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:
+8
-1
@@ -95,6 +95,7 @@ set(CST_CORE_SOURCES
|
|||||||
src/subagent.c
|
src/subagent.c
|
||||||
src/attach.c
|
src/attach.c
|
||||||
src/clipboard.c
|
src/clipboard.c
|
||||||
|
src/pdf_text.c
|
||||||
src/session.c
|
src/session.c
|
||||||
src/tools/tools.c
|
src/tools/tools.c
|
||||||
src/tools/patch.c
|
src/tools/patch.c
|
||||||
@@ -279,7 +280,7 @@ endif()
|
|||||||
enable_testing()
|
enable_testing()
|
||||||
set(_CST_SMALL_TESTS test_ctl_frontier test_ctl_heredoc test_json_baseline
|
set(_CST_SMALL_TESTS test_ctl_frontier test_ctl_heredoc test_json_baseline
|
||||||
test_replay_starvation test_replay_real test_mm_tool test_textsel
|
test_replay_starvation test_replay_real test_mm_tool test_textsel
|
||||||
test_markdown)
|
test_pdf_text test_markdown)
|
||||||
foreach(t ${_CST_SMALL_TESTS})
|
foreach(t ${_CST_SMALL_TESTS})
|
||||||
if(t STREQUAL test_textsel)
|
if(t STREQUAL test_textsel)
|
||||||
add_executable(${t} tests/${t}.c src/textsel.c src/os_compat.c src/os_win.c
|
add_executable(${t} tests/${t}.c src/textsel.c src/os_compat.c src/os_win.c
|
||||||
@@ -287,6 +288,12 @@ foreach(t ${_CST_SMALL_TESTS})
|
|||||||
if(WIN32)
|
if(WIN32)
|
||||||
target_link_libraries(${t} PRIVATE advapi32 user32)
|
target_link_libraries(${t} PRIVATE advapi32 user32)
|
||||||
endif()
|
endif()
|
||||||
|
elseif(t STREQUAL test_pdf_text)
|
||||||
|
add_executable(${t} tests/${t}.c src/pdf_text.c src/os_compat.c src/os_win.c
|
||||||
|
src/util.c)
|
||||||
|
if(WIN32)
|
||||||
|
target_link_libraries(${t} PRIVATE advapi32 user32)
|
||||||
|
endif()
|
||||||
elseif(t STREQUAL test_markdown)
|
elseif(t STREQUAL test_markdown)
|
||||||
add_executable(${t} tests/${t}.c)
|
add_executable(${t} tests/${t}.c)
|
||||||
target_include_directories(${t} PRIVATE "${SD_DIR}/include")
|
target_include_directories(${t} PRIVATE "${SD_DIR}/include")
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ CORE_SRC = \
|
|||||||
src/keymap.c \
|
src/keymap.c \
|
||||||
src/skills.c \
|
src/skills.c \
|
||||||
src/extensions.c src/plugins.c src/mcp.c src/subagent.c src/attach.c src/clipboard.c \
|
src/extensions.c src/plugins.c src/mcp.c src/subagent.c src/attach.c src/clipboard.c \
|
||||||
|
src/pdf_text.c \
|
||||||
src/session.c \
|
src/session.c \
|
||||||
src/tools/tools.c \
|
src/tools/tools.c \
|
||||||
src/tools/patch.c \
|
src/tools/patch.c \
|
||||||
@@ -135,6 +136,7 @@ TESTS = \
|
|||||||
tests/test_apply_patch \
|
tests/test_apply_patch \
|
||||||
tests/test_session_roundtrip \
|
tests/test_session_roundtrip \
|
||||||
tests/test_attach \
|
tests/test_attach \
|
||||||
|
tests/test_pdf_text \
|
||||||
tests/test_shell \
|
tests/test_shell \
|
||||||
tests/test_diff \
|
tests/test_diff \
|
||||||
tests/test_tools_fs \
|
tests/test_tools_fs \
|
||||||
@@ -229,6 +231,9 @@ tests/test_mm_tool: tests/test_mm_tool.c src/mm_tool.c include/mm_tool.h $(TEST_
|
|||||||
tests/test_textsel: tests/test_textsel.c src/textsel.c $(TEST_OS_SRC) include/cst_textsel.h
|
tests/test_textsel: tests/test_textsel.c src/textsel.c $(TEST_OS_SRC) include/cst_textsel.h
|
||||||
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_textsel.c src/textsel.c $(TEST_OS_SRC) $(TEST_SYS_LIBS)
|
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_textsel.c src/textsel.c $(TEST_OS_SRC) $(TEST_SYS_LIBS)
|
||||||
|
|
||||||
|
tests/test_pdf_text: tests/test_pdf_text.c src/pdf_text.c include/cst_pdf.h $(TEST_OS_SRC)
|
||||||
|
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_pdf_text.c src/pdf_text.c $(TEST_OS_SRC) $(TEST_SYS_LIBS)
|
||||||
|
|
||||||
tests/test_markdown: tests/test_markdown.c $(SD_DIR)/libsashidown.a
|
tests/test_markdown: tests/test_markdown.c $(SD_DIR)/libsashidown.a
|
||||||
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_markdown.c $(LDFLAGS) -lsashidown -lm
|
$(CC) $(CFLAGS) $(CPPFLAGS) -o $@ tests/test_markdown.c $(LDFLAGS) -lsashidown -lm
|
||||||
|
|
||||||
|
|||||||
@@ -233,6 +233,7 @@ Windows x86_64 的 zip 和安装包(Debian 的 mingw-w64 UCRT gcc + NSIS)。
|
|||||||
|---|---|
|
|---|---|
|
||||||
| `CST_PROTOCOL` / `--protocol` | 工具调用协议(`native-eos`) |
|
| `CST_PROTOCOL` / `--protocol` | 工具调用协议(`native-eos`) |
|
||||||
| `CST_TOOLS` / `--tools` | 工具集:`default`(全部)/ `minimal`(论文 4 件套) |
|
| `CST_TOOLS` / `--tools` | 工具集:`default`(全部)/ `minimal`(论文 4 件套) |
|
||||||
|
| `CST_MODE` / `--mode` | 工作模式:`coding`(默认,代码口径)/ `general`(通用助手口径) |
|
||||||
| `CST_EFFORT` / `--effort` | 推理强度 `low` / `medium` / `high` / `max` |
|
| `CST_EFFORT` / `--effort` | 推理强度 `low` / `medium` / `high` / `max` |
|
||||||
| `CST_CONFIRM` / `--confirm` | 哪些类别要确认:`shell,write,net`(默认)或 `none` |
|
| `CST_CONFIRM` / `--confirm` | 哪些类别要确认:`shell,write,net`(默认)或 `none` |
|
||||||
| `CST_MAX_STEPS` / `--max-steps` | 每个用户回合最多调用模型几次(30) |
|
| `CST_MAX_STEPS` / `--max-steps` | 每个用户回合最多调用模型几次(30) |
|
||||||
@@ -276,6 +277,7 @@ cst-agent --headless --prompt 'List files'
|
|||||||
| `-y` / `--yolo` | 等同 `--approval yolo`,只对本次运行有效 |
|
| `-y` / `--yolo` | 等同 `--approval yolo`,只对本次运行有效 |
|
||||||
| `-C LIST` / `--confirm LIST` | 需要确认的类别:`shell,write,net` 的任意组合或 `none` |
|
| `-C LIST` / `--confirm LIST` | 需要确认的类别:`shell,write,net` 的任意组合或 `none` |
|
||||||
| `-T PROFILE` / `--tools PROFILE` | `default` 或 `minimal` |
|
| `-T PROFILE` / `--tools PROFILE` | `default` 或 `minimal` |
|
||||||
|
| `-M MODE` / `--mode MODE` | 工作模式 `coding`(默认)/ `general`,也读 `CST_MODE`,并记进 `settings.json` |
|
||||||
| `-P NAME` / `-m NAME` / `-e LEVEL` | 本次使用的 provider / 模型 / 推理强度(不传则用上次的) |
|
| `-P NAME` / `-m NAME` / `-e LEVEL` | 本次使用的 provider / 模型 / 推理强度(不传则用上次的) |
|
||||||
| `-a` / `--ask-key` | 在命令行输入并保存当前 provider 的 key |
|
| `-a` / `--ask-key` | 在命令行输入并保存当前 provider 的 key |
|
||||||
|
|
||||||
@@ -297,6 +299,8 @@ cst-agent --headless --prompt 'List files'
|
|||||||
| `delete_file` / `move_file` | 写 | 删除 / 移动(重命名)文件 |
|
| `delete_file` / `move_file` | 写 | 删除 / 移动(重命名)文件 |
|
||||||
| `run_shell` | shell | 非交互命令,超时整组进程被杀,Esc 可中断 |
|
| `run_shell` | shell | 非交互命令,超时整组进程被杀,Esc 可中断 |
|
||||||
| `web_fetch` | 网络 | 抓取 URL,HTML 转纯文本 |
|
| `web_fetch` | 网络 | 抓取 URL,HTML 转纯文本 |
|
||||||
|
| `clipboard_read` | 读 | 读系统剪贴板里的文本(`max_bytes` 默认 64 KiB,超出截断并说明) |
|
||||||
|
| `clipboard_write` | 写 | 把文本放进系统剪贴板(按写类权限确认;本机走 pbcopy / wl-copy / xclip / Win32 API) |
|
||||||
| `todo_write` | 元 | 维护任务清单(TUI 右侧面板,Ctrl+T 开关) |
|
| `todo_write` | 元 | 维护任务清单(TUI 右侧面板,Ctrl+T 开关) |
|
||||||
| `ask_user` | 询问 | 向用户提问(可给选项);headless 下返回错误让模型自行决定 |
|
| `ask_user` | 询问 | 向用户提问(可给选项);headless 下返回错误让模型自行决定 |
|
||||||
| `skill` | 元 | 加载一个 skill 的说明(`SKILL.md`)或其中的文件;有 skill 时才出现 |
|
| `skill` | 元 | 加载一个 skill 的说明(`SKILL.md`)或其中的文件;有 skill 时才出现 |
|
||||||
@@ -308,6 +312,26 @@ cst-agent --headless --prompt 'List files'
|
|||||||
`--tools minimal` 只保留 `read_file` / `run_shell` / `write_file` / `apply_patch`(论文实验口径)。
|
`--tools minimal` 只保留 `read_file` / `run_shell` / `write_file` / `apply_patch`(论文实验口径)。
|
||||||
TUI 中 `/tools off NAME`、`/tools on NAME` 可临时开关单个工具。
|
TUI 中 `/tools off NAME`、`/tools on NAME` 可临时开关单个工具。
|
||||||
|
|
||||||
|
### 工作模式(coding / general)
|
||||||
|
|
||||||
|
`--mode general`(或 TUI 里 `/mode general`、WebUI 运行设置里的「工作模式」、`CST_MODE=general`)把默认口径
|
||||||
|
从"代码助手"换成"通用助手":工作流从改代码/跑测试换成"先弄清要什么、再动手、给出处和不确定的地方",
|
||||||
|
工具说明从"仓库"改成"文件与数据",并说明 `web_fetch` 可以拿外部资料;`/status` 第一行显示当前模式,
|
||||||
|
切换后 history 里会留一条系统提示。默认仍是 `coding`,选择记在 `settings.json`;
|
||||||
|
`--tools minimal`(论文口径)的提示词按字节冻结,不受模式影响。
|
||||||
|
|
||||||
|
### 剪贴板与文档附件
|
||||||
|
|
||||||
|
- **剪贴板**:`clipboard_read` / `clipboard_write` 两个工具(写剪贴板是不可逆发射,要确认);
|
||||||
|
TUI 里 `/attach paste` 在剪贴板里没有图片时,把剪贴板的**文本**放进输入框(丢掉 CR 与控制字节,尊重输入长度上限)。
|
||||||
|
- **附件**(`/attach <path>`、拖入终端、剪贴板图片都算):图片按原样存进会话目录,发给模型时是 image part;
|
||||||
|
**PDF 在摄取时就抽出文本层**(零依赖解析器:Flate / ASCIIHex / ASCII85、对象流、CID 字体 ToUnicode、
|
||||||
|
内联图像跳过),存的是文本、`mime` 仍是 `application/pdf`;其余文件必须是 UTF-8 文本。
|
||||||
|
文档以 **text part 内联**发出,所以不受 `CST_NO_IMAGES` 或非视觉模型影响;图片照旧会被换成一行"未发送"说明。
|
||||||
|
上限:单个附件 ≤16 MB(文档文本 ≤4 MiB),每条消息最多 8 个。
|
||||||
|
加密 PDF、扫描件(无文本层)会被明确拒绝并说明原因;
|
||||||
|
`view_image` 仍只收图片,文本/PDF 走 `/attach`。
|
||||||
|
|
||||||
### 权限、撤销与项目说明
|
### 权限、撤销与项目说明
|
||||||
|
|
||||||
- **审批模式**(Shift+Tab 循环切换,`/approval`,`--approval`,`CST_APPROVAL`):
|
- **审批模式**(Shift+Tab 循环切换,`/approval`,`--approval`,`CST_APPROVAL`):
|
||||||
@@ -426,6 +450,7 @@ Thought(推理过程)默认折叠成一行“Thought · 3.2s”,生成中
|
|||||||
| `/provider [name]`、`/model [name]`、`/effort [level]` | 切换 provider(缺 key 时当场输入并用 `/models` 校验)、模型(列表优先显示 API 实际返回的模型,标 `(api)`)、推理强度;都会记住 |
|
| `/provider [name]`、`/model [name]`、`/effort [level]` | 切换 provider(缺 key 时当场输入并用 `/models` 校验)、模型(列表优先显示 API 实际返回的模型,标 `(api)`)、推理强度;都会记住 |
|
||||||
| `/key` | 输入或更换当前 provider 的 API key(遮罩输入并保存) |
|
| `/key` | 输入或更换当前 provider 的 API key(遮罩输入并保存) |
|
||||||
| `/protocol [name]` | 切换工具调用协议 |
|
| `/protocol [name]` | 切换工具调用协议 |
|
||||||
|
| `/mode [coding\|general]` | 工作模式(通用助手 / 代码助手口径) |
|
||||||
| `/tools [on\|off NAME\|minimal\|default]` | 查看或开关工具 |
|
| `/tools [on\|off NAME\|minimal\|default]` | 查看或开关工具 |
|
||||||
| `/approval [ask\|auto\|yolo]` *、`/yolo` * | 审批模式;本次开关 yolo |
|
| `/approval [ask\|auto\|yolo]` *、`/yolo` * | 审批模式;本次开关 yolo |
|
||||||
| `/permissions [reset]` *、`/max-steps [N]` * | 权限规则、每轮步数 |
|
| `/permissions [reset]` *、`/max-steps [N]` * | 权限规则、每轮步数 |
|
||||||
@@ -435,6 +460,7 @@ Thought(推理过程)默认折叠成一行“Thought · 3.2s”,生成中
|
|||||||
| `/diff [path]` * | `git status` + `git diff` |
|
| `/diff [path]` * | `git status` + `git diff` |
|
||||||
| `/compact` | 手动压缩上下文 |
|
| `/compact` | 手动压缩上下文 |
|
||||||
| `/export [file]` *、`/copy` * | 导出 Markdown 记录;复制上一条回复(OSC 52,需终端支持) |
|
| `/export [file]` *、`/copy` * | 导出 Markdown 记录;复制上一条回复(OSC 52,需终端支持) |
|
||||||
|
| `/attach [path\|paste\|list\|clear]` | 给下一条消息带附件:图片、PDF(自动取文本层)、文本文件;`paste` 优先取剪贴板图片,没有就把剪贴板文本放进输入框 |
|
||||||
| `/web` * | 后台起一个 WebUI 进程(同 workspace、新会话,日志写到 `.agent/webui.log`) |
|
| `/web` * | 后台起一个 WebUI 进程(同 workspace、新会话,日志写到 `.agent/webui.log`) |
|
||||||
| `/init` | 让 agent 写 `AGENTS.md` |
|
| `/init` | 让 agent 写 `AGENTS.md` |
|
||||||
| `/details` *、`/thinking [on\|off]`、`/keys` * | 展开全部 tool 输出;Thought 默认展开/折叠;快捷键列表 |
|
| `/details` *、`/thinking [on\|off]`、`/keys` * | 展开全部 tool 输出;Thought 默认展开/折叠;快捷键列表 |
|
||||||
|
|||||||
+21
-6
@@ -47,15 +47,18 @@ int cst_protocol_is_text(cst_protocol_t p);
|
|||||||
/* Eager protocols execute a call during the stream, at confirmation time. */
|
/* Eager protocols execute a call during the stream, at confirmation time. */
|
||||||
int cst_protocol_is_eager(cst_protocol_t p);
|
int cst_protocol_is_eager(cst_protocol_t p);
|
||||||
|
|
||||||
/* One image carried by a message (see cst_attach.h). The bytes live beside
|
/* One file carried by a message (see cst_attach.h): an image, or a document
|
||||||
* the session; this is the metadata plus the name of the stored copy. */
|
* 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 {
|
typedef struct {
|
||||||
char id[32]; /* unique inside the session ("img-1a2b3c4d") */
|
char id[32]; /* unique inside the session ("img-1a2b3c4d") */
|
||||||
char name[256]; /* display name (original file name) */
|
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 */
|
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 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 detail[8]; /* auto | low | high (wire hint, "" = auto) */
|
||||||
char origin[32]; /* user | view_image | mcp (for display only) */
|
char origin[32]; /* user | view_image | mcp (for display only) */
|
||||||
} cst_attachment_t;
|
} cst_attachment_t;
|
||||||
@@ -82,6 +85,7 @@ typedef struct {
|
|||||||
char model[128];
|
char model[128];
|
||||||
char provider[32];
|
char provider[32];
|
||||||
char effort[16]; /* low | medium | high | max */
|
char effort[16]; /* low | medium | high | max */
|
||||||
|
char mode[16]; /* coding | general (CST_DEFAULT_MODE) */
|
||||||
cst_protocol_t protocol;
|
cst_protocol_t protocol;
|
||||||
int max_steps; /* model calls per user turn */
|
int max_steps; /* model calls per user turn */
|
||||||
size_t tool_out_cap; /* bytes of one tool output sent to the model */
|
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);
|
const char *cst_approval_name(int a);
|
||||||
int cst_approval_parse(const char *s, int *out);
|
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_session cst_session_t;
|
||||||
typedef struct cst_agent cst_agent_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). */
|
/* Hidden console prompt (no echo). */
|
||||||
int cst_creds_prompt_console(const char *provider, char *out, size_t out_sz);
|
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. */
|
* <config>/settings.json. */
|
||||||
#define CST_SETTINGS_MAX_MODELS 16
|
#define CST_SETTINGS_MAX_MODELS 16
|
||||||
#define CST_SETTINGS_MAX_TRUSTED 32
|
#define CST_SETTINGS_MAX_TRUSTED 32
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char provider[32];
|
char provider[32];
|
||||||
char effort[16];
|
char effort[16];
|
||||||
|
char mode[16]; /* coding | general, "" = not chosen yet */
|
||||||
char approval[8]; /* "" = not chosen yet */
|
char approval[8]; /* "" = not chosen yet */
|
||||||
int show_thinking; /* TUI: Thought blocks start expanded */
|
int show_thinking; /* TUI: Thought blocks start expanded */
|
||||||
/* workspaces whose .agent/plugins and .agent/mcp.json may run */
|
/* 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_load(cst_settings_t *s);
|
||||||
int cst_settings_save(const 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);
|
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);
|
int cst_settings_remember(const cst_agent_config_t *cfg);
|
||||||
/* Record an approval mode the user picked (CST_APPROVAL_*). */
|
/* Record an approval mode the user picked (CST_APPROVAL_*). */
|
||||||
int cst_settings_remember_approval(int approval);
|
int cst_settings_remember_approval(int approval);
|
||||||
int cst_settings_remember_thinking(int show_thinking);
|
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). */
|
/* Workspace trust for project plugins / MCP servers (by real path). */
|
||||||
int cst_settings_trusted(const char *workspace);
|
int cst_settings_trusted(const char *workspace);
|
||||||
int cst_settings_trust(const char *workspace, int on);
|
int cst_settings_trust(const char *workspace, int on);
|
||||||
|
|||||||
+24
-14
@@ -9,16 +9,24 @@
|
|||||||
extern "C" {
|
extern "C" {
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/* --- image attachments ---------------------------------------------------
|
/* --- attachments ---------------------------------------------------------
|
||||||
* An attachment is a validated copy of an image file, stored beside the
|
* An attachment is a validated copy of a file, stored beside the session so
|
||||||
* session so the bytes a model is shown stay the bytes that were picked.
|
* what a model is shown stays what was picked. Images are stored as they are
|
||||||
* Only the metadata travels in the JSONL; the bytes are encoded into the
|
* and encoded into the request when it is built; a PDF is reduced to its text
|
||||||
* request when it is built. */
|
* 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). */
|
/* Supported: PNG, JPEG, GIF, WebP, BMP (static images), PDF, UTF-8 text. */
|
||||||
#define CST_ATTACH_MAX_BYTES (16 * 1024 * 1024) /* one stored image */
|
#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_TOTAL (32 * 1024 * 1024) /* per request */
|
||||||
#define CST_ATTACH_MAX_COUNT 8 /* per message */
|
#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
|
/* Magic-byte test: 0 and a canonical mime when data holds a supported
|
||||||
* image, -1 otherwise (an extension alone never decides). */
|
* 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,
|
int cst_attach_full_path(const char *session_path, const cst_attachment_t *a,
|
||||||
char *out, size_t out_sz);
|
char *out, size_t out_sz);
|
||||||
|
|
||||||
/* Read src_path, verify it is a supported image within limits, and copy it
|
/* Read src_path and copy it into the session's attachment directory: an image
|
||||||
* into the session's attachment directory. Returns a malloc'd entry the
|
* as it is, a PDF as its extracted text, anything else only when it is text.
|
||||||
* caller owns (cst_attach_free), or NULL with err filled. */
|
* 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,
|
cst_attachment_t *cst_attach_import_file(const char *session_path,
|
||||||
const char *src_path, char *err,
|
const char *src_path, char *err,
|
||||||
size_t err_sz);
|
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,
|
cst_attachment_t *cst_attach_import_bytes(const char *session_path,
|
||||||
const char *name, const char *data,
|
const char *name, const char *data,
|
||||||
size_t n, char *err, size_t err_sz);
|
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,
|
char *cst_attach_read(const char *session_path, const cst_attachment_t *a,
|
||||||
size_t *out_len, char *err, size_t err_sz);
|
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);
|
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_describe(const cst_attachment_t *a, char *out, size_t out_sz);
|
||||||
|
|
||||||
void cst_attach_free(cst_attachment_t *a);
|
void cst_attach_free(cst_attachment_t *a);
|
||||||
|
|||||||
@@ -13,6 +13,20 @@ extern "C" {
|
|||||||
* ("no image on the clipboard", "install wl-clipboard", ...). */
|
* ("no image on the clipboard", "install wl-clipboard", ...). */
|
||||||
int cst_clipboard_image(char **data, size_t *n, char *err, size_t err_sz);
|
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
|
/* Windows CF_DIB bytes (BITMAPINFOHEADER + pixels) as an image file: a PNG
|
||||||
* for uncompressed 24/32-bit bitmaps, else a BMP. NULL when malformed. */
|
* 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);
|
char *cst_dib_to_image(const void *dib, size_t sz, size_t *n);
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
#define CST_VERSION_MAJOR 0
|
#define CST_VERSION_MAJOR 0
|
||||||
#define CST_VERSION_MINOR 8
|
#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) #x
|
||||||
#define CST_VERSION_STR(x) CST_VERSION_STR_(x)
|
#define CST_VERSION_STR(x) CST_VERSION_STR_(x)
|
||||||
|
|||||||
+104
-2
@@ -177,8 +177,62 @@ static int tool_on(const cst_agent_config_t *cfg, const char *name) {
|
|||||||
return cst_tool_active(cfg, cst_tool_find(name));
|
return cst_tool_active(cfg, cst_tool_find(name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* /mode general (cst_mode_name covers "" and unknown values). */
|
||||||
|
static int general_mode(const cst_agent_config_t *cfg) {
|
||||||
|
return cfg && strcmp(cst_mode_name(cfg->mode), "general") == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Workflow for /mode general: gather, then do the work, then check it. */
|
||||||
|
static void put_workflow_general(cst_buf_t *b, const cst_agent_config_t *cfg) {
|
||||||
|
cst_buf_puts(b, "\nWorkflow:\n1) Gather what you need");
|
||||||
|
const char *explore[] = {"list_dir", "glob", "grep"};
|
||||||
|
int n = 0;
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
if (tool_on(cfg, explore[i]))
|
||||||
|
cst_buf_printf(b, "%s%s", n++ ? ", " : " (", explore[i]);
|
||||||
|
}
|
||||||
|
if (n)
|
||||||
|
cst_buf_puts(b, ")");
|
||||||
|
if (tool_on(cfg, "read_file"))
|
||||||
|
cst_buf_puts(b, "; read big files in slices with read_file offset/limit, "
|
||||||
|
"and use the documents the user attached");
|
||||||
|
cst_buf_puts(b, ".\n2) Do the work");
|
||||||
|
n = 0;
|
||||||
|
if (tool_on(cfg, "write_file")) {
|
||||||
|
cst_buf_puts(b, ": write_file for new files");
|
||||||
|
n++;
|
||||||
|
}
|
||||||
|
if (tool_on(cfg, "edit_file"))
|
||||||
|
cst_buf_printf(b, "%sedit_file for small exact replacements",
|
||||||
|
n++ ? ", " : ": ");
|
||||||
|
if (tool_on(cfg, "apply_patch"))
|
||||||
|
cst_buf_printf(b, "%sapply_patch for multi-hunk diffs to existing files",
|
||||||
|
n++ ? ", " : ": ");
|
||||||
|
if (tool_on(cfg, "run_shell"))
|
||||||
|
cst_buf_printf(b, "%srun_shell for commands, computations, data "
|
||||||
|
"processing and other CLIs",
|
||||||
|
n++ ? ", " : ": ");
|
||||||
|
cst_buf_puts(b, ".\n");
|
||||||
|
if (tool_on(cfg, "run_shell"))
|
||||||
|
cst_buf_puts(b, "3) Check the result before saying it works (re-read it, "
|
||||||
|
"run it, or verify the numbers); fix and re-run until it "
|
||||||
|
"holds.\n");
|
||||||
|
if (tool_on(cfg, "todo_write"))
|
||||||
|
cst_buf_puts(b, "- For work with several steps keep a todo list with "
|
||||||
|
"todo_write and update it as you go.\n");
|
||||||
|
if (tool_on(cfg, "ask_user"))
|
||||||
|
cst_buf_puts(b, "- Use ask_user only when a decision is truly the user's; "
|
||||||
|
"otherwise choose sensibly and say so.\n");
|
||||||
|
cst_buf_puts(b, "Finish with a short plain-text summary of the result.\n"
|
||||||
|
"Never invent other tool names.\n");
|
||||||
|
}
|
||||||
|
|
||||||
/* Workflow section for the default tool profile; names only active tools. */
|
/* Workflow section for the default tool profile; names only active tools. */
|
||||||
static void put_workflow(cst_buf_t *b, const cst_agent_config_t *cfg) {
|
static void put_workflow(cst_buf_t *b, const cst_agent_config_t *cfg) {
|
||||||
|
if (general_mode(cfg)) {
|
||||||
|
put_workflow_general(b, cfg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
cst_buf_puts(b, "\nWorkflow:\n1) Inspect only what you need");
|
cst_buf_puts(b, "\nWorkflow:\n1) Inspect only what you need");
|
||||||
const char *explore[] = {"list_dir", "glob", "grep"};
|
const char *explore[] = {"list_dir", "glob", "grep"};
|
||||||
int n = 0;
|
int n = 0;
|
||||||
@@ -235,6 +289,25 @@ static const char DEF_INTRO[] =
|
|||||||
"could not verify, say so.\n"
|
"could not verify, say so.\n"
|
||||||
"- Keep going until the task is done or truly blocked.\n\n";
|
"- Keep going until the task is done or truly blocked.\n\n";
|
||||||
|
|
||||||
|
/* Same role, framed for /mode general: the workspace may hold documents,
|
||||||
|
* data or anything else, not just code. */
|
||||||
|
static const char DEF_INTRO_GENERAL[] =
|
||||||
|
"You are CST-Agent, a general-purpose assistant working in the user's "
|
||||||
|
"local workspace. All tool paths are relative to the workspace root and "
|
||||||
|
"shell commands run there; the material you work on may be code, "
|
||||||
|
"documents, data or anything else.\n\n"
|
||||||
|
"Principles:\n"
|
||||||
|
"- Reply in the user's language. Be concise: lead with the result, no "
|
||||||
|
"filler.\n"
|
||||||
|
"- Understand before answering: read the files, documents and sources the "
|
||||||
|
"task refers to instead of guessing; never invent content, numbers or "
|
||||||
|
"tool results.\n"
|
||||||
|
"- Do what was asked, at the scope asked: leave unrelated files, data and "
|
||||||
|
"material alone.\n"
|
||||||
|
"- Check your work when you can (re-read it, run it, cross-check the "
|
||||||
|
"source); if you could not verify something, say so.\n"
|
||||||
|
"- Keep going until the task is done or truly blocked.\n\n";
|
||||||
|
|
||||||
static void put_tool_use(cst_buf_t *b, const cst_agent_config_t *cfg) {
|
static void put_tool_use(cst_buf_t *b, const cst_agent_config_t *cfg) {
|
||||||
cst_buf_puts(b, "\nTool use:\n");
|
cst_buf_puts(b, "\nTool use:\n");
|
||||||
if (tool_on(cfg, "run_shell") &&
|
if (tool_on(cfg, "run_shell") &&
|
||||||
@@ -248,11 +321,22 @@ static void put_tool_use(cst_buf_t *b, const cst_agent_config_t *cfg) {
|
|||||||
cst_buf_puts(b, "- Read a file before editing it and again after a failed "
|
cst_buf_puts(b, "- Read a file before editing it and again after a failed "
|
||||||
"edit. The `N|` prefixes in read_file output are not part "
|
"edit. The `N|` prefixes in read_file output are not part "
|
||||||
"of the file.\n");
|
"of the file.\n");
|
||||||
|
if (general_mode(cfg) && tool_on(cfg, "web_fetch"))
|
||||||
|
cst_buf_puts(b, "- Use web_fetch for pages the user points you to; say so "
|
||||||
|
"when a page could not be fetched instead of guessing its "
|
||||||
|
"content.\n");
|
||||||
cst_buf_puts(b, "- When a call fails, read the error and change approach "
|
cst_buf_puts(b, "- When a call fails, read the error and change approach "
|
||||||
"instead of repeating the same call.\n");
|
"instead of repeating the same call.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
static void put_safety(cst_buf_t *b, const cst_agent_config_t *cfg) {
|
static void put_safety(cst_buf_t *b, const cst_agent_config_t *cfg) {
|
||||||
|
if (general_mode(cfg))
|
||||||
|
cst_buf_puts(b, "\nSafety:\n"
|
||||||
|
"- Do not delete or overwrite files or data you did not "
|
||||||
|
"create unless the task requires it, and do not send the "
|
||||||
|
"user's content anywhere the task did not ask for. Never "
|
||||||
|
"print secrets or API keys.\n");
|
||||||
|
else
|
||||||
cst_buf_puts(b, "\nSafety:\n"
|
cst_buf_puts(b, "\nSafety:\n"
|
||||||
"- Do not commit, push or rewrite git history unless asked. "
|
"- Do not commit, push or rewrite git history unless asked. "
|
||||||
"Do not delete or overwrite files you did not create unless "
|
"Do not delete or overwrite files you did not create unless "
|
||||||
@@ -365,7 +449,9 @@ char *cst_agent_build_prompt(const cst_agent_config_t *cfg, cst_protocol_t p,
|
|||||||
const char *project_notes) {
|
const char *project_notes) {
|
||||||
int minimal = !cfg || cfg->tool_profile == CST_TOOLS_MINIMAL;
|
int minimal = !cfg || cfg->tool_profile == CST_TOOLS_MINIMAL;
|
||||||
cst_buf_t b = {0};
|
cst_buf_t b = {0};
|
||||||
cst_buf_puts(&b, minimal ? PROMPT_BASE : DEF_INTRO);
|
cst_buf_puts(&b, minimal ? PROMPT_BASE
|
||||||
|
: general_mode(cfg) ? DEF_INTRO_GENERAL
|
||||||
|
: DEF_INTRO);
|
||||||
if (cst_protocol_is_native(p))
|
if (cst_protocol_is_native(p))
|
||||||
cst_buf_puts(&b, minimal ? SEC_NATIVE
|
cst_buf_puts(&b, minimal ? SEC_NATIVE
|
||||||
: "Call tools through the native "
|
: "Call tools through the native "
|
||||||
@@ -2241,11 +2327,12 @@ int cst_agent_undo_available(const cst_agent_t *a) {
|
|||||||
* later changes reach the model as persisted notes appended to the history,
|
* later changes reach the model as persisted notes appended to the history,
|
||||||
* which keeps the cached prefix intact. */
|
* which keeps the cached prefix intact. */
|
||||||
|
|
||||||
#define PROMPT_VERSION 2
|
#define PROMPT_VERSION 3
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
int plan;
|
int plan;
|
||||||
char approval[96];
|
char approval[96];
|
||||||
|
char mode[16];
|
||||||
char branch[128];
|
char branch[128];
|
||||||
char day[16];
|
char day[16];
|
||||||
char notes[24]; /* hash of AGENTS.md, "" = none */
|
char notes[24]; /* hash of AGENTS.md, "" = none */
|
||||||
@@ -2255,6 +2342,7 @@ static void state_now(const cst_agent_t *a, pstate_t *s) {
|
|||||||
memset(s, 0, sizeof(*s));
|
memset(s, 0, sizeof(*s));
|
||||||
s->plan = a->cfg.plan_mode;
|
s->plan = a->cfg.plan_mode;
|
||||||
snprintf(s->approval, sizeof(s->approval), "%s", approval_desc(&a->cfg));
|
snprintf(s->approval, sizeof(s->approval), "%s", approval_desc(&a->cfg));
|
||||||
|
snprintf(s->mode, sizeof(s->mode), "%s", cst_mode_name(a->cfg.mode));
|
||||||
git_branch(a->cfg.workspace_root, s->branch, sizeof(s->branch));
|
git_branch(a->cfg.workspace_root, s->branch, sizeof(s->branch));
|
||||||
today(s->day, sizeof(s->day));
|
today(s->day, sizeof(s->day));
|
||||||
if (a->notes && a->notes[0])
|
if (a->notes && a->notes[0])
|
||||||
@@ -2267,6 +2355,7 @@ static cJSON *state_json(const pstate_t *s) {
|
|||||||
cJSON *o = cJSON_CreateObject();
|
cJSON *o = cJSON_CreateObject();
|
||||||
cJSON_AddNumberToObject(o, "plan", s->plan);
|
cJSON_AddNumberToObject(o, "plan", s->plan);
|
||||||
cJSON_AddStringToObject(o, "approval", s->approval);
|
cJSON_AddStringToObject(o, "approval", s->approval);
|
||||||
|
cJSON_AddStringToObject(o, "mode", s->mode);
|
||||||
cJSON_AddStringToObject(o, "branch", s->branch);
|
cJSON_AddStringToObject(o, "branch", s->branch);
|
||||||
cJSON_AddStringToObject(o, "day", s->day);
|
cJSON_AddStringToObject(o, "day", s->day);
|
||||||
cJSON_AddStringToObject(o, "notes", s->notes);
|
cJSON_AddStringToObject(o, "notes", s->notes);
|
||||||
@@ -2282,6 +2371,7 @@ static void state_parse(const cJSON *o, pstate_t *s) {
|
|||||||
if (cJSON_IsString(v)) \
|
if (cJSON_IsString(v)) \
|
||||||
snprintf(s->field, sizeof(s->field), "%s", v->valuestring);
|
snprintf(s->field, sizeof(s->field), "%s", v->valuestring);
|
||||||
STATE_STR("approval", approval)
|
STATE_STR("approval", approval)
|
||||||
|
STATE_STR("mode", mode)
|
||||||
STATE_STR("branch", branch)
|
STATE_STR("branch", branch)
|
||||||
STATE_STR("day", day)
|
STATE_STR("day", day)
|
||||||
STATE_STR("notes", notes)
|
STATE_STR("notes", notes)
|
||||||
@@ -2293,6 +2383,7 @@ static char *system_meta(const cst_agent_t *a, const pstate_t *s) {
|
|||||||
cJSON_AddStringToObject(o, "protocol", cst_protocol_name(a->cfg.protocol));
|
cJSON_AddStringToObject(o, "protocol", cst_protocol_name(a->cfg.protocol));
|
||||||
cJSON_AddNumberToObject(o, "pv", PROMPT_VERSION);
|
cJSON_AddNumberToObject(o, "pv", PROMPT_VERSION);
|
||||||
cJSON_AddNumberToObject(o, "tools", a->cfg.tool_profile);
|
cJSON_AddNumberToObject(o, "tools", a->cfg.tool_profile);
|
||||||
|
cJSON_AddStringToObject(o, "mode", cst_mode_name(a->cfg.mode));
|
||||||
cJSON_AddItemToObject(o, "state", state_json(s));
|
cJSON_AddItemToObject(o, "state", state_json(s));
|
||||||
char *out = cJSON_PrintUnformatted(o);
|
char *out = cJSON_PrintUnformatted(o);
|
||||||
cJSON_Delete(o);
|
cJSON_Delete(o);
|
||||||
@@ -2312,10 +2403,13 @@ static int system_current(const cst_message_t *m, const cst_agent_config_t *c) {
|
|||||||
const cJSON *pv = cJSON_GetObjectItemCaseSensitive(meta, "pv");
|
const cJSON *pv = cJSON_GetObjectItemCaseSensitive(meta, "pv");
|
||||||
const cJSON *pr = cJSON_GetObjectItemCaseSensitive(meta, "protocol");
|
const cJSON *pr = cJSON_GetObjectItemCaseSensitive(meta, "protocol");
|
||||||
const cJSON *tp = cJSON_GetObjectItemCaseSensitive(meta, "tools");
|
const cJSON *tp = cJSON_GetObjectItemCaseSensitive(meta, "tools");
|
||||||
|
const cJSON *md = cJSON_GetObjectItemCaseSensitive(meta, "mode");
|
||||||
int ok = cJSON_IsNumber(pv) && pv->valueint == PROMPT_VERSION &&
|
int ok = cJSON_IsNumber(pv) && pv->valueint == PROMPT_VERSION &&
|
||||||
cJSON_IsString(pr) &&
|
cJSON_IsString(pr) &&
|
||||||
strcmp(pr->valuestring, cst_protocol_name(c->protocol)) == 0 &&
|
strcmp(pr->valuestring, cst_protocol_name(c->protocol)) == 0 &&
|
||||||
cJSON_IsNumber(tp) && tp->valueint == c->tool_profile;
|
cJSON_IsNumber(tp) && tp->valueint == c->tool_profile;
|
||||||
|
ok = ok && cJSON_IsString(md) &&
|
||||||
|
strcmp(md->valuestring, cst_mode_name(c->mode)) == 0;
|
||||||
cJSON_Delete(meta);
|
cJSON_Delete(meta);
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
@@ -2393,6 +2487,14 @@ static void sync_notes(cst_agent_t *a) {
|
|||||||
"is available again.\n");
|
"is available again.\n");
|
||||||
if (strcmp(now.approval, was.approval) != 0)
|
if (strcmp(now.approval, was.approval) != 0)
|
||||||
cst_buf_printf(&b, "[cst] Approval mode is now %s.\n", now.approval);
|
cst_buf_printf(&b, "[cst] Approval mode is now %s.\n", now.approval);
|
||||||
|
if (strcmp(now.mode, was.mode) != 0)
|
||||||
|
cst_buf_printf(&b,
|
||||||
|
"[cst] Working mode is now %s%s. Earlier instructions about "
|
||||||
|
"how to frame the work no longer apply.\n",
|
||||||
|
now.mode,
|
||||||
|
strcmp(now.mode, "general") == 0
|
||||||
|
? " (general: documents, data, research)"
|
||||||
|
: " (coding: repo, build, tests)");
|
||||||
if (strcmp(now.branch, was.branch) != 0)
|
if (strcmp(now.branch, was.branch) != 0)
|
||||||
cst_buf_printf(&b, "[cst] The git branch is now %s.\n",
|
cst_buf_printf(&b, "[cst] The git branch is now %s.\n",
|
||||||
now.branch[0] ? now.branch : "(none)");
|
now.branch[0] ? now.branch : "(none)");
|
||||||
|
|||||||
+123
-18
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
#include "cJSON.h"
|
#include "cJSON.h"
|
||||||
#include "cst_attach.h"
|
#include "cst_attach.h"
|
||||||
|
#include "cst_pdf.h"
|
||||||
#include "cst_util.h"
|
#include "cst_util.h"
|
||||||
#include "os_compat.h"
|
#include "os_compat.h"
|
||||||
|
|
||||||
@@ -102,6 +103,9 @@ static int le16(const unsigned char *p) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static const char *mime_of_ext(const char *mime) {
|
static const char *mime_of_ext(const char *mime) {
|
||||||
|
if (strcmp(mime, "application/pdf") == 0 ||
|
||||||
|
strcmp(mime, "text/plain") == 0)
|
||||||
|
return "txt"; /* a document is stored as the text it was reduced to */
|
||||||
if (strcmp(mime, "image/png") == 0)
|
if (strcmp(mime, "image/png") == 0)
|
||||||
return "png";
|
return "png";
|
||||||
if (strcmp(mime, "image/jpeg") == 0)
|
if (strcmp(mime, "image/jpeg") == 0)
|
||||||
@@ -352,23 +356,73 @@ static void make_id(const char *name, const char *data, size_t n, char *out,
|
|||||||
snprintf(out, out_sz, "img-%08x", (unsigned)(h & 0xFFFFFFFFu));
|
snprintf(out, out_sz, "img-%08x", (unsigned)(h & 0xFFFFFFFFu));
|
||||||
}
|
}
|
||||||
|
|
||||||
static cst_attachment_t *store(const char *session_path, const char *name,
|
/* n bytes, NUL-terminated: a text payload is handed around as a C string. */
|
||||||
const char *data, size_t n, const char *origin,
|
static char *memdup_z(const char *data, size_t n) {
|
||||||
char *err, size_t err_sz) {
|
char *copy = malloc(n + 1);
|
||||||
if (!name || !name[0])
|
if (!copy)
|
||||||
name = "image";
|
|
||||||
char mime[32];
|
|
||||||
if (cst_attach_sniff(data, n, mime, sizeof(mime)) != 0) {
|
|
||||||
snprintf(err, err_sz,
|
|
||||||
"not a supported image (PNG, JPEG, GIF, WebP or BMP; "
|
|
||||||
"checked by content, not by extension)");
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
memcpy(copy, data, n);
|
||||||
|
copy[n] = 0;
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* What a picked file is, plus the payload to store for it: an image stays as
|
||||||
|
* it is, a PDF is reduced to its text layer (the mime still says PDF, the
|
||||||
|
* stored file holds text), anything else must be UTF-8 text. The caller frees
|
||||||
|
* the result; *out_len is its length. */
|
||||||
|
static char *prepare(const char *data, size_t n, char *mime, size_t mime_sz,
|
||||||
|
int *pages, size_t *out_len, char *err, size_t err_sz) {
|
||||||
|
*pages = 0;
|
||||||
|
*out_len = n;
|
||||||
|
if (cst_attach_sniff(data, n, mime, mime_sz) == 0) {
|
||||||
if (n > CST_ATTACH_MAX_BYTES) {
|
if (n > CST_ATTACH_MAX_BYTES) {
|
||||||
snprintf(err, err_sz, "image is %.1f MB; the limit is %d MB",
|
snprintf(err, err_sz, "image is %.1f MB; the limit is %d MB",
|
||||||
(double)n / (1024 * 1024), (int)(CST_ATTACH_MAX_BYTES >> 20));
|
(double)n / (1024 * 1024), (int)(CST_ATTACH_MAX_BYTES >> 20));
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
char *copy = memdup_z(data, n);
|
||||||
|
if (!copy)
|
||||||
|
snprintf(err, err_sz, "out of memory");
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
if (n >= 4 && memcmp(data, "%PDF", 4) == 0) {
|
||||||
|
int pg = 0;
|
||||||
|
char perr[256];
|
||||||
|
char *text = cst_pdf_text(data, n, &pg, perr, sizeof(perr));
|
||||||
|
if (!text) {
|
||||||
|
snprintf(err, err_sz, "cannot read the PDF: %.200s", perr);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
snprintf(mime, mime_sz, "application/pdf");
|
||||||
|
*pages = pg;
|
||||||
|
*out_len = strlen(text);
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
if (n > CST_ATTACH_TEXT_MAX) {
|
||||||
|
snprintf(err, err_sz, "text is %.1f MB; the limit is %d MB",
|
||||||
|
(double)n / (1024 * 1024), (int)(CST_ATTACH_TEXT_MAX >> 20));
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (memchr(data, 0, n) || !cst_utf8_valid(data, n)) {
|
||||||
|
snprintf(err, err_sz,
|
||||||
|
"not an image (PNG, JPEG, GIF, WebP or BMP), a PDF or UTF-8 "
|
||||||
|
"text; the content decides, not the extension");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
snprintf(mime, mime_sz, "text/plain");
|
||||||
|
char *copy = memdup_z(data, n);
|
||||||
|
if (!copy)
|
||||||
|
snprintf(err, err_sz, "out of memory");
|
||||||
|
return copy;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Write one payload whose kind is already known. */
|
||||||
|
static cst_attachment_t *store(const char *session_path, const char *name,
|
||||||
|
const char *data, size_t n, const char *mime,
|
||||||
|
int pages, const char *origin, char *err,
|
||||||
|
size_t err_sz) {
|
||||||
|
if (!name || !name[0])
|
||||||
|
name = "image";
|
||||||
char dir[OS_PATH_MAX];
|
char dir[OS_PATH_MAX];
|
||||||
if (cst_attach_dir(session_path, dir, sizeof(dir)) != 0) {
|
if (cst_attach_dir(session_path, dir, sizeof(dir)) != 0) {
|
||||||
snprintf(err, err_sz, "cannot create the attachment directory");
|
snprintf(err, err_sz, "cannot create the attachment directory");
|
||||||
@@ -386,6 +440,8 @@ static cst_attachment_t *store(const char *session_path, const char *name,
|
|||||||
snprintf(a->origin, sizeof(a->origin), "%s", origin ? origin : "user");
|
snprintf(a->origin, sizeof(a->origin), "%s", origin ? origin : "user");
|
||||||
snprintf(a->detail, sizeof(a->detail), "auto");
|
snprintf(a->detail, sizeof(a->detail), "auto");
|
||||||
a->bytes = (long long)n;
|
a->bytes = (long long)n;
|
||||||
|
a->pages = pages;
|
||||||
|
if (cst_attach_is_image(a))
|
||||||
cst_attach_dimensions(data, n, &a->width, &a->height);
|
cst_attach_dimensions(data, n, &a->width, &a->height);
|
||||||
|
|
||||||
char full[OS_PATH_MAX];
|
char full[OS_PATH_MAX];
|
||||||
@@ -419,14 +475,26 @@ cst_attachment_t *cst_attach_import_bytes(const char *session_path,
|
|||||||
}
|
}
|
||||||
err[0] = 0;
|
err[0] = 0;
|
||||||
if (!session_path || !session_path[0]) {
|
if (!session_path || !session_path[0]) {
|
||||||
snprintf(err, err_sz, "no session to store the image in");
|
snprintf(err, err_sz, "no session to store the file in");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
if (!data || !n) {
|
if (!data || !n) {
|
||||||
snprintf(err, err_sz, "image is empty");
|
snprintf(err, err_sz, "the file is empty");
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
return store(session_path, name, data, n, "user", err, err_sz);
|
char mime[32];
|
||||||
|
if (cst_attach_sniff(data, n, mime, sizeof(mime)) != 0) {
|
||||||
|
snprintf(err, err_sz,
|
||||||
|
"not a supported image (PNG, JPEG, GIF, WebP or BMP; "
|
||||||
|
"checked by content, not by extension)");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
if (n > CST_ATTACH_MAX_BYTES) {
|
||||||
|
snprintf(err, err_sz, "image is %.1f MB; the limit is %d MB",
|
||||||
|
(double)n / (1024 * 1024), (int)(CST_ATTACH_MAX_BYTES >> 20));
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return store(session_path, name, data, n, mime, 0, "user", err, err_sz);
|
||||||
}
|
}
|
||||||
|
|
||||||
cst_attachment_t *cst_attach_import_file(const char *session_path,
|
cst_attachment_t *cst_attach_import_file(const char *session_path,
|
||||||
@@ -449,11 +517,30 @@ cst_attachment_t *cst_attach_import_file(const char *session_path,
|
|||||||
for (const char *p = src_path; *p; p++)
|
for (const char *p = src_path; *p; p++)
|
||||||
if (*p == '/' || *p == '\\')
|
if (*p == '/' || *p == '\\')
|
||||||
leaf = p + 1;
|
leaf = p + 1;
|
||||||
cst_attachment_t *a = store(session_path, leaf, data, n, "user", err, err_sz);
|
char mime[32];
|
||||||
|
int pages = 0;
|
||||||
|
size_t pn = 0;
|
||||||
|
char *payload =
|
||||||
|
prepare(data, n, mime, sizeof(mime), &pages, &pn, err, err_sz);
|
||||||
free(data);
|
free(data);
|
||||||
|
if (!payload)
|
||||||
|
return NULL;
|
||||||
|
cst_attachment_t *a =
|
||||||
|
store(session_path, leaf, payload, pn, mime, pages, "user", err, err_sz);
|
||||||
|
free(payload);
|
||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Images are opaque and travel as image parts; everything else is text the
|
||||||
|
* store holds and the request inlines. */
|
||||||
|
int cst_attach_is_image(const cst_attachment_t *a) {
|
||||||
|
return a && strncmp(a->mime, "image/", 6) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_attach_is_text(const cst_attachment_t *a) {
|
||||||
|
return a && !cst_attach_is_image(a);
|
||||||
|
}
|
||||||
|
|
||||||
char *cst_attach_read(const char *session_path, const cst_attachment_t *a,
|
char *cst_attach_read(const char *session_path, const cst_attachment_t *a,
|
||||||
size_t *out_len, char *err, size_t err_sz) {
|
size_t *out_len, char *err, size_t err_sz) {
|
||||||
char dummy[8];
|
char dummy[8];
|
||||||
@@ -470,11 +557,12 @@ char *cst_attach_read(const char *session_path, const cst_attachment_t *a,
|
|||||||
char *data = read_whole(full, out_len, CST_ATTACH_MAX_BYTES);
|
char *data = read_whole(full, out_len, CST_ATTACH_MAX_BYTES);
|
||||||
if (!data) {
|
if (!data) {
|
||||||
snprintf(err, err_sz,
|
snprintf(err, err_sz,
|
||||||
"%.200s is missing; the image was deleted from the session "
|
"%.200s is missing; it was deleted from the session store",
|
||||||
"store",
|
|
||||||
a->name);
|
a->name);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
if (!cst_attach_is_image(a)) /* a document is text, nothing to verify */
|
||||||
|
return data;
|
||||||
char mime[32];
|
char mime[32];
|
||||||
if (cst_attach_sniff(data, *out_len, mime, sizeof(mime)) != 0) {
|
if (cst_attach_sniff(data, *out_len, mime, sizeof(mime)) != 0) {
|
||||||
snprintf(err, err_sz, "%.200s in the session store is no longer an image",
|
snprintf(err, err_sz, "%.200s in the session store is no longer an image",
|
||||||
@@ -486,6 +574,8 @@ char *cst_attach_read(const char *session_path, const cst_attachment_t *a,
|
|||||||
}
|
}
|
||||||
|
|
||||||
char *cst_attach_data_url(const char *session_path, const cst_attachment_t *a) {
|
char *cst_attach_data_url(const char *session_path, const cst_attachment_t *a) {
|
||||||
|
if (!cst_attach_is_image(a)) /* a document travels as text, not as a URL */
|
||||||
|
return NULL;
|
||||||
char err[256];
|
char err[256];
|
||||||
size_t n = 0;
|
size_t n = 0;
|
||||||
char *data = cst_attach_read(session_path, a, &n, err, sizeof(err));
|
char *data = cst_attach_read(session_path, a, &n, err, sizeof(err));
|
||||||
@@ -509,10 +599,21 @@ void cst_attach_describe(const cst_attachment_t *a, char *out, size_t out_sz) {
|
|||||||
snprintf(out, out_sz, "(none)");
|
snprintf(out, out_sz, "(none)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const char *kind = strncmp(a->mime, "image/", 6) == 0 ? a->mime + 6 : a->mime;
|
const char *kind = a->mime;
|
||||||
char dim[32] = "";
|
char dim[32] = "";
|
||||||
|
if (cst_attach_is_image(a)) {
|
||||||
|
if (strncmp(a->mime, "image/", 6) == 0)
|
||||||
|
kind = a->mime + 6;
|
||||||
if (a->width > 0 && a->height > 0)
|
if (a->width > 0 && a->height > 0)
|
||||||
snprintf(dim, sizeof(dim), "%dx%d ", a->width, a->height);
|
snprintf(dim, sizeof(dim), "%dx%d ", a->width, a->height);
|
||||||
|
} else if (strcmp(a->mime, "application/pdf") == 0) {
|
||||||
|
kind = "pdf";
|
||||||
|
if (a->pages > 0)
|
||||||
|
snprintf(dim, sizeof(dim), "%d page%s ", a->pages,
|
||||||
|
a->pages == 1 ? "" : "s");
|
||||||
|
} else if (strcmp(a->mime, "text/plain") == 0 || !a->mime[0]) {
|
||||||
|
kind = "text";
|
||||||
|
}
|
||||||
out[0] = 0;
|
out[0] = 0;
|
||||||
if (a->bytes >= 1024 * 1024)
|
if (a->bytes >= 1024 * 1024)
|
||||||
snprintf(out, out_sz, "%s%s %.1f MB", dim, kind,
|
snprintf(out, out_sz, "%s%s %.1f MB", dim, kind,
|
||||||
@@ -580,6 +681,8 @@ char *cst_attachments_to_json(const cst_attachment_t *list, int n) {
|
|||||||
cJSON_AddNumberToObject(o, "width", a->width);
|
cJSON_AddNumberToObject(o, "width", a->width);
|
||||||
cJSON_AddNumberToObject(o, "height", a->height);
|
cJSON_AddNumberToObject(o, "height", a->height);
|
||||||
}
|
}
|
||||||
|
if (a->pages > 0)
|
||||||
|
cJSON_AddNumberToObject(o, "pages", a->pages);
|
||||||
if (a->detail[0] && strcmp(a->detail, "auto") != 0)
|
if (a->detail[0] && strcmp(a->detail, "auto") != 0)
|
||||||
cJSON_AddStringToObject(o, "detail", a->detail);
|
cJSON_AddStringToObject(o, "detail", a->detail);
|
||||||
if (a->origin[0])
|
if (a->origin[0])
|
||||||
@@ -628,6 +731,8 @@ cst_attachment_t *cst_attachments_from_json(const void *array_json, int *n_out)
|
|||||||
a->width = cJSON_IsNumber(v) ? v->valueint : 0;
|
a->width = cJSON_IsNumber(v) ? v->valueint : 0;
|
||||||
v = cJSON_GetObjectItemCaseSensitive(it, "height");
|
v = cJSON_GetObjectItemCaseSensitive(it, "height");
|
||||||
a->height = cJSON_IsNumber(v) ? v->valueint : 0;
|
a->height = cJSON_IsNumber(v) ? v->valueint : 0;
|
||||||
|
v = cJSON_GetObjectItemCaseSensitive(it, "pages");
|
||||||
|
a->pages = cJSON_IsNumber(v) ? v->valueint : 0;
|
||||||
v = cJSON_GetObjectItemCaseSensitive(it, "detail");
|
v = cJSON_GetObjectItemCaseSensitive(it, "detail");
|
||||||
snprintf(a->detail, sizeof(a->detail), "%s",
|
snprintf(a->detail, sizeof(a->detail), "%s",
|
||||||
cJSON_IsString(v) ? v->valuestring : "auto");
|
cJSON_IsString(v) ? v->valuestring : "auto");
|
||||||
|
|||||||
+169
@@ -390,6 +390,175 @@ int cst_clipboard_image(char **data, size_t *n, char *err, size_t err_sz) {
|
|||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/* --- text clipboard ------------------------------------------------------ */
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
|
||||||
|
int cst_clipboard_text(char **data, size_t *n, char *err, size_t err_sz) {
|
||||||
|
*data = NULL;
|
||||||
|
*n = 0;
|
||||||
|
if (!IsClipboardFormatAvailable(CF_UNICODETEXT) || !OpenClipboard(NULL)) {
|
||||||
|
set_err(err, err_sz, "no text on the clipboard");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
HANDLE h = GetClipboardData(CF_UNICODETEXT);
|
||||||
|
const wchar_t *w = h ? GlobalLock(h) : NULL;
|
||||||
|
if (!w) {
|
||||||
|
if (h)
|
||||||
|
GlobalUnlock(h);
|
||||||
|
CloseClipboard();
|
||||||
|
set_err(err, err_sz, "no text on the clipboard");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
char *utf8 = os_wide_to_utf8(w);
|
||||||
|
size_t len = utf8 ? strlen(utf8) : 0;
|
||||||
|
GlobalUnlock(h);
|
||||||
|
CloseClipboard();
|
||||||
|
if (!utf8 || !len) {
|
||||||
|
free(utf8);
|
||||||
|
set_err(err, err_sz, "no text on the clipboard");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (len > CST_CLIPBOARD_TEXT_MAX) {
|
||||||
|
free(utf8);
|
||||||
|
set_err(err, err_sz, "clipboard text is too large");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
*data = utf8;
|
||||||
|
*n = len;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_clipboard_set_text(const char *text, size_t n, char *err,
|
||||||
|
size_t err_sz) {
|
||||||
|
if (!text || os_clipboard_set(text, n) != 0) {
|
||||||
|
set_err(err, err_sz, "could not put text on the clipboard");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
|
/* stdout of a clipboard reader, capped and NUL-terminated. */
|
||||||
|
static int clip_read(const char *cmd, const char *tool, char **data, size_t *n,
|
||||||
|
char *err, size_t err_sz) {
|
||||||
|
os_run_result_t r;
|
||||||
|
memset(&r, 0, sizeof(r));
|
||||||
|
if (os_run_shell_c(NULL, cmd, CST_CLIPBOARD_TEXT_MAX + 1, &r) != 0) {
|
||||||
|
os_run_result_free(&r);
|
||||||
|
set_err(err, err_sz, "could not read the clipboard");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (r.exit_code != 0 || r.out_len == 0) {
|
||||||
|
int missing = r.exit_code == 127;
|
||||||
|
os_run_result_free(&r);
|
||||||
|
if (missing) {
|
||||||
|
char msg[96];
|
||||||
|
snprintf(msg, sizeof(msg), "reading the clipboard needs %s", tool);
|
||||||
|
set_err(err, err_sz, msg);
|
||||||
|
} else {
|
||||||
|
set_err(err, err_sz, "no text on the clipboard");
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (r.out_len > CST_CLIPBOARD_TEXT_MAX) {
|
||||||
|
os_run_result_free(&r);
|
||||||
|
set_err(err, err_sz, "clipboard text is too large");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
char *copy = malloc(r.out_len + 1);
|
||||||
|
if (!copy) {
|
||||||
|
os_run_result_free(&r);
|
||||||
|
set_err(err, err_sz, "out of memory");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
memcpy(copy, r.out, r.out_len);
|
||||||
|
copy[r.out_len] = 0;
|
||||||
|
os_run_result_free(&r);
|
||||||
|
*data = copy;
|
||||||
|
*n = r.out_len;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_clipboard_text(char **data, size_t *n, char *err, size_t err_sz) {
|
||||||
|
*data = NULL;
|
||||||
|
*n = 0;
|
||||||
|
#ifdef __APPLE__
|
||||||
|
return clip_read("pbpaste 2>/dev/null", "pbpaste", data, n, err, err_sz);
|
||||||
|
#else
|
||||||
|
const char *wl = getenv("WAYLAND_DISPLAY");
|
||||||
|
if (wl && *wl)
|
||||||
|
return clip_read("wl-paste --type text/plain --no-newline 2>/dev/null",
|
||||||
|
"wl-clipboard (wl-paste)", data, n, err, err_sz);
|
||||||
|
const char *x = getenv("DISPLAY");
|
||||||
|
if (x && *x)
|
||||||
|
return clip_read("xclip -selection clipboard -o 2>/dev/null", "xclip",
|
||||||
|
data, n, err, err_sz);
|
||||||
|
set_err(err, err_sz, "no clipboard here (no Wayland or X11 display)");
|
||||||
|
return -1;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
int cst_clipboard_set_text(const char *text, size_t n, char *err,
|
||||||
|
size_t err_sz) {
|
||||||
|
if (!text)
|
||||||
|
return -1;
|
||||||
|
if (n > CST_CLIPBOARD_TEXT_MAX) {
|
||||||
|
set_err(err, err_sz, "text is too large for the clipboard");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
const char *fmt = NULL, *tool = NULL;
|
||||||
|
#ifdef __APPLE__
|
||||||
|
fmt = "pbcopy < '%s'";
|
||||||
|
tool = "pbcopy";
|
||||||
|
#else
|
||||||
|
if (getenv("WAYLAND_DISPLAY")) {
|
||||||
|
fmt = "wl-copy < '%s'";
|
||||||
|
tool = "wl-clipboard (wl-copy)";
|
||||||
|
} else if (getenv("DISPLAY")) {
|
||||||
|
fmt = "xclip -selection clipboard -i < '%s'";
|
||||||
|
tool = "xclip";
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
if (!fmt) {
|
||||||
|
set_err(err, err_sz, "no clipboard here (no Wayland or X11 display)");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
char tmp[OS_PATH_MAX];
|
||||||
|
int fd = os_mkstemp(tmp, sizeof(tmp), "cst-clip");
|
||||||
|
if (fd < 0) {
|
||||||
|
set_err(err, err_sz, "could not create a temporary file");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
FILE *fp = fdopen(fd, "wb");
|
||||||
|
int ok = fp && fwrite(text, 1, n, fp) == n && fclose(fp) == 0;
|
||||||
|
if (!ok) {
|
||||||
|
if (fp)
|
||||||
|
fclose(fp);
|
||||||
|
remove(tmp);
|
||||||
|
set_err(err, err_sz, "could not write the text");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
char cmd[OS_PATH_MAX + 64];
|
||||||
|
snprintf(cmd, sizeof(cmd), fmt, tmp);
|
||||||
|
os_run_result_t r;
|
||||||
|
memset(&r, 0, sizeof(r));
|
||||||
|
int rc = os_run_shell_c(NULL, cmd, 4096, &r);
|
||||||
|
int code = r.exit_code;
|
||||||
|
os_run_result_free(&r);
|
||||||
|
remove(tmp);
|
||||||
|
if (rc != 0 || code != 0) {
|
||||||
|
char msg[96];
|
||||||
|
snprintf(msg, sizeof(msg), "writing the clipboard needs %s", tool);
|
||||||
|
set_err(err, err_sz, msg);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif
|
||||||
|
|
||||||
static int hexval(int c) {
|
static int hexval(int c) {
|
||||||
if (c >= '0' && c <= '9')
|
if (c >= '0' && c <= '9')
|
||||||
return c - '0';
|
return c - '0';
|
||||||
|
|||||||
+87
-20
@@ -255,28 +255,78 @@ static cJSON *image_part(const cst_llm_request_t *req,
|
|||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Append every attachment of m as image parts (or a note saying why not). */
|
/* One stored document as a text part: its text, framed so the model knows
|
||||||
static void parts_add_images(const cst_llm_request_t *req, cJSON *parts,
|
* where it came from. Documents are plain text on the wire, so a text-only
|
||||||
const cst_message_t *m, size_t *budget) {
|
* model and CST_NO_IMAGES both still get them. */
|
||||||
|
static cJSON *document_part(const cst_llm_request_t *req,
|
||||||
|
const cst_attachment_t *a, size_t *budget,
|
||||||
|
char *why, size_t why_sz) {
|
||||||
|
why[0] = 0;
|
||||||
|
if (!req->session_path || !req->session_path[0]) {
|
||||||
|
snprintf(why, why_sz, "there is no session store to read it from");
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
char err[256];
|
||||||
|
size_t n = 0;
|
||||||
|
char *text = cst_attach_read(req->session_path, a, &n, err, sizeof(err));
|
||||||
|
if (!text) {
|
||||||
|
snprintf(why, why_sz, "%s", err);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
int cut = 0;
|
||||||
|
if (n > *budget) {
|
||||||
|
n = *budget;
|
||||||
|
cut = 1;
|
||||||
|
}
|
||||||
|
*budget -= n;
|
||||||
|
char desc[128];
|
||||||
|
cst_attach_describe(a, desc, sizeof(desc));
|
||||||
|
cst_buf_t b = {0};
|
||||||
|
cst_buf_printf(&b, "[document \"%.120s\" (%s)%s]\n", a->name, desc,
|
||||||
|
cut ? " -- only the start is included" : "");
|
||||||
|
cst_buf_append(&b, text, n);
|
||||||
|
cst_buf_puts(&b, cut ? "\n[-- document truncated --]"
|
||||||
|
: "\n[-- end of document --]");
|
||||||
|
free(text);
|
||||||
|
cJSON *p = cJSON_CreateObject();
|
||||||
|
cJSON_AddStringToObject(p, "type", "text");
|
||||||
|
cJSON_AddStringToObject(p, "text", b.buf ? b.buf : "");
|
||||||
|
cst_buf_free(&b);
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Append every attachment of m: images as image parts (or a note saying why
|
||||||
|
* not), documents inline as text. */
|
||||||
|
static void parts_add_attachments(const cst_llm_request_t *req, cJSON *parts,
|
||||||
|
const cst_message_t *m, size_t *budget,
|
||||||
|
size_t *doc_budget, const char *img_off) {
|
||||||
int n = m->nattach;
|
int n = m->nattach;
|
||||||
if (n > CST_ATTACH_MAX_COUNT) {
|
if (n > CST_ATTACH_MAX_COUNT) {
|
||||||
char note[160];
|
char note[160];
|
||||||
snprintf(note, sizeof(note), "[%d of %d images sent]", CST_ATTACH_MAX_COUNT,
|
snprintf(note, sizeof(note),
|
||||||
n);
|
"[only the first %d of %d attachments are sent]",
|
||||||
|
CST_ATTACH_MAX_COUNT, n);
|
||||||
parts_add_text(parts, cJSON_CreateString(note));
|
parts_add_text(parts, cJSON_CreateString(note));
|
||||||
n = CST_ATTACH_MAX_COUNT;
|
n = CST_ATTACH_MAX_COUNT;
|
||||||
}
|
}
|
||||||
for (int i = 0; i < n; i++) {
|
for (int i = 0; i < n; i++) {
|
||||||
const cst_attachment_t *a = &m->attach[i];
|
const cst_attachment_t *a = &m->attach[i];
|
||||||
|
int is_img = cst_attach_is_image(a);
|
||||||
char why[256];
|
char why[256];
|
||||||
cJSON *p = image_part(req, a, budget, why, sizeof(why));
|
cJSON *p = NULL;
|
||||||
|
if (is_img && img_off)
|
||||||
|
snprintf(why, sizeof(why), "%s", img_off);
|
||||||
|
else if (is_img)
|
||||||
|
p = image_part(req, a, budget, why, sizeof(why));
|
||||||
|
else
|
||||||
|
p = document_part(req, a, doc_budget, why, sizeof(why));
|
||||||
if (p) {
|
if (p) {
|
||||||
cJSON_AddItemToArray(parts, p);
|
cJSON_AddItemToArray(parts, p);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
char note[512];
|
char note[512];
|
||||||
snprintf(note, sizeof(note), "[image \"%.120s\" not sent: %.200s]",
|
snprintf(note, sizeof(note), "[%s \"%.120s\" not sent: %.200s]",
|
||||||
a->name, why);
|
is_img ? "image" : "document", a->name, why);
|
||||||
parts_add_text(parts, cJSON_CreateString(note));
|
parts_add_text(parts, cJSON_CreateString(note));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -284,8 +334,10 @@ static void parts_add_images(const cst_llm_request_t *req, cJSON *parts,
|
|||||||
/* Images a tool result produced are deferred: an assistant tool_call must be
|
/* Images a tool result produced are deferred: an assistant tool_call must be
|
||||||
* answered by tool messages only, so they ride in one extra user message
|
* answered by tool messages only, so they ride in one extra user message
|
||||||
* after the run of tool messages ends. *pending collects it. */
|
* after the run of tool messages ends. *pending collects it. */
|
||||||
static void defer_images(const cst_llm_request_t *req, const cst_message_t *m,
|
static void defer_attachments(const cst_llm_request_t *req,
|
||||||
cJSON **pending, size_t *budget) {
|
const cst_message_t *m, cJSON **pending,
|
||||||
|
size_t *budget, size_t *doc_budget,
|
||||||
|
const char *img_off) {
|
||||||
cJSON *p = *pending;
|
cJSON *p = *pending;
|
||||||
if (!p) {
|
if (!p) {
|
||||||
p = cJSON_CreateObject();
|
p = cJSON_CreateObject();
|
||||||
@@ -293,15 +345,18 @@ static void defer_images(const cst_llm_request_t *req, const cst_message_t *m,
|
|||||||
cJSON_AddArrayToObject(p, "content");
|
cJSON_AddArrayToObject(p, "content");
|
||||||
*pending = p;
|
*pending = p;
|
||||||
}
|
}
|
||||||
parts_add_images(req, cJSON_GetObjectItemCaseSensitive(p, "content"), m,
|
parts_add_attachments(req, cJSON_GetObjectItemCaseSensitive(p, "content"), m,
|
||||||
budget);
|
budget, doc_budget, img_off);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* m's text plus one line per image it would have carried (malloc'd). */
|
/* m's text plus one line per image it would have carried (malloc'd); a
|
||||||
|
* document is not affected (it is text and is sent as such). */
|
||||||
static char *image_notes(const cst_message_t *m, const char *why) {
|
static char *image_notes(const cst_message_t *m, const char *why) {
|
||||||
cst_buf_t b = {0};
|
cst_buf_t b = {0};
|
||||||
cst_buf_puts(&b, m->content ? m->content : "");
|
cst_buf_puts(&b, m->content ? m->content : "");
|
||||||
for (int i = 0; i < m->nattach; i++) {
|
for (int i = 0; i < m->nattach; i++) {
|
||||||
|
if (!cst_attach_is_image(&m->attach[i]))
|
||||||
|
continue;
|
||||||
char d[128];
|
char d[128];
|
||||||
cst_attach_describe(&m->attach[i], d, sizeof(d));
|
cst_attach_describe(&m->attach[i], d, sizeof(d));
|
||||||
cst_buf_printf(&b, "%s[image \"%.120s\" (%s) not sent: %s]",
|
cst_buf_printf(&b, "%s[image \"%.120s\" (%s) not sent: %s]",
|
||||||
@@ -394,6 +449,7 @@ char *cst_llm_build_body(const cst_llm_request_t *req) {
|
|||||||
cJSON *prev_user = NULL; /* merge consecutive user messages */
|
cJSON *prev_user = NULL; /* merge consecutive user messages */
|
||||||
cJSON *pending = NULL; /* images from tool results, waiting for a break */
|
cJSON *pending = NULL; /* images from tool results, waiting for a break */
|
||||||
size_t img_budget = CST_ATTACH_MAX_TOTAL;
|
size_t img_budget = CST_ATTACH_MAX_TOTAL;
|
||||||
|
size_t doc_budget = CST_ATTACH_MAX_TOTAL;
|
||||||
const char *noimg = getenv("CST_NO_IMAGES");
|
const char *noimg = getenv("CST_NO_IMAGES");
|
||||||
const char *img_off =
|
const char *img_off =
|
||||||
noimg && noimg[0] && noimg[0] != '0'
|
noimg && noimg[0] && noimg[0] != '0'
|
||||||
@@ -407,15 +463,26 @@ char *cst_llm_build_body(const cst_llm_request_t *req) {
|
|||||||
cst_role_t role = m->role;
|
cst_role_t role = m->role;
|
||||||
if (role == CST_ROLE_TOOL && !m->tool_call_id[0])
|
if (role == CST_ROLE_TOOL && !m->tool_call_id[0])
|
||||||
role = CST_ROLE_USER;
|
role = CST_ROLE_USER;
|
||||||
int has_img = m->attach && m->nattach > 0;
|
int n_att = m->attach ? m->nattach : 0;
|
||||||
|
int has_img = 0, has_doc = 0;
|
||||||
|
for (int k = 0; k < n_att; k++) {
|
||||||
|
if (cst_attach_is_image(&m->attach[k]))
|
||||||
|
has_img = 1;
|
||||||
|
else
|
||||||
|
has_doc = 1;
|
||||||
|
}
|
||||||
char *noted = NULL;
|
char *noted = NULL;
|
||||||
if (has_img && img_off) {
|
if (has_img && img_off) {
|
||||||
|
has_img = 0;
|
||||||
|
if (!has_doc) {
|
||||||
/* plain text: text-only APIs may refuse content arrays */
|
/* plain text: text-only APIs may refuse content arrays */
|
||||||
noted = image_notes(m, img_off);
|
noted = image_notes(m, img_off);
|
||||||
if (noted)
|
if (noted)
|
||||||
mm.content = noted;
|
mm.content = noted;
|
||||||
has_img = 0;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
/* a document needs a content array even when no image can go out */
|
||||||
|
int att_parts = has_img || has_doc;
|
||||||
|
|
||||||
/* Every tool_call must be answered by tool messages, so tool-produced
|
/* Every tool_call must be answered by tool messages, so tool-produced
|
||||||
* images only leave the queue when the run of tool results is over. */
|
* images only leave the queue when the run of tool results is over. */
|
||||||
@@ -431,7 +498,7 @@ char *cst_llm_build_body(const cst_llm_request_t *req) {
|
|||||||
cJSON *pc = prev_user
|
cJSON *pc = prev_user
|
||||||
? cJSON_GetObjectItemCaseSensitive(prev_user, "content")
|
? cJSON_GetObjectItemCaseSensitive(prev_user, "content")
|
||||||
: NULL;
|
: NULL;
|
||||||
if (!has_img && !cJSON_IsArray(pc)) {
|
if (!att_parts && !cJSON_IsArray(pc)) {
|
||||||
cJSON *c = clean_string(m->content, 0);
|
cJSON *c = clean_string(m->content, 0);
|
||||||
if (prev_user) {
|
if (prev_user) {
|
||||||
cst_buf_t b = {0};
|
cst_buf_t b = {0};
|
||||||
@@ -463,8 +530,8 @@ char *cst_llm_build_body(const cst_llm_request_t *req) {
|
|||||||
}
|
}
|
||||||
cJSON *parts = content_parts(o);
|
cJSON *parts = content_parts(o);
|
||||||
parts_add_text(parts, clean_string(m->content, 0));
|
parts_add_text(parts, clean_string(m->content, 0));
|
||||||
if (has_img)
|
if (att_parts)
|
||||||
parts_add_images(req, parts, m, &img_budget);
|
parts_add_attachments(req, parts, m, &img_budget, &doc_budget, img_off);
|
||||||
free(noted);
|
free(noted);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -498,8 +565,8 @@ char *cst_llm_build_body(const cst_llm_request_t *req) {
|
|||||||
cJSON_AddTrueToObject(o, "prefix");
|
cJSON_AddTrueToObject(o, "prefix");
|
||||||
}
|
}
|
||||||
cJSON_AddItemToArray(msgs, o);
|
cJSON_AddItemToArray(msgs, o);
|
||||||
if (role == CST_ROLE_TOOL && has_img)
|
if (role == CST_ROLE_TOOL && n_att)
|
||||||
defer_images(req, m, &pending, &img_budget);
|
defer_attachments(req, m, &pending, &img_budget, &doc_budget, img_off);
|
||||||
free(noted);
|
free(noted);
|
||||||
}
|
}
|
||||||
if (pending)
|
if (pending)
|
||||||
|
|||||||
+25
-8
@@ -26,6 +26,8 @@ static void usage(const char *argv0) {
|
|||||||
" (last used)\n"
|
" (last used)\n"
|
||||||
" -m, --model NAME model (last used for the provider)\n"
|
" -m, --model NAME model (last used for the provider)\n"
|
||||||
" -e, --effort LEVEL low | medium | high | max (last used)\n"
|
" -e, --effort LEVEL low | medium | high | max (last used)\n"
|
||||||
|
" -M, --mode NAME coding (default, code framed) | general\n"
|
||||||
|
" (documents, data, research; last used)\n"
|
||||||
" -w, --workspace DIR project directory (default: current)\n"
|
" -w, --workspace DIR project directory (default: current)\n"
|
||||||
" -c, --continue reopen the last session (default)\n"
|
" -c, --continue reopen the last session (default)\n"
|
||||||
" -N, --new start a new session\n"
|
" -N, --new start a new session\n"
|
||||||
@@ -59,13 +61,13 @@ static void usage(const char *argv0) {
|
|||||||
"API keys: the TUI asks for a missing key and saves it per provider\n"
|
"API keys: the TUI asks for a missing key and saves it per provider\n"
|
||||||
" in the config dir (credentials, mode 0600); /key changes it.\n"
|
" in the config dir (credentials, mode 0600); /key changes it.\n"
|
||||||
" Fallbacks: <provider env> (DEEPSEEK_API_KEY, ...), CST_API_KEY.\n"
|
" Fallbacks: <provider env> (DEEPSEEK_API_KEY, ...), CST_API_KEY.\n"
|
||||||
"Settings: provider, models and effort are remembered in\n"
|
"Settings: provider, models, effort and mode are remembered in\n"
|
||||||
" settings.json next to the credentials.\n"
|
" settings.json next to the credentials.\n"
|
||||||
"Env: CST_PROVIDER CST_MODEL CST_EFFORT CST_PROTOCOL CST_TOOLS\n"
|
"Env: CST_PROVIDER CST_MODEL CST_EFFORT CST_MODE CST_PROTOCOL\n"
|
||||||
" CST_CONFIRM CST_MAX_STEPS CST_TOOL_OUT_CAP CST_CTX_WINDOW\n"
|
" CST_TOOLS CST_CONFIRM CST_MAX_STEPS CST_TOOL_OUT_CAP\n"
|
||||||
" CST_CTX_TOKENS CST_AUTO_COMPACT=0 CST_SHELL_TIMEOUT\n"
|
" CST_CTX_WINDOW CST_CTX_TOKENS CST_AUTO_COMPACT=0\n"
|
||||||
" CST_METRICS=0 CST_RECORD_SSE CST_REPLAY_SSE CST_MODELS\n"
|
" CST_SHELL_TIMEOUT CST_METRICS=0 CST_RECORD_SSE CST_REPLAY_SSE\n"
|
||||||
" CST_APPROVAL=ask|auto|yolo CST_AUTO_APPROVE=1 (=yolo)\n"
|
" CST_MODELS CST_APPROVAL=ask|auto|yolo CST_AUTO_APPROVE=1 (=yolo)\n"
|
||||||
" CST_SANDBOX=off CST_SHELL CST_NO_PROJECT_NOTES=1 NO_COLOR\n"
|
" CST_SANDBOX=off CST_SHELL CST_NO_PROJECT_NOTES=1 NO_COLOR\n"
|
||||||
"TUI: type /help inside for commands and keys\n",
|
"TUI: type /help inside for commands and keys\n",
|
||||||
argv0);
|
argv0);
|
||||||
@@ -374,6 +376,7 @@ int main(int argc, char **argv) {
|
|||||||
{"approval", required_argument, 0, 'A'},
|
{"approval", required_argument, 0, 'A'},
|
||||||
{"confirm", required_argument, 0, 'C'},
|
{"confirm", required_argument, 0, 'C'},
|
||||||
{"effort", required_argument, 0, 'e'},
|
{"effort", required_argument, 0, 'e'},
|
||||||
|
{"mode", required_argument, 0, 'M'},
|
||||||
{"help", no_argument, 0, 'h'},
|
{"help", no_argument, 0, 'h'},
|
||||||
{"version", no_argument, 0, 'V'},
|
{"version", no_argument, 0, 'V'},
|
||||||
{"list-models", optional_argument, 0, 0x100},
|
{"list-models", optional_argument, 0, 0x100},
|
||||||
@@ -385,8 +388,8 @@ int main(int argc, char **argv) {
|
|||||||
{"web-open", no_argument, 0, 0x106},
|
{"web-open", no_argument, 0, 0x106},
|
||||||
{0, 0, 0, 0}};
|
{0, 0, 0, 0}};
|
||||||
int c;
|
int c;
|
||||||
while ((c = getopt_long(argc, argv, "w:s:b:k:m:P:r:n:T:Hp:dahVNcR:LyA:C:e:", opts, NULL)) !=
|
while ((c = getopt_long(argc, argv, "w:s:b:k:m:P:r:n:T:Hp:dahVNcR:LyA:C:e:M:",
|
||||||
-1) {
|
opts, NULL)) != -1) {
|
||||||
switch (c) {
|
switch (c) {
|
||||||
case 'w':
|
case 'w':
|
||||||
snprintf(cfg.workspace_root, sizeof(cfg.workspace_root), "%s", optarg);
|
snprintf(cfg.workspace_root, sizeof(cfg.workspace_root), "%s", optarg);
|
||||||
@@ -478,6 +481,14 @@ int main(int argc, char **argv) {
|
|||||||
}
|
}
|
||||||
snprintf(cfg.effort, sizeof(cfg.effort), "%s", optarg);
|
snprintf(cfg.effort, sizeof(cfg.effort), "%s", optarg);
|
||||||
break;
|
break;
|
||||||
|
case 'M':
|
||||||
|
if (!cst_mode_valid(optarg)) {
|
||||||
|
fprintf(stderr, "bad --mode %s (coding | general)\n", optarg);
|
||||||
|
curl_global_cleanup();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
snprintf(cfg.mode, sizeof(cfg.mode), "%s", optarg);
|
||||||
|
break;
|
||||||
case 'h':
|
case 'h':
|
||||||
usage(argv[0]);
|
usage(argv[0]);
|
||||||
curl_global_cleanup();
|
curl_global_cleanup();
|
||||||
@@ -557,6 +568,12 @@ int main(int argc, char **argv) {
|
|||||||
snprintf(cfg.effort, sizeof(cfg.effort), "%s",
|
snprintf(cfg.effort, sizeof(cfg.effort), "%s",
|
||||||
cst_effort_valid(e) ? e : CST_DEFAULT_EFFORT);
|
cst_effort_valid(e) ? e : CST_DEFAULT_EFFORT);
|
||||||
}
|
}
|
||||||
|
if (!cfg.mode[0]) {
|
||||||
|
const char *m = getenv("CST_MODE");
|
||||||
|
if (!cst_mode_valid(m))
|
||||||
|
m = saved.mode;
|
||||||
|
snprintf(cfg.mode, sizeof(cfg.mode), "%s", cst_mode_name(m));
|
||||||
|
}
|
||||||
if (!cfg.api_base[0]) {
|
if (!cfg.api_base[0]) {
|
||||||
const char *env_base = getenv("CST_API_BASE");
|
const char *env_base = getenv("CST_API_BASE");
|
||||||
if (!env_base || !env_base[0])
|
if (!env_base || !env_base[0])
|
||||||
|
|||||||
+1901
File diff suppressed because it is too large
Load Diff
+4
-1
@@ -452,7 +452,10 @@ char *cst_session_export_md(cst_session_t *s, const char *title,
|
|||||||
for (int k = 0; k < m->nattach; k++) {
|
for (int k = 0; k < m->nattach; k++) {
|
||||||
char d[128];
|
char d[128];
|
||||||
cst_attach_describe(&m->attach[k], d, sizeof(d));
|
cst_attach_describe(&m->attach[k], d, sizeof(d));
|
||||||
cst_buf_printf(&b, "- image `%s` (%s)\n", m->attach[k].name, d);
|
cst_buf_printf(&b, "- %s `%s` (%s)\n",
|
||||||
|
cst_attach_is_image(&m->attach[k]) ? "image"
|
||||||
|
: "document",
|
||||||
|
m->attach[k].name, d);
|
||||||
}
|
}
|
||||||
if (m->nattach)
|
if (m->nattach)
|
||||||
cst_buf_puts(&b, "\n");
|
cst_buf_puts(&b, "\n");
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ int cst_settings_load(cst_settings_t *s) {
|
|||||||
cJSON_GetObjectItemCaseSensitive(root, "provider"));
|
cJSON_GetObjectItemCaseSensitive(root, "provider"));
|
||||||
copy_str(s->effort, sizeof(s->effort),
|
copy_str(s->effort, sizeof(s->effort),
|
||||||
cJSON_GetObjectItemCaseSensitive(root, "effort"));
|
cJSON_GetObjectItemCaseSensitive(root, "effort"));
|
||||||
|
copy_str(s->mode, sizeof(s->mode),
|
||||||
|
cJSON_GetObjectItemCaseSensitive(root, "mode"));
|
||||||
copy_str(s->approval, sizeof(s->approval),
|
copy_str(s->approval, sizeof(s->approval),
|
||||||
cJSON_GetObjectItemCaseSensitive(root, "approval"));
|
cJSON_GetObjectItemCaseSensitive(root, "approval"));
|
||||||
s->show_thinking =
|
s->show_thinking =
|
||||||
@@ -86,6 +88,8 @@ int cst_settings_save(const cst_settings_t *s) {
|
|||||||
cJSON_AddStringToObject(root, "provider", s->provider);
|
cJSON_AddStringToObject(root, "provider", s->provider);
|
||||||
if (s->effort[0])
|
if (s->effort[0])
|
||||||
cJSON_AddStringToObject(root, "effort", s->effort);
|
cJSON_AddStringToObject(root, "effort", s->effort);
|
||||||
|
if (s->mode[0])
|
||||||
|
cJSON_AddStringToObject(root, "mode", s->mode);
|
||||||
if (s->approval[0])
|
if (s->approval[0])
|
||||||
cJSON_AddStringToObject(root, "approval", s->approval);
|
cJSON_AddStringToObject(root, "approval", s->approval);
|
||||||
if (s->show_thinking)
|
if (s->show_thinking)
|
||||||
@@ -135,6 +139,21 @@ int cst_approval_parse(const char *s, int *out) {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const char *const MODE_NAMES[] = {"coding", "general"};
|
||||||
|
|
||||||
|
int cst_mode_valid(const char *m) {
|
||||||
|
for (size_t i = 0; m && m[0] && i < sizeof(MODE_NAMES) / sizeof(*MODE_NAMES);
|
||||||
|
i++) {
|
||||||
|
if (strcmp(m, MODE_NAMES[i]) == 0)
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *cst_mode_name(const char *m) {
|
||||||
|
return cst_mode_valid(m) ? m : CST_DEFAULT_MODE;
|
||||||
|
}
|
||||||
|
|
||||||
const char *cst_settings_model(const cst_settings_t *s, const char *provider) {
|
const char *cst_settings_model(const cst_settings_t *s, const char *provider) {
|
||||||
for (int i = 0; provider && i < s->nmodels; i++) {
|
for (int i = 0; provider && i < s->nmodels; i++) {
|
||||||
if (strcmp(s->models[i].provider, provider) == 0 && s->models[i].model[0])
|
if (strcmp(s->models[i].provider, provider) == 0 && s->models[i].model[0])
|
||||||
@@ -148,6 +167,8 @@ int cst_settings_remember(const cst_agent_config_t *cfg) {
|
|||||||
cst_settings_load(&s);
|
cst_settings_load(&s);
|
||||||
snprintf(s.provider, sizeof(s.provider), "%s", cfg->provider);
|
snprintf(s.provider, sizeof(s.provider), "%s", cfg->provider);
|
||||||
snprintf(s.effort, sizeof(s.effort), "%s", cfg->effort);
|
snprintf(s.effort, sizeof(s.effort), "%s", cfg->effort);
|
||||||
|
if (cfg->mode[0])
|
||||||
|
snprintf(s.mode, sizeof(s.mode), "%s", cfg->mode);
|
||||||
int i = 0;
|
int i = 0;
|
||||||
while (i < s.nmodels && strcmp(s.models[i].provider, cfg->provider) != 0)
|
while (i < s.nmodels && strcmp(s.models[i].provider, cfg->provider) != 0)
|
||||||
i++;
|
i++;
|
||||||
@@ -176,6 +197,13 @@ int cst_settings_remember_thinking(int show_thinking) {
|
|||||||
return cst_settings_save(&s);
|
return cst_settings_save(&s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int cst_settings_remember_mode(const char *mode) {
|
||||||
|
cst_settings_t s;
|
||||||
|
cst_settings_load(&s);
|
||||||
|
snprintf(s.mode, sizeof(s.mode), "%s", cst_mode_name(mode));
|
||||||
|
return cst_settings_save(&s);
|
||||||
|
}
|
||||||
|
|
||||||
static void real_ws(const char *ws, char *out, size_t n) {
|
static void real_ws(const char *ws, char *out, size_t n) {
|
||||||
if (!ws || os_realpath(ws, out, n) != 0)
|
if (!ws || os_realpath(ws, out, n) != 0)
|
||||||
snprintf(out, n, "%s", ws ? ws : "");
|
snprintf(out, n, "%s", ws ? ws : "");
|
||||||
|
|||||||
+5
-5
@@ -40,7 +40,7 @@ static const struct {
|
|||||||
"runs commands as needed.",
|
"runs commands as needed.",
|
||||||
"", 0},
|
"", 0},
|
||||||
{"explore",
|
{"explore",
|
||||||
"Fast read-only explorer: searches and reads the codebase to answer a "
|
"Fast read-only explorer: searches and reads the workspace to answer a "
|
||||||
"question (where is X, how does Y work, which files do Z). Several can "
|
"question (where is X, how does Y work, which files do Z). Several can "
|
||||||
"run at once.",
|
"run at once.",
|
||||||
"You only read. Search broadly first (grep, glob, list_dir), then read "
|
"You only read. Search broadly first (grep, glob, list_dir), then read "
|
||||||
@@ -49,11 +49,11 @@ static const struct {
|
|||||||
"you looked for and did not find.",
|
"you looked for and did not find.",
|
||||||
1},
|
1},
|
||||||
{"plan",
|
{"plan",
|
||||||
"Read-only planner: investigates the code involved and returns a "
|
"Read-only planner: investigates the material involved and returns a "
|
||||||
"step-by-step implementation plan.",
|
"step-by-step implementation plan.",
|
||||||
"You only read. Investigate the code involved, then return a concrete "
|
"You only read. Investigate what is involved, then return a concrete "
|
||||||
"plan: the files to change and what to change in each, risks and edge "
|
"plan: what to produce or change and where, risks and edge cases, and "
|
||||||
"cases, and how to verify. Sketch code only where it removes ambiguity.",
|
"how to check the result. Sketch code only where it removes ambiguity.",
|
||||||
1},
|
1},
|
||||||
};
|
};
|
||||||
#define NBUILTIN ((int)(sizeof(BUILTIN) / sizeof(BUILTIN[0])))
|
#define NBUILTIN ((int)(sizeof(BUILTIN) / sizeof(BUILTIN[0])))
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#include "cst_tools.h"
|
#include "cst_tools.h"
|
||||||
|
#include "cst_clipboard.h"
|
||||||
#include "cst_util.h"
|
#include "cst_util.h"
|
||||||
#include "tools_internal.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);
|
1);
|
||||||
return tool_set_resultf(r, 0, "user answered: %s", ans);
|
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));
|
cst_attachment_t *a = cst_attach_import_file(session, full, err, sizeof(err));
|
||||||
if (!a)
|
if (!a)
|
||||||
return tool_set_resultf(r, 1, "error: cannot show %.200s: %s", path, err);
|
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");
|
snprintf(a->origin, sizeof(a->origin), "view_image");
|
||||||
const char *d = tool_arg(c, "detail");
|
const char *d = tool_arg(c, "detail");
|
||||||
if (d && (strcmp(d, "low") == 0 || strcmp(d, "high") == 0))
|
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)"}},
|
{"detail", S, 0, "low or high (provider hint; default auto)"}},
|
||||||
CST_TP_READ, 0, cst_tool_view_image, NULL, NO_DYN},
|
CST_TP_READ, 0, cst_tool_view_image, NULL, NO_DYN},
|
||||||
{"run_shell",
|
{"run_shell",
|
||||||
"Run a non-interactive shell command in the workspace root for builds, "
|
"Run a non-interactive shell command in the workspace root: builds, "
|
||||||
"tests, git and other CLIs (stdin is empty; long runs are killed). "
|
"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 "
|
"Returns stdout+stderr with colors stripped and long output cut to head "
|
||||||
"+ tail. Prefer read_file/grep/glob/list_dir over cat/grep/find.",
|
"+ tail. Prefer read_file/grep/glob/list_dir over cat/grep/find.",
|
||||||
"Run a shell command in the workspace root (non-interactive, stdin is "
|
"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"},
|
{{"question", S, 1, "the question"},
|
||||||
{"options", S, 0, "suggested answers, one per line"}},
|
{"options", S, 0, "suggested answers, one per line"}},
|
||||||
CST_TP_ASK, 0, cst_tool_ask_user, NULL, NO_DYN},
|
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])))
|
#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);
|
cst_tool_result_t *r);
|
||||||
int cst_tool_ask_user(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
int cst_tool_ask_user(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||||
cst_tool_result_t *r);
|
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,
|
int cst_tool_web_fetch(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||||
cst_tool_result_t *r);
|
cst_tool_result_t *r);
|
||||||
int cst_tool_view_image(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
int cst_tool_view_image(const cst_tool_ctx_t *ctx, const ctl_tool_call_t *c,
|
||||||
|
|||||||
@@ -2354,6 +2354,7 @@ static const char *const TIPS[] = {
|
|||||||
"Ctrl+P opens the command palette",
|
"Ctrl+P opens the command palette",
|
||||||
"Type @ to mention a file in your message",
|
"Type @ to mention a file in your message",
|
||||||
"/effort sets reasoning effort: low, medium, high, max",
|
"/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",
|
"/model lists the models your API key can use",
|
||||||
"/undo reverts the file changes of the last turn",
|
"/undo reverts the file changes of the last turn",
|
||||||
"/provider switches provider; keys are saved per provider",
|
"/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++) {
|
for (int k = 0; k < m->nattach; k++) {
|
||||||
char d[128];
|
char d[128];
|
||||||
cst_attach_describe(&m->attach[k], d, sizeof(d));
|
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);
|
block_set_body(b, ub.buf ? ub.buf : "", ub.len);
|
||||||
cst_buf_free(&ub);
|
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);
|
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) {
|
static void cmd_provider(tui_t *t, char *arg, int *quit) {
|
||||||
(void)quit;
|
(void)quit;
|
||||||
cst_agent_config_t *cfg = cst_agent_config_mut(t->agent);
|
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)"
|
: !same ? "this run only (--api-key / CST_API_KEY)"
|
||||||
: strcmp(from, "saved") == 0 ? "saved (/key to replace)"
|
: strcmp(from, "saved") == 0 ? "saved (/key to replace)"
|
||||||
: from);
|
: 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_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];
|
char appr[64];
|
||||||
approval_label(t, appr, sizeof(appr));
|
approval_label(t, appr, sizeof(appr));
|
||||||
push_system_line(t, " approval %s — %s", appr,
|
push_system_line(t, " approval %s — %s", appr,
|
||||||
approval_help(approval_mode(t)));
|
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");
|
cfg->plan_mode ? "Plan (read-only tools)" : "Build");
|
||||||
const char *notes = cst_agent_notes(t->agent);
|
const char *notes = cst_agent_notes(t->agent);
|
||||||
push_system_line(t, " notes %s",
|
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);
|
cst_agent_config(t->agent)->model);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* /attach: stage images for the next user message. The bytes are copied into
|
/* /attach paste: the clipboard image if there is one, else its text goes
|
||||||
* the session's attachment directory now, so a picture picked from /tmp keeps
|
* into the composer (CR dropped, control bytes kept out, INPUT_CAP respected). */
|
||||||
* working after the file is gone. */
|
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) {
|
static void cmd_attach(tui_t *t, char *arg, int *quit) {
|
||||||
(void)quit;
|
(void)quit;
|
||||||
int n = cst_agent_pending_count(t->agent);
|
int n = cst_agent_pending_count(t->agent);
|
||||||
if (!arg || !arg[0] || strcmp(arg, "list") == 0) {
|
if (!arg || !arg[0] || strcmp(arg, "list") == 0) {
|
||||||
if (n == 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;
|
return;
|
||||||
}
|
}
|
||||||
push_system_line(t, "%d image%s with the next message (sent once):", n,
|
push_system_line(t, "%d attachment%s with the next message (sent once):",
|
||||||
n > 1 ? "s" : "");
|
n, n > 1 ? "s" : "");
|
||||||
for (int i = 0; i < n; i++) {
|
for (int i = 0; i < n; i++) {
|
||||||
char line[256];
|
char line[256];
|
||||||
if (cst_agent_pending_describe(t->agent, i, line, sizeof(line)) == 0)
|
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) {
|
if (strcmp(arg, "clear") == 0 || strcmp(arg, "none") == 0) {
|
||||||
cst_agent_pending_clear(t->agent);
|
cst_agent_pending_clear(t->agent);
|
||||||
push_system_line(t, "Dropped the picked images.");
|
push_system_line(t, "Dropped the attachments.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
char err[256];
|
char err[256];
|
||||||
@@ -4856,7 +4916,24 @@ static void cmd_attach(tui_t *t, char *arg, int *quit) {
|
|||||||
char *img = NULL;
|
char *img = NULL;
|
||||||
size_t len = 0;
|
size_t len = 0;
|
||||||
if (cst_clipboard_image(&img, &len, err, sizeof(err)) != 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;
|
return;
|
||||||
}
|
}
|
||||||
char mime[32], name[32];
|
char mime[32], name[32];
|
||||||
@@ -4881,19 +4958,29 @@ static void cmd_attach(tui_t *t, char *arg, int *quit) {
|
|||||||
report_attached(t, 1);
|
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");
|
FILE *f = fopen(path, "rb");
|
||||||
if (!f)
|
if (!f)
|
||||||
return 0;
|
return 0;
|
||||||
unsigned char head[16];
|
unsigned char head[64];
|
||||||
size_t got = fread(head, 1, sizeof(head), f);
|
size_t got = fread(head, 1, sizeof(head), f);
|
||||||
fclose(f);
|
fclose(f);
|
||||||
char mime[32];
|
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
|
/* A paste that is nothing but file paths (a file dropped on the terminal)
|
||||||
* terminal) attaches those files instead of typing the paths. */
|
* attaches those files instead of typing the paths. */
|
||||||
static void paste_image_paths(tui_t *t) {
|
static void paste_image_paths(tui_t *t) {
|
||||||
size_t from = t->paste_at, to = t->cur;
|
size_t from = t->paste_at, to = t->cur;
|
||||||
if (to <= from || to - from != t->in_len - t->paste_len0 ||
|
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)
|
if (n <= 0)
|
||||||
return;
|
return;
|
||||||
for (int i = 0; i < n; i++)
|
for (int i = 0; i < n; i++)
|
||||||
if (!file_is_image(paths[i]))
|
if (!file_is_attachable(paths[i]))
|
||||||
return;
|
return;
|
||||||
input_erase(t, from, to);
|
input_erase(t, from, to);
|
||||||
int added = 0;
|
int added = 0;
|
||||||
@@ -5441,6 +5528,8 @@ static const slash_cmd_t SLASH[] = {
|
|||||||
cmd_protocol},
|
cmd_protocol},
|
||||||
{"tools", "", "[on|off NAME|minimal]", "list or toggle tools", 0,
|
{"tools", "", "[on|off NAME|minimal]", "list or toggle tools", 0,
|
||||||
cmd_tools},
|
cmd_tools},
|
||||||
|
{"mode", "", "[coding|general]", "coding or general (non-code) framing", 1,
|
||||||
|
cmd_mode},
|
||||||
{"permissions", "perms", "[reset]", "show saved always-allow rules", 1,
|
{"permissions", "perms", "[reset]", "show saved always-allow rules", 1,
|
||||||
cmd_permissions},
|
cmd_permissions},
|
||||||
{"sandbox", "", "[reset-labels]", "shell sandbox and labeled folders", 1,
|
{"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,
|
{"export", "", "[file]", "write the transcript as Markdown", 1,
|
||||||
cmd_export},
|
cmd_export},
|
||||||
{"copy", "", "", "copy the last reply (OSC 52)", 1, cmd_copy},
|
{"copy", "", "", "copy the last reply (OSC 52)", 1, cmd_copy},
|
||||||
{"attach", "image", "[path|paste|clear]",
|
{"attach", "image|file", "[path|paste|clear]",
|
||||||
"send image(s) with the next message",
|
"send files (image, PDF, text) or clipboard text with the next message",
|
||||||
1, cmd_attach},
|
1, cmd_attach},
|
||||||
{"init", "", "", "have the agent write AGENTS.md", 0, cmd_init},
|
{"init", "", "", "have the agent write AGENTS.md", 0, cmd_init},
|
||||||
{"details", "", "", "expand/collapse all tool output", 1, cmd_details},
|
{"details", "", "", "expand/collapse all tool output", 1, cmd_details},
|
||||||
|
|||||||
+16
@@ -263,6 +263,7 @@ static cJSON *wui_state_new(wui_t *w) {
|
|||||||
cJSON_AddStringToObject(o, "provider", cfg->provider);
|
cJSON_AddStringToObject(o, "provider", cfg->provider);
|
||||||
cJSON_AddStringToObject(o, "model", cfg->model);
|
cJSON_AddStringToObject(o, "model", cfg->model);
|
||||||
cJSON_AddStringToObject(o, "effort", cfg->effort);
|
cJSON_AddStringToObject(o, "effort", cfg->effort);
|
||||||
|
cJSON_AddStringToObject(o, "mode", cst_mode_name(cfg->mode));
|
||||||
cJSON_AddStringToObject(o, "approval", cst_approval_name(cfg->approval));
|
cJSON_AddStringToObject(o, "approval", cst_approval_name(cfg->approval));
|
||||||
cJSON_AddStringToObject(o, "protocol_tools", cst_protocol_name(cfg->protocol));
|
cJSON_AddStringToObject(o, "protocol_tools", cst_protocol_name(cfg->protocol));
|
||||||
cJSON_AddBoolToObject(o, "plan_mode", cfg->plan_mode);
|
cJSON_AddBoolToObject(o, "plan_mode", cfg->plan_mode);
|
||||||
@@ -1670,6 +1671,12 @@ static int h_settings_get(cst_http_conn_t *c, void *ud) {
|
|||||||
cJSON *pr = cJSON_AddArrayToObject(o, "profiles");
|
cJSON *pr = cJSON_AddArrayToObject(o, "profiles");
|
||||||
for (int p = CST_TOOLS_DEFAULT; pr && p <= CST_TOOLS_MINIMAL; p++)
|
for (int p = CST_TOOLS_DEFAULT; pr && p <= CST_TOOLS_MINIMAL; p++)
|
||||||
cJSON_AddItemToArray(pr, cJSON_CreateString(cst_tool_profile_name(p)));
|
cJSON_AddItemToArray(pr, cJSON_CreateString(cst_tool_profile_name(p)));
|
||||||
|
cJSON_AddStringToObject(o, "mode", cst_mode_name(cfg->mode));
|
||||||
|
cJSON *md = cJSON_AddArrayToObject(o, "modes");
|
||||||
|
if (md) {
|
||||||
|
cJSON_AddItemToArray(md, cJSON_CreateString("coding"));
|
||||||
|
cJSON_AddItemToArray(md, cJSON_CreateString("general"));
|
||||||
|
}
|
||||||
cJSON *cf = cJSON_AddObjectToObject(o, "confirm");
|
cJSON *cf = cJSON_AddObjectToObject(o, "confirm");
|
||||||
if (cf) {
|
if (cf) {
|
||||||
cJSON_AddBoolToObject(cf, "shell", cfg->confirm_shell);
|
cJSON_AddBoolToObject(cf, "shell", cfg->confirm_shell);
|
||||||
@@ -1728,6 +1735,7 @@ static int h_settings_x(cst_http_conn_t *c, void *ud) {
|
|||||||
const cJSON *proto = cJSON_GetObjectItem(j, "protocol");
|
const cJSON *proto = cJSON_GetObjectItem(j, "protocol");
|
||||||
const cJSON *steps = cJSON_GetObjectItem(j, "max_steps");
|
const cJSON *steps = cJSON_GetObjectItem(j, "max_steps");
|
||||||
const cJSON *prof = cJSON_GetObjectItem(j, "tool_profile");
|
const cJSON *prof = cJSON_GetObjectItem(j, "tool_profile");
|
||||||
|
const cJSON *mode = cJSON_GetObjectItem(j, "mode");
|
||||||
const cJSON *tools = cJSON_GetObjectItem(j, "tools");
|
const cJSON *tools = cJSON_GetObjectItem(j, "tools");
|
||||||
const cJSON *conf = cJSON_GetObjectItem(j, "confirm");
|
const cJSON *conf = cJSON_GetObjectItem(j, "confirm");
|
||||||
cst_protocol_t p = cfg->protocol;
|
cst_protocol_t p = cfg->protocol;
|
||||||
@@ -1742,6 +1750,9 @@ static int h_settings_x(cst_http_conn_t *c, void *ud) {
|
|||||||
else if (prof && (!cJSON_IsString(prof) ||
|
else if (prof && (!cJSON_IsString(prof) ||
|
||||||
cst_tool_profile_parse(prof->valuestring, &profile) != 0))
|
cst_tool_profile_parse(prof->valuestring, &profile) != 0))
|
||||||
snprintf(bad, sizeof(bad), "未知的工具集(default 或 minimal)");
|
snprintf(bad, sizeof(bad), "未知的工具集(default 或 minimal)");
|
||||||
|
else if (mode && (!cJSON_IsString(mode) ||
|
||||||
|
!cst_mode_valid(mode->valuestring)))
|
||||||
|
snprintf(bad, sizeof(bad), "工作模式只能是 coding / general");
|
||||||
else if (tools && !cJSON_IsObject(tools))
|
else if (tools && !cJSON_IsObject(tools))
|
||||||
snprintf(bad, sizeof(bad), "tools 要是 {\"名字\": true/false}");
|
snprintf(bad, sizeof(bad), "tools 要是 {\"名字\": true/false}");
|
||||||
else if (conf && !cJSON_IsObject(conf))
|
else if (conf && !cJSON_IsObject(conf))
|
||||||
@@ -1760,6 +1771,11 @@ static int h_settings_x(cst_http_conn_t *c, void *ud) {
|
|||||||
}
|
}
|
||||||
cfg->protocol = p;
|
cfg->protocol = p;
|
||||||
cfg->tool_profile = profile;
|
cfg->tool_profile = profile;
|
||||||
|
if (mode) {
|
||||||
|
snprintf(cfg->mode, sizeof(cfg->mode), "%s",
|
||||||
|
cst_mode_name(mode->valuestring));
|
||||||
|
cst_settings_remember_mode(cfg->mode);
|
||||||
|
}
|
||||||
if (steps)
|
if (steps)
|
||||||
cfg->max_steps = (int)steps->valuedouble;
|
cfg->max_steps = (int)steps->valuedouble;
|
||||||
for (const cJSON *t = tools ? tools->child : NULL; t; t = t->next)
|
for (const cJSON *t = tools ? tools->child : NULL; t; t = t->next)
|
||||||
|
|||||||
@@ -325,6 +325,100 @@ static void test_import_file(const char *root) {
|
|||||||
"missing file refused");
|
"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) {
|
static void test_json_roundtrip(void) {
|
||||||
cst_attachment_t list[3];
|
cst_attachment_t list[3];
|
||||||
memset(list, 0, sizeof(list));
|
memset(list, 0, sizeof(list));
|
||||||
@@ -344,6 +438,8 @@ static void test_json_roundtrip(void) {
|
|||||||
list[1].bytes = 4096;
|
list[1].bytes = 4096;
|
||||||
snprintf(list[1].detail, sizeof(list[1].detail), "high");
|
snprintf(list[1].detail, sizeof(list[1].detail), "high");
|
||||||
snprintf(list[1].origin, sizeof(list[1].origin), "view_image");
|
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].id, sizeof(list[2].id), "img-3");
|
||||||
snprintf(list[2].name, sizeof(list[2].name), "c.webp");
|
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 &&
|
EXPECT(strcmp(back[1].detail, "high") == 0 &&
|
||||||
strcmp(back[1].origin, "view_image") == 0,
|
strcmp(back[1].origin, "view_image") == 0,
|
||||||
"detail and origin survive");
|
"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,
|
EXPECT(strcmp(back[2].mime, "image/webp") == 0 && back[2].width == 0,
|
||||||
"entry without dimensions");
|
"entry without dimensions");
|
||||||
EXPECT(strcmp(back[0].detail, "auto") == 0, "missing detail defaults");
|
EXPECT(strcmp(back[0].detail, "auto") == 0, "missing detail defaults");
|
||||||
@@ -530,6 +629,7 @@ int main(void) {
|
|||||||
test_dib();
|
test_dib();
|
||||||
test_store(root);
|
test_store(root);
|
||||||
test_import_file(root);
|
test_import_file(root);
|
||||||
|
test_documents(root);
|
||||||
test_json_roundtrip();
|
test_json_roundtrip();
|
||||||
test_lists();
|
test_lists();
|
||||||
test_agent_pending(root);
|
test_agent_pending(root);
|
||||||
|
|||||||
@@ -106,11 +106,13 @@ static void test_settings(void) {
|
|||||||
snprintf(cfg.provider, sizeof(cfg.provider), "openai");
|
snprintf(cfg.provider, sizeof(cfg.provider), "openai");
|
||||||
snprintf(cfg.model, sizeof(cfg.model), "gpt-5");
|
snprintf(cfg.model, sizeof(cfg.model), "gpt-5");
|
||||||
snprintf(cfg.effort, sizeof(cfg.effort), "max");
|
snprintf(cfg.effort, sizeof(cfg.effort), "max");
|
||||||
|
snprintf(cfg.mode, sizeof(cfg.mode), "general");
|
||||||
CHECK(cst_settings_remember(&cfg) == 0);
|
CHECK(cst_settings_remember(&cfg) == 0);
|
||||||
|
|
||||||
CHECK(cst_settings_load(&s) == 0);
|
CHECK(cst_settings_load(&s) == 0);
|
||||||
CHECK(strcmp(s.provider, "openai") == 0);
|
CHECK(strcmp(s.provider, "openai") == 0);
|
||||||
CHECK(strcmp(s.effort, "max") == 0);
|
CHECK(strcmp(s.effort, "max") == 0);
|
||||||
|
CHECK(strcmp(s.mode, "general") == 0);
|
||||||
CHECK(strcmp(cst_settings_model(&s, "deepseek"), "deepseek-v4-pro") == 0);
|
CHECK(strcmp(cst_settings_model(&s, "deepseek"), "deepseek-v4-pro") == 0);
|
||||||
CHECK(strcmp(cst_settings_model(&s, "openai"), "gpt-5") == 0);
|
CHECK(strcmp(cst_settings_model(&s, "openai"), "gpt-5") == 0);
|
||||||
CHECK(s.nmodels == 2);
|
CHECK(s.nmodels == 2);
|
||||||
@@ -145,6 +147,24 @@ static void test_effort(void) {
|
|||||||
free(body);
|
free(body);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void test_mode(void) {
|
||||||
|
CHECK(cst_mode_valid("coding") && cst_mode_valid("general"));
|
||||||
|
CHECK(!cst_mode_valid("code") && !cst_mode_valid(NULL));
|
||||||
|
CHECK(!cst_mode_valid(""));
|
||||||
|
CHECK(strcmp(cst_mode_name("general"), "general") == 0);
|
||||||
|
CHECK(strcmp(cst_mode_name(NULL), CST_DEFAULT_MODE) == 0);
|
||||||
|
CHECK(strcmp(cst_mode_name("nonsense"), "coding") == 0);
|
||||||
|
|
||||||
|
CHECK(cst_settings_remember_mode("general") == 0);
|
||||||
|
cst_settings_t s;
|
||||||
|
CHECK(cst_settings_load(&s) == 0);
|
||||||
|
CHECK(strcmp(s.mode, "general") == 0);
|
||||||
|
/* an unknown value is recorded as the default, not as itself */
|
||||||
|
CHECK(cst_settings_remember_mode("junk") == 0);
|
||||||
|
CHECK(cst_settings_load(&s) == 0);
|
||||||
|
CHECK(strcmp(s.mode, "coding") == 0);
|
||||||
|
}
|
||||||
|
|
||||||
int main(void) {
|
int main(void) {
|
||||||
CHECK(test_tmpdir(g_dir, sizeof(g_dir), "cst-config") == 0);
|
CHECK(test_tmpdir(g_dir, sizeof(g_dir), "cst-config") == 0);
|
||||||
setenv("XDG_CONFIG_HOME", g_dir, 1);
|
setenv("XDG_CONFIG_HOME", g_dir, 1);
|
||||||
@@ -158,6 +178,7 @@ int main(void) {
|
|||||||
test_creds();
|
test_creds();
|
||||||
test_settings();
|
test_settings();
|
||||||
test_effort();
|
test_effort();
|
||||||
|
test_mode();
|
||||||
|
|
||||||
CHECK(test_rmrf(g_dir) == 0);
|
CHECK(test_rmrf(g_dir) == 0);
|
||||||
printf("test_config: OK\n");
|
printf("test_config: OK\n");
|
||||||
|
|||||||
@@ -406,9 +406,118 @@ static void test_images_on(void) {
|
|||||||
cst_attach_free(img);
|
cst_attach_free(img);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Every text part of message i, joined (malloc'd, "" when there is none). */
|
||||||
|
static char *text_of(const cJSON *root, int i) {
|
||||||
|
const cJSON *m =
|
||||||
|
cJSON_GetArrayItem(cJSON_GetObjectItemCaseSensitive(root, "messages"), i);
|
||||||
|
const cJSON *c = cJSON_GetObjectItemCaseSensitive(m, "content");
|
||||||
|
if (cJSON_IsString(c)) {
|
||||||
|
size_t n = strlen(c->valuestring);
|
||||||
|
char *out = malloc(n + 1);
|
||||||
|
if (out)
|
||||||
|
memcpy(out, c->valuestring, n + 1);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
size_t len = 1;
|
||||||
|
const cJSON *p, *t, *s;
|
||||||
|
cJSON_ArrayForEach(p, c) {
|
||||||
|
t = cJSON_GetObjectItemCaseSensitive(p, "type");
|
||||||
|
s = cJSON_GetObjectItemCaseSensitive(p, "text");
|
||||||
|
if (cJSON_IsString(t) && strcmp(t->valuestring, "text") == 0 &&
|
||||||
|
cJSON_IsString(s))
|
||||||
|
len += strlen(s->valuestring) + 1;
|
||||||
|
}
|
||||||
|
char *out = calloc(1, len);
|
||||||
|
if (!out)
|
||||||
|
return NULL;
|
||||||
|
cJSON_ArrayForEach(p, c) {
|
||||||
|
t = cJSON_GetObjectItemCaseSensitive(p, "type");
|
||||||
|
s = cJSON_GetObjectItemCaseSensitive(p, "text");
|
||||||
|
if (cJSON_IsString(t) && strcmp(t->valuestring, "text") == 0 &&
|
||||||
|
cJSON_IsString(s)) {
|
||||||
|
strcat(out, s->valuestring);
|
||||||
|
strcat(out, "\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A document is inlined as text, so a text-only model and a message that
|
||||||
|
* also carries an image both keep it; the image is what becomes a note. */
|
||||||
|
static void test_document_parts(void) {
|
||||||
|
char dir[1024], sess[1100], err[256], src[1200];
|
||||||
|
if (test_tmpdir(dir, sizeof(dir), "cst-body-doc") != 0) {
|
||||||
|
EXPECT(0, "scratch dir");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
snprintf(sess, sizeof(sess), "%s/s.jsonl", dir);
|
||||||
|
snprintf(src, sizeof(src), "%s/paper.pdf", dir);
|
||||||
|
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 (Inlined paper text) Tj ET\n"
|
||||||
|
"endstream\nendobj\n"
|
||||||
|
"trailer\n<< /Root 1 0 R >>\n%%EOF\n";
|
||||||
|
FILE *f = fopen(src, "wb");
|
||||||
|
EXPECT(f != NULL, "write the PDF");
|
||||||
|
if (!f)
|
||||||
|
return;
|
||||||
|
fwrite(PDF, 1, strlen(PDF), f);
|
||||||
|
fclose(f);
|
||||||
|
cst_attachment_t *doc = cst_attach_import_file(sess, src, err, sizeof(err));
|
||||||
|
EXPECT(doc != NULL, "PDF stored");
|
||||||
|
if (!doc)
|
||||||
|
return;
|
||||||
|
static const unsigned char png[32] = {0x89, 'P', 'N', 'G', '\r', '\n', 0x1A,
|
||||||
|
'\n', 0, 0, 0, 13, 'I', 'H', 'D', 'R',
|
||||||
|
0, 0, 0, 2, 0, 0, 0, 2};
|
||||||
|
cst_attachment_t *img = cst_attach_import_bytes(
|
||||||
|
sess, "shot.png", (const char *)png, sizeof(png), err, sizeof(err));
|
||||||
|
EXPECT(img != NULL, "image stored");
|
||||||
|
if (!img) {
|
||||||
|
cst_attach_free(doc);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cst_attachment_t both[2] = {*doc, *img};
|
||||||
|
cst_message_t m[2];
|
||||||
|
m[0] = msg(CST_ROLE_USER, "what does this say?");
|
||||||
|
m[0].attach = both;
|
||||||
|
m[0].nattach = 2;
|
||||||
|
m[1] = msg(CST_ROLE_USER, "thanks");
|
||||||
|
cst_llm_request_t r;
|
||||||
|
memset(&r, 0, sizeof(r));
|
||||||
|
r.model = "deepseek-flash";
|
||||||
|
r.msgs = m;
|
||||||
|
r.nmsgs = 2;
|
||||||
|
r.images_off = 1; /* a text-only model */
|
||||||
|
r.session_path = sess;
|
||||||
|
cJSON *j = build_req(&r);
|
||||||
|
char *txt = text_of(j, 0);
|
||||||
|
EXPECT(txt && strstr(txt, "[document \"paper.pdf\"") &&
|
||||||
|
strstr(txt, "Inlined paper text") &&
|
||||||
|
strstr(txt, "[-- end of document --]"),
|
||||||
|
"the PDF text is inlined even when images are off");
|
||||||
|
EXPECT(txt && strstr(txt, "[image \"shot.png\"") && strstr(txt, "not sent"),
|
||||||
|
"the image beside it is still only a note");
|
||||||
|
free(txt);
|
||||||
|
char *s = cJSON_PrintUnformatted(j);
|
||||||
|
EXPECT(s && !strstr(s, "image_url"), "no image part with images off");
|
||||||
|
free(s);
|
||||||
|
cJSON_Delete(j);
|
||||||
|
cst_attach_free(doc);
|
||||||
|
cst_attach_free(img);
|
||||||
|
}
|
||||||
|
|
||||||
int main(void) {
|
int main(void) {
|
||||||
test_images_off();
|
test_images_off();
|
||||||
test_images_on();
|
test_images_on();
|
||||||
|
test_document_parts();
|
||||||
test_reasoning_and_prefix();
|
test_reasoning_and_prefix();
|
||||||
test_cache_hints();
|
test_cache_hints();
|
||||||
test_tools_snapshot();
|
test_tools_snapshot();
|
||||||
|
|||||||
@@ -0,0 +1,250 @@
|
|||||||
|
/* Text extraction from PDFs: hand-made fixtures for the shapes that matter
|
||||||
|
* (plain content, FlateDecode, CID fonts with ToUnicode CMaps, object
|
||||||
|
* streams) plus the two error paths (encrypted, not a PDF). */
|
||||||
|
|
||||||
|
#include "cst_pdf.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
static int failures = 0;
|
||||||
|
|
||||||
|
#define CHECK(cond, what) \
|
||||||
|
do { \
|
||||||
|
if (!(cond)) { \
|
||||||
|
fprintf(stderr, "FAIL %s:%d: %s\n", __FILE__, __LINE__, what); \
|
||||||
|
failures++; \
|
||||||
|
} \
|
||||||
|
} while (0)
|
||||||
|
|
||||||
|
static char *extract(const char *pdf, int *pages, char *err, size_t err_sz) {
|
||||||
|
return cst_pdf_text(pdf, strlen(pdf), pages, err, err_sz);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int has(const char *hay, const char *needle) {
|
||||||
|
return hay && strstr(hay, needle) != NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- 1. plain, uncompressed content, two lines via Td --------------- */
|
||||||
|
|
||||||
|
static const char PLAIN[] =
|
||||||
|
"%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 /MediaBox [0 0 612 792] "
|
||||||
|
"/Resources << /Font << /F1 4 0 R >> >> /Contents 5 0 R >>\nendobj\n"
|
||||||
|
"4 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>\nendobj\n"
|
||||||
|
"5 0 obj\n<< >>\nstream\n"
|
||||||
|
"BT\n/F1 12 Tf\n72 720 Td\n(Line one) Tj\n0 -14 Td\n"
|
||||||
|
"(A \\(paren\\) and a \\\\ backslash) Tj\nET\n"
|
||||||
|
"endstream\nendobj\n"
|
||||||
|
"trailer\n<< /Root 1 0 R >>\n%%EOF\n";
|
||||||
|
|
||||||
|
static void test_plain(void) {
|
||||||
|
char err[256] = "";
|
||||||
|
int pages = 0;
|
||||||
|
char *t = extract(PLAIN, &pages, err, sizeof(err));
|
||||||
|
CHECK(t != NULL, err);
|
||||||
|
CHECK(pages == 1, "one page");
|
||||||
|
CHECK(has(t, "Line one"), "first line");
|
||||||
|
CHECK(has(t, "A (paren) and a \\ backslash"), "escapes");
|
||||||
|
CHECK(!has(t, "\r"), "no CR left");
|
||||||
|
free(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- 2. FlateDecode content stream ---------------------------------- */
|
||||||
|
|
||||||
|
/* raw deflate (zlib, level 9, no header) of
|
||||||
|
* "BT /F1 12 Tf 72 720 Td (Hello, compressed world) Tj ET\n" */
|
||||||
|
static const unsigned char FLATE[] = {
|
||||||
|
0x73, 0x0a, 0x51, 0xd0, 0x77, 0x33, 0x54, 0x30, 0x34, 0x52, 0x08, 0x49,
|
||||||
|
0x53, 0x30, 0x37, 0x02, 0x22, 0x03, 0x85, 0x90, 0x14, 0x05, 0x0d, 0x8f,
|
||||||
|
0xd4, 0x9c, 0x9c, 0x7c, 0x1d, 0x85, 0xe4, 0xfc, 0xdc, 0x82, 0xa2, 0xd4,
|
||||||
|
0xe2, 0xe2, 0xd4, 0x14, 0x85, 0xf2, 0xfc, 0xa2, 0x9c, 0x14, 0x4d, 0x85,
|
||||||
|
0x90, 0x2c, 0x05, 0xd7, 0x10, 0x2e, 0x00};
|
||||||
|
|
||||||
|
static void test_flate(void) {
|
||||||
|
char pdf[2048];
|
||||||
|
int n = snprintf(pdf, sizeof(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 >>\nendobj\n"
|
||||||
|
"5 0 obj\n<< /Filter /FlateDecode >>\nstream\n");
|
||||||
|
memcpy(pdf + n, FLATE, sizeof(FLATE));
|
||||||
|
n += (int)sizeof(FLATE);
|
||||||
|
n += snprintf(pdf + n, sizeof(pdf) - (size_t)n,
|
||||||
|
"\nendstream\nendobj\ntrailer\n<< /Root 1 0 R >>\n%%EOF\n");
|
||||||
|
char err[256] = "";
|
||||||
|
int pages = 0;
|
||||||
|
char *t = cst_pdf_text(pdf, (size_t)n, &pages, err, sizeof(err));
|
||||||
|
CHECK(t != NULL, err);
|
||||||
|
CHECK(has(t, "Hello, compressed world"), "inflated text");
|
||||||
|
free(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2b. zlib-framed FlateDecode (RFC 1950), which is what real writers emit:
|
||||||
|
* zlib.compress(b"BT /F1 12 Tf 72 720 Td (Zlib framed hello) Tj ET\n"). */
|
||||||
|
static const unsigned char ZLIB[] = {
|
||||||
|
0x78, 0xda, 0x73, 0x0a, 0x51, 0xd0, 0x77, 0x33, 0x54, 0x30, 0x34, 0x52,
|
||||||
|
0x08, 0x49, 0x53, 0x30, 0x37, 0x02, 0x22, 0x03, 0x85, 0x90, 0x14, 0x05,
|
||||||
|
0x8d, 0xa8, 0x9c, 0xcc, 0x24, 0x85, 0xb4, 0xa2, 0xc4, 0xdc, 0xd4, 0x14,
|
||||||
|
0x85, 0x8c, 0xd4, 0x9c, 0x9c, 0x7c, 0x4d, 0x85, 0x90, 0x2c, 0x05, 0xd7,
|
||||||
|
0x10, 0x2e, 0x00, 0x33, 0x0f, 0x0d, 0x3a};
|
||||||
|
|
||||||
|
static void test_zlib_flate(void) {
|
||||||
|
char pdf[2048];
|
||||||
|
int n = snprintf(pdf, sizeof(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 >>\nendobj\n"
|
||||||
|
"5 0 obj\n<< /Filter /FlateDecode >>\nstream\n");
|
||||||
|
memcpy(pdf + n, ZLIB, sizeof(ZLIB));
|
||||||
|
n += (int)sizeof(ZLIB);
|
||||||
|
n += snprintf(pdf + n, sizeof(pdf) - (size_t)n,
|
||||||
|
"\nendstream\nendobj\ntrailer\n<< /Root 1 0 R >>\n%%EOF\n");
|
||||||
|
char err[256] = "";
|
||||||
|
int pages = 0;
|
||||||
|
char *t = cst_pdf_text(pdf, (size_t)n, &pages, err, sizeof(err));
|
||||||
|
CHECK(t != NULL, err);
|
||||||
|
CHECK(has(t, "Zlib framed hello"), "zlib header skipped, text inflated");
|
||||||
|
free(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2c. an inline image: its bytes are not text and must be skipped. */
|
||||||
|
|
||||||
|
static const char INLINE_IMG[] =
|
||||||
|
"%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 >>\nendobj\n"
|
||||||
|
"5 0 obj\n<< >>\nstream\n"
|
||||||
|
"BT /F1 12 Tf 72 720 Td (Before the image) Tj ET\n"
|
||||||
|
"q 20 0 0 20 72 700 cm\n"
|
||||||
|
"BI /W 8 /H 8 /CS /RGB /BPC 8 /F /AHx ID\n"
|
||||||
|
"00ff0000ff00ff00ff00ff00 8123<0001> EI Q\n"
|
||||||
|
"BT /F1 12 Tf 72 680 Td (After the image) Tj ET\n"
|
||||||
|
"endstream\nendobj\n"
|
||||||
|
"trailer\n<< /Root 1 0 R >>\n%%EOF\n";
|
||||||
|
|
||||||
|
static void test_inline_image(void) {
|
||||||
|
char err[256] = "";
|
||||||
|
int pages = 0;
|
||||||
|
char *t = extract(INLINE_IMG, &pages, err, sizeof(err));
|
||||||
|
CHECK(t != NULL, err);
|
||||||
|
CHECK(has(t, "Before the image"), "text before the inline image");
|
||||||
|
CHECK(has(t, "After the image"), "text after it is still found");
|
||||||
|
CHECK(!has(t, "00ff0000"), "the image bytes were skipped");
|
||||||
|
CHECK(!has(t, "8123"), "its hex data was skipped too");
|
||||||
|
free(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- 3. CID font with a ToUnicode CMap ------------------------------ */
|
||||||
|
|
||||||
|
static const char CID[] =
|
||||||
|
"%PDF-1.5\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 /Type0 /Encoding /Identity-H "
|
||||||
|
"/ToUnicode 6 0 R >>\nendobj\n"
|
||||||
|
"5 0 obj\n<< >>\nstream\n"
|
||||||
|
"BT /F1 12 Tf 72 720 Td <0001000200030004> Tj ET\n"
|
||||||
|
"endstream\nendobj\n"
|
||||||
|
"6 0 obj\n<< >>\nstream\n"
|
||||||
|
"1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n"
|
||||||
|
"2 beginbfchar\n<0001> <4F60>\n<0002> <597D>\nendbfchar\n"
|
||||||
|
"1 beginbfrange\n<0003> <0004> <0041>\nendbfrange\n"
|
||||||
|
"endcmap\nendstream\nendobj\n"
|
||||||
|
"trailer\n<< /Root 1 0 R >>\n%%EOF\n";
|
||||||
|
|
||||||
|
static void test_cid(void) {
|
||||||
|
char err[256] = "";
|
||||||
|
int pages = 0;
|
||||||
|
char *t = extract(CID, &pages, err, sizeof(err));
|
||||||
|
CHECK(t != NULL, err);
|
||||||
|
CHECK(has(t, "\xe4\xbd\xa0\xe5\xa5\xbd"), "bfchar maps to 你好");
|
||||||
|
CHECK(has(t, "AB"), "bfrange walks the code points");
|
||||||
|
free(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- 4. objects inside an object stream ----------------------------- */
|
||||||
|
|
||||||
|
static void test_objstm(void) {
|
||||||
|
char pdf[4096];
|
||||||
|
const char *page = "<< /Type /Page /Parent 2 0 R /Resources << /Font << /F1 "
|
||||||
|
"4 0 R >> >> /Contents 5 0 R >>";
|
||||||
|
const char *font = "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>";
|
||||||
|
/* the stream is "num1 off1 num2 off2" -- /N and /First are in the dict --
|
||||||
|
* and offsets count from /First, which is where the bodies start, so
|
||||||
|
* /First is the length of the pairs list itself */
|
||||||
|
char hdr[64];
|
||||||
|
int off4 = (int)strlen(page) + 1; /* object 4 follows the page */
|
||||||
|
int first = snprintf(hdr, sizeof(hdr), "3 0 4 %d ", off4);
|
||||||
|
int n = snprintf(pdf, sizeof(pdf),
|
||||||
|
"%%PDF-1.5\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"
|
||||||
|
"5 0 obj\n<< >>\nstream\n"
|
||||||
|
"BT /F1 12 Tf 72 720 Td (Inside an object stream) Tj ET\n"
|
||||||
|
"endstream\nendobj\n"
|
||||||
|
"6 0 obj\n<< /Type /ObjStm /N 2 /First %d >>\nstream\n"
|
||||||
|
"%s%s\n%s",
|
||||||
|
first, hdr, page, font);
|
||||||
|
n += snprintf(pdf + n, sizeof(pdf) - (size_t)n,
|
||||||
|
"\nendstream\nendobj\n"
|
||||||
|
"trailer\n<< /Root 1 0 R >>\n%%EOF\n");
|
||||||
|
char err[256] = "";
|
||||||
|
int pages = 0;
|
||||||
|
char *t = cst_pdf_text(pdf, (size_t)n, &pages, err, sizeof(err));
|
||||||
|
CHECK(t != NULL, err);
|
||||||
|
CHECK(pages == 1, "one page through the object stream");
|
||||||
|
CHECK(has(t, "Inside an object stream"), "text of the page in the ObjStm");
|
||||||
|
free(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- 5. error paths -------------------------------------------------- */
|
||||||
|
|
||||||
|
static void test_errors(void) {
|
||||||
|
char err[256] = "";
|
||||||
|
char *t = extract("%PDF-1.7\ntrailer\n<< /Root 1 0 R /Encrypt 9 0 R >>\n",
|
||||||
|
NULL, err, sizeof(err));
|
||||||
|
CHECK(t == NULL, "encrypted file is refused");
|
||||||
|
CHECK(has(err, "encrypted"), "says it is encrypted");
|
||||||
|
|
||||||
|
err[0] = 0;
|
||||||
|
t = extract("hello, not a pdf at all\n", NULL, err, sizeof(err));
|
||||||
|
CHECK(t == NULL, "non-PDF is refused");
|
||||||
|
CHECK(has(err, "not a PDF"), "says it is not a PDF");
|
||||||
|
|
||||||
|
err[0] = 0;
|
||||||
|
t = extract("%PDF-1.4\n1 0 obj\n<< >>\nendobj\n", NULL, err, sizeof(err));
|
||||||
|
CHECK(t == NULL, "a PDF without text is refused");
|
||||||
|
CHECK(has(err, "no extractable text") || has(err, "no readable pages"),
|
||||||
|
"explains the missing text");
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(void) {
|
||||||
|
test_plain();
|
||||||
|
test_flate();
|
||||||
|
test_zlib_flate();
|
||||||
|
test_inline_image();
|
||||||
|
test_cid();
|
||||||
|
test_objstm();
|
||||||
|
test_errors();
|
||||||
|
if (failures) {
|
||||||
|
fprintf(stderr, "test_pdf_text: %d failure(s)\n", failures);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
printf("test_pdf_text: OK\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -1717,6 +1717,7 @@ async function showRunSettings() {
|
|||||||
const proto = field('工具协议', select(s.protocols || [], s.protocol));
|
const proto = field('工具协议', select(s.protocols || [], s.protocol));
|
||||||
const steps = field('每轮步数', Object.assign(el('input'), { type: 'number', min: 1, max: 1000, value: s.max_steps }));
|
const steps = field('每轮步数', Object.assign(el('input'), { type: 'number', min: 1, max: 1000, value: s.max_steps }));
|
||||||
const prof = field('工具集', select(s.profiles || [], s.tool_profile));
|
const prof = field('工具集', select(s.profiles || [], s.tool_profile));
|
||||||
|
const mode = field('工作模式', select(s.modes || [], s.mode));
|
||||||
|
|
||||||
box.appendChild(el('h4', 'sub', '需要确认(审批模式 ' + (s.approval || '-') + ')'));
|
box.appendChild(el('h4', 'sub', '需要确认(审批模式 ' + (s.approval || '-') + ')'));
|
||||||
const conf = {};
|
const conf = {};
|
||||||
@@ -1786,6 +1787,7 @@ async function showRunSettings() {
|
|||||||
if (proto.value !== s.protocol) body.protocol = proto.value;
|
if (proto.value !== s.protocol) body.protocol = proto.value;
|
||||||
if (+steps.value !== s.max_steps) body.max_steps = +steps.value;
|
if (+steps.value !== s.max_steps) body.max_steps = +steps.value;
|
||||||
if (prof.value !== s.tool_profile) body.tool_profile = prof.value;
|
if (prof.value !== s.tool_profile) body.tool_profile = prof.value;
|
||||||
|
if (mode.value !== s.mode) body.mode = mode.value;
|
||||||
const c = {};
|
const c = {};
|
||||||
for (const k of Object.keys(conf)) if (conf[k].checked !== !!(s.confirm || {})[k]) c[k] = conf[k].checked;
|
for (const k of Object.keys(conf)) if (conf[k].checked !== !!(s.confirm || {})[k]) c[k] = conf[k].checked;
|
||||||
if (Object.keys(c).length) body.confirm = c;
|
if (Object.keys(c).length) body.confirm = c;
|
||||||
|
|||||||
Reference in New Issue
Block a user