diff --git a/docs/screenshots/tui-home-r4.png b/docs/screenshots/tui-home-r4.png new file mode 100644 index 0000000..0b29af1 Binary files /dev/null and b/docs/screenshots/tui-home-r4.png differ diff --git a/docs/screenshots/tui-session-r4.png b/docs/screenshots/tui-session-r4.png new file mode 100644 index 0000000..c4a3341 Binary files /dev/null and b/docs/screenshots/tui-session-r4.png differ diff --git a/include/cst_version.h b/include/cst_version.h index 496819e..981ca65 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 24 +#define CST_VERSION_PATCH 25 #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 d3fa8c9..798d111 100644 --- a/src/tui.c +++ b/src/tui.c @@ -10,6 +10,7 @@ #include "cst_ext.h" #include "cst_mcp.h" #include "cst_models.h" +#include "cst_discover.h" #include "cst_parliament.h" #include "cst_plugins.h" #include "cst_realm.h" @@ -311,6 +312,7 @@ typedef struct { int todo_rows; int home; /* empty transcript: centered start screen */ int show_side; /* /sidebar */ + int motion_on; /* /motion — Seat Deck ASCII micro-motion */ int side_w; int in_x, in_y, in_w, in_h; /* input box, screen coords */ int log_x, log_y; /* logw origin, screen coords */ @@ -1976,6 +1978,67 @@ static const char *spinner_frame(const tui_t *t) { return g_ascii ? asc[t->spin % 4] : uni[t->spin % 10]; } +/* Seat Deck motion: short glyph sequences driven by t->spin (80ms ticks). */ +static const char *motion_fleet_frame(const tui_t *t) { + static const char *const uni[] = {"✦ · ·", "· ✦ ·", "· · ✦", "· ✦ ·"}; + static const char *const asc[] = {"* . .", ". * .", ". . *", ". * ."}; + int i = (t->spin / 2) % 4; + return g_ascii ? asc[i] : uni[i]; +} + +static const char *seat_pulse_dot(const tui_t *t) { + int on = 1; + if (t->motion_on) { + if (t->busy) + on = (t->spin % 2) == 0; + else + on = ((t->spin / 6) % 2) == 0; + } + if (g_ascii) + return on ? "*" : "."; + return on ? "●" : "○"; +} + +static int realm_fp_short(char out[9]) { + out[0] = 0; + cst_peer_info_t peers[1]; + if (cst_discover_local_list(NULL, peers, 1) > 0 && peers[0].realm_fp[0]) { + snprintf(out, 9, "%.8s", peers[0].realm_fp); + return 0; + } + char dir[OS_PATH_MAX]; + cst_realm_keys_t k; + char fp[33]; + if (cst_realm_dir(dir, sizeof(dir)) == 0 && cst_realm_load(dir, &k) == 0 && + cst_realm_fingerprint(k.pk, fp) == 0) { + snprintf(out, 9, "%.8s", fp); + return 0; + } + return -1; +} + +static void draw_ctx_bar(char *out, size_t sz, int pct, int pulse_on) { + int bar_w = 12; + int fill = pct <= 0 ? 0 : pct >= 100 ? bar_w : (pct * bar_w) / 100; + size_t o = 0; + out[0] = 0; + for (int i = 0; i < bar_w && o + 4 < sz; i++) { + const char *cell; + if (i < fill) { + if (pulse_on && pct >= 80 && i == fill - 1) + cell = g_ascii ? "*" : "▓"; + else + cell = g_ascii ? "=" : "█"; + } else { + cell = g_ascii ? "." : "░"; + } + size_t n = strlen(cell); + memcpy(out + o, cell, n); + o += n; + out[o] = 0; + } +} + static void fmt_tokens(long n, char *out, size_t sz) { if (n >= 1000000) snprintf(out, sz, "%.1fM", (double)n / 1e6); @@ -2511,103 +2574,149 @@ static void draw_side(tui_t *t) { werase(s); int h, w; getmaxyx(s, h, w); - int x = 2, mw = w - 4, y = 1; + const char *rail = g_ascii ? "|" : "┃"; + for (int ry = 0; ry < h; ry++) + draw_ansi_at(s, ry, 0, 1, rail, CP_SIDE_PRIMARY, 0, 0); + int x = 2, mw = w - 3, y = 1; + if (mw < 8) + mw = w > 2 ? w - 2 : 1; char line[512]; + int bottom = h - 3; /* where + version */ - const char *title = t->title[0] ? t->title : "New session"; - size_t fit = utf8_fit_cols(title, strlen(title), mw); - draw_ansi_at(s, y++, x, mw, title, CP_SIDE_BRIGHT, 1, 0); - if (fit < strlen(title) && y < h) - draw_ansi_at(s, y++, x, mw, title + fit, CP_SIDE_BRIGHT, 1, 0); + /* SEAT — local role + instance + short realm fp */ + draw_ansi_at(s, y++, x, mw, "seat", CP_SIDE_MUTED, 0, 0); + { + char sid[64] = {0}; + int sess = cst_parliament_session_ok(sid, sizeof(sid)); + int chair = sess && cst_parliament_is_chair(); + const char *role = !sess ? "logged-out" : chair ? "chair" : "member"; + int pair = !sess ? CP_SIDE_MUTED + : chair ? CP_SIDE_SUCCESS : CP_SIDE_PRIMARY; + int dx = x + draw_ansi_at(s, y, x, 2, seat_pulse_dot(t), pair, 0, 0); + snprintf(line, sizeof(line), "%s", role); + dx += draw_ansi_at(s, y, dx + 1, mw - (dx + 1 - x), line, pair, 1, 0); + y++; + if (!sid[0]) + cst_parliament_default_instance(sid, sizeof(sid)); + if (sid[0] && y < bottom) { + size_t fit = utf8_fit_cols(sid, strlen(sid), mw); + snprintf(line, sizeof(line), "%.*s", (int)fit, sid); + draw_ansi_at(s, y++, x, mw, line, CP_SIDE_MUTED, 0, 0); + } + char fp[9]; + if (realm_fp_short(fp) == 0 && y < bottom) { + snprintf(line, sizeof(line), "realm %.8s", fp); + draw_ansi_at(s, y++, x, mw, line, CP_SIDE_MUTED, 0, 0); + } + } y++; - long used; - int pct = ctx_percent(t, &used); - draw_ansi_at(s, y++, x, mw, "Context", CP_SIDE_BRIGHT, 1, 0); - snprintf(line, sizeof(line), "%ld tokens", used); - draw_ansi_at(s, y++, x, mw, line, CP_SIDE_MUTED, 0, 0); - snprintf(line, sizeof(line), "%d%% used", pct); - draw_ansi_at(s, y++, x, mw, line, CP_SIDE_MUTED, 0, 0); - y++; + /* FLEET — peers / daemon / revision */ + if (y + 1 < bottom) { + draw_ansi_at(s, y++, x, mw, "fleet", CP_SIDE_MUTED, 0, 0); + int pid = 0, port = 0; + char diid[64] = {0}; + int svc = cst_parliament_daemon_info(&pid, diid, sizeof(diid), &port) == 0; + cst_parliament_meta_t meta; + memset(&meta, 0, sizeof(meta)); + int have_meta = cst_parliament_load(&meta) == 0; + cst_peer_info_t peers[16]; + int np = cst_discover_local_list(NULL, peers, 16); + if (!svc) { + draw_ansi_at(s, y++, x, mw, "offline", CP_SIDE_MUTED, 0, 0); + } else { + snprintf(line, sizeof(line), "port %d · %d peer%s", port, np, + np == 1 ? "" : "s"); + draw_ansi_at(s, y++, x, mw, line, CP_SIDE, 0, 0); + if (have_meta && y < bottom) { + int dx = x; + snprintf(line, sizeof(line), "rev=%lu", meta.revision); + dx += draw_ansi_at(s, y, x, mw, line, CP_SIDE_BRIGHT, 0, 0); + if (t->motion_on && np > 0 && dx + 8 < x + mw) + draw_ansi_at(s, y, dx + 1, x + mw - (dx + 1), motion_fleet_frame(t), + CP_SIDE_PRIMARY, 0, 0); + y++; + } + if (np == 0 && y < bottom) + draw_ansi_at(s, y++, x, mw, "no peers", CP_SIDE_MUTED, 0, 0); + else { + for (int i = 0; i < np && y < bottom && i < 4; i++) { + int is_chair = + have_meta && meta.chair_instance[0] && + !strcmp(peers[i].instance_id, meta.chair_instance); + snprintf(line, sizeof(line), "%s%.28s", is_chair ? "+ " : " ", + peers[i].instance_id); + draw_ansi_at(s, y++, x, mw, line, + is_chair ? CP_SIDE_SUCCESS : CP_SIDE_MUTED, 0, 0); + } + } + } + y++; + } - int bottom = h - 3; /* keep two rows for where + version */ - char appr[64]; - approval_label(t, appr, sizeof(appr)); + /* CTX — context meter */ if (y + 2 < bottom) { - draw_ansi_at(s, y++, x, mw, "Approval", CP_SIDE_BRIGHT, 1, 0); - draw_ansi_at(s, y++, x, mw, appr, - approval_mode(t) == CST_APPROVAL_YOLO ? CP_SIDE_PRIMARY - : CP_SIDE_MUTED, + draw_ansi_at(s, y++, x, mw, "ctx", CP_SIDE_MUTED, 0, 0); + long used; + int pct = ctx_percent(t, &used); + char tok[32], bar[40]; + fmt_tokens(used, tok, sizeof(tok)); + int pulse = t->motion_on && pct >= 80 && (t->spin % 2) == 0; + draw_ctx_bar(bar, sizeof(bar), pct, pulse); + snprintf(line, sizeof(line), "%s %d%% %s%s", bar, pct, g_ascii ? "" : "· ", + tok); + draw_ansi_at(s, y++, x, mw, line, pct >= 80 ? CP_SIDE_PRIMARY : CP_SIDE, 0, 0); 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) { - draw_ansi_at(s, y++, x, mw, "Todo", CP_SIDE_BRIGHT, 1, 0); - for (const char *p = t->todo; *p && y < bottom - 1;) { + 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 = "[✓]"; + pair = CP_SIDE_MUTED, mark = g_ascii ? "[x]" : "[✓]"; else if (strncmp(p, "[~]", 3) == 0) - pair = CP_SIDE_PRIMARY, mark = "[•]"; + 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; } - y++; - } - - int mready = 0, mtotal = 0; - cst_mcp_counts(&mready, &mtotal); - if (mtotal > 0 && y + 2 < bottom) { - cst_mcp_info_t mi[CST_MCP_MAX]; - int n = cst_mcp_info(mi, CST_MCP_MAX); - snprintf(line, sizeof(line), "MCP %d/%d", mready, mtotal); - draw_ansi_at(s, y++, x, mw, line, CP_SIDE_BRIGHT, 1, 0); - for (int i = 0; i < n && y < bottom - 1; i++) { - if (mi[i].state == CST_MCP_OFF) - continue; - int ok = mi[i].state == CST_MCP_READY; - int pend = mi[i].state == CST_MCP_STARTING; - int dx = x + draw_ansi_at(s, y, x, 2, g_ascii ? "*" : "●", - ok ? CP_SIDE_SUCCESS - : pend ? CP_SIDE_MUTED : CP_SIDE_PRIMARY, - 0, 0); - if (ok) - snprintf(line, sizeof(line), "%.63s · %d tools", mi[i].name, - mi[i].ntools); - else - snprintf(line, sizeof(line), "%.63s · %s", mi[i].name, - pend ? "starting" : "failed"); - draw_ansi_at(s, y++, dx + 1, mw - (dx + 1 - x), line, CP_SIDE_MUTED, 0, - 0); - } - y++; } mod_file_t mods[16]; int nm = collect_modified(t, mods, 16); - if (nm > 0 && y + 2 < bottom) { - draw_ansi_at(s, y++, x, mw, "Modified Files", CP_SIDE_BRIGHT, 1, 0); + 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; + } for (int i = 0; i < nm && y < bottom; i++) { - char stat[48]; - snprintf(stat, sizeof(stat), "+%d -%d", mods[i].add, mods[i].del); - int sw = (int)strlen(stat); - draw_ansi_at(s, y, x, mw - sw - 1, mods[i].path, CP_SIDE_MUTED, 0, 0); char pl[16], mi[16]; snprintf(pl, sizeof(pl), "+%d", mods[i].add); snprintf(mi, sizeof(mi), "-%d", mods[i].del); + int sw = (int)strlen(pl) + 1 + (int)strlen(mi); + draw_ansi_at(s, y, x, mw - sw - 1, mods[i].path, CP_SIDE_MUTED, 0, 0); int px = x + mw - sw; px += draw_ansi_at(s, y, px, (int)strlen(pl), pl, CP_SIDE_SUCCESS, 0, 0); draw_ansi_at(s, y, px + 1, (int)strlen(mi), mi, CP_SIDE_PRIMARY, 0, 0); @@ -2617,10 +2726,8 @@ static void draw_side(tui_t *t) { if (h >= 4) { draw_ansi_at(s, h - 3, x, mw, t->where, CP_SIDE_MUTED, 0, 0); - int dx = x + draw_ansi_at(s, h - 2, x, 2, g_ascii ? "*" : "●", - CP_SIDE_SUCCESS, 0, 0); - dx += draw_ansi_at(s, h - 2, dx + 1, mw, "CST-Agent", CP_SIDE_BRIGHT, 1, 0); - draw_ansi_at(s, h - 2, dx + 2, mw - dx, CST_VERSION, CP_SIDE_MUTED, 0, 0); + snprintf(line, sizeof(line), "CST %s", CST_VERSION); + draw_ansi_at(s, h - 2, x, mw, line, CP_SIDE_MUTED, 0, 0); } wrefresh(s); } @@ -3117,7 +3224,7 @@ static void layout(tui_t *t) { t->in_x = (t->cols - t->in_w) / 2; } else { if (t->show_side && t->cols >= 110) - t->side_w = clampi(t->cols / 4, 30, 40); + t->side_w = clampi(t->cols / 5, 28, 36); int mw = t->cols - t->side_w; t->in_x = 1; t->in_w = mw - 2; @@ -4715,9 +4822,27 @@ static void cmd_sidebar(tui_t *t, char *arg, int *quit) { (void)quit; t->show_side = !t->show_side; t->need_layout = 1; - if (t->show_side && t->cols < 120) - push_system_line(t, "(the sidebar shows when the terminal is at least 120 " - "columns wide)"); + if (t->show_side && t->cols < 110) + push_system_line(t, "(the Seat Deck shows when the terminal is at least " + "110 columns wide)"); +} + +static void cmd_motion(tui_t *t, char *arg, int *quit) { + (void)quit; + if (arg && arg[0]) { + if (!strcasecmp(arg, "on") || !strcmp(arg, "1")) + t->motion_on = 1; + else if (!strcasecmp(arg, "off") || !strcmp(arg, "0")) + t->motion_on = 0; + else { + push_system_line(t, "Usage: /motion [on|off]"); + return; + } + } else { + t->motion_on = !t->motion_on; + } + push_system_line(t, t->motion_on ? "(Seat Deck motion on)" + : "(Seat Deck motion off)"); } static void cmd_mouse(tui_t *t, char *arg, int *quit) { @@ -5828,7 +5953,8 @@ static const slash_cmd_t SLASH[] = { {"approval", "", "[ask|auto|yolo]", "how tools get approved (Shift+Tab)", 1, cmd_approval}, {"yolo", "", "", "toggle approving everything", 1, cmd_yolo}, - {"sidebar", "", "", "show or hide the session sidebar", 1, cmd_sidebar}, + {"sidebar", "", "", "show or hide the Seat Deck sidebar", 1, cmd_sidebar}, + {"motion", "", "[on|off]", "Seat Deck ASCII micro-motion", 1, cmd_motion}, {"mouse", "", "", "toggle mouse support (drag copies)", 1, cmd_mouse}, {"max-steps", "", "[N]", "tool steps allowed per turn", 1, cmd_max_steps}, @@ -7202,7 +7328,11 @@ int cst_tui_run(cst_agent_t *agent) { t.focus_think_idx = -1; t.hist_browse = -1; t.show_todo = 1; - t.show_side = 0; /* /sidebar to open; keeps the main column calm by default */ + t.show_side = 1; /* Seat Deck; narrow terminals auto-hide in layout() */ + { + const char *mo = getenv("CST_ASCII_MOTION"); + t.motion_on = !(mo && mo[0] == '0'); + } { cst_settings_t st; cst_settings_load(&st); @@ -7307,6 +7437,8 @@ int cst_tui_run(cst_agent_t *agent) { t.spin++; draw_status(&t); draw_input(&t); + if (t.motion_on && t.side) + draw_side(&t); } if (turn_done) { @@ -7336,8 +7468,13 @@ int cst_tui_run(cst_agent_t *agent) { wint_t wch = 0; int rc = tui_wget_wch(t.input, &wch); - if (rc == ERR) + if (rc == ERR) { + if (!busy && t.motion_on && t.side && !t.home) { + t.spin++; + draw_side(&t); + } continue; + } if (t.sel_state == SEL_SHOWN && !(rc == KEY_CODE_YES && (wch == KEY_MOUSE || wch == KEY_RESIZE))) sel_clear(&t);