Use delayed shimmer for plugin loading headers in tui and tui_app_server (#15674)

- Add a small delayed loading header for plugin list/detail loading
messages in the TUI. Keep existing text for the first 1s, then show
shimmer on the loading line.
- Apply the same behavior in both tui and tui_app_server.


https://github.com/user-attachments/assets/71dd35e4-7e3b-4e7b-867a-3c13dc395d3a
This commit is contained in:
canvrno-oai
2026-03-24 16:03:40 -07:00
committed by GitHub
parent 301b17c2a1
commit 6323f0104d
4 changed files with 168 additions and 32 deletions

View File

@@ -1,4 +1,6 @@
use std::path::PathBuf;
use std::time::Duration;
use std::time::Instant;
use super::ChatWidget;
use crate::app_event::AppEvent;
@@ -7,6 +9,9 @@ use crate::bottom_pane::SelectionItem;
use crate::bottom_pane::SelectionViewParams;
use crate::history_cell;
use crate::render::renderable::ColumnRenderable;
use crate::render::renderable::Renderable;
use crate::shimmer::shimmer_spans;
use crate::tui::FrameRequester;
use codex_app_server_protocol::PluginDetail;
use codex_app_server_protocol::PluginInstallPolicy;
use codex_app_server_protocol::PluginInstallResponse;
@@ -17,10 +22,76 @@ use codex_app_server_protocol::PluginSummary;
use codex_app_server_protocol::PluginUninstallResponse;
use codex_features::Feature;
use codex_utils_absolute_path::AbsolutePathBuf;
use ratatui::buffer::Buffer;
use ratatui::layout::Rect;
use ratatui::style::Stylize;
use ratatui::text::Line;
use ratatui::widgets::Paragraph;
use ratatui::widgets::WidgetRef;
const PLUGINS_SELECTION_VIEW_ID: &str = "plugins-selection";
const LOADING_ANIMATION_DELAY: Duration = Duration::from_secs(1);
const LOADING_ANIMATION_INTERVAL: Duration = Duration::from_millis(100);
struct DelayedLoadingHeader {
started_at: Instant,
frame_requester: FrameRequester,
animations_enabled: bool,
loading_text: String,
note: Option<String>,
}
impl DelayedLoadingHeader {
fn new(
frame_requester: FrameRequester,
animations_enabled: bool,
loading_text: String,
note: Option<String>,
) -> Self {
Self {
started_at: Instant::now(),
frame_requester,
animations_enabled,
loading_text,
note,
}
}
}
impl Renderable for DelayedLoadingHeader {
fn render(&self, area: Rect, buf: &mut Buffer) {
if area.is_empty() {
return;
}
let mut lines = Vec::with_capacity(3);
lines.push(Line::from("Plugins".bold()));
let now = Instant::now();
let elapsed = now.saturating_duration_since(self.started_at);
if elapsed < LOADING_ANIMATION_DELAY {
self.frame_requester
.schedule_frame_in(LOADING_ANIMATION_DELAY - elapsed);
lines.push(Line::from(self.loading_text.as_str().dim()));
} else if self.animations_enabled {
self.frame_requester
.schedule_frame_in(LOADING_ANIMATION_INTERVAL);
lines.push(Line::from(shimmer_spans(self.loading_text.as_str())));
} else {
lines.push(Line::from(self.loading_text.as_str().dim()));
}
if let Some(note) = &self.note {
lines.push(Line::from(note.as_str().dim()));
}
Paragraph::new(lines).render_ref(area, buf);
}
fn desired_height(&self, _width: u16) -> u16 {
2 + u16::from(self.note.is_some())
}
}
#[derive(Debug, Clone, Default)]
pub(super) enum PluginsCacheState {
@@ -474,16 +545,14 @@ impl ChatWidget {
}
fn plugins_loading_popup_params(&self) -> SelectionViewParams {
let mut header = ColumnRenderable::new();
header.push(Line::from("Plugins".bold()));
header.push(Line::from("Loading available plugins...".dim()));
header.push(Line::from(
"Available marketplaces will appear here when ready.".dim(),
));
SelectionViewParams {
view_id: Some(PLUGINS_SELECTION_VIEW_ID),
header: Box::new(header),
header: Box::new(DelayedLoadingHeader::new(
self.frame_requester.clone(),
self.config.animations,
"Loading available plugins...".to_string(),
Some("This first pass shows the ChatGPT marketplace only.".to_string()),
)),
items: vec![SelectionItem {
name: "Loading plugins...".to_string(),
description: Some("This updates when the marketplace list is ready.".to_string()),
@@ -495,15 +564,14 @@ impl ChatWidget {
}
fn plugin_detail_loading_popup_params(&self, plugin_display_name: &str) -> SelectionViewParams {
let mut header = ColumnRenderable::new();
header.push(Line::from("Plugins".bold()));
header.push(Line::from(
format!("Loading details for {plugin_display_name}...").dim(),
));
SelectionViewParams {
view_id: Some(PLUGINS_SELECTION_VIEW_ID),
header: Box::new(header),
header: Box::new(DelayedLoadingHeader::new(
self.frame_requester.clone(),
self.config.animations,
format!("Loading details for {plugin_display_name}..."),
/*note*/ None,
)),
items: vec![SelectionItem {
name: "Loading plugin details...".to_string(),
description: Some("This updates when plugin details load.".to_string()),

View File

@@ -4,6 +4,6 @@ expression: popup
---
Plugins
Loading available plugins...
Available marketplaces will appear here when ready.
This first pass shows the ChatGPT marketplace only.
Loading plugins... This updates when the marketplace list is ready.