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:
@@ -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)
|
||||
|
||||
@@ -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,10 +7654,14 @@ int cst_tui_run(cst_agent_t *agent) {
|
||||
}
|
||||
if (busy) {
|
||||
tui_lock(&t);
|
||||
if (t.as_live && t.md_rendered != t.md_len &&
|
||||
ms_since(&t.md_render_at) >= LIVE_RENDER_MS) {
|
||||
live_render(&t);
|
||||
dirty = 1;
|
||||
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];
|
||||
|
||||
Reference in New Issue
Block a user