diff --git a/docs/screenshots/tui-home-r5.png b/docs/screenshots/tui-home-r5.png new file mode 100644 index 0000000..a93bd6e Binary files /dev/null and b/docs/screenshots/tui-home-r5.png differ diff --git a/docs/screenshots/tui-session-r5.png b/docs/screenshots/tui-session-r5.png new file mode 100644 index 0000000..9c995f2 Binary files /dev/null and b/docs/screenshots/tui-session-r5.png differ diff --git a/include/cst_version.h b/include/cst_version.h index 3c4df5b..843e28c 100644 --- a/include/cst_version.h +++ b/include/cst_version.h @@ -3,7 +3,7 @@ #define CST_VERSION_MAJOR 0 #define CST_VERSION_MINOR 9 -#define CST_VERSION_PATCH 30 +#define CST_VERSION_PATCH 31 #define CST_VERSION_STR_(x) #x #define CST_VERSION_STR(x) CST_VERSION_STR_(x) diff --git a/src/tui.c b/src/tui.c index d3067ff..7ed2c3b 100644 --- a/src/tui.c +++ b/src/tui.c @@ -2369,7 +2369,7 @@ static void approval_label(const tui_t *t, char *out, size_t sz) { snprintf(out, sz, "ask"); } -/* Bottom row: where/status on the left, context + palette hint on the right. */ +/* Bottom row: path / status left; palette hint right. CTX lives in Seat Deck. */ static void draw_status(tui_t *t) { if (!t->status) return; @@ -2386,29 +2386,11 @@ static void draw_status(tui_t *t) { rw = utf8_cols(right, strlen(right)); draw_ansi_at(t->status, 0, w - rw, rw, right, CP_MUTED, 0, 0); } else { - long used; - int pct = ctx_percent(t, &used); - char tok[32]; - fmt_tokens(used, tok, sizeof(tok)); - char usage[64] = ""; - cst_usage_t u; - cst_agent_usage(t->agent, &u, NULL); - if (u.requests > 0 && u.prompt > 0) { - int n = snprintf(usage, sizeof(usage), "%shit %ld%%", dot_sep(), - u.cached * 100 / u.prompt); - if (u.priced && n > 0 && (size_t)n < sizeof(usage)) - snprintf(usage + n, sizeof(usage) - (size_t)n, "%s%s%s%.2f", - dot_sep(), g_ascii ? "~" : "≈", money_sym(u.currency), - u.cost); - } - snprintf(right, sizeof(right), - "%s (%d%%)%s%s\033[97mctrl+p\033[39m commands", tok, pct, usage, - dot_sep()); + snprintf(right, sizeof(right), "\033[97mctrl+p\033[39m commands"); rw = vis_cols(right); if (rw > w) rw = w; - draw_ansi_at(t->status, 0, w - rw, rw, right, - pct >= 80 ? CP_ERROR : CP_MUTED, 0, 0); + draw_ansi_at(t->status, 0, w - rw, rw, right, CP_MUTED, 0, 0); t->cmd_x1 = sx + w - 1; t->cmd_x0 = t->cmd_x1 - (int)strlen("ctrl+p commands") + 1; } @@ -2428,7 +2410,7 @@ static void draw_status(tui_t *t) { wrefresh(t->status); } -/* "Build · model provider · effort" on the input box's last row. */ +/* Short mode row: Build/Plan · effort — model lives in the topbar. */ static void draw_mode_line(tui_t *t, WINDOW *w, int y, int maxw) { const cst_agent_config_t *cfg = cst_agent_config(t->agent); if (!cfg) @@ -2439,7 +2421,7 @@ static void draw_mode_line(tui_t *t, WINDOW *w, int y, int maxw) { char right[160]; int rpair = CP_BOX_MUTED; if (t->busy) { - snprintf(right, sizeof(right), "%s %.0fs esc interrupt", spinner_frame(t), + snprintf(right, sizeof(right), "%s %.0fs esc", spinner_frame(t), since_ms(&t->turn_t0) / 1000.0); rpair = CP_BOX_PRIMARY; } else { @@ -2455,10 +2437,6 @@ static void draw_mode_line(tui_t *t, WINDOW *w, int y, int maxw) { } x += draw_ansi_at(w, y, x, room, cfg->plan_mode ? "Plan" : "Build", cfg->plan_mode ? CP_BOX_PRIMARY : CP_BOX_ACCENT, 1, 0); - x += draw_ansi_at(w, y, x, room - x + 2, dot_sep(), CP_BOX_MUTED, 0, 0); - x += draw_ansi_at(w, y, x, room - x + 2, cfg->model, CP_BOX_BRIGHT, 0, 0); - x += draw_ansi_at(w, y, x, room - x + 2, " ", CP_BOX, 0, 0); - x += draw_ansi_at(w, y, x, room - x + 2, cfg->provider, CP_BOX_MUTED, 0, 0); if (eff) { x += draw_ansi_at(w, y, x, room - x + 2, dot_sep(), CP_BOX_MUTED, 0, 0); x += draw_ansi_at(w, y, x, room - x + 2, eff, CP_BOX_PRIMARY, 0, 0); @@ -2466,7 +2444,7 @@ static void draw_mode_line(tui_t *t, WINDOW *w, int y, int maxw) { int npick = cst_agent_pending_count(t->agent); if (npick > 0) { char mark[32]; - snprintf(mark, sizeof(mark), "%d image%s", npick, npick > 1 ? "s" : ""); + snprintf(mark, sizeof(mark), "%d img", npick); x += draw_ansi_at(w, y, x, room - x + 2, dot_sep(), CP_BOX_MUTED, 0, 0); draw_ansi_at(w, y, x, room - x + 2, mark, CP_BOX_PRIMARY, 0, 0); } @@ -2961,47 +2939,38 @@ static void draw_side(tui_t *t) { y++; } - /* WORK — todo + modified (quiet) */ - int work_hdr = 0; - int total = 0; - int todo_n = 0; - if (t->show_todo && t->todo && todo_open_items(t->todo, &total) >= 0 && - total > 0 && y + 2 < bottom) { - if (!work_hdr) { - draw_ansi_at(s, y++, x, mw, "work", CP_SIDE_MUTED, 0, 0); - work_hdr = 1; - } - for (const char *p = t->todo; *p && y < bottom - 1 && todo_n < 4;) { - const char *nl = strchr(p, '\n'); - size_t len = nl ? (size_t)(nl - p) : strlen(p); - if (len >= 3) { - int pair = CP_SIDE; - const char *mark = "[ ]"; - if (strncmp(p, "[x]", 3) == 0) - pair = CP_SIDE_MUTED, mark = g_ascii ? "[x]" : "[✓]"; - else if (strncmp(p, "[~]", 3) == 0) - pair = CP_SIDE_PRIMARY, mark = g_ascii ? "[~]" : "[•]"; - const char *txt = p + 3; - while (txt < p + len && *txt == ' ') - txt++; - snprintf(line, sizeof(line), "%s %.*s", mark, (int)(p + len - txt), - txt); - draw_ansi_at(s, y++, x, mw, line, pair, 0, 0); - todo_n++; + /* WORK — todo + modified, or a designed idle line (no empty void). */ + if (y + 2 < bottom) { + draw_ansi_at(s, y++, x, mw, "work", CP_SIDE_MUTED, 0, 0); + int total = 0; + int todo_n = 0; + if (t->show_todo && t->todo && todo_open_items(t->todo, &total) >= 0 && + total > 0) { + for (const char *p = t->todo; *p && y < bottom - 1 && todo_n < 4;) { + const char *nl = strchr(p, '\n'); + size_t len = nl ? (size_t)(nl - p) : strlen(p); + if (len >= 3) { + int pair = CP_SIDE; + const char *mark = "[ ]"; + if (strncmp(p, "[x]", 3) == 0) + pair = CP_SIDE_MUTED, mark = g_ascii ? "[x]" : "[✓]"; + else if (strncmp(p, "[~]", 3) == 0) + pair = CP_SIDE_PRIMARY, mark = g_ascii ? "[~]" : "[•]"; + const char *txt = p + 3; + while (txt < p + len && *txt == ' ') + txt++; + snprintf(line, sizeof(line), "%s %.*s", mark, (int)(p + len - txt), + txt); + draw_ansi_at(s, y++, x, mw, line, pair, 0, 0); + todo_n++; + } + p = nl ? nl + 1 : p + len; } - p = nl ? nl + 1 : p + len; - } - } - - mod_file_t mods[16]; - int nm = collect_modified(t, mods, 16); - if (nm > 5) - nm = 5; - if (nm > 0 && y + 1 < bottom) { - if (!work_hdr) { - draw_ansi_at(s, y++, x, mw, "work", CP_SIDE_MUTED, 0, 0); - work_hdr = 1; } + mod_file_t mods[16]; + int nm = collect_modified(t, mods, 16); + if (nm > 5) + nm = 5; for (int i = 0; i < nm && y < bottom; i++) { char pl[16], mi[16]; snprintf(pl, sizeof(pl), "+%d", mods[i].add); @@ -3013,6 +2982,29 @@ static void draw_side(tui_t *t) { draw_ansi_at(s, y, px + 1, (int)strlen(mi), mi, CP_SIDE_PRIMARY, 0, 0); y++; } + if (todo_n == 0 && nm == 0 && y < bottom) + draw_ansi_at(s, y++, x, mw, "idle", CP_SIDE_MUTED, 0, 0); + y++; + } + + /* SYNC — parliament quiet / pending */ + if (y + 1 < bottom) { + draw_ansi_at(s, y++, x, mw, "sync", CP_SIDE_MUTED, 0, 0); + if (cst_parliament_sync_pending()) + draw_ansi_at(s, y++, x, mw, "pending", CP_SIDE_PRIMARY, 0, 0); + else if (cst_parliament_is_active()) + draw_ansi_at(s, y++, x, mw, "quiet", CP_SIDE_MUTED, 0, 0); + else + draw_ansi_at(s, y++, x, mw, "quiet", CP_SIDE_MUTED, 0, 0); + y++; + } + + /* KEYS — always one designed hint row */ + if (y + 1 < bottom) { + draw_ansi_at(s, y++, x, mw, "keys", CP_SIDE_MUTED, 0, 0); + draw_ansi_at(s, y++, x, mw, + g_ascii ? "tab mode . ^p cmd" : "tab mode · ^p cmd", + CP_SIDE_MUTED, 0, 0); } if (h >= 4) { @@ -3133,16 +3125,13 @@ static void draw_log(tui_t *t) { } draw_selection(t, &dl, h, w); if (t->log_scroll > 0 && h > 2) { - char more[128]; - snprintf(more, sizeof(more), - g_ascii ? " -- %d more lines below - End/PgDn - %d above -- " - : " ── %d more lines below · End/PgDn · %d above ── ", - t->log_scroll, start); + char more[64]; + snprintf(more, sizeof(more), g_ascii ? " v%d" : " ↓%d", t->log_scroll); wmove(t->logw, h - 1, 0); wclrtoeol(t->logw); - wattron(t->logw, A_REVERSE); - draw_ansi_line(t->logw, h - 1, w, more, CP_PRIMARY, 1, 0); - wattroff(t->logw, A_REVERSE); + int mw = utf8_cols(more, strlen(more)); + draw_ansi_at(t->logw, h - 1, w - mw - 1 > 0 ? w - mw - 1 : 0, mw, more, + CP_MUTED, 0, 0); } dlines_free(&dl); draw_completion(t, h - 1, 0, w < 90 ? w : 90, h - 1, 1); @@ -3185,8 +3174,10 @@ static void draw_topbar(tui_t *t) { if (hexish) title = "New session"; } - /* Title + model only. Role lives in the Seat Deck — a lone chair/seat - * chip on the far right just duplicates it and looks stranded. */ + /* CST mark + title + model. Role lives only in the Seat Deck. */ + int x = 1; + x += draw_ansi_at(w, 0, x, 3, "CST", CP_TOPBAR_ACCENT, 1, 0); + x += draw_ansi_at(w, 0, x, 1, " ", CP_TOPBAR_MUTED, 0, 0); int model_room = 0; char mid[160] = ""; if (cfg) { @@ -3194,12 +3185,12 @@ static void draw_topbar(tui_t *t) { cfg->provider[0] ? " · " : "", cfg->provider); model_room = utf8_cols(mid, strlen(mid)) + 2; } - int title_max = bw - 2 - model_room; + int title_max = bw - x - 1 - model_room; if (title_max < 8) title_max = 8; size_t tfit = utf8_fit_cols(title, strlen(title), title_max); - snprintf(left, sizeof(left), " %.*s", (int)tfit, title); - int x = draw_ansi_at(w, 0, 0, bw, left, CP_TOPBAR, 1, 0); + snprintf(left, sizeof(left), "%.*s", (int)tfit, title); + x += draw_ansi_at(w, 0, x, bw - x, left, CP_TOPBAR, 1, 0); if (mid[0] && x + 2 < bw) draw_ansi_at(w, 0, x + 2, bw - (x + 2), mid, CP_TOPBAR_MUTED, 0, 0); wrefresh(w); @@ -4556,17 +4547,69 @@ static int obtain_key(tui_t *t, const char *provider, char *out, size_t n, /* --- slash commands ------------------------------------------------------- */ +/* First-user dumps / JSON / ctl noise make a bad session chrome title. */ +static int title_is_noisy(const char *s) { + if (!s || !s[0]) + return 1; + while (*s == ' ' || *s == '\t') + s++; + if (*s == '{' || *s == '[') + return 1; + if (!strncmp(s, "```", 3)) + return 1; + if (strstr(s, "\"version\"") || strstr(s, "task controls") || + strstr(s, "\"revision\"") || strstr(s, "\"sequence\"")) + return 1; + return 0; +} + +/* One-line human summary into out; empty if nothing usable. */ +static void title_summarize(char *out, size_t sz, const char *src) { + out[0] = 0; + if (!src || title_is_noisy(src)) + return; + while (*src == ' ' || *src == '\t' || *src == '\n' || *src == '\r') + src++; + char buf[160]; + size_t o = 0; + int sp = 0; + for (const char *p = src; *p && *p != '\n' && *p != '\r' && o + 1 < sizeof(buf); + p++) { + unsigned char c = (unsigned char)*p; + if (c == ' ' || c == '\t') { + if (o == 0 || sp) + continue; + buf[o++] = ' '; + sp = 1; + continue; + } + sp = 0; + buf[o++] = (char)c; + } + buf[o] = 0; + if (!buf[0] || title_is_noisy(buf)) + return; + size_t fit = utf8_fit_cols(buf, strlen(buf), 40); + snprintf(out, sz, "%.*s", (int)fit, buf); +} + static void refresh_title(tui_t *t) { const cst_agent_config_t *cfg = cst_agent_config(t->agent); char sid[128]; cst_agent_session_id(t->agent, sid, sizeof(sid)); cst_session_info_t in; + memset(&in, 0, sizeof(in)); t->title[0] = 0; - if (cfg && cst_agent_store_info(cfg->workspace_root, sid, &in) == 0) - snprintf(t->title, sizeof(t->title), "%s", - in.title[0] ? in.title : in.first_user); + if (cfg) + (void)cst_agent_store_info(cfg->workspace_root, sid, &in); + if (in.title[0] && !title_is_noisy(in.title)) { + size_t fit = utf8_fit_cols(in.title, strlen(in.title), 40); + snprintf(t->title, sizeof(t->title), "%.*s", (int)fit, in.title); + } else { + title_summarize(t->title, sizeof(t->title), in.first_user); + } if (!t->title[0]) - snprintf(t->title, sizeof(t->title), "%.8s", sid); + snprintf(t->title, sizeof(t->title), "New session"); } static void refresh_todo(tui_t *t) {