Fix final transcript updates
This commit is contained in:
+2
-7
@@ -1,19 +1,14 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum AccessMode {
|
||||
#[default]
|
||||
ReadOnly,
|
||||
FullAccess,
|
||||
}
|
||||
|
||||
impl Default for AccessMode {
|
||||
fn default() -> Self {
|
||||
Self::ReadOnly
|
||||
}
|
||||
}
|
||||
|
||||
impl AccessMode {
|
||||
pub fn toggle(self) -> Self {
|
||||
match self {
|
||||
|
||||
+251
-101
@@ -103,6 +103,56 @@ async fn run_tui(
|
||||
let mut autofill_selected = 0usize;
|
||||
|
||||
loop {
|
||||
drain_agent_events(
|
||||
&mut rx,
|
||||
&mut AgentEventContext {
|
||||
terminal: &terminal,
|
||||
input: &input,
|
||||
transcript: &mut transcript,
|
||||
active_assistant: &mut active_assistant,
|
||||
status: &mut status,
|
||||
stick_to_bottom,
|
||||
show_full_tools,
|
||||
scroll: &mut scroll,
|
||||
},
|
||||
)?;
|
||||
|
||||
if handle.as_ref().map(|h| h.is_finished()).unwrap_or(false) {
|
||||
let h = handle.take().unwrap();
|
||||
let result = h.await;
|
||||
drain_agent_events(
|
||||
&mut rx,
|
||||
&mut AgentEventContext {
|
||||
terminal: &terminal,
|
||||
input: &input,
|
||||
transcript: &mut transcript,
|
||||
active_assistant: &mut active_assistant,
|
||||
status: &mut status,
|
||||
stick_to_bottom,
|
||||
show_full_tools,
|
||||
scroll: &mut scroll,
|
||||
},
|
||||
)?;
|
||||
match result {
|
||||
Ok(Ok(updated)) => {
|
||||
conversation = updated;
|
||||
ensure_final_assistant_visible(&mut transcript, &conversation);
|
||||
}
|
||||
Ok(Err(err)) => transcript.push(TranscriptBlock {
|
||||
kind: TranscriptKind::Error,
|
||||
title: "agent error".into(),
|
||||
content: err.to_string(),
|
||||
}),
|
||||
Err(err) => transcript.push(TranscriptBlock {
|
||||
kind: TranscriptKind::Error,
|
||||
title: "agent task error".into(),
|
||||
content: err.to_string(),
|
||||
}),
|
||||
}
|
||||
active_assistant = None;
|
||||
status = "idle".into();
|
||||
}
|
||||
|
||||
let autofill = build_autofill(&input, autofill_selected, &config, &cwd)?;
|
||||
autofill_selected = autofill.as_ref().map(|m| m.selected).unwrap_or(0);
|
||||
scroll = if stick_to_bottom {
|
||||
@@ -131,104 +181,6 @@ async fn run_tui(
|
||||
)
|
||||
})?;
|
||||
|
||||
while let Ok(event) = rx.try_recv() {
|
||||
match event {
|
||||
AgentEvent::AssistantChunk(s) => {
|
||||
if active_assistant.is_none() && s.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
let idx = match active_assistant {
|
||||
Some(i) => i,
|
||||
None => {
|
||||
transcript.push(TranscriptBlock {
|
||||
kind: TranscriptKind::Assistant,
|
||||
title: "response".into(),
|
||||
content: String::new(),
|
||||
});
|
||||
let i = transcript.len() - 1;
|
||||
active_assistant = Some(i);
|
||||
i
|
||||
}
|
||||
};
|
||||
transcript[idx].content.push_str(&s);
|
||||
if stick_to_bottom {
|
||||
scroll = bottom_scroll(&terminal, &input, &transcript, show_full_tools)?;
|
||||
}
|
||||
}
|
||||
AgentEvent::ToolCallStarted {
|
||||
id,
|
||||
name,
|
||||
arguments,
|
||||
} => {
|
||||
active_assistant = None;
|
||||
transcript.push(TranscriptBlock {
|
||||
kind: TranscriptKind::Tool,
|
||||
title: format!("call: {name} ({})", short_call_id(&id)),
|
||||
content: serde_json::to_string_pretty(&arguments)
|
||||
.unwrap_or_else(|_| arguments.to_string()),
|
||||
});
|
||||
if stick_to_bottom {
|
||||
scroll = bottom_scroll(&terminal, &input, &transcript, show_full_tools)?;
|
||||
}
|
||||
}
|
||||
AgentEvent::ToolResult {
|
||||
id,
|
||||
name,
|
||||
ok,
|
||||
content,
|
||||
} => {
|
||||
transcript.push(TranscriptBlock {
|
||||
kind: if ok {
|
||||
TranscriptKind::Tool
|
||||
} else {
|
||||
TranscriptKind::Error
|
||||
},
|
||||
title: format!(
|
||||
"result: {name} {} ({})",
|
||||
if ok { "✓" } else { "✗" },
|
||||
short_call_id(&id)
|
||||
),
|
||||
content,
|
||||
});
|
||||
if stick_to_bottom {
|
||||
scroll = bottom_scroll(&terminal, &input, &transcript, show_full_tools)?;
|
||||
}
|
||||
}
|
||||
AgentEvent::Status(s) => {
|
||||
transcript.push(TranscriptBlock {
|
||||
kind: TranscriptKind::Status,
|
||||
title: "status".into(),
|
||||
content: s,
|
||||
});
|
||||
if stick_to_bottom {
|
||||
scroll = bottom_scroll(&terminal, &input, &transcript, show_full_tools)?;
|
||||
}
|
||||
}
|
||||
AgentEvent::TurnFinished => {
|
||||
active_assistant = None;
|
||||
status = "turn finished".into();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if handle.as_ref().map(|h| h.is_finished()).unwrap_or(false) {
|
||||
let h = handle.take().unwrap();
|
||||
match h.await {
|
||||
Ok(Ok(updated)) => conversation = updated,
|
||||
Ok(Err(err)) => transcript.push(TranscriptBlock {
|
||||
kind: TranscriptKind::Error,
|
||||
title: "agent error".into(),
|
||||
content: err.to_string(),
|
||||
}),
|
||||
Err(err) => transcript.push(TranscriptBlock {
|
||||
kind: TranscriptKind::Error,
|
||||
title: "agent task error".into(),
|
||||
content: err.to_string(),
|
||||
}),
|
||||
}
|
||||
status = "idle".into();
|
||||
}
|
||||
|
||||
if let Some(event) = poll_event(Duration::from_millis(40))? {
|
||||
match event {
|
||||
Event::Key(key) if key.kind == KeyEventKind::Press => {
|
||||
@@ -497,6 +449,147 @@ async fn run_tui(
|
||||
}
|
||||
}
|
||||
|
||||
struct AgentEventContext<'a> {
|
||||
terminal: &'a terminal::CassTerminal,
|
||||
input: &'a str,
|
||||
transcript: &'a mut Vec<TranscriptBlock>,
|
||||
active_assistant: &'a mut Option<usize>,
|
||||
status: &'a mut String,
|
||||
stick_to_bottom: bool,
|
||||
show_full_tools: bool,
|
||||
scroll: &'a mut u16,
|
||||
}
|
||||
|
||||
fn drain_agent_events(
|
||||
rx: &mut mpsc::UnboundedReceiver<AgentEvent>,
|
||||
ctx: &mut AgentEventContext<'_>,
|
||||
) -> Result<()> {
|
||||
while let Ok(event) = rx.try_recv() {
|
||||
apply_agent_event(event, ctx)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_agent_event(event: AgentEvent, ctx: &mut AgentEventContext<'_>) -> Result<()> {
|
||||
match event {
|
||||
AgentEvent::AssistantChunk(s) => {
|
||||
if ctx.active_assistant.is_none() && s.trim().is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let idx = match *ctx.active_assistant {
|
||||
Some(i) => i,
|
||||
None => {
|
||||
ctx.transcript.push(TranscriptBlock {
|
||||
kind: TranscriptKind::Assistant,
|
||||
title: "response".into(),
|
||||
content: String::new(),
|
||||
});
|
||||
let i = ctx.transcript.len() - 1;
|
||||
*ctx.active_assistant = Some(i);
|
||||
i
|
||||
}
|
||||
};
|
||||
ctx.transcript[idx].content.push_str(&s);
|
||||
update_bottom_scroll(ctx)?;
|
||||
}
|
||||
AgentEvent::ToolCallStarted {
|
||||
id,
|
||||
name,
|
||||
arguments,
|
||||
} => {
|
||||
*ctx.active_assistant = None;
|
||||
ctx.transcript.push(TranscriptBlock {
|
||||
kind: TranscriptKind::Tool,
|
||||
title: format!("call: {name} ({})", short_call_id(&id)),
|
||||
content: serde_json::to_string_pretty(&arguments)
|
||||
.unwrap_or_else(|_| arguments.to_string()),
|
||||
});
|
||||
update_bottom_scroll(ctx)?;
|
||||
}
|
||||
AgentEvent::ToolResult {
|
||||
id,
|
||||
name,
|
||||
ok,
|
||||
content,
|
||||
} => {
|
||||
ctx.transcript.push(TranscriptBlock {
|
||||
kind: if ok {
|
||||
TranscriptKind::Tool
|
||||
} else {
|
||||
TranscriptKind::Error
|
||||
},
|
||||
title: format!(
|
||||
"result: {name} {} ({})",
|
||||
if ok { "✓" } else { "✗" },
|
||||
short_call_id(&id)
|
||||
),
|
||||
content,
|
||||
});
|
||||
update_bottom_scroll(ctx)?;
|
||||
}
|
||||
AgentEvent::Status(s) => {
|
||||
ctx.transcript.push(TranscriptBlock {
|
||||
kind: TranscriptKind::Status,
|
||||
title: "status".into(),
|
||||
content: s,
|
||||
});
|
||||
update_bottom_scroll(ctx)?;
|
||||
}
|
||||
AgentEvent::TurnFinished => {
|
||||
*ctx.active_assistant = None;
|
||||
*ctx.status = "turn finished".into();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_bottom_scroll(ctx: &mut AgentEventContext<'_>) -> Result<()> {
|
||||
if ctx.stick_to_bottom {
|
||||
*ctx.scroll = bottom_scroll(ctx.terminal, ctx.input, ctx.transcript, ctx.show_full_tools)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_final_assistant_visible(
|
||||
transcript: &mut Vec<TranscriptBlock>,
|
||||
conversation: &Conversation,
|
||||
) -> bool {
|
||||
let Some(content) = conversation.records.last().and_then(|record| match record {
|
||||
conversation::Record::Assistant {
|
||||
content,
|
||||
tool_calls,
|
||||
..
|
||||
} if tool_calls.is_empty() && !content.trim().is_empty() => Some(content),
|
||||
_ => None,
|
||||
}) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if let Some(last) = transcript.last_mut() {
|
||||
if matches!(last.kind, TranscriptKind::Assistant) {
|
||||
if assistant_content_matches(&last.content, content) {
|
||||
return false;
|
||||
}
|
||||
if content.trim_start().starts_with(last.content.trim_start()) {
|
||||
last.content = content.clone();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
transcript.push(TranscriptBlock {
|
||||
kind: TranscriptKind::Assistant,
|
||||
title: "response".into(),
|
||||
content: content.clone(),
|
||||
});
|
||||
true
|
||||
}
|
||||
|
||||
fn assistant_content_matches(a: &str, b: &str) -> bool {
|
||||
a == b || (!a.trim().is_empty() && a.trim() == b.trim())
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum LocalCommand {
|
||||
Model(String),
|
||||
@@ -901,9 +994,11 @@ mod tests {
|
||||
fn config_with_models(models_json: &str) -> (tempfile::TempDir, Config) {
|
||||
let root = tempdir().unwrap();
|
||||
std::fs::write(root.path().join("models.json"), models_json).unwrap();
|
||||
let mut config = Config::default();
|
||||
config.root = root.path().to_path_buf();
|
||||
config.model = "alpha-model".to_string();
|
||||
let config = Config {
|
||||
root: root.path().to_path_buf(),
|
||||
model: "alpha-model".to_string(),
|
||||
..Config::default()
|
||||
};
|
||||
(root, config)
|
||||
}
|
||||
|
||||
@@ -962,4 +1057,59 @@ mod tests {
|
||||
.unwrap()
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_final_assistant_visible_appends_missing_final() {
|
||||
let conversation = Conversation {
|
||||
id: "chat".into(),
|
||||
path: PathBuf::new(),
|
||||
records: vec![conversation::Record::Assistant {
|
||||
content: "Done.".into(),
|
||||
tool_calls: Vec::new(),
|
||||
ts: "now".into(),
|
||||
}],
|
||||
};
|
||||
let mut transcript = vec![TranscriptBlock {
|
||||
kind: TranscriptKind::Tool,
|
||||
title: "result: read ✓ (call_1)".into(),
|
||||
content: "ok".into(),
|
||||
}];
|
||||
|
||||
assert!(ensure_final_assistant_visible(
|
||||
&mut transcript,
|
||||
&conversation
|
||||
));
|
||||
assert!(matches!(
|
||||
transcript.last(),
|
||||
Some(TranscriptBlock {
|
||||
kind: TranscriptKind::Assistant,
|
||||
content,
|
||||
..
|
||||
}) if content == "Done."
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ensure_final_assistant_visible_does_not_duplicate_streamed_final() {
|
||||
let conversation = Conversation {
|
||||
id: "chat".into(),
|
||||
path: PathBuf::new(),
|
||||
records: vec![conversation::Record::Assistant {
|
||||
content: "\nDone.".into(),
|
||||
tool_calls: Vec::new(),
|
||||
ts: "now".into(),
|
||||
}],
|
||||
};
|
||||
let mut transcript = vec![TranscriptBlock {
|
||||
kind: TranscriptKind::Assistant,
|
||||
title: "response".into(),
|
||||
content: "Done.".into(),
|
||||
}];
|
||||
|
||||
assert!(!ensure_final_assistant_visible(
|
||||
&mut transcript,
|
||||
&conversation
|
||||
));
|
||||
assert_eq!(transcript.len(), 1);
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -173,9 +173,11 @@ impl Config {
|
||||
let models = load_or_create_default_model_registry(&root)?;
|
||||
let file = load_config_file(&root)?;
|
||||
|
||||
let mut cfg = Config::default();
|
||||
cfg.root = root.clone();
|
||||
cfg.docs_dir = docs_dir;
|
||||
let mut cfg = Config {
|
||||
root: root.clone(),
|
||||
docs_dir,
|
||||
..Config::default()
|
||||
};
|
||||
|
||||
if let Some(file) = &file {
|
||||
if let Some(v) = file.default_access_mode {
|
||||
|
||||
+114
-96
@@ -91,12 +91,12 @@ pub fn max_transcript_scroll(
|
||||
area: Rect,
|
||||
) -> u16 {
|
||||
// Paragraph::scroll is measured in rendered rows, not logical lines. Long
|
||||
// tool output and assistant messages wrap, so count the wrapped rows when
|
||||
// deciding where the bottom of the transcript is.
|
||||
// tool output and assistant messages wrap, so count rows with the same
|
||||
// word-wrapping behavior ratatui uses for Paragraph::wrap(trim: false).
|
||||
let content_width = area.width.saturating_sub(2).max(1) as usize;
|
||||
let row_count = transcript_lines_from(transcript, show_full_tools)
|
||||
.iter()
|
||||
.map(|line| wrapped_row_count(&line.to_string(), content_width))
|
||||
.map(|line| ratatui_wrapped_row_count(&line.to_string(), content_width))
|
||||
.sum::<usize>();
|
||||
let viewport_rows = area.height.saturating_sub(2) as usize;
|
||||
row_count
|
||||
@@ -215,107 +215,101 @@ fn transcript_lines_from(
|
||||
lines
|
||||
}
|
||||
|
||||
fn wrapped_row_count(line: &str, content_width: usize) -> usize {
|
||||
let content_width = content_width.max(1);
|
||||
if line.is_empty() {
|
||||
return 1;
|
||||
}
|
||||
|
||||
let mut rows = 1usize;
|
||||
let mut col = 0usize;
|
||||
let mut token_is_whitespace: Option<bool> = None;
|
||||
let mut token_width = 0usize;
|
||||
fn ratatui_wrapped_row_count(line: &str, content_width: usize) -> usize {
|
||||
// This mirrors ratatui's WordWrapper::process_input for Wrap { trim: false }
|
||||
// closely enough for scroll bounds. Details like whitespace-only wrapped
|
||||
// lines matter: if we undercount rows, the scroll bottom stops before the
|
||||
// final assistant message even though it is present in the transcript.
|
||||
let max_width = content_width.max(1);
|
||||
let mut rows = 0usize;
|
||||
let mut pending_line_has_symbols = false;
|
||||
let mut line_width = 0usize;
|
||||
let mut word_width = 0usize;
|
||||
let mut word_symbols = 0usize;
|
||||
let mut whitespace_width = 0usize;
|
||||
let mut whitespace_symbols = 0usize;
|
||||
let mut pending_whitespace = std::collections::VecDeque::new();
|
||||
let mut non_whitespace_previous = false;
|
||||
|
||||
for ch in line.chars() {
|
||||
let is_whitespace = ch.is_whitespace();
|
||||
if token_is_whitespace.is_some_and(|current| current != is_whitespace) {
|
||||
append_wrapped_token(
|
||||
token_width,
|
||||
token_is_whitespace.unwrap(),
|
||||
content_width,
|
||||
&mut rows,
|
||||
&mut col,
|
||||
);
|
||||
token_width = 0;
|
||||
let symbol_width = UnicodeWidthChar::width(ch).unwrap_or(0);
|
||||
if symbol_width > max_width {
|
||||
continue;
|
||||
}
|
||||
token_is_whitespace = Some(is_whitespace);
|
||||
token_width = token_width.saturating_add(UnicodeWidthChar::width(ch).unwrap_or(0));
|
||||
let is_whitespace = ch.is_whitespace();
|
||||
let word_found = non_whitespace_previous && is_whitespace;
|
||||
let untrimmed_overflow = !pending_line_has_symbols
|
||||
&& word_width
|
||||
.saturating_add(whitespace_width)
|
||||
.saturating_add(symbol_width)
|
||||
> max_width;
|
||||
|
||||
if word_found || untrimmed_overflow {
|
||||
if whitespace_symbols > 0 {
|
||||
pending_line_has_symbols = true;
|
||||
line_width = line_width.saturating_add(whitespace_width);
|
||||
}
|
||||
if word_symbols > 0 {
|
||||
pending_line_has_symbols = true;
|
||||
line_width = line_width.saturating_add(word_width);
|
||||
}
|
||||
pending_whitespace.clear();
|
||||
whitespace_width = 0;
|
||||
whitespace_symbols = 0;
|
||||
word_width = 0;
|
||||
word_symbols = 0;
|
||||
}
|
||||
|
||||
let line_full = line_width >= max_width;
|
||||
let pending_word_overflow = symbol_width > 0
|
||||
&& line_width
|
||||
.saturating_add(whitespace_width)
|
||||
.saturating_add(word_width)
|
||||
>= max_width;
|
||||
|
||||
if line_full || pending_word_overflow {
|
||||
rows = rows.saturating_add(1);
|
||||
let mut remaining_width = max_width.saturating_sub(line_width);
|
||||
line_width = 0;
|
||||
pending_line_has_symbols = false;
|
||||
|
||||
while let Some(width) = pending_whitespace.front().copied() {
|
||||
if width > remaining_width {
|
||||
break;
|
||||
}
|
||||
whitespace_width = whitespace_width.saturating_sub(width);
|
||||
whitespace_symbols = whitespace_symbols.saturating_sub(1);
|
||||
remaining_width = remaining_width.saturating_sub(width);
|
||||
pending_whitespace.pop_front();
|
||||
}
|
||||
|
||||
if is_whitespace && pending_whitespace.is_empty() {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if is_whitespace {
|
||||
whitespace_width = whitespace_width.saturating_add(symbol_width);
|
||||
whitespace_symbols = whitespace_symbols.saturating_add(1);
|
||||
pending_whitespace.push_back(symbol_width);
|
||||
} else {
|
||||
word_width = word_width.saturating_add(symbol_width);
|
||||
word_symbols = word_symbols.saturating_add(1);
|
||||
}
|
||||
|
||||
non_whitespace_previous = !is_whitespace;
|
||||
}
|
||||
|
||||
if let Some(is_whitespace) = token_is_whitespace {
|
||||
append_wrapped_token(
|
||||
token_width,
|
||||
is_whitespace,
|
||||
content_width,
|
||||
&mut rows,
|
||||
&mut col,
|
||||
);
|
||||
if !pending_line_has_symbols && word_symbols == 0 && whitespace_symbols > 0 {
|
||||
rows = rows.saturating_add(1);
|
||||
}
|
||||
|
||||
rows
|
||||
}
|
||||
|
||||
fn append_wrapped_token(
|
||||
token_width: usize,
|
||||
is_whitespace: bool,
|
||||
content_width: usize,
|
||||
rows: &mut usize,
|
||||
col: &mut usize,
|
||||
) {
|
||||
if token_width == 0 {
|
||||
return;
|
||||
if whitespace_symbols > 0 || word_symbols > 0 {
|
||||
pending_line_has_symbols = true;
|
||||
}
|
||||
|
||||
if !is_whitespace && *col > 0 && col.saturating_add(token_width) > content_width {
|
||||
*rows = rows.saturating_add(1);
|
||||
*col = 0;
|
||||
append_hard_wrapped(token_width, content_width, rows, col);
|
||||
} else {
|
||||
append_hard_wrapped_from_current(token_width, content_width, rows, col);
|
||||
}
|
||||
}
|
||||
|
||||
fn append_hard_wrapped_from_current(
|
||||
token_width: usize,
|
||||
content_width: usize,
|
||||
rows: &mut usize,
|
||||
col: &mut usize,
|
||||
) {
|
||||
if *col == 0 {
|
||||
append_hard_wrapped(token_width, content_width, rows, col);
|
||||
return;
|
||||
}
|
||||
|
||||
let available = content_width.saturating_sub(*col);
|
||||
if token_width <= available {
|
||||
*col = col.saturating_add(token_width);
|
||||
} else {
|
||||
*rows = rows.saturating_add(1);
|
||||
*col = 0;
|
||||
append_hard_wrapped(
|
||||
token_width.saturating_sub(available),
|
||||
content_width,
|
||||
rows,
|
||||
col,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn append_hard_wrapped(
|
||||
token_width: usize,
|
||||
content_width: usize,
|
||||
rows: &mut usize,
|
||||
col: &mut usize,
|
||||
) {
|
||||
let full_rows = token_width / content_width;
|
||||
let remainder = token_width % content_width;
|
||||
if remainder == 0 {
|
||||
*rows = rows.saturating_add(full_rows.saturating_sub(1));
|
||||
*col = content_width;
|
||||
} else {
|
||||
*rows = rows.saturating_add(full_rows);
|
||||
*col = remainder;
|
||||
if pending_line_has_symbols {
|
||||
rows = rows.saturating_add(1);
|
||||
}
|
||||
rows.max(1)
|
||||
}
|
||||
|
||||
fn style_for(kind: &TranscriptKind) -> Style {
|
||||
@@ -482,4 +476,28 @@ mod tests {
|
||||
|
||||
assert_eq!(max_transcript_scroll(&transcript, false, area), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn max_scroll_counts_whitespace_only_render_rows() {
|
||||
let content = (0..60)
|
||||
.map(|i| if i % 2 == 0 { "x" } else { "" })
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let transcript = vec![
|
||||
TranscriptBlock {
|
||||
kind: TranscriptKind::Tool,
|
||||
title: "result: read ✓ (call_1)".into(),
|
||||
content,
|
||||
},
|
||||
TranscriptBlock {
|
||||
kind: TranscriptKind::Assistant,
|
||||
title: "response".into(),
|
||||
content: "Done.".into(),
|
||||
},
|
||||
];
|
||||
let area = Rect::new(0, 0, 80, 5);
|
||||
let max = max_transcript_scroll(&transcript, false, area);
|
||||
|
||||
assert!(max > 80, "max scroll too low: {max}");
|
||||
}
|
||||
}
|
||||
|
||||
+11
-6
@@ -39,12 +39,17 @@ async fn empty_final_response_is_reprompted_and_persisted() {
|
||||
let root = tempdir().unwrap();
|
||||
let cwd = tempdir().unwrap();
|
||||
let docs = tempdir().unwrap();
|
||||
let mut config = Config::default();
|
||||
config.root = root.path().to_path_buf();
|
||||
config.docs_dir = docs.path().to_path_buf();
|
||||
config.model = "test-model".into();
|
||||
config.active_provider.base_url = server.uri();
|
||||
config.active_provider.api_key = "test-key".into();
|
||||
let config = Config {
|
||||
root: root.path().to_path_buf(),
|
||||
docs_dir: docs.path().to_path_buf(),
|
||||
model: "test-model".into(),
|
||||
active_provider: cassady::config::ResolvedProviderConfig {
|
||||
base_url: server.uri(),
|
||||
api_key: "test-key".into(),
|
||||
..Config::default().active_provider
|
||||
},
|
||||
..Config::default()
|
||||
};
|
||||
|
||||
let conversation = Conversation::create(
|
||||
&config.conversations_dir(),
|
||||
|
||||
Reference in New Issue
Block a user