feat: adaptive drip buffer for stream reveal (0.9.27)

Network tokens fill md_live immediately; the UI reveals md_shown at a
non-linear rate (slow at the tip, accelerates with backlog) so glitch
and text stay readable without lagging far behind a fast model.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
WHC
2026-10-29 19:05:20 +08:00
co-authored by Cursor
parent 7d183d5bba
commit 1b0f14533a
2 changed files with 140 additions and 15 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
#define CST_VERSION_MAJOR 0
#define CST_VERSION_MINOR 9
#define CST_VERSION_PATCH 26
#define CST_VERSION_PATCH 27
#define CST_VERSION_STR_(x) #x
#define CST_VERSION_STR(x) CST_VERSION_STR_(x)
+136 -11
View File
@@ -339,7 +339,10 @@ typedef struct {
char *md_live;
size_t md_len;
size_t md_cap;
size_t md_rendered; /* md_len at the last live_render */
size_t md_shown; /* bytes of md_live revealed on screen (drip buffer) */
size_t md_rendered; /* md_shown at the last live_render */
double drip_carry; /* fractional codepoints awaiting reveal */
struct timespec drip_at;
char *md_prefix_ansi; /* rendered md_live[0, md_prefix) for long replies */
size_t md_prefix;
struct timespec md_render_at;
@@ -855,7 +858,9 @@ static void live_end_cleanup(tui_t *t) {
t->md_live = NULL;
t->md_len = 0;
t->md_cap = 0;
t->md_shown = 0;
t->md_rendered = 0;
t->drip_carry = 0;
free(t->md_prefix_ansi);
t->md_prefix_ansi = NULL;
t->md_prefix = 0;
@@ -915,12 +920,13 @@ static char *md_render_part(const char *md, size_t n, int close_fence) {
}
/* Markdown → ANSI while streaming: unfinished fences are closed for display,
* and long replies keep the finished prefix rendered once. Caller locks. */
* and long replies keep the finished prefix rendered once. Renders only the
* dripped prefix [0, md_shown). Caller locks. */
static void live_render(tui_t *t) {
if (!t->as_live || !t->md_live || t->md_rendered == t->md_len)
if (!t->as_live || !t->md_live || t->md_rendered == t->md_shown)
return;
const char *md = t->md_live;
size_t n = t->md_len, stable = 0;
size_t n = t->md_shown, stable = 0;
if (n >= LIVE_PREFIX_MIN) {
md_scan(md, t->md_prefix, n, &stable);
if (stable > t->md_prefix + 4096) {
@@ -1057,11 +1063,130 @@ static char *glitch_stream_frontier(const char *src, int spin) {
return out;
}
static size_t utf8_codepoints(const char *s, size_t n) {
size_t i = 0, c = 0;
while (i < n) {
wchar_t wc;
size_t eaten;
utf8_decode(s + i, n - i, &wc, &eaten);
if (eaten == 0)
break;
i += eaten;
c++;
}
return c;
}
static size_t utf8_advance_n(const char *s, size_t n, size_t max_chars) {
size_t i = 0, c = 0;
while (i < n && c < max_chars) {
wchar_t wc;
size_t eaten;
utf8_decode(s + i, n - i, &wc, &eaten);
if (eaten == 0)
break;
i += eaten;
c++;
}
return i;
}
static int isqrt_sz(size_t n) {
if (n <= 1)
return (int)n;
size_t x = n, y = (x + 1) / 2;
while (y < x) {
x = y;
y = (x + n / x) / 2;
}
return (int)x;
}
/* Non-linear drip: slow and readable near the tip; accelerates with backlog
* so a fast model never leaves us many seconds behind. */
static double drip_cps(size_t backlog_chars) {
if (backlog_chars == 0)
return 0;
if (backlog_chars <= 6)
return 14.0; /* ~one glyph / tick — glitch is visible */
if (backlog_chars <= 40)
return 18.0 + (double)backlog_chars * 0.9;
/* ~ 22 + 14*sqrt(backlog) */
double cps = 22.0 + 14.0 * (double)isqrt_sz(backlog_chars);
if (backlog_chars > 600)
cps = (double)backlog_chars * 8.0; /* drain hard */
if (cps > 900.0)
cps = 900.0;
return cps;
}
static int live_apply_shown(tui_t *t) {
free(t->live.body);
t->live.body = t->md_shown ? xstrndup(t->md_live, t->md_shown) : xstrdup("");
t->md_rendered = 0; /* force live_render */
live_render(t);
return 1;
}
/* Advance md_shown toward md_len. Returns 1 if the display changed. */
static int live_drip(tui_t *t) {
if (!t->as_live || !t->md_live || t->md_shown >= t->md_len)
return 0;
long dt = ms_since(&t->drip_at);
if (dt < 12)
return 0;
if (dt > 200)
dt = 200;
clock_gettime(CLOCK_MONOTONIC, &t->drip_at);
size_t backlog =
utf8_codepoints(t->md_live + t->md_shown, t->md_len - t->md_shown);
if (backlog == 0) {
t->md_shown = t->md_len;
return live_apply_shown(t);
}
t->drip_carry += drip_cps(backlog) * ((double)dt / 1000.0);
size_t want = (size_t)t->drip_carry;
if (want < 1) {
if (dt >= 70 && backlog > 0) {
want = 1; /* guarantee tip motion */
t->drip_carry = 0;
} else {
return 0;
}
} else {
t->drip_carry -= (double)want;
}
if (want > backlog)
want = backlog;
if (want > 64)
want = 64; /* hard per-frame cap for readability */
size_t adv =
utf8_advance_n(t->md_live + t->md_shown, t->md_len - t->md_shown, want);
if (adv == 0)
return 0;
t->md_shown += adv;
return live_apply_shown(t);
}
static void live_flush(tui_t *t) {
if (!t->as_live || !t->md_live)
return;
if (t->md_shown >= t->md_len)
return;
t->md_shown = t->md_len;
t->drip_carry = 0;
(void)live_apply_shown(t);
}
static void live_begin(tui_t *t) {
live_end_cleanup(t);
t->md_cap = MD_INIT;
t->md_live = calloc(1, t->md_cap);
t->md_len = 0;
t->md_shown = 0;
t->drip_carry = 0;
clock_gettime(CLOCK_MONOTONIC, &t->drip_at);
t->live.kind = BLK_ASSISTANT;
block_set_title(&t->live, "Assistant");
t->live.expanded = 1;
@@ -1084,17 +1209,13 @@ static void live_feed(tui_t *t, const char *text, size_t len) {
memcpy(t->md_live + t->md_len, text, len);
t->md_len += len;
t->md_live[t->md_len] = 0;
/* The last render stays on screen until the next throttled live_render;
* before the first one the raw text shows. */
free(t->live.body);
t->live.body = xstrndup(t->md_live, t->md_len);
if (!t->live.render || ms_since(&t->md_render_at) >= LIVE_RENDER_MS)
live_render(t);
/* Network buffer only — screen catch-up is live_drip on the UI tick. */
}
static void live_seal(tui_t *t) {
if (!t->as_live)
return;
live_flush(t);
block_t *b = blocks_push(&t->hist);
if (b) {
b->kind = BLK_ASSISTANT;
@@ -7533,11 +7654,15 @@ int cst_tui_run(cst_agent_t *agent) {
}
if (busy) {
tui_lock(&t);
if (t.as_live && t.md_rendered != t.md_len &&
if (t.as_live) {
if (live_drip(&t))
dirty = 1;
else if (t.md_rendered != t.md_shown &&
ms_since(&t.md_render_at) >= LIVE_RENDER_MS) {
live_render(&t);
dirty = 1;
}
}
if (t.think_timing && t.think_idx >= 0 && t.think_idx < t.hist.n) {
char title[64];
snprintf(title, sizeof(title), "Thinking… %.1fs",