Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9ea3d361b1 | ||
|
|
271721694b |
@@ -1,3 +1,10 @@
|
|||||||
|
## [1.18.4](https://github.com/asepharyana/zesdex/compare/v1.18.3...v1.18.4) (2026-08-27)
|
||||||
|
|
||||||
|
|
||||||
|
### Performance Improvements
|
||||||
|
|
||||||
|
* **tui:** render streaming token secara inkremental + kurangi redraw sia-sia ([2717216](https://github.com/asepharyana/zesdex/commit/271721694beb62e9fa5ff932312b293aa1d56823))
|
||||||
|
|
||||||
## [1.18.3](https://github.com/asepharyana/zesdex/compare/v1.18.2...v1.18.3) (2026-08-27)
|
## [1.18.3](https://github.com/asepharyana/zesdex/compare/v1.18.2...v1.18.3) (2026-08-27)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -15,7 +15,7 @@ members = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "1.18.3"
|
version = "1.18.4"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["asepharyana <superaseph@gmail.com>"]
|
authors = ["asepharyana <superaseph@gmail.com>"]
|
||||||
|
|
||||||
|
|||||||
@@ -112,9 +112,12 @@ fn run_loop_inner(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Drain expired toasts exactly once per loop iteration (was being
|
// Drain expired toasts exactly once per loop iteration (was being
|
||||||
// done here AND in Action::Tick before this fix).
|
// done here AND in Action::Tick before this fix). Skip the clock
|
||||||
|
// syscall + Vec scan when there are no toasts at all.
|
||||||
|
if !state.misc.toasts.is_empty() {
|
||||||
let now_ms = chrono::Utc::now().timestamp_millis();
|
let now_ms = chrono::Utc::now().timestamp_millis();
|
||||||
state.misc.drain_expired_toasts(now_ms);
|
state.misc.drain_expired_toasts(now_ms);
|
||||||
|
}
|
||||||
|
|
||||||
// Skip render when nothing has changed — avoids expensive markdown
|
// Skip render when nothing has changed — avoids expensive markdown
|
||||||
// re-parse and layout recalculation every cycle while idle.
|
// re-parse and layout recalculation every cycle while idle.
|
||||||
|
|||||||
@@ -179,16 +179,9 @@ impl MiscState {
|
|||||||
self.toasts.push(toast);
|
self.toasts.push(toast);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove and return all toasts whose lifetime has expired at `now_ms`.
|
/// Remove all toasts whose lifetime has expired at `now_ms`.
|
||||||
pub fn drain_expired_toasts(&mut self, now_ms: i64) -> Vec<Toast> {
|
pub fn drain_expired_toasts(&mut self, now_ms: i64) {
|
||||||
let expired: Vec<_> = self
|
|
||||||
.toasts
|
|
||||||
.iter()
|
|
||||||
.filter(|t| t.expired(now_ms))
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
self.toasts.retain(|t| !t.expired(now_ms));
|
self.toasts.retain(|t| !t.expired(now_ms));
|
||||||
expired
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,6 +103,13 @@ pub struct AppStateRest {
|
|||||||
pub last_render_width: u16,
|
pub last_render_width: u16,
|
||||||
/// Number of messages that were in the cache when it was last built.
|
/// Number of messages that were in the cache when it was last built.
|
||||||
pub cached_msg_count: usize,
|
pub cached_msg_count: usize,
|
||||||
|
/// Cache index where the last message's rendered lines begin. Used to
|
||||||
|
/// splice streaming updates (only re-render the trailing message).
|
||||||
|
pub cached_last_start: usize,
|
||||||
|
/// Content+reasoning byte length of the last message when it was last
|
||||||
|
/// rendered. Guards the streaming branch from re-rendering on ticks
|
||||||
|
/// where no new token arrived (spinner-only frames).
|
||||||
|
pub cached_last_len: usize,
|
||||||
/// Terminal width at the time of the last full cache build.
|
/// Terminal width at the time of the last full cache build.
|
||||||
pub render_width_at_cache: u16,
|
pub render_width_at_cache: u16,
|
||||||
/// Atomic flag set when the user aborts the current turn.
|
/// Atomic flag set when the user aborts the current turn.
|
||||||
@@ -180,6 +187,8 @@ impl Default for AppStateRest {
|
|||||||
token_count_dirty: true,
|
token_count_dirty: true,
|
||||||
last_render_width: 0,
|
last_render_width: 0,
|
||||||
cached_msg_count: 0,
|
cached_msg_count: 0,
|
||||||
|
cached_last_start: 0,
|
||||||
|
cached_last_len: 0,
|
||||||
render_width_at_cache: 0,
|
render_width_at_cache: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -230,6 +239,8 @@ impl AppStateRest {
|
|||||||
token_count_dirty: true,
|
token_count_dirty: true,
|
||||||
last_render_width: 0,
|
last_render_width: 0,
|
||||||
cached_msg_count: 0,
|
cached_msg_count: 0,
|
||||||
|
cached_last_start: 0,
|
||||||
|
cached_last_len: 0,
|
||||||
render_width_at_cache: 0,
|
render_width_at_cache: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,6 +190,8 @@ impl Component for ChatComponent {
|
|||||||
}
|
}
|
||||||
state.display_lines_cache = all_lines;
|
state.display_lines_cache = all_lines;
|
||||||
state.cached_msg_count = msg_count;
|
state.cached_msg_count = msg_count;
|
||||||
|
state.cached_last_start = state.display_lines_cache.len();
|
||||||
|
state.cached_last_len = last_msg_len(state);
|
||||||
state.render_width_at_cache = state.last_render_width;
|
state.render_width_at_cache = state.last_render_width;
|
||||||
state.transcript_cache.dirty = false;
|
state.transcript_cache.dirty = false;
|
||||||
} else if state.transcript_cache.dirty && msg_count > cached_count {
|
} else if state.transcript_cache.dirty && msg_count > cached_count {
|
||||||
@@ -205,6 +207,27 @@ impl Component for ChatComponent {
|
|||||||
state.display_lines_cache.extend(msg_lines);
|
state.display_lines_cache.extend(msg_lines);
|
||||||
}
|
}
|
||||||
state.cached_msg_count = msg_count;
|
state.cached_msg_count = msg_count;
|
||||||
|
state.cached_last_start = state.display_lines_cache.len();
|
||||||
|
state.cached_last_len = last_msg_len(state);
|
||||||
|
state.transcript_cache.dirty = false;
|
||||||
|
} else if state.transcript_cache.dirty {
|
||||||
|
// Streaming update: message count unchanged, but the last
|
||||||
|
// assistant message's content grew (StreamToken). Re-render only
|
||||||
|
// that message and splice its lines back into the cache — this
|
||||||
|
// keeps updates O(new content), not O(whole history) per token.
|
||||||
|
// Without this branch, streamed tokens would never appear (the
|
||||||
|
// cache is only rebuilt on new-message or resize paths).
|
||||||
|
let cur_len = last_msg_len(state);
|
||||||
|
if cur_len != state.cached_last_len {
|
||||||
|
if let Some(last_msg) = state.transcript_cache.messages.back() {
|
||||||
|
if state.cached_last_start <= state.display_lines_cache.len() {
|
||||||
|
let new_lines = render_one_message(last_msg, content_width);
|
||||||
|
state.display_lines_cache.truncate(state.cached_last_start);
|
||||||
|
state.display_lines_cache.extend(new_lines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state.cached_last_len = cur_len;
|
||||||
|
}
|
||||||
state.transcript_cache.dirty = false;
|
state.transcript_cache.dirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -326,3 +349,15 @@ fn split_spans_into_lines(spans: Vec<Span<'_>>) -> Vec<Line<'_>> {
|
|||||||
}
|
}
|
||||||
lines
|
lines
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Byte length of the last message's content + reasoning. Used as a cheap
|
||||||
|
/// change detector for the streaming render branch — avoids re-rendering on
|
||||||
|
/// spinner-only ticks where no new token arrived.
|
||||||
|
fn last_msg_len(state: &crate::state::AppStateRest) -> usize {
|
||||||
|
state
|
||||||
|
.transcript_cache
|
||||||
|
.messages
|
||||||
|
.back()
|
||||||
|
.map(|m| m.content.len() + m.reasoning.len())
|
||||||
|
.unwrap_or(0)
|
||||||
|
}
|
||||||
|
|||||||
@@ -86,6 +86,10 @@ pub fn draw(frame: &mut Frame, state: &AppStateRest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_toasts(frame: &mut Frame, state: &AppStateRest) {
|
fn render_toasts(frame: &mut Frame, state: &AppStateRest) {
|
||||||
|
// Fast path: nothing to render.
|
||||||
|
if state.misc.toasts.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let now_ms = chrono::Utc::now().timestamp_millis();
|
let now_ms = chrono::Utc::now().timestamp_millis();
|
||||||
let active: Vec<&zesdex_infrastructure::Toast> = state
|
let active: Vec<&zesdex_infrastructure::Toast> = state
|
||||||
.misc
|
.misc
|
||||||
|
|||||||
Reference in New Issue
Block a user