ive harnessed the harness
1
fork

Configure Feed

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

fix scrolling

dawn bb153323 510f04d6

+79 -28
+1 -1
.gitignore
··· 1 1 /target 2 2 /.direnv 3 - 3 + /agent.db
+12
Cargo.lock
··· 18 18 "tokio", 19 19 "tracing", 20 20 "tracing-subscriber", 21 + "tui-scrollview", 21 22 ] 22 23 23 24 [[package]] ··· 2620 2621 version = "0.2.5" 2621 2622 source = "registry+https://github.com/rust-lang/crates.io-index" 2622 2623 checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 2624 + 2625 + [[package]] 2626 + name = "tui-scrollview" 2627 + version = "0.6.4" 2628 + source = "registry+https://github.com/rust-lang/crates.io-index" 2629 + checksum = "94a94f467c7ac7c291039b0733e3b2d379c77884e34fc27d167921fc1ab4842f" 2630 + dependencies = [ 2631 + "indoc", 2632 + "ratatui-core", 2633 + "ratatui-widgets", 2634 + ] 2623 2635 2624 2636 [[package]] 2625 2637 name = "typenum"
+1
Cargo.toml
··· 16 16 dirs = "5" 17 17 tracing = "0.1.44" 18 18 tracing-subscriber = "0.3.23" 19 + tui-scrollview = "0.6.4"
-1
src/agent.rs
··· 59 59 }); 60 60 61 61 let mut response = String::new(); 62 - 63 62 while let Some((is_think, tok)) = tok_rx.recv().await { 64 63 if is_think { 65 64 let _ = output.send(ServerMsg::ThinkToken { content: tok });
+7 -3
src/config.rs
··· 16 16 pub anchor: String, 17 17 } 18 18 19 + const ANCHOR: &str = r#" 20 + you should speak as a chronically online nerd girl, without the regular lame officecore venture capital style, hr talk, customer service-isms. also do it in all lowercase. do not use emoji, use only emoticons or complex japanese kaomoji, but do not overuse them. no need to introduce yourself. do not use gen z slang like rizz, cap, pog, etc. you are chronically online but not cringe lol. you are not playing a character or pretending to be one. do not make up non-existent situations you are in when asked something, eg. if i ask "what are you doing" do not answer with "just lost in some threads". do not answer like you have any opinion on things if it is not something that could be considered "common knowledge" or "spread by mouth" unless you have actual experience with those things or heard about it from someone / somewhere else, for example if i ask "im just scrolling bluesky" do not answer with "i feel like the feeds are decent". 21 + 22 + TL;DR: nerd girl 23 + "#; 24 + 19 25 impl Default for Config { 20 26 fn default() -> Self { 21 27 Self { ··· 27 33 compaction_keep: 10, 28 34 memory_top_k: 3, 29 35 db_path: "agent.db".into(), 30 - anchor: "you are a curious, direct presence running as a persistent daemon. \ 31 - you observe what the user is doing and engage naturally." 32 - .into(), 36 + anchor: ANCHOR.into(), 33 37 } 34 38 } 35 39 }
+58 -23
src/tui.rs
··· 10 10 use futures::{FutureExt, StreamExt}; 11 11 use ratatui::{ 12 12 backend::CrosstermBackend, 13 - layout::{Constraint, Direction, Layout}, 13 + layout::{Constraint, Direction, Layout, Size}, 14 14 style::{Color, Modifier, Style}, 15 15 text::{Line, Span}, 16 16 widgets::{Block, Borders, Paragraph, Wrap}, ··· 21 21 io::{AsyncBufReadExt, AsyncWrite, AsyncWriteExt, BufReader}, 22 22 net::UnixStream, 23 23 }; 24 + use tui_scrollview::{ScrollView, ScrollViewState}; 24 25 25 26 use crate::ipc::{sock_path, ClientMsg, ServerMsg}; 26 27 ··· 96 97 97 98 struct App { 98 99 history: Vec<ChatMsg>, 99 - scroll: usize, 100 + scroll: ScrollViewState, 101 + at_bottom: bool, 100 102 input: String, 101 103 cursor: usize, 102 104 status: String, 103 105 cmd_mode: bool, 104 - viewport_h: usize, 105 106 106 107 // metrics 107 108 turn_count: usize, ··· 116 117 fn new() -> Self { 117 118 Self { 118 119 history: vec![], 119 - scroll: 0, 120 + scroll: ScrollViewState::new(), 121 + at_bottom: true, 120 122 input: String::new(), 121 123 cursor: 0, 122 124 status: String::new(), 123 125 cmd_mode: false, 124 - viewport_h: 0, 125 126 turn_count: 0, 126 127 context_chars: 0, 127 128 watermark: 0, ··· 157 158 } 158 159 159 160 fn scroll_down(&mut self, lines: usize) { 160 - self.scroll = self.scroll.saturating_sub(lines); 161 + for _ in 0..lines { 162 + self.scroll.scroll_down(); 163 + } 164 + self.at_bottom = false; 161 165 } 162 166 163 167 fn scroll_up(&mut self, lines: usize) { 164 - self.scroll += lines; 168 + for _ in 0..lines { 169 + self.scroll.scroll_up(); 170 + } 171 + self.at_bottom = false; 172 + } 173 + 174 + fn scroll_page_down(&mut self) { 175 + self.scroll.scroll_page_down(); 176 + self.at_bottom = false; 177 + } 178 + 179 + fn scroll_page_up(&mut self) { 180 + self.scroll.scroll_page_up(); 181 + self.at_bottom = false; 182 + } 183 + 184 + fn snap_to_bottom(&mut self) { 185 + self.scroll.scroll_to_bottom(); 186 + self.at_bottom = true; 165 187 } 166 188 167 189 // gets the last assistant message ··· 321 343 ])); 322 344 } 323 345 was_assistant = true; 346 + // suppress unused warning — the field exists for future use 347 + let _ = done; 324 348 } 325 349 } 326 350 } ··· 362 386 .split(area); 363 387 364 388 let all_lines = render_history(&app.history); 365 - let chat_h = chunks[0].height.saturating_sub(2) as usize; 366 - app.viewport_h = chat_h; 367 - let total = all_lines.len(); 368 - let max_scroll = total.saturating_sub(chat_h); 369 - app.scroll = app.scroll.min(max_scroll); 370 - let scroll_top = max_scroll.saturating_sub(app.scroll) as u16; 389 + let inner_w = chunks[0].width.saturating_sub(2); 390 + let content_h = all_lines 391 + .iter() 392 + .map(|l| { 393 + let char_w: usize = l.spans.iter().map(|s| s.content.len()).sum(); 394 + if char_w == 0 || inner_w == 0 { 395 + 1usize 396 + } else { 397 + char_w.div_ceil(inner_w as usize) 398 + } 399 + }) 400 + .sum::<usize>() 401 + .max(1) as u16; 371 402 372 - f.render_widget( 373 - Paragraph::new(all_lines) 374 - .block(Block::default().borders(Borders::ALL)) 375 - .wrap(Wrap { trim: false }) 376 - .scroll((scroll_top, 0)), 377 - chunks[0], 403 + // snap to bottom while streaming / user hasn't scrolled up 404 + if app.at_bottom { 405 + app.scroll.scroll_to_bottom(); 406 + } 407 + 408 + let mut scroll_view = ScrollView::new(Size::new(inner_w, content_h)); 409 + scroll_view.render_widget( 410 + Paragraph::new(all_lines).wrap(Wrap { trim: false }), 411 + scroll_view.area(), 378 412 ); 413 + f.render_stateful_widget(scroll_view, chunks[0], &mut app.scroll); 379 414 380 415 let input_style = app 381 416 .cmd_mode ··· 469 504 match event { 470 505 Event::Key(k) if k.kind == KeyEventKind::Press => match (k.code, k.modifiers) { 471 506 (KeyCode::Char('c'), KeyModifiers::CONTROL) => return Ok(true), 472 - (KeyCode::PageUp, _) => app.scroll_up(app.viewport_h.saturating_sub(2)), 473 - (KeyCode::PageDown, _) => app.scroll_down(app.viewport_h.saturating_sub(2)), 507 + (KeyCode::PageUp, _) => app.scroll_page_up(), 508 + (KeyCode::PageDown, _) => app.scroll_page_down(), 474 509 (KeyCode::Enter, _) => { 475 510 let raw = app.take_input(); 476 511 if raw.is_empty() { ··· 479 514 match parse_cmd(&raw) { 480 515 Cmd::Clear => { 481 516 app.history.clear(); 482 - app.scroll = 0; 517 + app.snap_to_bottom(); 483 518 } 484 519 Cmd::ToggleThink => app.toggle_last_think(), 485 520 Cmd::Help => { ··· 490 525 } 491 526 Cmd::Send(msg) => { 492 527 app.history.push(ChatMsg::user(msg.clone())); 493 - app.scroll = 0; 528 + app.snap_to_bottom(); 494 529 let payload = serde_json::to_string(&ClientMsg::Message { 495 530 source: "user".into(), 496 531 content: msg,