ive harnessed the harness
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

add prompt processing indicator

dawn 1781bc44 eaa3582f

+384 -84
+253 -8
flake.lock
··· 1 1 { 2 2 "nodes": { 3 + "crane": { 4 + "flake": false, 5 + "locked": { 6 + "lastModified": 1758758545, 7 + "narHash": "sha256-NU5WaEdfwF6i8faJ2Yh+jcK9vVFrofLcwlD/mP65JrI=", 8 + "owner": "ipetkov", 9 + "repo": "crane", 10 + "rev": "95d528a5f54eaba0d12102249ce42f4d01f4e364", 11 + "type": "github" 12 + }, 13 + "original": { 14 + "owner": "ipetkov", 15 + "ref": "v0.21.1", 16 + "repo": "crane", 17 + "type": "github" 18 + } 19 + }, 20 + "dream2nix": { 21 + "inputs": { 22 + "nixpkgs": [ 23 + "nci", 24 + "nixpkgs" 25 + ], 26 + "purescript-overlay": "purescript-overlay", 27 + "pyproject-nix": "pyproject-nix" 28 + }, 29 + "locked": { 30 + "lastModified": 1765953015, 31 + "narHash": "sha256-5FBZbbWR1Csp3Y2icfRkxMJw/a/5FGg8hCXej2//bbI=", 32 + "owner": "nix-community", 33 + "repo": "dream2nix", 34 + "rev": "69eb01fa0995e1e90add49d8ca5bcba213b0416f", 35 + "type": "github" 36 + }, 37 + "original": { 38 + "owner": "nix-community", 39 + "repo": "dream2nix", 40 + "type": "github" 41 + } 42 + }, 43 + "flake-compat": { 44 + "flake": false, 45 + "locked": { 46 + "lastModified": 1696426674, 47 + "narHash": "sha256-kvjfFW7WAETZlt09AgDn1MrtKzP7t90Vf7vypd3OL1U=", 48 + "owner": "edolstra", 49 + "repo": "flake-compat", 50 + "rev": "0f9255e01c2351cc7d116c072cb317785dd33b33", 51 + "type": "github" 52 + }, 53 + "original": { 54 + "owner": "edolstra", 55 + "repo": "flake-compat", 56 + "type": "github" 57 + } 58 + }, 59 + "mk-naked-shell": { 60 + "flake": false, 61 + "locked": { 62 + "lastModified": 1681286841, 63 + "narHash": "sha256-3XlJrwlR0nBiREnuogoa5i1b4+w/XPe0z8bbrJASw0g=", 64 + "owner": "90-008", 65 + "repo": "mk-naked-shell", 66 + "rev": "7612f828dd6f22b7fb332cc69440e839d7ffe6bd", 67 + "type": "github" 68 + }, 69 + "original": { 70 + "owner": "90-008", 71 + "repo": "mk-naked-shell", 72 + "type": "github" 73 + } 74 + }, 75 + "nci": { 76 + "inputs": { 77 + "crane": "crane", 78 + "dream2nix": "dream2nix", 79 + "mk-naked-shell": "mk-naked-shell", 80 + "nixpkgs": "nixpkgs", 81 + "parts": "parts", 82 + "rust-overlay": "rust-overlay", 83 + "treefmt": "treefmt" 84 + }, 85 + "locked": { 86 + "lastModified": 1775459945, 87 + "narHash": "sha256-52p03BHauMmZyYYhx1QFtPooDwL6YxDtFMFNDtqB5Oo=", 88 + "owner": "90-008", 89 + "repo": "nix-cargo-integration", 90 + "rev": "e53db757c693e2f1265a3b680f5360c3f528b7a7", 91 + "type": "github" 92 + }, 93 + "original": { 94 + "owner": "90-008", 95 + "repo": "nix-cargo-integration", 96 + "type": "github" 97 + } 98 + }, 3 99 "nixpkgs": { 4 100 "locked": { 5 - "lastModified": 1775403759, 6 - "narHash": "sha256-cGyKiTspHEUx3QwAnV3RfyT+VOXhHLs+NEr17HU34Wo=", 7 - "owner": "nixos", 101 + "lastModified": 1775036866, 102 + "narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=", 103 + "owner": "NixOS", 8 104 "repo": "nixpkgs", 9 - "rev": "5e11f7acce6c3469bef9df154d78534fa7ae8b6c", 105 + "rev": "6201e203d09599479a3b3450ed24fa81537ebc4e", 10 106 "type": "github" 11 107 }, 12 108 "original": { 13 - "owner": "nixos", 14 - "ref": "nixpkgs-unstable", 109 + "owner": "NixOS", 110 + "ref": "nixos-unstable", 15 111 "repo": "nixpkgs", 16 112 "type": "github" 17 113 } ··· 31 127 "type": "github" 32 128 } 33 129 }, 130 + "nixpkgs_2": { 131 + "locked": { 132 + "lastModified": 1775403759, 133 + "narHash": "sha256-cGyKiTspHEUx3QwAnV3RfyT+VOXhHLs+NEr17HU34Wo=", 134 + "owner": "nixos", 135 + "repo": "nixpkgs", 136 + "rev": "5e11f7acce6c3469bef9df154d78534fa7ae8b6c", 137 + "type": "github" 138 + }, 139 + "original": { 140 + "owner": "nixos", 141 + "ref": "nixpkgs-unstable", 142 + "repo": "nixpkgs", 143 + "type": "github" 144 + } 145 + }, 34 146 "parts": { 35 147 "inputs": { 148 + "nixpkgs-lib": [ 149 + "nci", 150 + "nixpkgs" 151 + ] 152 + }, 153 + "locked": { 154 + "lastModified": 1775087534, 155 + "narHash": "sha256-91qqW8lhL7TLwgQWijoGBbiD4t7/q75KTi8NxjVmSmA=", 156 + "owner": "hercules-ci", 157 + "repo": "flake-parts", 158 + "rev": "3107b77cd68437b9a76194f0f7f9c55f2329ca5b", 159 + "type": "github" 160 + }, 161 + "original": { 162 + "owner": "hercules-ci", 163 + "repo": "flake-parts", 164 + "type": "github" 165 + } 166 + }, 167 + "parts_2": { 168 + "inputs": { 36 169 "nixpkgs-lib": "nixpkgs-lib" 37 170 }, 38 171 "locked": { ··· 49 182 "type": "github" 50 183 } 51 184 }, 185 + "purescript-overlay": { 186 + "inputs": { 187 + "flake-compat": "flake-compat", 188 + "nixpkgs": [ 189 + "nci", 190 + "dream2nix", 191 + "nixpkgs" 192 + ], 193 + "slimlock": "slimlock" 194 + }, 195 + "locked": { 196 + "lastModified": 1728546539, 197 + "narHash": "sha256-Sws7w0tlnjD+Bjck1nv29NjC5DbL6nH5auL9Ex9Iz2A=", 198 + "owner": "thomashoneyman", 199 + "repo": "purescript-overlay", 200 + "rev": "4ad4c15d07bd899d7346b331f377606631eb0ee4", 201 + "type": "github" 202 + }, 203 + "original": { 204 + "owner": "thomashoneyman", 205 + "repo": "purescript-overlay", 206 + "type": "github" 207 + } 208 + }, 209 + "pyproject-nix": { 210 + "inputs": { 211 + "nixpkgs": [ 212 + "nci", 213 + "dream2nix", 214 + "nixpkgs" 215 + ] 216 + }, 217 + "locked": { 218 + "lastModified": 1763017646, 219 + "narHash": "sha256-Z+R2lveIp6Skn1VPH3taQIuMhABg1IizJd8oVdmdHsQ=", 220 + "owner": "pyproject-nix", 221 + "repo": "pyproject.nix", 222 + "rev": "47bd6f296502842643078d66128f7b5e5370790c", 223 + "type": "github" 224 + }, 225 + "original": { 226 + "owner": "pyproject-nix", 227 + "repo": "pyproject.nix", 228 + "type": "github" 229 + } 230 + }, 52 231 "root": { 53 232 "inputs": { 54 - "nixpkgs": "nixpkgs", 55 - "parts": "parts" 233 + "nci": "nci", 234 + "nixpkgs": "nixpkgs_2", 235 + "parts": "parts_2" 236 + } 237 + }, 238 + "rust-overlay": { 239 + "inputs": { 240 + "nixpkgs": [ 241 + "nci", 242 + "nixpkgs" 243 + ] 244 + }, 245 + "locked": { 246 + "lastModified": 1775445266, 247 + "narHash": "sha256-3fgIj85WHQbOamrpIw9WY3ZL1PoEvjPOjmzMYNsEQJo=", 248 + "owner": "oxalica", 249 + "repo": "rust-overlay", 250 + "rev": "61747bc3cf2da179b9af356ae9e70f3b895b0c24", 251 + "type": "github" 252 + }, 253 + "original": { 254 + "owner": "oxalica", 255 + "repo": "rust-overlay", 256 + "type": "github" 257 + } 258 + }, 259 + "slimlock": { 260 + "inputs": { 261 + "nixpkgs": [ 262 + "nci", 263 + "dream2nix", 264 + "purescript-overlay", 265 + "nixpkgs" 266 + ] 267 + }, 268 + "locked": { 269 + "lastModified": 1688756706, 270 + "narHash": "sha256-xzkkMv3neJJJ89zo3o2ojp7nFeaZc2G0fYwNXNJRFlo=", 271 + "owner": "thomashoneyman", 272 + "repo": "slimlock", 273 + "rev": "cf72723f59e2340d24881fd7bf61cb113b4c407c", 274 + "type": "github" 275 + }, 276 + "original": { 277 + "owner": "thomashoneyman", 278 + "repo": "slimlock", 279 + "type": "github" 280 + } 281 + }, 282 + "treefmt": { 283 + "inputs": { 284 + "nixpkgs": [ 285 + "nci", 286 + "nixpkgs" 287 + ] 288 + }, 289 + "locked": { 290 + "lastModified": 1775125835, 291 + "narHash": "sha256-2qYcPgzFhnQWchHo0SlqLHrXpux5i6ay6UHA+v2iH4U=", 292 + "owner": "numtide", 293 + "repo": "treefmt-nix", 294 + "rev": "75925962939880974e3ab417879daffcba36c4a3", 295 + "type": "github" 296 + }, 297 + "original": { 298 + "owner": "numtide", 299 + "repo": "treefmt-nix", 300 + "type": "github" 56 301 } 57 302 } 58 303 },
+10 -14
flake.nix
··· 1 1 { 2 2 inputs.parts.url = "github:hercules-ci/flake-parts"; 3 3 inputs.nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable"; 4 + inputs.nci.url = "github:90-008/nix-cargo-integration"; 4 5 5 6 outputs = 6 7 inp: 7 8 inp.parts.lib.mkFlake { inputs = inp; } { 8 9 systems = [ "x86_64-linux" ]; 10 + imports = [inp.nci.flakeModule]; 9 11 perSystem = 10 12 { 11 13 pkgs, ··· 13 15 ... 14 16 }: 15 17 { 18 + nci.projects."klbr".path = ./.; 16 19 packages.default = pkgs.callPackage ./default.nix {}; 17 - devShells = { 18 - default = pkgs.mkShell { 19 - packages = with pkgs; [ 20 - rustPlatform.rustLibSrc 21 - rust-analyzer 22 - cargo 23 - cargo-outdated 24 - rustc 25 - rustfmt 26 - clang 27 - wild 28 - ]; 29 - }; 30 - }; 20 + devShells.default = config.nci.outputs."klbr".devShell.overrideAttrs (old: { 21 + packages = (old.packages or []) ++ (with pkgs; [ 22 + cargo-outdated 23 + clang 24 + wild 25 + ]); 26 + }); 31 27 }; 32 28 }; 33 29 }
+5 -1
klbr-core/src/agent.rs
··· 26 26 27 27 // resume: replay the sliding window from the last run 28 28 if let Ok(prior) = memory.recent_turns(config.compaction_keep) { 29 - let pairs: Vec<(String, String)> = prior.into_iter().map(|(_, r, c)| (r, c)).collect(); 29 + let pairs: Vec<(String, String)> = prior 30 + .into_iter() 31 + .map(|entry| (entry.role, entry.content)) 32 + .collect(); 30 33 ctx.load_turns(&pairs); 31 34 turn_count = pairs.len(); 32 35 } ··· 66 69 tokio::spawn(async move { 67 70 let _ = llm2.stream(&msgs, tok_tx).await; 68 71 }); 72 + let _ = output.send(ServerMsg::Started); 69 73 70 74 let mut response = String::new(); 71 75 let mut thinking = String::new();
+1 -1
klbr-core/src/daemon.rs
··· 89 89 ClientMsg::FetchHistory { before_id, limit } => { 90 90 let limit = limit.min(HISTORY_PAGE); 91 91 let turns = memory.turns_before(before_id, limit).unwrap_or_default(); 92 - send_msg(&mut sock_tx, &ServerMsg::HistoryPage { turns }).await?; 92 + send_msg(&mut sock_tx, &ServerMsg::History { turns }).await?; 93 93 } 94 94 } 95 95 }
+19 -14
klbr-core/src/memory.rs
··· 1 1 use anyhow::Result; 2 + use klbr_ipc::HistoryEntry; 2 3 use rusqlite::{ffi::sqlite3_auto_extension, params, Connection}; 3 4 use sqlite_vec::sqlite3_vec_init; 4 5 use std::sync::{Arc, Mutex}; ··· 103 104 } 104 105 105 106 /// last `n` turns in chronological order (oldest first, ready to replay into context). 106 - pub fn recent_turns(&self, n: usize) -> Result<Vec<(i64, String, String)>> { 107 + pub fn recent_turns(&self, n: usize) -> Result<Vec<HistoryEntry>> { 107 108 let conn = self.0.lock().unwrap(); 108 109 let mut stmt = conn.prepare( 109 - "SELECT id, role, content FROM (SELECT id, role, content, ts FROM turns ORDER BY ts DESC LIMIT ?1) ORDER BY ts ASC", 110 + "SELECT id, role, content, thinking, ts FROM (SELECT id, role, content, thinking, ts FROM turns ORDER BY ts DESC LIMIT ?1) ORDER BY ts ASC", 110 111 )?; 111 112 let results = stmt 112 113 .query_map(params![n as i64], |row| { 113 - Ok(( 114 - row.get::<_, i64>(0)?, 115 - row.get::<_, String>(1)?, 116 - row.get::<_, String>(2)?, 117 - )) 114 + Ok(HistoryEntry { 115 + id: row.get(0)?, 116 + role: row.get(1)?, 117 + content: row.get(2)?, 118 + reasoning: row.get(3)?, 119 + timestamp: row.get(4)?, 120 + }) 118 121 })? 119 122 .filter_map(|r| r.ok()) 120 123 .collect(); ··· 122 125 } 123 126 124 127 /// turns older than `before_id`, newest-first then reversed, for scroll-back paging. 125 - pub fn turns_before(&self, before_id: i64, limit: usize) -> Result<Vec<(i64, String, String)>> { 128 + pub fn turns_before(&self, before_id: i64, limit: usize) -> Result<Vec<HistoryEntry>> { 126 129 let conn = self.0.lock().unwrap(); 127 130 let mut stmt = conn.prepare( 128 - "SELECT id, role, content FROM (SELECT id, role, content FROM turns WHERE id < ?1 ORDER BY id DESC LIMIT ?2) ORDER BY id ASC", 131 + "SELECT id, role, content, thinking, ts FROM (SELECT id, role, content, thinking, ts FROM turns WHERE id < ?1 ORDER BY id DESC LIMIT ?2) ORDER BY id ASC", 129 132 )?; 130 133 let results = stmt 131 134 .query_map(params![before_id, limit as i64], |row| { 132 - Ok(( 133 - row.get::<_, i64>(0)?, 134 - row.get::<_, String>(1)?, 135 - row.get::<_, String>(2)?, 136 - )) 135 + Ok(HistoryEntry { 136 + id: row.get(0)?, 137 + role: row.get(1)?, 138 + content: row.get(2)?, 139 + reasoning: row.get(3)?, 140 + timestamp: row.get(4)?, 141 + }) 137 142 })? 138 143 .filter_map(|r| r.ok()) 139 144 .collect();
+12 -6
klbr-ipc/src/lib.rs
··· 9 9 FetchHistory { before_id: i64, limit: usize }, 10 10 } 11 11 12 + #[derive(Debug, Serialize, Deserialize, Clone)] 13 + #[serde(rename_all = "snake_case")] 14 + pub struct HistoryEntry { 15 + pub id: i64, 16 + pub timestamp: i64, 17 + pub role: String, 18 + pub content: String, 19 + pub reasoning: Option<String>, 20 + } 21 + 12 22 /// daemon → client 13 23 #[derive(Debug, Serialize, Deserialize, Clone)] 14 24 #[serde(tag = "type", rename_all = "snake_case")] 15 25 pub enum ServerMsg { 26 + Started, 16 27 Token { 17 28 content: String, 18 29 }, ··· 29 40 context_tokens: usize, 30 41 watermark: usize, 31 42 }, 32 - /// pushed on connect: current context window turns 33 43 History { 34 - turns: Vec<(i64, String, String)>, 35 - }, 36 - /// response to FetchHistory 37 - HistoryPage { 38 - turns: Vec<(i64, String, String)>, 44 + turns: Vec<HistoryEntry>, 39 45 }, 40 46 } 41 47
+81 -40
klbr-tui/src/main.rs
··· 35 35 } 36 36 37 37 #[derive(Clone)] 38 + enum AssistantStep { 39 + PromptProcessing, 40 + Reasoning, 41 + Response, 42 + Done, 43 + } 44 + 45 + #[derive(Clone)] 38 46 enum Role { 39 47 System, 40 48 User, 41 - Assistant { reason: Option<Reason>, done: bool }, 49 + Assistant { 50 + reason: Option<Reason>, 51 + step: AssistantStep, 52 + }, 42 53 } 43 54 44 55 impl Role { ··· 48 59 49 60 fn if_assistant<F, T>(&mut self, f: F) -> Option<T> 50 61 where 51 - F: FnOnce(&mut Option<Reason>, &mut bool) -> T, 62 + F: FnOnce(&mut Option<Reason>, &mut AssistantStep) -> T, 52 63 { 53 64 match self { 54 - Role::Assistant { reason, done } => Some(f(reason, done)), 65 + Role::Assistant { reason, step } => Some(f(reason, step)), 55 66 _ => None, 56 67 } 57 68 } ··· 84 95 fn assistant() -> Self { 85 96 Self { 86 97 role: Role::Assistant { 87 - done: false, 98 + step: AssistantStep::PromptProcessing, 88 99 reason: None, 89 100 }, 90 101 content: String::new(), ··· 189 200 self.at_bottom = false; 190 201 } 191 202 192 - fn prepend_turns(&mut self, turns: Vec<(i64, String, String)>) -> bool { 203 + fn prepend_turns(&mut self, turns: Vec<klbr_ipc::HistoryEntry>) -> bool { 193 204 if turns.is_empty() { 194 205 return false; 195 206 } 196 207 let msgs: Vec<ChatMsg> = turns 197 - .iter() 198 - .filter_map(|(id, role, content)| { 208 + .into_iter() 209 + .filter_map(|entry| { 199 210 // track oldest id for next page request 200 - if self.oldest_turn_id.map_or(true, |cur| *id < cur) { 201 - self.oldest_turn_id = Some(*id); 211 + if self.oldest_turn_id.map_or(true, |cur| entry.id < cur) { 212 + self.oldest_turn_id = Some(entry.id); 202 213 } 203 - match role.as_str() { 204 - "user" => Some(ChatMsg::user(content.clone())), 214 + match entry.role.as_str() { 215 + "user" => Some(ChatMsg::user(entry.content)), 216 + "system" => Some(ChatMsg::system(entry.content)), 205 217 "assistant" => { 206 218 let mut m = ChatMsg::assistant(); 207 - m.content = content.clone(); 219 + m.content = entry.content; 208 220 m.role = Role::Assistant { 209 - reason: None, 210 - done: true, 221 + reason: entry.reasoning.map(|content| Reason { 222 + content, 223 + expanded: false, 224 + done: true, 225 + }), 226 + step: AssistantStep::Done, 211 227 }; 212 228 Some(m) 213 229 } ··· 226 242 self.at_bottom = true; 227 243 } 228 244 229 - // gets the last assistant message 245 + // gets the last assistant message that we are streaming 230 246 fn streaming_mut(&mut self) -> Option<&mut ChatMsg> { 231 - self.history 232 - .last_mut() 233 - .filter(|m| matches!(m.role, Role::Assistant { done: false, .. })) 247 + self.history.last_mut().filter(|m| { 248 + matches!( 249 + m.role, 250 + Role::Assistant { 251 + step: AssistantStep::PromptProcessing 252 + | AssistantStep::Reasoning 253 + | AssistantStep::Response, 254 + .. 255 + } 256 + ) 257 + }) 234 258 } 235 259 236 260 fn toggle_last_think(&mut self) { ··· 250 274 }); 251 275 } 252 276 253 - fn get_assistant_entry(&mut self) -> &mut ChatMsg { 277 + fn token_entry(&mut self) -> &mut ChatMsg { 278 + if self.stream_start.is_none() { 279 + self.stream_start = Some(Instant::now()); 280 + } 254 281 if self.streaming_mut().is_none() { 255 282 self.history.push(ChatMsg::assistant()); 256 - self.stream_start = Some(Instant::now()); 257 283 self.stream_tokens = 0; 258 284 } 259 285 self.stream_tokens += 1; ··· 329 355 } 330 356 was_assistant = false; 331 357 } 332 - Role::Assistant { reason, done } => { 358 + Role::Assistant { reason, step } => { 359 + was_assistant = true; 360 + let klbr_span = Span::styled( 361 + "klbr", 362 + Style::default() 363 + .fg(Color::Green) 364 + .add_modifier(Modifier::DIM), 365 + ); 366 + if let AssistantStep::PromptProcessing = step { 367 + lines.push(Line::from(vec![ 368 + klbr_span, 369 + Span::styled( 370 + " is processing prompt...", 371 + Style::default() 372 + .fg(Color::DarkGray) 373 + .add_modifier(Modifier::DIM), 374 + ), 375 + ])); 376 + continue; 377 + } 333 378 let think_expanded = if let Some(think) = reason { 334 379 let think_token = think 335 380 .done 336 381 .then_some("has reasoned") 337 382 .unwrap_or("is reasoning..."); 338 - let klbr_span = Span::styled( 339 - "klbr", 340 - Style::default() 341 - .fg(Color::Green) 342 - .add_modifier(Modifier::DIM), 343 - ); 344 383 if think.expanded { 345 384 lines.push(Line::from(vec![ 346 385 klbr_span, ··· 382 421 Span::raw(line), 383 422 ])); 384 423 } 385 - was_assistant = true; 386 - // suppress unused warning — the field exists for future use 387 - let _ = done; 388 424 } 389 425 } 390 426 } ··· 595 631 _ => {} 596 632 }, 597 633 Event::Mouse(m) => match m.kind { 598 - MouseEventKind::ScrollUp => app.scroll_up(1), 599 - MouseEventKind::ScrollDown => app.scroll_down(1), 634 + MouseEventKind::ScrollUp => app.scroll_up(2), 635 + MouseEventKind::ScrollDown => app.scroll_down(2), 600 636 _ => {} 601 637 }, 602 638 _ => {} ··· 607 643 fn handle_message(app: &mut App, line: String) -> Result<()> { 608 644 let msg: ServerMsg = serde_json::from_str(&line)?; 609 645 match msg { 646 + ServerMsg::Started => { 647 + if app.streaming_mut().is_none() { 648 + app.history.push(ChatMsg::assistant()); 649 + app.stream_tokens = 0; 650 + } 651 + } 610 652 ServerMsg::Token { content } => { 611 - let entry = app.get_assistant_entry(); 653 + let entry = app.token_entry(); 612 654 entry.content.push_str(&content); 613 655 // mark reasoning as done 614 - entry.role.if_assistant(|r, _| { 656 + entry.role.if_assistant(|r, step| { 657 + *step = AssistantStep::Response; 615 658 if let Some(r) = r { 616 659 r.done = true; 617 660 } 618 661 }); 619 662 } 620 663 ServerMsg::ThinkToken { content } => { 621 - app.get_assistant_entry().role.if_assistant(|r, _| { 664 + app.token_entry().role.if_assistant(|r, step| { 665 + *step = AssistantStep::Reasoning; 622 666 r.get_or_insert_with(Reason::default) 623 667 .content 624 668 .push_str(&content) ··· 626 670 } 627 671 ServerMsg::Done => { 628 672 // mark assistant message as done 629 - app.get_assistant_entry() 673 + app.token_entry() 630 674 .role 631 - .if_assistant(|_, done| *done = true); 675 + .if_assistant(|_, step| *step = AssistantStep::Done); 632 676 app.status.clear(); 633 677 app.stream_start = None; 634 678 } ··· 645 689 app.watermark = watermark; 646 690 } 647 691 ServerMsg::History { turns } => { 648 - app.prepend_turns(turns); 649 - } 650 - ServerMsg::HistoryPage { turns } => { 651 692 app.loading_history = false; 652 693 if !app.prepend_turns(turns) { 653 694 app.history_exhausted = true;
+3
rust-toolchain.toml
··· 1 + [toolchain] 2 + channel = "stable" 3 + components = ["rust-analyzer", "rust-src", "rustfmt"]