feat: redesign launcher palette and stabilize AI hook warnings

This commit is contained in:
kuddev committed 2026-08-08 16:16:04 +08:00
1 parent 0d395f2162
commit 4e4b3be7fd
7 files changed
+743 -220

No files matched your search

+25 -5
View File
@@ -1036,15 +1036,35 @@ export default function (pi: ExtensionAPI) {
}
/// Absolute path of the bridge exe.
///
/// The helper is an optional runtime asset, so a development checkout or
/// an incomplete standalone directory is a valid state. `helper_path()`
/// is called from several self-healing paths and from the pipe bootstrap;
/// logging on every probe turns that state into an apparent infinite
/// warning loop when a config watcher is busy. Keep the state transition
/// noisy once, but make repeated probes silent until the helper is found
/// again (or removed later).
static HELPER_MISSING_ANNOUNCED: AtomicBool = AtomicBool::new(false);
fn helper_path() -> Option<PathBuf> {
let exe = std::env::current_exe().ok()?;
let helper = helper_path_from_exe(&exe);
if helper.is_none() {
log::warn!(
"ai_hook: nebula-hook.exe missing from runtime/ and executable directory; AI integrations not installed"
);
match helper {
Some(path) => {
// Permit a later installation/removal to be reported once on
// the next state transition rather than caching a stale path.
HELPER_MISSING_ANNOUNCED.store(false, Ordering::Relaxed);
Some(path)
},
None => {
if !HELPER_MISSING_ANNOUNCED.swap(true, Ordering::Relaxed) {
log::warn!(
"ai_hook: nebula-hook.exe missing from runtime/ and executable directory; AI integrations not installed"
);
}
None
},
}
helper
}
fn helper_path_from_exe(exe: &Path) -> Option<PathBuf> {
File diff suppressed because it is too large. Load diff
+36 -2
View File
@@ -4446,11 +4446,21 @@ impl Display {
/// any config profiles. Detection runs once and is cached — the chevron
/// beside the "+" opens this — the familiar profile menu.
pub fn open_shell_menu(&mut self, profiles: &[crate::config::ui_config::Profile]) {
let ssh_rows: Vec<(String, String)> = self
.nebula_ssh_hosts
.iter()
.map(|host| {
let label =
self.nebula_ssh_labels.get(host).cloned().unwrap_or_else(|| host.clone());
(label, host.clone())
})
.collect();
let shells =
self.nebula_detected_shells.get_or_insert_with(crate::shell_detect::detect_shells);
let default_shell =
self.nebula_shell_id.as_deref().unwrap_or_else(|| self.nebula_shell.settings_value());
self.nebula_palette.set_shell_menu(shells, profiles, default_shell);
self.nebula_palette.set_ssh_hosts(&ssh_rows);
self.nebula_palette.open_profiles();
self.pending_update.dirty = true;
}
@@ -4551,6 +4561,25 @@ impl Display {
self.pending_update.dirty = true;
}
pub fn palette_tab(&mut self, delta: i32) {
if !self.nebula_palette.cycle_launcher_filter(delta) {
self.palette_move(delta);
return;
}
self.pending_update.dirty = true;
}
pub fn palette_select_launcher_filter(
&mut self,
filter: command_palette::LauncherFilter,
) -> bool {
if self.nebula_palette.set_launcher_filter(filter) {
self.pending_update.dirty = true;
return true;
}
false
}
pub fn palette_scroll_by(&mut self, rows: i32, max_rows: usize) -> bool {
if self.nebula_palette.scroll_by(rows, max_rows) {
self.pending_update.dirty = true;
@@ -4613,8 +4642,13 @@ impl Display {
/// Update palette hover state. `row` is the visual row index, or `None` when
/// the mouse left the palette area.
pub fn palette_hover(&mut self, pos: (f32, f32), row: Option<usize>) -> bool {
if self.nebula_palette.pointer_hover(pos, row) {
pub fn palette_hover(
&mut self,
pos: (f32, f32),
row: Option<usize>,
chip: Option<command_palette::LauncherFilter>,
) -> bool {
if self.nebula_palette.pointer_hover(pos, row, chip) {
self.pending_update.dirty = true;
return true;
}
+3 -1
View File
@@ -367,7 +367,9 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
let layout = self.ctx.display().command_palette_layout();
let (px, py, pw, ph) = layout.panel;
if x >= px && x < px + pw && y >= py && y < py + ph {
if self.ctx.display().palette_scrollbar_press(x, y, &layout) {
if let Some(filter) = layout.chip_at(x, y) {
self.ctx.display().palette_select_launcher_filter(filter);
} else if self.ctx.display().palette_scrollbar_press(x, y, &layout) {
self.ctx.window().set_mouse_cursor(winit::window::CursorIcon::Grabbing);
} else if let Some(row) = layout.row_at(x, y) {
if let Some(action) = self.ctx.display().palette_click(row, layout.max_rows)
+2 -1
View File
@@ -533,7 +533,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
}
},
Key::Named(NamedKey::Tab) => {
self.ctx.display().palette_move(if mods.shift_key() { -1 } else { 1 });
self.ctx.display().palette_tab(if mods.shift_key() { -1 } else { 1 });
},
Key::Named(NamedKey::ArrowDown) => self.ctx.display().palette_move(1),
Key::Named(NamedKey::ArrowUp) => self.ctx.display().palette_move(-1),
@@ -923,6 +923,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
name: shell.name.clone(),
shell: shell.shell(),
}),
LaunchSsh(host) => self.ctx.nebula_tab(TabRequest::NewSsh(host)),
SetDefaultShell(shell) => self.ctx.display().set_default_shell(&shell),
SetDefaultProfile(profile) => self.ctx.display().set_default_profile(&profile),
NewAtDirectory(path) => self.ctx.nebula_tab(TabRequest::NewAtDirectory(path)),
+6 -6
View File
@@ -280,17 +280,19 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
}
let over_scrollbar =
layout.scrollbar.is_some_and(|scrollbar| scrollbar.hit_test(px, py));
let hover_chip = if over_scrollbar { None } else { layout.chip_at(px, py) };
// Hover detection rides the layout's per-row rects (`row_at`): the
// picker's card geometry is non-uniform (hero card, section gaps),
// so dividing by row_h would light up captions and gaps too. The
// scrollbar's widened hit target owns its pixels and suppresses row hover.
let hover_row = if over_scrollbar { None } else { layout.row_at(px, py) };
if self.ctx.display().palette_hover((px, py), hover_row) {
let hover_row =
if over_scrollbar || hover_chip.is_some() { None } else { layout.row_at(px, py) };
if self.ctx.display().palette_hover((px, py), hover_row, hover_chip) {
self.ctx.mark_dirty();
}
self.ctx.window().set_mouse_cursor(if over_scrollbar {
CursorIcon::Grab
} else if hover_row.is_some() {
} else if hover_row.is_some() || hover_chip.is_some() {
CursorIcon::Pointer
} else {
CursorIcon::Default
@@ -474,9 +476,7 @@ impl<T: EventListener, A: ActionContext<T>> Processor<T, A> {
self.ctx.window().set_mouse_cursor(CursorIcon::Grab);
return;
}
if self.ctx.image_view().is_some()
&& rect_contains(image_area, x as f32, y as f32)
{
if self.ctx.image_view().is_some() && rect_contains(image_area, x as f32, y as f32) {
self.ctx.window().set_mouse_cursor(CursorIcon::Grab);
return;
}
+9
View File
@@ -128,6 +128,15 @@ fn main() -> Result<(), Box<dyn Error>> {
// Load command line options.
let options = Options::new();
#[cfg(windows)]
if options.subcommands.is_none() && env::var_os("NEBULA_DETACHED_LAUNCH").is_some() {
// 独立验证实例必须在创建窗口和 ConPTY 前脱离短生命周期的启动控制台,
// 否则启动器退出会连带关闭窗口,只留下后台会话进程。
unsafe {
FreeConsole();
}
}
match options.subcommands {
#[cfg(unix)]
Some(Subcommands::Msg(options)) => msg(options)?,