Skip to content

Commit b0637c9

Browse files
Merge pull request #403 from SlugThug/refactor/split-tui-app
refactor(tui): split 7.8k-line app.rs into cohesive app/ modules
2 parents 4d4c119 + ff364a4 commit b0637c9

13 files changed

Lines changed: 7893 additions & 7807 deletions

File tree

src-rust/crates/tui/src/app.rs

Lines changed: 0 additions & 7807 deletions
This file was deleted.

src-rust/crates/tui/src/app/commands.rs

Lines changed: 391 additions & 0 deletions
Large diffs are not rendered by default.

src-rust/crates/tui/src/app/keys.rs

Lines changed: 2726 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
//! Transcript operations: add/replace/push, notifications, scroll.
2+
3+
use claurst_core::types::{Message, Role};
4+
use crate::notifications::NotificationKind;
5+
use super::App;
6+
use super::types::{SystemAnnotation, SystemMessageStyle};
7+
8+
impl App {
9+
/// Add a message directly (e.g. from a non-streaming source).
10+
pub fn add_message(&mut self, role: Role, text: String) {
11+
let msg = match role {
12+
Role::User => Message::user(text),
13+
Role::Assistant => Message::assistant(text),
14+
};
15+
if role == Role::User {
16+
self.begin_user_turn_snapshot();
17+
}
18+
self.messages.push(msg);
19+
self.invalidate_transcript();
20+
self.on_new_message();
21+
}
22+
23+
pub fn replace_messages(&mut self, messages: Vec<Message>) {
24+
self.messages = messages;
25+
self.sync_turn_metadata_to_messages();
26+
self.invalidate_transcript();
27+
}
28+
29+
pub fn push_message(&mut self, message: Message) {
30+
if message.role == Role::User {
31+
self.begin_user_turn_snapshot();
32+
}
33+
self.messages.push(message);
34+
self.sync_turn_metadata_to_messages();
35+
self.invalidate_transcript();
36+
self.on_new_message();
37+
}
38+
39+
/// Push a synthetic system annotation into the conversation pane.
40+
/// It will appear after the current last message.
41+
/// Push a notification and, for Error-kind notifications, reset the error
42+
/// modal scroll offset so a newly arrived error is always shown from the top.
43+
pub fn push_notification(&mut self, kind: NotificationKind, msg: String, duration_secs: Option<u64>) {
44+
if kind == NotificationKind::Error {
45+
self.error_modal_scroll_offset = 0;
46+
}
47+
self.notifications.push(kind, msg, duration_secs);
48+
}
49+
50+
pub fn push_system_message(&mut self, text: String, style: SystemMessageStyle) {
51+
self.system_annotations.push(SystemAnnotation {
52+
after_index: self.messages.len(),
53+
text,
54+
style,
55+
});
56+
self.invalidate_transcript();
57+
}
58+
59+
/// Called whenever a new message is appended to `messages`.
60+
/// Manages the auto-scroll / new-message-counter state.
61+
pub(super) fn on_new_message(&mut self) {
62+
if self.auto_scroll {
63+
// Auto-scroll: keep offset at 0 so render shows the bottom.
64+
self.scroll_offset = 0;
65+
} else {
66+
self.new_messages_while_scrolled =
67+
self.new_messages_while_scrolled.saturating_add(1);
68+
}
69+
}
70+
71+
pub fn invalidate_transcript(&self) {
72+
self.transcript_version
73+
.set(self.transcript_version.get().wrapping_add(1));
74+
}
75+
76+
/// Check current token usage and push token warning notifications as
77+
/// appropriate. Call this after updating `token_count`.
78+
pub fn check_token_warnings(&mut self) {
79+
let window =
80+
claurst_query::context_window_for_model(&self.model_name) as u32;
81+
if window == 0 {
82+
return;
83+
}
84+
let pct = (self.token_count as f64 / window as f64 * 100.0) as u8;
85+
86+
// Only escalate — never repeat a threshold already shown.
87+
if pct >= 100 && self.token_warning_threshold_shown < 100 {
88+
self.token_warning_threshold_shown = 100;
89+
self.push_notification(
90+
NotificationKind::Error,
91+
"Context window full. Running auto-compact\u{2026}".to_string(),
92+
None,
93+
);
94+
} else if pct >= 95 && self.token_warning_threshold_shown < 95 {
95+
self.token_warning_threshold_shown = 95;
96+
self.push_notification(
97+
NotificationKind::Error,
98+
"Context window 95% full! Run /compact now.".to_string(),
99+
None, // persistent until dismissed
100+
);
101+
} else if pct >= 80 && self.token_warning_threshold_shown < 80 {
102+
self.token_warning_threshold_shown = 80;
103+
self.push_notification(
104+
NotificationKind::Warning,
105+
"Context window 80% full. Consider /compact.".to_string(),
106+
Some(30),
107+
);
108+
}
109+
}
110+
111+
/// Take the current input buffer, push it to history, and return it.
112+
pub fn take_input(&mut self) -> String {
113+
let input = self.prompt_input.take();
114+
if !input.is_empty() {
115+
self.prompt_input.history.push(input.clone());
116+
self.prompt_input.history_pos = None;
117+
self.prompt_input.history_draft.clear();
118+
self.input_history = self.prompt_input.history.clone();
119+
self.history_index = self.prompt_input.history_pos;
120+
}
121+
self.refresh_prompt_input();
122+
input
123+
}
124+
125+
/// Scroll the transcript up by `amount` lines and disable auto-follow.
126+
///
127+
/// `scroll_offset` counts lines above the bottom (0 = pinned to the newest
128+
/// content). It is clamped to `last_max_scroll` — the maximum meaningful
129+
/// offset from the last render — so scrolling up past the top of the
130+
/// transcript can't inflate it unboundedly. Without the clamp, an over-scroll
131+
/// would leave `scroll_offset` far above `max_scroll`, and the user would
132+
/// have to press Down that many times before the view moved (#223).
133+
pub(super) fn scroll_up_by(&mut self, amount: usize) {
134+
self.scroll_offset = self
135+
.scroll_offset
136+
.saturating_add(amount)
137+
.min(self.last_max_scroll.get());
138+
self.auto_scroll = false;
139+
}
140+
141+
/// Compute the number of lines to scroll per wheel/trackpad event.
142+
/// Implements a simple acceleration model: rapid events (< 40 ms apart) are
143+
/// treated as trackpad bursts and accelerate up to 2×; slower events (mouse
144+
/// wheel) stay at the base 3-line step.
145+
pub(super) fn scroll_step(&mut self) -> usize {
146+
let now = std::time::Instant::now();
147+
let elapsed_ms = self.scroll_last_time
148+
.map(|t| now.duration_since(t).as_millis())
149+
.unwrap_or(u128::MAX);
150+
self.scroll_last_time = Some(now);
151+
if elapsed_ms < 40 {
152+
// Trackpad burst — gradually accelerate
153+
self.scroll_accel = (self.scroll_accel + 0.4).min(6.0);
154+
} else {
155+
// Mouse click or first event — reset to base
156+
self.scroll_accel = 3.0;
157+
}
158+
self.scroll_accel.round() as usize
159+
}
160+
161+
}

0 commit comments

Comments
 (0)