feat: polish TUI with Kimi-style footer, streaming thinking blocks, and cleanup
- Redesign footer: live clock, lowercase mode badges, color-coded context % - Stream thinking/reasoning blocks in real-time with sidebar style and cursor - Replace ThinkingSummary with richer Thinking variant in history cells - Remove dead code (unused footer helpers, context bar, copy hint) - Bump version to 0.3.3
This commit is contained in:
@@ -50,6 +50,7 @@ TODO*.md
|
||||
todo*.md
|
||||
CLAUDE.md
|
||||
NEXT_SESSION.md
|
||||
AI_HANDOFF.md
|
||||
|
||||
.codex/
|
||||
docs/rlm-paper.txt
|
||||
|
||||
Generated
+1
-1
@@ -646,7 +646,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deepseek-tui"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arboard",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deepseek-tui"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
edition = "2024"
|
||||
description = "Unofficial DeepSeek CLI - Just run 'deepseek' to start chatting"
|
||||
license = "MIT"
|
||||
|
||||
@@ -158,7 +158,7 @@ pub fn export(app: &mut App, path: Option<&str>) -> CommandResult {
|
||||
HistoryCell::User { content } => ("**You:**", content.clone()),
|
||||
HistoryCell::Assistant { content, .. } => ("**Assistant:**", content.clone()),
|
||||
HistoryCell::System { content } => ("*System:*", content.clone()),
|
||||
HistoryCell::ThinkingSummary { summary } => ("*Thinking:*", summary.clone()),
|
||||
HistoryCell::Thinking { content, .. } => ("*Thinking:*", content.clone()),
|
||||
HistoryCell::Tool(tool) => ("**Tool:**", render_tool_cell(tool, 80)),
|
||||
};
|
||||
|
||||
|
||||
+26
-2
@@ -351,11 +351,20 @@ fn enforce_tool_call_pairs(messages: &[Message], pinned_indices: &mut BTreeSet<u
|
||||
return;
|
||||
}
|
||||
|
||||
// Build maps for tool calls and results
|
||||
let mut tool_call_indices: HashMap<String, usize> = HashMap::new();
|
||||
let mut tool_result_indices: HashMap<String, usize> = HashMap::new();
|
||||
|
||||
for (idx, msg) in messages.iter().enumerate() {
|
||||
for block in &msg.content {
|
||||
if let ContentBlock::ToolUse { id, .. } = block {
|
||||
tool_call_indices.insert(id.clone(), idx);
|
||||
match block {
|
||||
ContentBlock::ToolUse { id, .. } => {
|
||||
tool_call_indices.insert(id.clone(), idx);
|
||||
}
|
||||
ContentBlock::ToolResult { tool_use_id, .. } => {
|
||||
tool_result_indices.insert(tool_use_id.clone(), idx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -363,6 +372,8 @@ fn enforce_tool_call_pairs(messages: &[Message], pinned_indices: &mut BTreeSet<u
|
||||
let mut to_add = Vec::new();
|
||||
let mut to_remove = Vec::new();
|
||||
|
||||
// Pass 1: If a tool result is pinned, ensure its tool call is also pinned.
|
||||
// If the tool call is not found, remove the orphaned result.
|
||||
for &idx in pinned_indices.iter() {
|
||||
let msg = &messages[idx];
|
||||
let mut tool_ids = Vec::new();
|
||||
@@ -387,6 +398,19 @@ fn enforce_tool_call_pairs(messages: &[Message], pinned_indices: &mut BTreeSet<u
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: If a tool call is pinned, ensure its tool result is also pinned.
|
||||
// This prevents "orphaned tool calls" API errors.
|
||||
for &idx in pinned_indices.iter() {
|
||||
let msg = &messages[idx];
|
||||
for block in &msg.content {
|
||||
if let ContentBlock::ToolUse { id, .. } = block {
|
||||
if let Some(result_idx) = tool_result_indices.get(id).copied() {
|
||||
to_add.push(result_idx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for idx in to_add {
|
||||
pinned_indices.insert(idx);
|
||||
}
|
||||
|
||||
+39
-7
@@ -28,7 +28,7 @@ pub enum HistoryCell {
|
||||
User { content: String },
|
||||
Assistant { content: String, streaming: bool },
|
||||
System { content: String },
|
||||
ThinkingSummary { summary: String },
|
||||
Thinking { content: String, streaming: bool },
|
||||
Tool(ToolCell),
|
||||
}
|
||||
|
||||
@@ -68,8 +68,17 @@ impl HistoryCell {
|
||||
HistoryCell::System { content } => {
|
||||
render_message("System", content, system_style(), width)
|
||||
}
|
||||
HistoryCell::ThinkingSummary { summary } => {
|
||||
render_message("Thinking", summary, thinking_style(), width)
|
||||
HistoryCell::Thinking { content, streaming } => {
|
||||
let mut lines = render_thinking(content, width);
|
||||
if *streaming {
|
||||
if let Some(last) = lines.last_mut() {
|
||||
last.spans.push(Span::styled(
|
||||
"▋",
|
||||
Style::default().fg(palette::DEEPSEEK_SKY),
|
||||
));
|
||||
}
|
||||
}
|
||||
lines
|
||||
}
|
||||
HistoryCell::Tool(cell) => cell.lines(width),
|
||||
}
|
||||
@@ -81,7 +90,7 @@ impl HistoryCell {
|
||||
options: TranscriptRenderOptions,
|
||||
) -> Vec<Line<'static>> {
|
||||
match self {
|
||||
HistoryCell::ThinkingSummary { .. } if !options.show_thinking => Vec::new(),
|
||||
HistoryCell::Thinking { .. } if !options.show_thinking => Vec::new(),
|
||||
HistoryCell::Tool(cell) if !options.show_tool_details => {
|
||||
let mut lines = cell.lines(width);
|
||||
if lines.len() > 2 {
|
||||
@@ -140,9 +149,10 @@ pub fn history_cells_from_message(msg: &Message) -> Vec<HistoryCell> {
|
||||
|
||||
if !thinking_blocks.is_empty() {
|
||||
let reasoning = thinking_blocks.join("\n");
|
||||
if let Some(summary) = extract_reasoning_summary(&reasoning) {
|
||||
cells.push(HistoryCell::ThinkingSummary { summary });
|
||||
}
|
||||
cells.push(HistoryCell::Thinking {
|
||||
content: reasoning,
|
||||
streaming: false,
|
||||
});
|
||||
}
|
||||
|
||||
cells
|
||||
@@ -958,6 +968,7 @@ pub fn output_is_image(output: &str) -> bool {
|
||||
.any(|ext| lower.contains(ext))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[must_use]
|
||||
pub fn extract_reasoning_summary(text: &str) -> Option<String> {
|
||||
let mut lines = text.lines().peekable();
|
||||
@@ -999,6 +1010,27 @@ pub fn extract_reasoning_summary(text: &str) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_thinking(content: &str, width: u16) -> Vec<Line<'static>> {
|
||||
let style = thinking_style();
|
||||
let prefix = "│ ";
|
||||
|
||||
let content_width = usize::from(width.saturating_sub(2).max(1));
|
||||
let rendered = markdown_render::render_markdown(content, content_width as u16, style);
|
||||
|
||||
let mut lines = Vec::new();
|
||||
|
||||
for line in rendered {
|
||||
let mut spans = vec![Span::styled(prefix, style)];
|
||||
spans.extend(line.spans);
|
||||
lines.push(Line::from(spans));
|
||||
}
|
||||
|
||||
if lines.is_empty() {
|
||||
lines.push(Line::from(vec![Span::styled(prefix, style)]));
|
||||
}
|
||||
lines
|
||||
}
|
||||
|
||||
fn render_message(prefix: &str, content: &str, style: Style, width: u16) -> Vec<Line<'static>> {
|
||||
let prefix_width = UnicodeWidthStr::width(prefix);
|
||||
let prefix_width_u16 = u16::try_from(prefix_width.saturating_add(2)).unwrap_or(u16::MAX);
|
||||
|
||||
+136
-194
@@ -7,6 +7,7 @@ use std::path::{Path, PathBuf};
|
||||
use std::time::Instant;
|
||||
|
||||
use anyhow::Result;
|
||||
use chrono::Local;
|
||||
use crossterm::{
|
||||
event::{
|
||||
self, DisableBracketedPaste, DisableMouseCapture, EnableBracketedPaste, EnableMouseCapture,
|
||||
@@ -59,8 +60,8 @@ use super::approval::{
|
||||
use super::history::{
|
||||
DiffPreviewCell, ExecCell, ExecSource, ExploringCell, ExploringEntry, GenericToolCell,
|
||||
HistoryCell, McpToolCell, PatchSummaryCell, PlanStep, PlanUpdateCell, ReviewCell, ToolCell,
|
||||
ToolStatus, ViewImageCell, WebSearchCell, extract_reasoning_summary,
|
||||
history_cells_from_message, summarize_mcp_output, summarize_tool_args, summarize_tool_output,
|
||||
ToolStatus, ViewImageCell, WebSearchCell, history_cells_from_message, summarize_mcp_output,
|
||||
summarize_tool_args, summarize_tool_output,
|
||||
};
|
||||
use super::views::{HelpView, ModalKind, ViewEvent};
|
||||
use super::widgets::{ChatWidget, ComposerWidget, HeaderData, HeaderWidget, Renderable};
|
||||
@@ -320,17 +321,36 @@ async fn run_event_loop(
|
||||
EngineEvent::ThinkingStarted { .. } => {
|
||||
app.reasoning_buffer.clear();
|
||||
app.reasoning_header = None;
|
||||
|
||||
app.add_message(HistoryCell::Thinking {
|
||||
content: String::new(),
|
||||
streaming: true,
|
||||
});
|
||||
app.streaming_message_index = Some(app.history.len().saturating_sub(1));
|
||||
}
|
||||
EngineEvent::ThinkingDelta { content, .. } => {
|
||||
app.reasoning_buffer.push_str(&content);
|
||||
if app.reasoning_header.is_none() {
|
||||
app.reasoning_header = extract_reasoning_header(&app.reasoning_buffer);
|
||||
}
|
||||
|
||||
if let Some(index) = app.streaming_message_index {
|
||||
if let Some(HistoryCell::Thinking { content: c, .. }) =
|
||||
app.history.get_mut(index)
|
||||
{
|
||||
c.push_str(&content);
|
||||
}
|
||||
}
|
||||
}
|
||||
EngineEvent::ThinkingComplete { .. } => {
|
||||
if let Some(summary) = extract_reasoning_summary(&app.reasoning_buffer) {
|
||||
app.add_message(HistoryCell::ThinkingSummary { summary });
|
||||
if let Some(index) = app.streaming_message_index.take() {
|
||||
if let Some(HistoryCell::Thinking { streaming, .. }) =
|
||||
app.history.get_mut(index)
|
||||
{
|
||||
*streaming = false;
|
||||
}
|
||||
}
|
||||
|
||||
if !app.reasoning_buffer.is_empty() {
|
||||
app.last_reasoning = Some(app.reasoning_buffer.clone());
|
||||
}
|
||||
@@ -2084,129 +2104,121 @@ fn render_status_indicator(f: &mut Frame, area: Rect, app: &App, queued: &[Strin
|
||||
|
||||
fn render_footer(f: &mut Frame, area: Rect, app: &App) {
|
||||
let width = area.width;
|
||||
let available = width as usize;
|
||||
let available_width = width as usize;
|
||||
|
||||
// Build left side: [MODE] context_bar
|
||||
let context_text = context_indicator(app);
|
||||
let mode_badge = Span::styled(
|
||||
format!(" {} ", app.mode.label()),
|
||||
mode_badge_style(app.mode),
|
||||
);
|
||||
|
||||
let context_info = Span::styled(
|
||||
context_text.clone(),
|
||||
Style::default().fg(palette::TEXT_MUTED),
|
||||
);
|
||||
|
||||
// Calculate widths for left side
|
||||
let mode_width = mode_badge.content.width();
|
||||
let context_width = context_text.width();
|
||||
let left_min_width = mode_width + 1 + context_width; // mode + space + context
|
||||
|
||||
// Build right side: key hints and other info
|
||||
let mut right_spans = Vec::new();
|
||||
|
||||
// Add scroll info if applicable
|
||||
let can_scroll = app.last_transcript_total > app.last_transcript_visible;
|
||||
if can_scroll && !matches!(app.transcript_scroll, TranscriptScroll::ToBottom) {
|
||||
right_spans.push(Span::styled(
|
||||
format!("{}% ", app.last_transcript_top + 1),
|
||||
Style::default().fg(palette::TEXT_DIM),
|
||||
));
|
||||
}
|
||||
|
||||
// Add selection hint if active
|
||||
if app.transcript_selection.is_active() {
|
||||
right_spans.push(Span::styled(
|
||||
copy_selection_hint(),
|
||||
Style::default().fg(palette::TEXT_DIM),
|
||||
));
|
||||
right_spans.push(Span::raw(" "));
|
||||
}
|
||||
|
||||
// Add RLM usage badge when in RLM mode
|
||||
if app.mode == AppMode::Rlm {
|
||||
if let Some((badge, style)) = rlm_usage_badge(app) {
|
||||
right_spans.push(Span::styled(badge, style));
|
||||
right_spans.push(Span::styled(" · ", Style::default().fg(palette::TEXT_DIM)));
|
||||
}
|
||||
}
|
||||
|
||||
// Add key hints
|
||||
right_spans.extend(footer_key_hints(width, app));
|
||||
|
||||
// Calculate right side width
|
||||
let spans_width = |spans: &[Span]| -> usize { spans.iter().map(|s| s.content.width()).sum() };
|
||||
let mut right_width = spans_width(&right_spans);
|
||||
|
||||
// Determine layout based on available space
|
||||
let left_spans: Vec<Span>;
|
||||
let left_width: usize;
|
||||
|
||||
if width >= 80 {
|
||||
// Wide: show full context bar
|
||||
left_spans = vec![mode_badge, Span::raw(" "), context_info];
|
||||
left_width = left_min_width;
|
||||
} else if width >= 50 {
|
||||
// Medium: show percentage only
|
||||
let percent = get_context_percent(app);
|
||||
let short_context = format!("{}%", percent);
|
||||
let short_width = mode_width + 1 + short_context.width();
|
||||
left_spans = vec![
|
||||
mode_badge,
|
||||
Span::raw(" "),
|
||||
Span::styled(short_context, Style::default().fg(palette::TEXT_MUTED)),
|
||||
];
|
||||
left_width = short_width;
|
||||
} else {
|
||||
// Narrow: just mode badge
|
||||
left_spans = vec![mode_badge];
|
||||
left_width = mode_width;
|
||||
}
|
||||
|
||||
// Trim right side if it doesn't fit
|
||||
let available_for_right = available.saturating_sub(left_width);
|
||||
if right_width > available_for_right {
|
||||
while right_width > available_for_right && !right_spans.is_empty() {
|
||||
if let Some(span) = right_spans.pop() {
|
||||
right_width = right_width.saturating_sub(span.content.width());
|
||||
}
|
||||
}
|
||||
while let Some(last) = right_spans.last() {
|
||||
let content = last.content.as_ref();
|
||||
if content.trim().is_empty() || content == " · " {
|
||||
let span = right_spans.pop().unwrap();
|
||||
right_width = right_width.saturating_sub(span.content.width());
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let mid_spacing = available.saturating_sub(left_width + right_width);
|
||||
|
||||
// Combine all spans
|
||||
let mut all_spans = left_spans;
|
||||
|
||||
// Add spacing between left and right
|
||||
if mid_spacing > 0 {
|
||||
all_spans.push(Span::raw(" ".repeat(mid_spacing)));
|
||||
}
|
||||
|
||||
// Add right side spans
|
||||
all_spans.extend(right_spans);
|
||||
|
||||
// Add status message if present (replaces everything)
|
||||
// Status message override (Toast)
|
||||
if let Some(ref msg) = app.status_message {
|
||||
let status_span = Span::styled(msg, Style::default().fg(palette::DEEPSEEK_SKY));
|
||||
all_spans = vec![status_span];
|
||||
f.render_widget(Paragraph::new(Line::from(vec![status_span])), area);
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Time (Left)
|
||||
let time_str = Local::now().format("%H:%M").to_string();
|
||||
let time_span = Span::styled(
|
||||
format!("{} ", time_str),
|
||||
Style::default().fg(palette::TEXT_DIM),
|
||||
);
|
||||
|
||||
// 2. Mode (Left) - Lowercase, colored
|
||||
let mode_str = app.mode.label().to_lowercase();
|
||||
let mode_style = mode_badge_style(app.mode);
|
||||
let mode_span = Span::styled(format!("{} ", mode_str), mode_style);
|
||||
|
||||
// 3. Agent Info (Left)
|
||||
let model = &app.model;
|
||||
let status_suffix = if app.is_loading { ", thinking" } else { "" };
|
||||
let agent_text = format!("agent ({}{})", model, status_suffix);
|
||||
let agent_span = Span::styled(agent_text, Style::default().fg(palette::TEXT_DIM));
|
||||
|
||||
// Left side assembly
|
||||
let left_spans = vec![time_span, mode_span, agent_span];
|
||||
|
||||
// 4. Context (Right)
|
||||
let percent = get_context_percent_decimal(app);
|
||||
let context_text = format!("context: {:.1}%", percent);
|
||||
let context_style = if percent > 90.0 {
|
||||
Style::default().fg(palette::STATUS_ERROR)
|
||||
} else if percent > 75.0 {
|
||||
Style::default().fg(palette::STATUS_WARNING)
|
||||
} else {
|
||||
Style::default().fg(palette::TEXT_DIM)
|
||||
};
|
||||
let context_span = Span::styled(context_text, context_style);
|
||||
|
||||
// 5. Right side extras (Scroll, Selection, RLM) - Minimalist
|
||||
let mut right_extras = Vec::new();
|
||||
|
||||
// Scroll %
|
||||
let can_scroll = app.last_transcript_total > app.last_transcript_visible;
|
||||
if can_scroll && !matches!(app.transcript_scroll, TranscriptScroll::ToBottom) {
|
||||
right_extras.push(Span::styled(
|
||||
format!(" {}% ", app.last_transcript_top + 1),
|
||||
Style::default().fg(palette::TEXT_DIM),
|
||||
));
|
||||
}
|
||||
|
||||
// Selection
|
||||
if app.transcript_selection.is_active() {
|
||||
right_extras.push(Span::styled(
|
||||
" [SEL] ",
|
||||
Style::default().fg(palette::TEXT_DIM),
|
||||
));
|
||||
}
|
||||
|
||||
// RLM Badge
|
||||
if app.mode == AppMode::Rlm {
|
||||
if let Some((badge, style)) = rlm_usage_badge(app) {
|
||||
right_extras.push(Span::styled(" ", Style::default()));
|
||||
right_extras.push(Span::styled(badge, style));
|
||||
}
|
||||
}
|
||||
|
||||
// Assemble Right Side
|
||||
// context_span is always last
|
||||
let mut right_spans = right_extras;
|
||||
right_spans.push(Span::raw(" ")); // Space before context
|
||||
right_spans.push(context_span);
|
||||
|
||||
// Calculate Widths
|
||||
let left_width: usize = left_spans.iter().map(|s| s.content.width()).sum();
|
||||
let right_width: usize = right_spans.iter().map(|s| s.content.width()).sum();
|
||||
|
||||
// Spacer
|
||||
let spacer_width = available_width.saturating_sub(left_width + right_width);
|
||||
|
||||
let mut all_spans = left_spans;
|
||||
if spacer_width > 0 {
|
||||
all_spans.push(Span::raw(" ".repeat(spacer_width)));
|
||||
all_spans.extend(right_spans);
|
||||
} else {
|
||||
// Fallback for narrow screens: Drop agent info
|
||||
let simple_left = vec![
|
||||
Span::styled(
|
||||
format!("{} ", time_str),
|
||||
Style::default().fg(palette::TEXT_DIM),
|
||||
),
|
||||
Span::styled(format!("{} ", mode_str), mode_style),
|
||||
];
|
||||
let simple_right = vec![Span::styled(
|
||||
format!(" {:.0}%", percent),
|
||||
Style::default().fg(palette::TEXT_DIM),
|
||||
)];
|
||||
|
||||
let sl_width: usize = simple_left.iter().map(|s| s.content.width()).sum();
|
||||
let sr_width: usize = simple_right.iter().map(|s| s.content.width()).sum();
|
||||
let sp_width = available_width.saturating_sub(sl_width + sr_width);
|
||||
|
||||
all_spans = simple_left;
|
||||
all_spans.push(Span::raw(" ".repeat(sp_width)));
|
||||
all_spans.extend(simple_right);
|
||||
}
|
||||
|
||||
let footer = Paragraph::new(Line::from(all_spans));
|
||||
f.render_widget(footer, area);
|
||||
}
|
||||
|
||||
/// Get context usage percentage for compact display
|
||||
fn get_context_percent(app: &App) -> u8 {
|
||||
fn get_context_percent_decimal(app: &App) -> f32 {
|
||||
let used = if app.total_conversation_tokens > 0 {
|
||||
Some(i64::from(app.total_conversation_tokens))
|
||||
} else {
|
||||
@@ -2215,48 +2227,18 @@ fn get_context_percent(app: &App) -> u8 {
|
||||
|
||||
if let Some(max) = context_window_for_model(&app.model) {
|
||||
if let Some(used) = used {
|
||||
let max_i64 = i64::from(max);
|
||||
let remaining = (max_i64 - used).max(0);
|
||||
let percent_remaining =
|
||||
((remaining.saturating_mul(100) + max_i64 / 2) / max_i64).clamp(0, 100);
|
||||
100 - percent_remaining as u8
|
||||
let max_f64 = max as f64;
|
||||
let used_f64 = used as f64;
|
||||
let percent = (used_f64 / max_f64) * 100.0;
|
||||
percent.clamp(0.0, 100.0) as f32
|
||||
} else {
|
||||
0
|
||||
0.0
|
||||
}
|
||||
} else {
|
||||
0
|
||||
0.0
|
||||
}
|
||||
}
|
||||
|
||||
fn footer_key_hints(width: u16, app: &App) -> Vec<Span<'static>> {
|
||||
let mut hints = vec!["F1 help", "Ctrl+R sessions", "l pager", "Ctrl+V paste"];
|
||||
if app.transcript_selection.is_active() {
|
||||
hints.push("Enter preview");
|
||||
}
|
||||
|
||||
let max_hints = if width < 60 {
|
||||
1
|
||||
} else if width < 90 {
|
||||
2
|
||||
} else if width < 120 {
|
||||
3
|
||||
} else {
|
||||
hints.len()
|
||||
};
|
||||
|
||||
let mut spans = Vec::new();
|
||||
for (idx, hint) in hints.into_iter().take(max_hints).enumerate() {
|
||||
if idx > 0 {
|
||||
spans.push(Span::styled(" · ", Style::default().fg(palette::TEXT_DIM)));
|
||||
}
|
||||
spans.push(Span::styled(
|
||||
hint.to_string(),
|
||||
Style::default().fg(palette::TEXT_DIM),
|
||||
));
|
||||
}
|
||||
spans
|
||||
}
|
||||
|
||||
fn rlm_usage_badge(app: &App) -> Option<(String, Style)> {
|
||||
let session = app.rlm_session.lock().ok()?;
|
||||
let usage = &session.usage;
|
||||
@@ -2325,42 +2307,6 @@ fn prompt_for_mode(mode: AppMode, rlm_repl_active: bool) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_context_bar(percentage: u8, width: usize) -> String {
|
||||
let filled = (percentage as usize * width / 100).min(width);
|
||||
let empty = width - filled;
|
||||
format!(
|
||||
"[{}{}] {}%",
|
||||
"█".repeat(filled),
|
||||
"░".repeat(empty),
|
||||
percentage
|
||||
)
|
||||
}
|
||||
|
||||
fn context_indicator(app: &App) -> String {
|
||||
let used = if app.total_conversation_tokens > 0 {
|
||||
Some(i64::from(app.total_conversation_tokens))
|
||||
} else {
|
||||
estimated_context_tokens(app)
|
||||
};
|
||||
|
||||
if let Some(max) = context_window_for_model(&app.model) {
|
||||
if let Some(used) = used {
|
||||
let max_i64 = i64::from(max);
|
||||
let remaining = (max_i64 - used).max(0);
|
||||
let percent_remaining =
|
||||
((remaining.saturating_mul(100) + max_i64 / 2) / max_i64).clamp(0, 100);
|
||||
let percent_used = (100 - percent_remaining) as u8;
|
||||
render_context_bar(percent_used, 10)
|
||||
} else {
|
||||
render_context_bar(0, 10)
|
||||
}
|
||||
} else if let Some(used) = used {
|
||||
format!("{used} used")
|
||||
} else {
|
||||
render_context_bar(0, 10)
|
||||
}
|
||||
}
|
||||
|
||||
fn estimated_context_tokens(app: &App) -> Option<i64> {
|
||||
let mut total_chars = estimate_message_chars(&app.api_messages);
|
||||
|
||||
@@ -2703,10 +2649,6 @@ fn is_paste_shortcut(key: &KeyEvent) -> bool {
|
||||
key.modifiers.contains(KeyModifiers::CONTROL)
|
||||
}
|
||||
|
||||
fn copy_selection_hint() -> &'static str {
|
||||
"Release to copy selection"
|
||||
}
|
||||
|
||||
fn should_scroll_with_arrows(_app: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user