Skip to content

Commit 5dabeda

Browse files
authored
Merge pull request #92 from 7df-lab/dev/tui0609
fix: update tui slash and turn rendering
2 parents c8881b2 + 479ea1f commit 5dabeda

16 files changed

Lines changed: 560 additions & 36 deletions

crates/tui/src/bottom_pane/chat_composer.rs

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3839,6 +3839,59 @@ impl ChatComposer {
38393839
} else {
38403840
StatefulWidgetRef::render_ref(&(&self.textarea), textarea_rect, buf, &mut state);
38413841
}
3842+
let slash_parameter_hint =
3843+
if !textarea_is_empty && self.input_enabled && mask_char.is_none() {
3844+
let text = self.textarea.text();
3845+
let cursor = self.textarea.cursor();
3846+
let first_line_end = text.find('\n').unwrap_or(text.len());
3847+
if cursor == text.len() && first_line_end == text.len() {
3848+
parse_slash_name(text).and_then(|(name, rest, _rest_offset)| {
3849+
if rest.is_empty() && !name.contains('/') {
3850+
slash_commands::find_builtin_command(name, self.builtin_command_flags())
3851+
.and_then(SlashCommand::parameter_hint)
3852+
} else {
3853+
None
3854+
}
3855+
})
3856+
} else {
3857+
None
3858+
}
3859+
} else {
3860+
None
3861+
};
3862+
if let Some(hint) = slash_parameter_hint
3863+
&& let Some((cursor_x, cursor_y)) =
3864+
self.textarea.cursor_pos_with_state(textarea_rect, *state)
3865+
&& cursor_y < textarea_rect.y.saturating_add(textarea_rect.height)
3866+
&& cursor_x < textarea_rect.x.saturating_add(textarea_rect.width)
3867+
{
3868+
let hint_text = if self
3869+
.textarea
3870+
.text()
3871+
.chars()
3872+
.next_back()
3873+
.is_some_and(char::is_whitespace)
3874+
{
3875+
hint.to_string()
3876+
} else {
3877+
format!(" {hint}")
3878+
};
3879+
let remaining_width = textarea_rect
3880+
.x
3881+
.saturating_add(textarea_rect.width)
3882+
.saturating_sub(cursor_x);
3883+
if remaining_width > 0 {
3884+
let hint_span = if is_zellij {
3885+
Span::styled(
3886+
hint_text,
3887+
textarea_style.fg(ratatui::style::Color::DarkGray),
3888+
)
3889+
} else {
3890+
Span::from(hint_text).dim()
3891+
};
3892+
buf.set_span(cursor_x, cursor_y, &hint_span, remaining_width);
3893+
}
3894+
}
38423895
if textarea_is_empty {
38433896
let text = if self.input_enabled {
38443897
self.placeholder_text.as_str().to_string()

crates/tui/src/bottom_pane/input_mode.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ use ratatui::style::Color;
22
use ratatui::style::Style;
33
use ratatui::text::Span;
44

5+
use devo_protocol::InteractionMode;
6+
57
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
68
pub(crate) enum InputMode {
79
#[default]
@@ -38,6 +40,20 @@ impl InputMode {
3840
Self::Shell => Color::Rgb(245, 142, 53),
3941
}
4042
}
43+
44+
pub(crate) fn interaction_mode(self) -> InteractionMode {
45+
match self {
46+
Self::Build | Self::Shell => InteractionMode::Build,
47+
Self::Plan => InteractionMode::Plan,
48+
}
49+
}
50+
51+
pub(crate) fn from_interaction_mode(interaction_mode: InteractionMode) -> Self {
52+
match interaction_mode {
53+
InteractionMode::Build => Self::Build,
54+
InteractionMode::Plan => Self::Plan,
55+
}
56+
}
4157
}
4258

4359
#[cfg(test)]
@@ -63,5 +79,16 @@ mod tests {
6379
InputMode::Shell.styled_span(false),
6480
Span::styled("SHELL", Style::default().fg(Color::Rgb(245, 142, 53)))
6581
);
82+
assert_eq!(InputMode::Build.interaction_mode(), InteractionMode::Build);
83+
assert_eq!(InputMode::Plan.interaction_mode(), InteractionMode::Plan);
84+
assert_eq!(InputMode::Shell.interaction_mode(), InteractionMode::Build);
85+
assert_eq!(
86+
InputMode::from_interaction_mode(InteractionMode::Build),
87+
InputMode::Build
88+
);
89+
assert_eq!(
90+
InputMode::from_interaction_mode(InteractionMode::Plan),
91+
InputMode::Plan
92+
);
6693
}
6794
}

crates/tui/src/bottom_pane/mod.rs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ pub(crate) enum InputResult {
171171
text_elements: Vec<TextElement>,
172172
local_images: Vec<LocalImageAttachment>,
173173
mention_bindings: Vec<MentionBinding>,
174+
input_mode: InputMode,
174175
interaction_mode: InteractionMode,
175176
},
176177
ShellCommand {
@@ -364,6 +365,7 @@ impl BottomPane {
364365
text_elements: Vec::new(),
365366
local_images: Vec::new(),
366367
mention_bindings: Vec::new(),
368+
input_mode: InputMode::Build,
367369
interaction_mode: InteractionMode::Build,
368370
};
369371
}
@@ -785,15 +787,14 @@ impl BottomPane {
785787
} else {
786788
(text, text_elements)
787789
};
790+
let input_mode = self.input_mode;
788791
InputResult::Submitted {
789792
text,
790793
text_elements,
791794
local_images,
792795
mention_bindings,
793-
interaction_mode: match self.input_mode {
794-
InputMode::Build | InputMode::Shell => InteractionMode::Build,
795-
InputMode::Plan => InteractionMode::Plan,
796-
},
796+
input_mode,
797+
interaction_mode: input_mode.interaction_mode(),
797798
}
798799
}
799800

crates/tui/src/chatwidget.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use devo_protocol::TurnId;
2424
use crate::app_event_sender::AppEventSender;
2525
use crate::bottom_pane::BottomPane;
2626
use crate::bottom_pane::BottomPaneParams;
27+
use crate::bottom_pane::InputMode;
2728
use crate::bottom_pane::LocalImageAttachment;
2829
use crate::bottom_pane::MentionBinding;
2930
use crate::history_cell::HistoryCell;
@@ -269,7 +270,10 @@ pub(crate) struct ChatWidget {
269270
last_query_total_tokens: usize,
270271
last_plan_progress: Option<(usize, usize)>,
271272
queued_count: usize,
273+
queued_input_modes: VecDeque<InputMode>,
274+
promoted_input_modes: VecDeque<InputMode>,
272275
active_turn_id: Option<TurnId>,
276+
current_turn_mode: InputMode,
273277
committed_server_assistant_in_turn: bool,
274278
current_turn_has_user_shell_command: bool,
275279
pending_approval: Option<PendingApprovalRequest>,
@@ -400,7 +404,10 @@ impl ChatWidget {
400404
last_query_total_tokens: 0,
401405
last_plan_progress: None,
402406
queued_count: 0,
407+
queued_input_modes: VecDeque::new(),
408+
promoted_input_modes: VecDeque::new(),
403409
active_turn_id: None,
410+
current_turn_mode: InputMode::Build,
404411
committed_server_assistant_in_turn: false,
405412
current_turn_has_user_shell_command: false,
406413
pending_approval: None,

crates/tui/src/chatwidget/configuration.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,13 @@ impl ChatWidget {
6262
}
6363

6464
if let Some(model) = self.session.model.as_mut() {
65+
let display_name = if model.slug == slug && !model.display_name.is_empty() {
66+
model.display_name.clone()
67+
} else {
68+
slug.clone()
69+
};
6570
model.slug = slug.clone();
66-
model.display_name = slug;
71+
model.display_name = display_name;
6772
self.session.reasoning_effort = model
6873
.resolve_thinking_selection(self.thinking_selection.as_deref())
6974
.effective_reasoning_effort;

crates/tui/src/chatwidget/input.rs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ use ratatui::text::Span;
1818
use crate::ansi_escape::ansi_escape_line;
1919
use crate::app_command::AppCommand;
2020
use crate::app_event::AppEvent;
21+
use crate::bottom_pane::InputMode;
2122
use crate::bottom_pane::InputResult;
2223
use crate::history_cell;
2324
use crate::history_cell::PlainHistoryCell;
@@ -87,6 +88,7 @@ impl ChatWidget {
8788
text_elements,
8889
local_images,
8990
mention_bindings,
91+
input_mode,
9092
interaction_mode,
9193
} => {
9294
let user_message = UserMessage {
@@ -100,6 +102,7 @@ impl ChatWidget {
100102
// Turn is active — show in bottom pane as pending cell.
101103
self.bottom_pane
102104
.push_pending_cell(user_message.text.clone());
105+
self.queued_input_modes.push_back(input_mode);
103106
self.queued_count += 1;
104107
self.app_event_tx.send(AppEvent::Command(
105108
AppCommand::user_turn_with_interaction_mode(
@@ -114,13 +117,14 @@ impl ChatWidget {
114117
));
115118
self.set_status_message("Message queued");
116119
} else {
117-
self.submit_user_message_with_interaction_mode(user_message, interaction_mode);
120+
self.submit_user_message_with_modes(user_message, interaction_mode, input_mode);
118121
}
119122
}
120123
InputResult::ShellCommand { command } => {
121124
if self.busy {
122125
self.set_status_message("Cannot run shell command while generating");
123126
} else {
127+
self.current_turn_mode = InputMode::Shell;
124128
self.app_event_tx
125129
.send(AppEvent::Command(AppCommand::execute_shell_command(
126130
command,
@@ -132,6 +136,7 @@ impl ChatWidget {
132136
if self.busy {
133137
self.set_status_message("Cannot run shell command while generating");
134138
} else {
139+
self.current_turn_mode = InputMode::Shell;
135140
self.app_event_tx
136141
.send(AppEvent::Command(AppCommand::submit_shell_input(command)));
137142
self.set_status_message("Shell command submitted");
@@ -215,9 +220,7 @@ impl ChatWidget {
215220
self.set_status_message("Persistent composer history is not available");
216221
}
217222
AppEvent::ClearTranscript => {
218-
self.history.clear();
219-
self.next_history_flush_index = 0;
220-
self.frame_requester.schedule_frame();
223+
self.clear_transcript_view();
221224
}
222225
AppEvent::Interrupt => self.set_status_message("Interrupted"),
223226
AppEvent::Command(command) => {
@@ -282,18 +285,20 @@ impl ChatWidget {
282285
}
283286

284287
pub(super) fn submit_user_message(&mut self, user_message: UserMessage) {
285-
self.submit_user_message_with_interaction_mode(user_message, InteractionMode::Build);
288+
self.submit_user_message_with_modes(user_message, InteractionMode::Build, InputMode::Build);
286289
}
287290

288-
pub(super) fn submit_user_message_with_interaction_mode(
291+
pub(super) fn submit_user_message_with_modes(
289292
&mut self,
290293
user_message: UserMessage,
291294
interaction_mode: InteractionMode,
295+
input_mode: InputMode,
292296
) {
293297
if user_message.text.trim().is_empty() {
294298
return;
295299
}
296300

301+
self.current_turn_mode = input_mode;
297302
let local_image_paths = user_message
298303
.local_images
299304
.iter()
@@ -306,6 +311,7 @@ impl ChatWidget {
306311
local_image_paths,
307312
user_message.remote_image_urls.clone(),
308313
self.active_accent_color(),
314+
input_mode,
309315
));
310316

311317
self.app_event_tx.send(AppEvent::Command(

crates/tui/src/chatwidget/restored_session.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::collections::HashMap;
77
use std::collections::HashSet;
88
use std::path::PathBuf;
99

10+
use crate::bottom_pane::InputMode;
1011
use crate::events::TranscriptItem;
1112
use crate::exec_cell::CommandOutput;
1213
use crate::exec_cell::ExecCell;
@@ -208,6 +209,7 @@ impl ChatWidget {
208209
Vec::new(),
209210
Vec::new(),
210211
self.active_accent_color(),
212+
InputMode::Build,
211213
)));
212214
}
213215
devo_protocol::SessionHistoryItemKind::Assistant => {
@@ -253,6 +255,7 @@ impl ChatWidget {
253255
devo_protocol::SessionHistoryItemKind::TurnSummary => {
254256
self.add_history_entry_without_redraw(Box::new(
255257
history_cell::TurnSummaryCell::new(
258+
InputMode::Build,
256259
item.title.clone(),
257260
item.duration_ms,
258261
self.active_accent_color(),

crates/tui/src/chatwidget/session_header.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,15 @@ impl ChatWidget {
371371
}
372372

373373
pub(super) fn refresh_header_box(&mut self) {
374-
if self.history.is_empty() {
374+
if self
375+
.history
376+
.first()
377+
.and_then(|cell| {
378+
cell.as_any()
379+
.downcast_ref::<history_cell::SessionInfoCell>()
380+
})
381+
.is_none()
382+
{
375383
return;
376384
}
377385
let accent = self.active_accent_color();

crates/tui/src/chatwidget/session_history.rs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ use ratatui::text::Span;
1111

1212
use devo_core::ItemId;
1313

14+
use crate::bottom_pane::InputMode;
1415
use crate::events::PlanStep;
1516
use crate::events::PlanStepStatus;
1617
use crate::events::TranscriptItem;
@@ -36,10 +37,28 @@ impl ChatWidget {
3637
self.active_tool_calls.clear();
3738
self.pending_tool_calls.clear();
3839
self.active_text_items.clear();
40+
self.queued_input_modes.clear();
41+
self.promoted_input_modes.clear();
42+
self.current_turn_mode = InputMode::Build;
3943
self.bottom_pane.clear_composer();
4044
self.set_status_message("Resuming session");
4145
}
4246

47+
pub(super) fn clear_transcript_view(&mut self) {
48+
self.history.clear();
49+
self.next_history_flush_index = 0;
50+
self.active_cell = None;
51+
self.active_cell_revision = self.active_cell_revision.wrapping_add(1);
52+
self.last_terminal_assistant_visible_hash = None;
53+
self.active_text_items.clear();
54+
self.active_proposed_plan = None;
55+
self.stream_chunking_policy.reset();
56+
self.selection_mode = false;
57+
self.selected_user_cell_index = None;
58+
self.user_cell_history_indices.clear();
59+
self.frame_requester.schedule_frame();
60+
}
61+
4362
pub(super) fn set_default_placeholder(&mut self) {
4463
self.bottom_pane
4564
.set_placeholder_text("Ask Devo".to_string());
@@ -264,6 +283,7 @@ impl ChatWidget {
264283
Vec::new(),
265284
Vec::new(),
266285
self.active_accent_color(),
286+
InputMode::Build,
267287
)));
268288
}
269289
TranscriptItemKind::Assistant => {
@@ -306,6 +326,7 @@ impl ChatWidget {
306326
// item.title contains model name, item.duration_ms contains seconds
307327
self.add_history_entry_without_redraw(Box::new(
308328
history_cell::TurnSummaryCell::new(
329+
InputMode::Build,
309330
item.title.clone(),
310331
item.duration_ms,
311332
self.active_accent_color(),
@@ -349,12 +370,18 @@ impl ChatWidget {
349370
/// as a normal user input cell.
350371
pub(super) fn unqueue_oldest_pending(&mut self) {
351372
if let Some(text) = self.bottom_pane.pop_oldest_pending_cell() {
373+
let input_mode = self
374+
.queued_input_modes
375+
.pop_front()
376+
.unwrap_or(InputMode::Build);
377+
self.promoted_input_modes.push_back(input_mode);
352378
self.add_to_history(history_cell::new_user_prompt(
353379
text,
354380
Vec::new(),
355381
Vec::new(),
356382
Vec::new(),
357383
self.active_accent_color(),
384+
input_mode,
358385
));
359386
}
360387
self.queued_count = self.queued_count.saturating_sub(1);

0 commit comments

Comments
 (0)