forked from 1b-agent/opencadstudio-upstream
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 <[email protected]>
This commit is contained in:
1 parent
0ee8f5265d
commit
b3aadf569e
3 files changed
+53
-20
No files matched your search
@@ -50,6 +50,18 @@ fn format_size(bytes: u64) -> String {
|
|||||||
|
|
||||||
impl OpenCADStudio {
|
impl OpenCADStudio {
|
||||||
pub fn update(&mut self, msg: Message) -> Task<Message> {
|
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 {
|
match msg {
|
||||||
Message::Tick(t) => {
|
Message::Tick(t) => {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
|
|||||||
+1
-6
@@ -1177,13 +1177,8 @@ impl OpenCADStudio {
|
|||||||
&& !tab.dyn_fields.is_empty())
|
&& !tab.dyn_fields.is_empty())
|
||||||
|| self.mtext_editor.as_ref().is_some_and(|e| e.show_preview)
|
|| self.mtext_editor.as_ref().is_some_and(|e| e.show_preview)
|
||||||
|| self.text_inline.is_some();
|
|| self.text_inline.is_some();
|
||||||
let active_prompt = tab.active_cmd.as_ref().map(|c| c.prompt());
|
|
||||||
let command_line_overlay =
|
let command_line_overlay =
|
||||||
iced::widget::container(self.command_line.view(
|
iced::widget::container(self.command_line.view(allow_autocomplete, dyn_capturing))
|
||||||
allow_autocomplete,
|
|
||||||
dyn_capturing,
|
|
||||||
active_prompt,
|
|
||||||
))
|
|
||||||
.width(Fill)
|
.width(Fill)
|
||||||
.height(Fill)
|
.height(Fill)
|
||||||
.align_x(iced::alignment::Horizontal::Center)
|
.align_x(iced::alignment::Horizontal::Center)
|
||||||
|
|||||||
+40
-14
@@ -37,6 +37,9 @@ pub struct CommandLine {
|
|||||||
/// `None` when the user hasn't yet started navigating with the
|
/// `None` when the user hasn't yet started navigating with the
|
||||||
/// arrow keys. Reset on every keystroke.
|
/// arrow keys. Reset on every keystroke.
|
||||||
pub autocomplete_cursor: Option<usize>,
|
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)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -47,6 +50,10 @@ pub struct HistoryEntry {
|
|||||||
/// out after `HISTORY_VISIBLE_SECS`. The dropdown popup ignores it
|
/// out after `HISTORY_VISIBLE_SECS`. The dropdown popup ignores it
|
||||||
/// and always shows the whole list.
|
/// and always shows the whole list.
|
||||||
pub created_at: Instant,
|
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)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -135,12 +142,41 @@ impl CommandLine {
|
|||||||
kind,
|
kind,
|
||||||
text,
|
text,
|
||||||
created_at: Instant::now(),
|
created_at: Instant::now(),
|
||||||
|
pinned: false,
|
||||||
});
|
});
|
||||||
if self.history.len() > MAX_HISTORY {
|
if self.history.len() > MAX_HISTORY {
|
||||||
self.history.remove(0);
|
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
|
/// `true` while at least one history entry is still within the
|
||||||
/// visible window — the host app uses this to drive a low-frequency
|
/// visible window — the host app uses this to drive a low-frequency
|
||||||
/// tick subscription so the overlay re-renders and fades the entry
|
/// tick subscription so the overlay re-renders and fades the entry
|
||||||
@@ -148,7 +184,7 @@ impl CommandLine {
|
|||||||
pub fn has_visible_history(&self) -> bool {
|
pub fn has_visible_history(&self) -> bool {
|
||||||
self.history
|
self.history
|
||||||
.iter()
|
.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) {
|
pub fn toggle_history(&mut self) {
|
||||||
@@ -226,19 +262,14 @@ impl CommandLine {
|
|||||||
matches
|
matches
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn view(
|
pub fn view(&self, show_autocomplete: bool, dyn_capturing: bool) -> Element<'_, Message> {
|
||||||
&self,
|
|
||||||
show_autocomplete: bool,
|
|
||||||
dyn_capturing: bool,
|
|
||||||
active_prompt: Option<String>,
|
|
||||||
) -> Element<'_, Message> {
|
|
||||||
// Only the most recent entries pushed within the last few
|
// Only the most recent entries pushed within the last few
|
||||||
// seconds show on the overlay. The dropdown button keeps the
|
// seconds show on the overlay. The dropdown button keeps the
|
||||||
// full backlog reachable when the user actually wants it.
|
// full backlog reachable when the user actually wants it.
|
||||||
let visible: Vec<&HistoryEntry> = self
|
let visible: Vec<&HistoryEntry> = self
|
||||||
.history
|
.history
|
||||||
.iter()
|
.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();
|
.collect();
|
||||||
let start = visible.len().saturating_sub(4);
|
let start = visible.len().saturating_sub(4);
|
||||||
let history_rows = visible[start..]
|
let history_rows = visible[start..]
|
||||||
@@ -252,12 +283,7 @@ impl CommandLine {
|
|||||||
};
|
};
|
||||||
col.push(container(text(&entry.text).size(11).color(color)).padding([1, 8]))
|
col.push(container(text(&entry.text).size(11).color(color)).padding([1, 8]))
|
||||||
});
|
});
|
||||||
// While a command is running, the prompt shows what the current
|
let prompt = container(text("Command:").size(11).color(PROMPT_COLOR)).padding([5, 8]);
|
||||||
// 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]);
|
|
||||||
// While dynamic input is capturing keystrokes, the command-line
|
// While dynamic input is capturing keystrokes, the command-line
|
||||||
// text field is left without an `on_input` handler so it can't
|
// text field is left without an `on_input` handler so it can't
|
||||||
// grab focus or swallow numeric keys — those flow through the
|
// grab focus or swallow numeric keys — those flow through the
|
||||||
|
|||||||
Reference in new issue
Block a user