fix(commandline): pin active step prompt instead of fading

The current step's prompt was written into the command-line widget
label. Instead, keep it as a normal history line but freeze its
cooldown: the step prompt is pinned so it does not fade while the user
is still on that step. When the step changes the previous line's
cooldown restarts; when the command ends the pin clears and it fades.

`update` is now a thin wrapper that mirrors the active command's prompt
into the command line after each message; the body moved to
`update_inner`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Opus 4.8 committed 2026-06-09 18:46:45 +03:00
1 parent 0ee8f5265d
commit b3aadf569e
3 files changed
+53 -20

No files matched your search

+12
View File
@@ -50,6 +50,18 @@ fn format_size(bytes: u64) -> String {
impl OpenCADStudio {
pub fn update(&mut self, msg: Message) -> Task<Message> {
let task = self.update_inner(msg);
// After every message, mirror the active command step's prompt so
// its history line stays pinned (non-fading) until the step changes.
let prompt = self.tabs[self.active_tab]
.active_cmd
.as_ref()
.map(|c| c.prompt());
self.command_line.set_step_prompt(prompt);
task
}
fn update_inner(&mut self, msg: Message) -> Task<Message> {
match msg {
Message::Tick(t) => {
let i = self.active_tab;
+1 -6
View File
@@ -1177,13 +1177,8 @@ impl OpenCADStudio {
&& !tab.dyn_fields.is_empty())
|| self.mtext_editor.as_ref().is_some_and(|e| e.show_preview)
|| self.text_inline.is_some();
let active_prompt = tab.active_cmd.as_ref().map(|c| c.prompt());
let command_line_overlay =
iced::widget::container(self.command_line.view(
allow_autocomplete,
dyn_capturing,
active_prompt,
))
iced::widget::container(self.command_line.view(allow_autocomplete, dyn_capturing))
.width(Fill)
.height(Fill)
.align_x(iced::alignment::Horizontal::Center)
+40 -14
View File
@@ -37,6 +37,9 @@ pub struct CommandLine {
/// `None` when the user hasn't yet started navigating with the
/// arrow keys. Reset on every keystroke.
pub autocomplete_cursor: Option<usize>,
/// The active command step's prompt, mirrored here so a step change
/// can be detected and the pinned (non-fading) history line updated.
step_prompt: Option<String>,
}
#[derive(Clone, Debug)]
@@ -47,6 +50,10 @@ pub struct HistoryEntry {
/// out after `HISTORY_VISIBLE_SECS`. The dropdown popup ignores it
/// and always shows the whole list.
pub created_at: Instant,
/// The active command step's prompt is pinned so it does not fade
/// while the user is still working on that step. When the step
/// completes the pin is cleared and the normal cooldown resumes.
pub pinned: bool,
}
#[derive(Clone, Debug)]
@@ -135,12 +142,41 @@ impl CommandLine {
kind,
text,
created_at: Instant::now(),
pinned: false,
});
if self.history.len() > MAX_HISTORY {
self.history.remove(0);
}
}
/// Mirror the active command step's prompt. While the step is current
/// its history line is pinned so it does not fade; when the step
/// changes (or the command ends, `prompt == None`) the previous line's
/// cooldown restarts so it fades normally from now. The dispatch /
/// step-transition code already pushes the prompt as an `Info` line, so
/// the matching tail entry is pinned in place rather than duplicated.
pub fn set_step_prompt(&mut self, prompt: Option<String>) {
if prompt == self.step_prompt {
return;
}
// Release the previous pin and let its cooldown start now.
for e in self.history.iter_mut().filter(|e| e.pinned) {
e.pinned = false;
e.created_at = Instant::now();
}
if let Some(p) = &prompt {
// Reuse the prompt line dispatch/step-transition just pushed;
// otherwise add it.
if self.history.last().map(|e| &e.text) != Some(p) {
self.push(EntryKind::Info, p.clone());
}
if let Some(last) = self.history.last_mut() {
last.pinned = true;
}
}
self.step_prompt = prompt;
}
/// `true` while at least one history entry is still within the
/// visible window — the host app uses this to drive a low-frequency
/// tick subscription so the overlay re-renders and fades the entry
@@ -148,7 +184,7 @@ impl CommandLine {
pub fn has_visible_history(&self) -> bool {
self.history
.iter()
.any(|e| e.created_at.elapsed().as_secs_f32() < HISTORY_VISIBLE_SECS)
.any(|e| e.pinned || e.created_at.elapsed().as_secs_f32() < HISTORY_VISIBLE_SECS)
}
pub fn toggle_history(&mut self) {
@@ -226,19 +262,14 @@ impl CommandLine {
matches
}
pub fn view(
&self,
show_autocomplete: bool,
dyn_capturing: bool,
active_prompt: Option<String>,
) -> Element<'_, Message> {
pub fn view(&self, show_autocomplete: bool, dyn_capturing: bool) -> Element<'_, Message> {
// Only the most recent entries pushed within the last few
// seconds show on the overlay. The dropdown button keeps the
// full backlog reachable when the user actually wants it.
let visible: Vec<&HistoryEntry> = self
.history
.iter()
.filter(|e| e.created_at.elapsed().as_secs_f32() < HISTORY_VISIBLE_SECS)
.filter(|e| e.pinned || e.created_at.elapsed().as_secs_f32() < HISTORY_VISIBLE_SECS)
.collect();
let start = visible.len().saturating_sub(4);
let history_rows = visible[start..]
@@ -252,12 +283,7 @@ impl CommandLine {
};
col.push(container(text(&entry.text).size(11).color(color)).padding([1, 8]))
});
// While a command is running, the prompt shows what the current
// step is asking for and stays put until that step completes — it
// does not fade like the history lines. With no active command it
// falls back to the generic entry prompt.
let prompt_label = active_prompt.unwrap_or_else(|| "Command:".to_string());
let prompt = container(text(prompt_label).size(11).color(PROMPT_COLOR)).padding([5, 8]);
let prompt = container(text("Command:").size(11).color(PROMPT_COLOR)).padding([5, 8]);
// While dynamic input is capturing keystrokes, the command-line
// text field is left without an `on_input` handler so it can't
// grab focus or swallow numeric keys — those flow through the