fix: SSE line buffering, provider trait plumbing, streaming safety, session resume, code block boxes, markdown improvements
Bug fixes: - Fix SSE chunk parsing: buffer partial lines across network boundaries (openai.rs) - Use Arc<dyn Provider> so send_message calls through the trait and reuses the HTTP connection pool, instead of hardcoding OpenAICompatibleProvider per request (app.rs) - Fix Ctrl+Q during streaming: persist partial response before quitting (app.rs) - Fix Ctrl+N duplicate JSONL messages: remove redundant save_current_session call and guard against new session while streaming (app.rs) - Fix code block content never rendered: text was accumulated into the wrong variable (markdown.rs) - Fix status bar stuck on 'Loading models...' after closing model selector (app.rs) - Fix CJK character panic in session title truncation: use char-level slicing (storage.rs) New features: - Session resume with Ctrl+O: list, browse, and load past chat sessions (app.rs, storage.rs) - Code block boxes: full-border rendering (top/left/right/bottom) with solid background for acrylic/transparent terminals (markdown.rs) - Code blocks fill chat area width with CJK-aware display width padding (markdown.rs, app.rs) - Markdown: add heading support (H1-H6), strikethrough, fix bold+italic by combining style stack layers (markdown.rs) Docs: - Replace CLAUDE.md with AGENTS.md containing repo-specific agent guidance
This commit is contained in:
+23
-22
@@ -89,10 +89,11 @@ impl Provider for OpenAICompatibleProvider {
|
||||
result.map_err(ProviderError::Http)
|
||||
});
|
||||
|
||||
let mut line_buffer = String::new();
|
||||
let parsed = stream
|
||||
.flat_map(|result| {
|
||||
.flat_map(move |result| {
|
||||
futures::stream::iter(match result {
|
||||
Ok(bytes) => parse_sse_chunk(&bytes),
|
||||
Ok(bytes) => parse_sse_chunk(&bytes, &mut line_buffer),
|
||||
Err(e) => vec![Err(e)],
|
||||
})
|
||||
})
|
||||
@@ -102,31 +103,31 @@ impl Provider for OpenAICompatibleProvider {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_sse_chunk(bytes: &[u8]) -> Vec<Result<String, ProviderError>> {
|
||||
let text = String::from_utf8_lossy(bytes);
|
||||
fn parse_sse_chunk(bytes: &[u8], buffer: &mut String) -> Vec<Result<String, ProviderError>> {
|
||||
buffer.push_str(&String::from_utf8_lossy(bytes));
|
||||
let mut results = Vec::new();
|
||||
|
||||
for line in text.lines() {
|
||||
let line = line.trim();
|
||||
if !line.starts_with("data: ") {
|
||||
continue;
|
||||
}
|
||||
let data = &line[6..];
|
||||
if data == "[DONE]" {
|
||||
break;
|
||||
}
|
||||
match serde_json::from_str::<ChatCompletionChunk>(data) {
|
||||
Ok(chunk) => {
|
||||
if let Some(choice) = chunk.choices.first() {
|
||||
if let Some(content) = &choice.delta.content {
|
||||
if !content.is_empty() {
|
||||
results.push(Ok(content.clone()));
|
||||
while let Some(nl_pos) = buffer.find('\n') {
|
||||
let line = buffer[..nl_pos].trim().to_string();
|
||||
*buffer = buffer[nl_pos + 1..].to_string();
|
||||
|
||||
if let Some(data) = line.strip_prefix("data: ") {
|
||||
if data == "[DONE]" {
|
||||
break;
|
||||
}
|
||||
match serde_json::from_str::<ChatCompletionChunk>(data) {
|
||||
Ok(chunk) => {
|
||||
if let Some(choice) = chunk.choices.first() {
|
||||
if let Some(content) = &choice.delta.content {
|
||||
if !content.is_empty() {
|
||||
results.push(Ok(content.clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
results.push(Err(ProviderError::StreamParse(e.to_string())));
|
||||
Err(e) => {
|
||||
results.push(Err(ProviderError::StreamParse(e.to_string())));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user