feat: Seat Deck sidebar with parliament telemetry and motion (0.9.25)

Replace the OpenCode-style inspector with a seat/fleet/ctx/work telemetry
rail, default-on for wide sessions, driven by 80ms ASCII micro-motion.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
WHC
2026-10-29 18:50:36 +08:00
co-authored by Cursor
parent 00288108cd
commit 6e09f79666
4 changed files with 209 additions and 72 deletions
+208 -71
View File
@@ -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);