Watch
1
0
Fork
You've already forked souveraine
0

feat: chunked-replay token streaming for subconscious

Mirrors the primary's pattern at `src/server/turn.rs:445-471` — bifrost
returns the full response in one shot, we chop it into 10-char chunks
and emit each as a `SubconsciousToken` with a 20ms sleep between, so
the subconscious appears to be typing in real time.

The TUI side needed a small refit so the chunks don't each become their
own line in the stream Vec (a 10-char-wide vertical waterfall):

- `ChatState.subconscious_current: String` — the live-building line.
  Token chunks `push_str` here. The render path shows it as the
  brightest bottom line, ahead of finalized history lines above.
- `SubconsciousToolCall`/`SubconsciousToolResult` flush the buffer to
  `subconscious_stream` (as a finalized line), clear it, then push
  their own line. New `SubconsciousPass(active=true)` clears both.
- Render packs the visible window from newest backward, including the
  live line as the bottom entry; old history lines above fade upward
  toward `agent_dim` and gain the `DIM` modifier in the upper third.

If the stream receiver is dropped mid-chunk, the inner loop bails out
but the round itself completes — bifrost is already done, only the UX
narration is interrupted.
This commit is contained in:
Fimeg 2026-05-22 15:11:09 -04:00
commit d8707611da
4 changed files with 78 additions and 22 deletions

View file

@ -782,11 +782,24 @@ Resolve entries: `[YYYY-MM-DD HH:MM] RESOLVED — note`"#;
"subconscious LLM call returned"
);
// Emit streaming events for live TUI visibility
// Chunked-replay streaming for live TUI visibility — mirrors the
// primary's pattern at `src/server/turn.rs:445-471`. Bifrost
// returns the full response in one shot; we slice it into small
// pieces and emit them with a small inter-chunk delay so the
// subconscious appears to be typing in real time.
if let Some(tx) = stream_tx {
let content = response.content.trim();
if !content.is_empty() {
let _ = tx.send(Ok(BackendEvent::SubconsciousToken(content.to_string()))).await;
let trimmed = response.content.trim();
if !trimmed.is_empty() {
let chars: Vec<char> = trimmed.chars().collect();
for chunk in chars.chunks(10) {
let s: String = chunk.iter().collect();
if tx.send(Ok(BackendEvent::SubconsciousToken(s))).await.is_err() {
// Receiver dropped — bail out of the streaming
// emission; the round itself still completes.
break;
}
tokio::time::sleep(tokio::time::Duration::from_millis(20)).await;
}
}
for tc in &response.tool_calls {
let _ = tx.send(Ok(BackendEvent::SubconsciousToolCall {

View file

@ -326,17 +326,28 @@ impl ChatState {
BackendEvent::SubconsciousPass(active) => {
if active {
self.subconscious_stream.clear();
self.subconscious_current.clear();
}
self.pending_consciousness.push(BackendEvent::SubconsciousPass(active));
}
BackendEvent::SubconsciousToken(content) => {
self.subconscious_stream.push(content);
// Chunked-replay: each event is a slice of the LLM's text
// response. Append to the live-building line; the render
// path shows this as the brightest bottom line.
self.subconscious_current.push_str(&content);
}
BackendEvent::SubconsciousToolCall { name, arguments } => {
if !self.subconscious_current.is_empty() {
let line = std::mem::take(&mut self.subconscious_current);
self.subconscious_stream.push(line);
}
self.subconscious_stream.push(format!("{}{}", name, arguments));
}
BackendEvent::SubconsciousToolResult { name, output, .. } => {
// Only show first line of result in the live stream
if !self.subconscious_current.is_empty() {
let line = std::mem::take(&mut self.subconscious_current);
self.subconscious_stream.push(line);
}
let snippet = output.lines().next().unwrap_or(&output);
let clipped = if snippet.len() > 80 {
format!("{}", &snippet[..snippet.floor_char_boundary(77)])

View file

@ -351,12 +351,19 @@ pub struct ChatState {
pub palette: ChatPalette,
pub stream_buffer: String,
/// Live subconscious reasoning stream — ephemeral lines from the N+1 pass.
/// Pushed to by SubconsciousToken/ToolCall/ToolResult events in events.rs.
/// Cleared on each new pass start. Rendered as a fading block below the
/// phase bar during TurnPhase::Subconscious.
/// Live subconscious reasoning stream — completed lines from the N+1 pass.
/// SubconsciousToken chunks build up in `subconscious_current` (below);
/// when a tool call/result event arrives, that buffer is flushed here as
/// a finalized line and cleared. Cleared on each new pass start.
pub subconscious_stream: Vec<String>,
/// The currently-streaming subconscious line — token chunks append here as
/// they arrive (chunked-replay pattern, mirroring `turn.rs` for the
/// primary). Rendered as the bottom-most, brightest line below the
/// fading history in `subconscious_stream`. Promoted to the history Vec
/// when a non-token event (tool call/result, or a new pass) arrives.
pub subconscious_current: String,
/// Current itinerary route-line for the header strip.
/// Empty string means no active itinerary.
pub itinerary_line: String,
@ -510,6 +517,7 @@ impl ChatState {
palette: ChatPalette::default(),
stream_buffer: String::new(),
subconscious_stream: Vec::new(),
subconscious_current: String::new(),
itinerary_line: String::new(),
})
}

View file

@ -29,7 +29,9 @@ pub fn draw(f: &mut Frame, state: &ChatState) {
let input_height = (input_visual_lines.min(max_input_lines) as u16) + 2;
let subconscious_stream_lines = if state.phase == TurnPhase::Subconscious {
let n = state.subconscious_stream.len();
let history = state.subconscious_stream.len();
let live = if state.subconscious_current.is_empty() { 0 } else { 1 };
let n = history + live;
if n > 0 { (n as u16).min(5) } else { 0 }
} else { 0 };
let phase_height: u16 = if state.busy
@ -168,21 +170,40 @@ fn draw_phase(f: &mut Frame, state: &ChatState, area: Rect) {
/// lines above fade upward toward `agent_dim`. Capped at 5 visible lines;
/// scrolls as new events arrive.
fn draw_subconscious_stream(f: &mut Frame, state: &ChatState, area: Rect) {
let stream = &state.subconscious_stream;
if stream.is_empty() || area.height == 0 {
if area.height == 0 {
return;
}
let history = &state.subconscious_stream;
let live = &state.subconscious_current;
let has_live = !live.is_empty();
let total = history.len() + if has_live { 1 } else { 0 };
if total == 0 {
return;
}
let cap = (area.height as usize).min(5);
let visible = stream.len().min(cap);
let start = stream.len() - visible;
let visible = total.min(cap);
let inner_width = (area.width as usize).saturating_sub(2).max(1);
let mut lines: Vec<Line> = Vec::with_capacity(visible);
for (idx, i) in (start..stream.len()).enumerate() {
let is_newest = i == stream.len() - 1;
let t = if visible <= 1 {
// Build the visible window from newest backward, then reverse for render
// so newest sits at the bottom of `area`. `slot_idx` indexes the visible
// window from 0 (top, dimmest) to visible-1 (bottom, brightest).
let mut entries: Vec<&str> = Vec::with_capacity(visible);
if has_live {
entries.push(live.as_str());
}
let history_take = visible - entries.len();
for s in history.iter().rev().take(history_take) {
entries.push(s.as_str());
}
entries.reverse();
let mut lines: Vec<Line> = Vec::with_capacity(entries.len());
for (slot_idx, body) in entries.iter().enumerate() {
let is_newest = slot_idx + 1 == entries.len();
let t = if entries.len() <= 1 {
1.0
} else {
idx as f32 / (visible - 1) as f32
slot_idx as f32 / (entries.len() - 1) as f32
};
let color = lerp_color(state.palette.agent_dim, state.palette.surfacing, t);
let mut style = Style::default().fg(color).add_modifier(Modifier::ITALIC);
@ -190,8 +211,11 @@ fn draw_subconscious_stream(f: &mut Frame, state: &ChatState, area: Rect) {
style = style.add_modifier(Modifier::DIM);
}
let prefix = if is_newest { "" } else { " " };
let body = clip_to_width(&stream[i], inner_width);
lines.push(Line::from(vec![Span::styled(format!("{}{}", prefix, body), style)]));
let clipped = clip_to_width(body, inner_width);
lines.push(Line::from(vec![Span::styled(
format!("{}{}", prefix, clipped),
style,
)]));
}
f.render_widget(Paragraph::new(lines).alignment(Alignment::Left), area);
}