v0.7.5: arrow keys, image request fix, deepseek-flash vision, musl CI
ci / linux (clang) (push) Successful in 4m42s
ci / linux (gcc) (push) Successful in 5m35s
ci / musl (push) Successful in 5m52s
ci / windows-cross (push) Successful in 12m51s

TUI keys: register both CSI and SS3 forms of the cursor/Home/End keys and
the xterm modifier sequences when terminfo lacks them, so an arrow sent in
the "other" form no longer turns into Esc + a typed letter. Up/Down move by
visual (soft-wrapped) row, and the draft survives a trip through history.
scripts/tui_keys drives the TUI in a pty to check all of this.

Images: text inside a multimodal content array is now a {"type":"text"}
part; DeepSeek rejected the bare string with HTTP 422. deepseek-flash
(and its deepseek-chat / deepseek-reasoner aliases) read images, so the
registry marks them vision; deepseek-v4-pro stays text only.

CI: models.c lost _DEFAULT_SOURCE in 0.7.0 and gmtime_r broke the musl
build; tests sleep with poll() instead of usleep for the same reason.
This commit is contained in:
WHC
2026-10-08 22:35:34 +08:00
parent 32b4afd772
commit c7b32c3fd6
13 changed files with 646 additions and 49 deletions
+79 -43
View File
@@ -378,6 +378,7 @@ typedef struct {
char *prompt_hist[PROMPT_HIST_CAP];
int prompt_hist_n;
int hist_browse;
char *hist_draft; /* the live input while browsing */
/* Busy-turn FIFO */
char *queue[QUEUE_CAP];
@@ -3163,28 +3164,16 @@ static size_t word_right(const tui_t *t, size_t at) {
return at;
}
/* Move to the previous/next logical line keeping the column (in chars).
* Returns 0 when already on the first/last line. */
static size_t input_hit(const tui_t *t, int want_row, int want_col);
/* Move to the previous/next visual row (soft-wrapped or not) keeping the
* screen column. Returns 0 when already on the first/last row. */
static int input_line_move(tui_t *t, int dir) {
size_t ls = line_start(t, t->cur);
int col = 0;
for (size_t i = ls; i < t->cur; i += next_len(t, i))
col++;
size_t target;
if (dir < 0) {
if (ls == 0)
return 0;
target = line_start(t, ls - 1);
} else {
size_t le = line_end(t, t->cur);
if (le >= t->in_len)
return 0;
target = le + 1;
}
size_t te = line_end(t, target);
while (col-- > 0 && target < te)
target += next_len(t, target);
t->cur = target;
int cr, cc;
int rows = input_layout(t, NULL, NULL, &cr, &cc);
if (cr + dir < 0 || cr + dir >= rows)
return 0;
t->cur = input_hit(t, cr + dir, cc);
return 1;
}
@@ -5854,7 +5843,7 @@ static int submit_input(tui_t *t, int busy) {
static size_t input_hit(const tui_t *t, int want_row, int want_col) {
int row = 0, col = IN_COL;
int right = (t->in_w > 0 ? t->in_w : t->cols) - 2;
size_t i = 0, best = t->in_len;
size_t i = 0, best = t->in_len, last = 0;
while (i < t->in_len) {
if (row == want_row && col >= want_col)
return i;
@@ -5873,12 +5862,14 @@ static size_t input_hit(const tui_t *t, int want_row, int want_col) {
utf8_decode(t->inbuf + i, t->in_len - i, &wc, &eaten);
int cw = utf8_width(wc == L'\t' ? L' ' : wc);
if (col + cw > right && col > IN_COL) {
/* offset i would be drawn at the start of the next row */
if (row == want_row)
return i;
return last;
row++;
col = IN_COL;
}
best = i + eaten;
last = i;
col += cw;
i += eaten;
}
@@ -6395,29 +6386,65 @@ static int g_k_alt_up, g_k_alt_down, g_k_ctrl_home, g_k_ctrl_end,
g_k_ctrl_up, g_k_ctrl_down, g_k_ctrl_left, g_k_ctrl_right, g_k_alt_left,
g_k_alt_right, g_k_ctrl_dc, g_k_alt_dc;
static int ext_key(const char *cap) {
/* The terminfo key, else the xterm sequence bound to our own code `own`. */
static int ext_key(const char *cap, const char *xterm, int own) {
char *s = tigetstr(cap);
if (!s || s == (char *)-1)
return 0;
int k = key_defined(s);
return k > 0 ? k : 0;
int k = s && s != (char *)-1 ? key_defined(s) : 0;
if (k > 0)
return k;
#ifndef _WIN32
k = key_defined(xterm);
if (k > 0)
return k;
if (k == 0 && define_key(xterm, own) == OK)
return own;
#else
(void)xterm, (void)own;
#endif
return 0;
}
#endif
#if defined(NCURSES_VERSION) && !defined(_WIN32)
/* Terminfo lists one form per key, but terminals disagree: cursor keys come
* as CSI or SS3 whatever DECCKM says (tmux, mosh, a wrong TERM), Home/End as
* CSI H, SS3 H or CSI 1~/7~. Unknown forms would be read as Esc + text. */
static void vt_keys_init(void) {
static const struct {
const char *seq;
int key;
} K[] = {
{"\033[A", KEY_UP}, {"\033OA", KEY_UP}, {"\033[B", KEY_DOWN},
{"\033OB", KEY_DOWN}, {"\033[C", KEY_RIGHT}, {"\033OC", KEY_RIGHT},
{"\033[D", KEY_LEFT}, {"\033OD", KEY_LEFT}, {"\033[H", KEY_HOME},
{"\033OH", KEY_HOME}, {"\033[1~", KEY_HOME}, {"\033[7~", KEY_HOME},
{"\033[F", KEY_END}, {"\033OF", KEY_END}, {"\033[4~", KEY_END},
{"\033[8~", KEY_END}, {"\033[2~", KEY_IC}, {"\033[3~", KEY_DC},
{"\033[5~", KEY_PPAGE}, {"\033[6~", KEY_NPAGE}, {"\033[Z", KEY_BTAB},
{"\033[1;2A", KEY_SR}, {"\033[1;2B", KEY_SF}, {"\033[1;2C", KEY_SRIGHT},
{"\033[1;2D", KEY_SLEFT},
};
for (size_t i = 0; i < sizeof(K) / sizeof(K[0]); i++)
if (key_defined(K[i].seq) == 0)
define_key(K[i].seq, K[i].key);
}
#endif
static void keys_init(void) {
#ifdef NCURSES_VERSION
g_k_alt_up = ext_key("kUP3");
g_k_alt_down = ext_key("kDN3");
g_k_ctrl_home = ext_key("kHOM5");
g_k_ctrl_end = ext_key("kEND5");
g_k_ctrl_up = ext_key("kUP5");
g_k_ctrl_down = ext_key("kDN5");
g_k_ctrl_left = ext_key("kLFT5");
g_k_ctrl_right = ext_key("kRIT5");
g_k_alt_left = ext_key("kLFT3");
g_k_alt_right = ext_key("kRIT3");
g_k_ctrl_dc = ext_key("kDC5");
g_k_alt_dc = ext_key("kDC3");
int own = KEY_OWN_BASE + 4; /* up to KEY_KITTY_BASE - 1 */
g_k_alt_up = ext_key("kUP3", "\033[1;3A", own++);
g_k_alt_down = ext_key("kDN3", "\033[1;3B", own++);
g_k_ctrl_home = ext_key("kHOM5", "\033[1;5H", own++);
g_k_ctrl_end = ext_key("kEND5", "\033[1;5F", own++);
g_k_ctrl_up = ext_key("kUP5", "\033[1;5A", own++);
g_k_ctrl_down = ext_key("kDN5", "\033[1;5B", own++);
g_k_ctrl_left = ext_key("kLFT5", "\033[1;5D", own++);
g_k_ctrl_right = ext_key("kRIT5", "\033[1;5C", own++);
g_k_alt_left = ext_key("kLFT3", "\033[1;3D", own++);
g_k_alt_right = ext_key("kRIT3", "\033[1;3C", own++);
g_k_ctrl_dc = ext_key("kDC5", "\033[3;5~", own++);
g_k_alt_dc = ext_key("kDC3", "\033[3;3~", own++);
#endif
}
@@ -6630,6 +6657,7 @@ static void tui_free(tui_t *t) {
queue_free_all(t);
for (int i = 0; i < t->prompt_hist_n; i++)
free(t->prompt_hist[i]);
free(t->hist_draft);
for (int i = 0; i < t->nfiles; i++)
free(t->files[i]);
free(t->files);
@@ -6720,6 +6748,7 @@ int cst_tui_run(cst_agent_t *agent) {
/* the Windows console driver reads key events, not VT sequences */
define_key("\033[200~", KEY_PASTE_BEGIN);
define_key("\033[201~", KEY_PASTE_END);
vt_keys_init();
printf("\033[?2004h");
fflush(stdout);
kitty_init();
@@ -7032,15 +7061,22 @@ int cst_tui_run(cst_agent_t *agent) {
if (t.prompt_hist_n <= 0)
continue;
int idx = t.hist_browse;
if (idx < 0) {
free(t.hist_draft);
t.hist_draft = strdup(t.inbuf);
}
idx = idx < 0 ? t.prompt_hist_n - 1 : (idx > 0 ? idx - 1 : 0);
hist_load_into_input(&t, idx);
} else {
if (t.hist_browse < 0)
continue;
if (t.hist_browse + 1 >= t.prompt_hist_n)
input_clear(&t);
else
if (t.hist_browse + 1 >= t.prompt_hist_n) {
input_set(&t, t.hist_draft);
free(t.hist_draft);
t.hist_draft = NULL;
} else {
hist_load_into_input(&t, t.hist_browse + 1);
}
}
after_edit(&t);
continue;