mirror of
https://github.com/openai/codex.git
synced 2026-11-06 20:20:34 +00:00
Rebase BTW TUI state onto current main
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
co-authored by
Codex
parent
312076fdf1
commit
9b8f7f1d09
+715
-108
File diff suppressed because it is too large
Load Diff
@@ -10,12 +10,18 @@
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use codex_app_server_protocol::PluginInstallResponse;
|
||||
use codex_app_server_protocol::PluginListResponse;
|
||||
use codex_app_server_protocol::PluginReadParams;
|
||||
use codex_app_server_protocol::PluginReadResponse;
|
||||
use codex_app_server_protocol::PluginUninstallResponse;
|
||||
use codex_chatgpt::connectors::AppInfo;
|
||||
use codex_file_search::FileMatch;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::protocol::Event;
|
||||
use codex_protocol::protocol::RateLimitSnapshot;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_approval_presets::ApprovalPreset;
|
||||
|
||||
use crate::bottom_pane::ApprovalRequest;
|
||||
@@ -25,7 +31,7 @@ use crate::chatwidget::UserMessage;
|
||||
use crate::history_cell::HistoryCell;
|
||||
|
||||
use codex_core::config::types::ApprovalsReviewer;
|
||||
use codex_core::features::Feature;
|
||||
use codex_features::Feature;
|
||||
use codex_protocol::config_types::CollaborationModeMask;
|
||||
use codex_protocol::config_types::Personality;
|
||||
use codex_protocol::config_types::ServiceTier;
|
||||
@@ -76,7 +82,6 @@ pub(crate) enum AppEvent {
|
||||
OpenAgentPicker,
|
||||
/// Switch the active thread to the selected agent.
|
||||
SelectAgentThread(ThreadId),
|
||||
|
||||
/// Fork the current thread into a transient BTW child and submit a side-question there.
|
||||
StartBtw {
|
||||
parent_thread_id: ThreadId,
|
||||
@@ -169,6 +174,84 @@ pub(crate) enum AppEvent {
|
||||
force_refetch: bool,
|
||||
},
|
||||
|
||||
/// Fetch plugin marketplace state for the provided working directory.
|
||||
FetchPluginsList {
|
||||
cwd: PathBuf,
|
||||
},
|
||||
|
||||
/// Result of fetching plugin marketplace state.
|
||||
PluginsLoaded {
|
||||
cwd: PathBuf,
|
||||
result: Result<PluginListResponse, String>,
|
||||
},
|
||||
|
||||
/// Replace the plugins popup with a plugin-detail loading state.
|
||||
OpenPluginDetailLoading {
|
||||
plugin_display_name: String,
|
||||
},
|
||||
|
||||
/// Fetch detail for a specific plugin from a marketplace.
|
||||
FetchPluginDetail {
|
||||
cwd: PathBuf,
|
||||
params: PluginReadParams,
|
||||
},
|
||||
|
||||
/// Result of fetching plugin detail.
|
||||
PluginDetailLoaded {
|
||||
cwd: PathBuf,
|
||||
result: Result<PluginReadResponse, String>,
|
||||
},
|
||||
|
||||
/// Replace the plugins popup with an install loading state.
|
||||
OpenPluginInstallLoading {
|
||||
plugin_display_name: String,
|
||||
},
|
||||
|
||||
/// Replace the plugins popup with an uninstall loading state.
|
||||
OpenPluginUninstallLoading {
|
||||
plugin_display_name: String,
|
||||
},
|
||||
|
||||
/// Install a specific plugin from a marketplace.
|
||||
FetchPluginInstall {
|
||||
cwd: PathBuf,
|
||||
marketplace_path: AbsolutePathBuf,
|
||||
plugin_name: String,
|
||||
plugin_display_name: String,
|
||||
},
|
||||
|
||||
/// Result of installing a plugin.
|
||||
PluginInstallLoaded {
|
||||
cwd: PathBuf,
|
||||
marketplace_path: AbsolutePathBuf,
|
||||
plugin_name: String,
|
||||
plugin_display_name: String,
|
||||
result: Result<PluginInstallResponse, String>,
|
||||
},
|
||||
|
||||
/// Uninstall a specific plugin by canonical plugin id.
|
||||
FetchPluginUninstall {
|
||||
cwd: PathBuf,
|
||||
plugin_id: String,
|
||||
plugin_display_name: String,
|
||||
},
|
||||
|
||||
/// Result of uninstalling a plugin.
|
||||
PluginUninstallLoaded {
|
||||
cwd: PathBuf,
|
||||
plugin_id: String,
|
||||
plugin_display_name: String,
|
||||
result: Result<PluginUninstallResponse, String>,
|
||||
},
|
||||
|
||||
/// Advance the post-install plugin app-auth flow.
|
||||
PluginInstallAuthAdvance {
|
||||
refresh_connectors: bool,
|
||||
},
|
||||
|
||||
/// Abandon the post-install plugin app-auth flow.
|
||||
PluginInstallAuthAbandon,
|
||||
|
||||
InsertHistoryCell(Box<dyn HistoryCell>),
|
||||
|
||||
/// Apply rollback semantics to local transcript cells.
|
||||
@@ -459,6 +542,16 @@ pub(crate) enum AppEvent {
|
||||
},
|
||||
/// Dismiss the status-line setup UI without changing config.
|
||||
StatusLineSetupCancelled,
|
||||
/// Apply a user-confirmed terminal-title item ordering/selection.
|
||||
TerminalTitleSetup {
|
||||
items: Vec<TerminalTitleItem>,
|
||||
},
|
||||
/// Apply a temporary terminal-title preview while the setup UI is open.
|
||||
TerminalTitleSetupPreview {
|
||||
items: Vec<TerminalTitleItem>,
|
||||
},
|
||||
/// Dismiss the terminal-title setup UI without changing config.
|
||||
TerminalTitleSetupCancelled,
|
||||
|
||||
/// Apply a user-confirmed syntax theme selection.
|
||||
SyntaxThemeSelected {
|
||||
|
||||
+523
-362
File diff suppressed because it is too large
Load Diff
@@ -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()),
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
+2438
-1028
File diff suppressed because it is too large
Load Diff
@@ -151,13 +151,11 @@ impl App {
|
||||
|
||||
let channel = self.thread_event_channels.get(&parent_thread_id)?;
|
||||
let store = channel.store.lock().await;
|
||||
match store.session_configured.as_ref().map(|event| &event.msg) {
|
||||
Some(EventMsg::SessionConfigured(session)) => session
|
||||
.thread_name
|
||||
.clone()
|
||||
.filter(|name| !name.trim().is_empty()),
|
||||
_ => None,
|
||||
}
|
||||
store
|
||||
.session
|
||||
.as_ref()
|
||||
.and_then(|session| session.thread_name.clone())
|
||||
.filter(|name| !name.trim().is_empty())
|
||||
}
|
||||
|
||||
pub(super) async fn select_agent_thread_and_discard_btw_chain(
|
||||
@@ -167,7 +165,7 @@ impl App {
|
||||
thread_id: ThreadId,
|
||||
) -> Result<()> {
|
||||
let btw_threads_to_discard = self.btw_threads_to_discard_after_switch(thread_id);
|
||||
self.select_agent_thread(tui, thread_id).await?;
|
||||
self.select_agent_thread(tui, app_server, thread_id).await?;
|
||||
if self.active_thread_id == Some(thread_id) {
|
||||
for btw_thread_id in btw_threads_to_discard {
|
||||
self.discard_btw_thread(app_server, btw_thread_id).await;
|
||||
@@ -193,45 +191,16 @@ impl App {
|
||||
|
||||
let mut fork_config = self.config.clone();
|
||||
fork_config.ephemeral = true;
|
||||
let fork_result = match app_server.fork_thread(fork_config, parent_thread_id).await {
|
||||
Ok(forked) => Ok(forked),
|
||||
Err(err) => {
|
||||
if self.current_displayed_thread_id() != Some(parent_thread_id) {
|
||||
self.chat_widget.add_error_message(format!(
|
||||
"Failed to fork BTW thread from {parent_thread_id}: {err}"
|
||||
));
|
||||
return Ok(AppRunControl::Continue);
|
||||
}
|
||||
let Some(parent_rollout_path) =
|
||||
self.chat_widget.rollout_path().filter(|path| path.exists())
|
||||
else {
|
||||
self.chat_widget.add_error_message(
|
||||
"A thread must contain at least one turn before /btw can fork it."
|
||||
.to_string(),
|
||||
);
|
||||
return Ok(AppRunControl::Continue);
|
||||
};
|
||||
let mut fork_config = self.config.clone();
|
||||
fork_config.ephemeral = true;
|
||||
app_server
|
||||
.fork_thread_from_path(fork_config, parent_thread_id, parent_rollout_path)
|
||||
.await
|
||||
}
|
||||
};
|
||||
|
||||
match fork_result {
|
||||
match app_server.fork_thread(fork_config, parent_thread_id).await {
|
||||
Ok(forked) => {
|
||||
let child_thread_id = forked.session_configured.session_id;
|
||||
let child_thread_id = forked.session.thread_id;
|
||||
let next_fork_banner_parent_label =
|
||||
self.fork_banner_parent_label(parent_thread_id).await;
|
||||
self.enqueue_thread_event(
|
||||
child_thread_id,
|
||||
Event {
|
||||
id: String::new(),
|
||||
msg: EventMsg::SessionConfigured(forked.session_configured),
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let channel = self.ensure_thread_channel(child_thread_id);
|
||||
{
|
||||
let mut store = channel.store.lock().await;
|
||||
store.set_session(forked.session, forked.turns);
|
||||
}
|
||||
self.btw_threads.insert(
|
||||
child_thread_id,
|
||||
BtwThreadState {
|
||||
@@ -259,7 +228,7 @@ impl App {
|
||||
}
|
||||
Err(err) => {
|
||||
self.chat_widget.add_error_message(format!(
|
||||
"Failed to start BTW thread from {parent_thread_id}: {err}"
|
||||
"Failed to fork BTW thread from {parent_thread_id}: {err}"
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use codex_app_server_client::AppServerClient;
|
||||
use codex_app_server_client::AppServerEvent;
|
||||
use codex_app_server_client::AppServerRequestHandle;
|
||||
use codex_app_server_client::TypedRequestError;
|
||||
use codex_app_server_protocol::Account;
|
||||
use codex_app_server_protocol::AuthMode;
|
||||
use codex_app_server_protocol::ClientRequest;
|
||||
@@ -42,10 +43,13 @@ use codex_app_server_protocol::ThreadRollbackParams;
|
||||
use codex_app_server_protocol::ThreadRollbackResponse;
|
||||
use codex_app_server_protocol::ThreadSetNameParams;
|
||||
use codex_app_server_protocol::ThreadSetNameResponse;
|
||||
use codex_app_server_protocol::ThreadShellCommandParams;
|
||||
use codex_app_server_protocol::ThreadShellCommandResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_app_server_protocol::ThreadUnsubscribeParams;
|
||||
use codex_app_server_protocol::ThreadUnsubscribeResponse;
|
||||
use codex_app_server_protocol::Turn;
|
||||
use codex_app_server_protocol::TurnInterruptParams;
|
||||
use codex_app_server_protocol::TurnInterruptResponse;
|
||||
use codex_app_server_protocol::TurnStartParams;
|
||||
@@ -53,17 +57,9 @@ use codex_app_server_protocol::TurnStartResponse;
|
||||
use codex_app_server_protocol::TurnSteerParams;
|
||||
use codex_app_server_protocol::TurnSteerResponse;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::message_history;
|
||||
use codex_otel::TelemetryAuthMode;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::items::AgentMessageContent;
|
||||
use codex_protocol::items::AgentMessageItem;
|
||||
use codex_protocol::items::ContextCompactionItem;
|
||||
use codex_protocol::items::ImageGenerationItem;
|
||||
use codex_protocol::items::PlanItem;
|
||||
use codex_protocol::items::ReasoningItem;
|
||||
use codex_protocol::items::TurnItem;
|
||||
use codex_protocol::items::UserMessageItem;
|
||||
use codex_protocol::items::WebSearchItem;
|
||||
use codex_protocol::openai_models::ModelAvailabilityNux;
|
||||
use codex_protocol::openai_models::ModelPreset;
|
||||
use codex_protocol::openai_models::ModelUpgrade;
|
||||
@@ -73,19 +69,18 @@ use codex_protocol::protocol::ConversationAudioParams;
|
||||
use codex_protocol::protocol::ConversationStartParams;
|
||||
use codex_protocol::protocol::ConversationTextParams;
|
||||
use codex_protocol::protocol::CreditsSnapshot;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::ItemCompletedEvent;
|
||||
use codex_protocol::protocol::RateLimitSnapshot;
|
||||
use codex_protocol::protocol::RateLimitWindow;
|
||||
use codex_protocol::protocol::ReviewRequest;
|
||||
use codex_protocol::protocol::ReviewTarget as CoreReviewTarget;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::SessionConfiguredEvent;
|
||||
use codex_protocol::protocol::SessionNetworkProxyRuntime;
|
||||
use color_eyre::eyre::ContextCompat;
|
||||
use color_eyre::eyre::Result;
|
||||
use color_eyre::eyre::WrapErr;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::bottom_pane::FeedbackAudience;
|
||||
use crate::status::StatusAccountDisplay;
|
||||
@@ -108,6 +103,25 @@ pub(crate) struct AppServerSession {
|
||||
next_request_id: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub(crate) struct ThreadSessionState {
|
||||
pub(crate) thread_id: ThreadId,
|
||||
pub(crate) forked_from_id: Option<ThreadId>,
|
||||
pub(crate) thread_name: Option<String>,
|
||||
pub(crate) model: String,
|
||||
pub(crate) model_provider_id: String,
|
||||
pub(crate) service_tier: Option<codex_protocol::config_types::ServiceTier>,
|
||||
pub(crate) approval_policy: AskForApproval,
|
||||
pub(crate) approvals_reviewer: codex_protocol::config_types::ApprovalsReviewer,
|
||||
pub(crate) sandbox_policy: SandboxPolicy,
|
||||
pub(crate) cwd: PathBuf,
|
||||
pub(crate) reasoning_effort: Option<codex_protocol::openai_models::ReasoningEffort>,
|
||||
pub(crate) history_log_id: u64,
|
||||
pub(crate) history_entry_count: u64,
|
||||
pub(crate) network_proxy: Option<SessionNetworkProxyRuntime>,
|
||||
pub(crate) rollout_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum ThreadParamsMode {
|
||||
Embedded,
|
||||
@@ -124,7 +138,8 @@ impl ThreadParamsMode {
|
||||
}
|
||||
|
||||
pub(crate) struct AppServerStartedThread {
|
||||
pub(crate) session_configured: SessionConfiguredEvent,
|
||||
pub(crate) session: ThreadSessionState,
|
||||
pub(crate) turns: Vec<Turn>,
|
||||
}
|
||||
|
||||
impl AppServerSession {
|
||||
@@ -164,16 +179,6 @@ impl AppServerSession {
|
||||
})
|
||||
.await
|
||||
.wrap_err("model/list failed during TUI bootstrap")?;
|
||||
let rate_limit_request_id = self.next_request_id();
|
||||
let rate_limits: GetAccountRateLimitsResponse = self
|
||||
.client
|
||||
.request_typed(ClientRequest::GetAccountRateLimits {
|
||||
request_id: rate_limit_request_id,
|
||||
params: None,
|
||||
})
|
||||
.await
|
||||
.wrap_err("account/rateLimits/read failed during TUI bootstrap")?;
|
||||
|
||||
let available_models = models
|
||||
.data
|
||||
.into_iter()
|
||||
@@ -238,6 +243,25 @@ impl AppServerSession {
|
||||
false,
|
||||
),
|
||||
};
|
||||
let rate_limit_snapshots = if account.requires_openai_auth && has_chatgpt_account {
|
||||
let rate_limit_request_id = self.next_request_id();
|
||||
match self
|
||||
.client
|
||||
.request_typed(ClientRequest::GetAccountRateLimits {
|
||||
request_id: rate_limit_request_id,
|
||||
params: None,
|
||||
})
|
||||
.await
|
||||
{
|
||||
Ok(rate_limits) => app_server_rate_limit_snapshots_to_core(rate_limits),
|
||||
Err(error) => {
|
||||
debug!(error = ?error, "failed to fetch rate limits during TUI bootstrap");
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
Ok(AppServerBootstrap {
|
||||
account_auth_mode,
|
||||
@@ -249,7 +273,7 @@ impl AppServerSession {
|
||||
feedback_audience,
|
||||
has_chatgpt_account,
|
||||
available_models,
|
||||
rate_limit_snapshots: app_server_rate_limit_snapshots_to_core(rate_limits),
|
||||
rate_limit_snapshots,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -267,7 +291,7 @@ impl AppServerSession {
|
||||
})
|
||||
.await
|
||||
.wrap_err("thread/start failed during TUI bootstrap")?;
|
||||
started_thread_from_start_response(&response)
|
||||
started_thread_from_start_response(response, config).await
|
||||
}
|
||||
|
||||
pub(crate) async fn resume_thread(
|
||||
@@ -275,21 +299,20 @@ impl AppServerSession {
|
||||
config: Config,
|
||||
thread_id: ThreadId,
|
||||
) -> Result<AppServerStartedThread> {
|
||||
let show_raw_agent_reasoning = config.show_raw_agent_reasoning;
|
||||
let request_id = self.next_request_id();
|
||||
let response: ThreadResumeResponse = self
|
||||
.client
|
||||
.request_typed(ClientRequest::ThreadResume {
|
||||
request_id,
|
||||
params: thread_resume_params_from_config(
|
||||
config,
|
||||
config.clone(),
|
||||
thread_id,
|
||||
self.thread_params_mode(),
|
||||
),
|
||||
})
|
||||
.await
|
||||
.wrap_err("thread/resume failed during TUI bootstrap")?;
|
||||
started_thread_from_resume_response(&response, show_raw_agent_reasoning)
|
||||
started_thread_from_resume_response(response, &config).await
|
||||
}
|
||||
|
||||
pub(crate) async fn fork_thread(
|
||||
@@ -297,37 +320,20 @@ impl AppServerSession {
|
||||
config: Config,
|
||||
thread_id: ThreadId,
|
||||
) -> Result<AppServerStartedThread> {
|
||||
self.fork_thread_with_path(config, thread_id, /*path*/ None)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn fork_thread_from_path(
|
||||
&mut self,
|
||||
config: Config,
|
||||
thread_id: ThreadId,
|
||||
path: PathBuf,
|
||||
) -> Result<AppServerStartedThread> {
|
||||
self.fork_thread_with_path(config, thread_id, Some(path))
|
||||
.await
|
||||
}
|
||||
|
||||
async fn fork_thread_with_path(
|
||||
&mut self,
|
||||
config: Config,
|
||||
thread_id: ThreadId,
|
||||
path: Option<PathBuf>,
|
||||
) -> Result<AppServerStartedThread> {
|
||||
let show_raw_agent_reasoning = config.show_raw_agent_reasoning;
|
||||
let request_id = self.next_request_id();
|
||||
let mut params =
|
||||
thread_fork_params_from_config(config, thread_id, self.thread_params_mode());
|
||||
params.path = path;
|
||||
let response: ThreadForkResponse = self
|
||||
.client
|
||||
.request_typed(ClientRequest::ThreadFork { request_id, params })
|
||||
.request_typed(ClientRequest::ThreadFork {
|
||||
request_id,
|
||||
params: thread_fork_params_from_config(
|
||||
config.clone(),
|
||||
thread_id,
|
||||
self.thread_params_mode(),
|
||||
),
|
||||
})
|
||||
.await
|
||||
.wrap_err("thread/fork failed during TUI bootstrap")?;
|
||||
started_thread_from_fork_response(&response, show_raw_agent_reasoning)
|
||||
started_thread_from_fork_response(response, &config).await
|
||||
}
|
||||
|
||||
fn thread_params_mode(&self) -> ThreadParamsMode {
|
||||
@@ -434,7 +440,7 @@ impl AppServerSession {
|
||||
thread_id: ThreadId,
|
||||
turn_id: String,
|
||||
items: Vec<codex_protocol::user_input::UserInput>,
|
||||
) -> Result<TurnSteerResponse> {
|
||||
) -> std::result::Result<TurnSteerResponse, TypedRequestError> {
|
||||
let request_id = self.next_request_id();
|
||||
self.client
|
||||
.request_typed(ClientRequest::TurnSteer {
|
||||
@@ -446,7 +452,6 @@ impl AppServerSession {
|
||||
},
|
||||
})
|
||||
.await
|
||||
.wrap_err("turn/steer failed in app-server TUI")
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_set_name(
|
||||
@@ -499,6 +504,26 @@ impl AppServerSession {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_shell_command(
|
||||
&mut self,
|
||||
thread_id: ThreadId,
|
||||
command: String,
|
||||
) -> Result<()> {
|
||||
let request_id = self.next_request_id();
|
||||
let _: ThreadShellCommandResponse = self
|
||||
.client
|
||||
.request_typed(ClientRequest::ThreadShellCommand {
|
||||
request_id,
|
||||
params: ThreadShellCommandParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
command,
|
||||
},
|
||||
})
|
||||
.await
|
||||
.wrap_err("thread/shellCommand failed in app-server TUI")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_background_terminals_clean(
|
||||
&mut self,
|
||||
thread_id: ThreadId,
|
||||
@@ -851,54 +876,50 @@ fn thread_cwd_from_config(config: &Config, thread_params_mode: ThreadParamsMode)
|
||||
}
|
||||
}
|
||||
|
||||
fn started_thread_from_start_response(
|
||||
response: &ThreadStartResponse,
|
||||
async fn started_thread_from_start_response(
|
||||
response: ThreadStartResponse,
|
||||
config: &Config,
|
||||
) -> Result<AppServerStartedThread> {
|
||||
let session_configured = session_configured_from_thread_start_response(response)
|
||||
.map_err(color_eyre::eyre::Report::msg)?;
|
||||
Ok(AppServerStartedThread { session_configured })
|
||||
}
|
||||
|
||||
fn started_thread_from_resume_response(
|
||||
response: &ThreadResumeResponse,
|
||||
show_raw_agent_reasoning: bool,
|
||||
) -> Result<AppServerStartedThread> {
|
||||
let session_configured = session_configured_from_thread_resume_response(response)
|
||||
let session = thread_session_state_from_thread_start_response(&response, config)
|
||||
.await
|
||||
.map_err(color_eyre::eyre::Report::msg)?;
|
||||
Ok(AppServerStartedThread {
|
||||
session_configured: SessionConfiguredEvent {
|
||||
initial_messages: thread_initial_messages(
|
||||
&session_configured.session_id,
|
||||
&response.thread.turns,
|
||||
show_raw_agent_reasoning,
|
||||
),
|
||||
..session_configured
|
||||
},
|
||||
session,
|
||||
turns: response.thread.turns,
|
||||
})
|
||||
}
|
||||
|
||||
fn started_thread_from_fork_response(
|
||||
response: &ThreadForkResponse,
|
||||
show_raw_agent_reasoning: bool,
|
||||
async fn started_thread_from_resume_response(
|
||||
response: ThreadResumeResponse,
|
||||
config: &Config,
|
||||
) -> Result<AppServerStartedThread> {
|
||||
let session_configured = session_configured_from_thread_fork_response(response)
|
||||
let session = thread_session_state_from_thread_resume_response(&response, config)
|
||||
.await
|
||||
.map_err(color_eyre::eyre::Report::msg)?;
|
||||
Ok(AppServerStartedThread {
|
||||
session_configured: SessionConfiguredEvent {
|
||||
initial_messages: thread_initial_messages(
|
||||
&session_configured.session_id,
|
||||
&response.thread.turns,
|
||||
show_raw_agent_reasoning,
|
||||
),
|
||||
..session_configured
|
||||
},
|
||||
session,
|
||||
turns: response.thread.turns,
|
||||
})
|
||||
}
|
||||
|
||||
fn session_configured_from_thread_start_response(
|
||||
async fn started_thread_from_fork_response(
|
||||
response: ThreadForkResponse,
|
||||
config: &Config,
|
||||
) -> Result<AppServerStartedThread> {
|
||||
let session = thread_session_state_from_thread_fork_response(&response, config)
|
||||
.await
|
||||
.map_err(color_eyre::eyre::Report::msg)?;
|
||||
Ok(AppServerStartedThread {
|
||||
session,
|
||||
turns: response.thread.turns,
|
||||
})
|
||||
}
|
||||
|
||||
async fn thread_session_state_from_thread_start_response(
|
||||
response: &ThreadStartResponse,
|
||||
) -> Result<SessionConfiguredEvent, String> {
|
||||
session_configured_from_thread_response(
|
||||
config: &Config,
|
||||
) -> Result<ThreadSessionState, String> {
|
||||
thread_session_state_from_thread_response(
|
||||
&response.thread.id,
|
||||
response.thread.name.clone(),
|
||||
response.thread.path.clone(),
|
||||
@@ -910,13 +931,16 @@ fn session_configured_from_thread_start_response(
|
||||
response.sandbox.to_core(),
|
||||
response.cwd.clone(),
|
||||
response.reasoning_effort,
|
||||
config,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn session_configured_from_thread_resume_response(
|
||||
async fn thread_session_state_from_thread_resume_response(
|
||||
response: &ThreadResumeResponse,
|
||||
) -> Result<SessionConfiguredEvent, String> {
|
||||
session_configured_from_thread_response(
|
||||
config: &Config,
|
||||
) -> Result<ThreadSessionState, String> {
|
||||
thread_session_state_from_thread_response(
|
||||
&response.thread.id,
|
||||
response.thread.name.clone(),
|
||||
response.thread.path.clone(),
|
||||
@@ -928,13 +952,16 @@ fn session_configured_from_thread_resume_response(
|
||||
response.sandbox.to_core(),
|
||||
response.cwd.clone(),
|
||||
response.reasoning_effort,
|
||||
config,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn session_configured_from_thread_fork_response(
|
||||
async fn thread_session_state_from_thread_fork_response(
|
||||
response: &ThreadForkResponse,
|
||||
) -> Result<SessionConfiguredEvent, String> {
|
||||
session_configured_from_thread_response(
|
||||
config: &Config,
|
||||
) -> Result<ThreadSessionState, String> {
|
||||
thread_session_state_from_thread_response(
|
||||
&response.thread.id,
|
||||
response.thread.name.clone(),
|
||||
response.thread.path.clone(),
|
||||
@@ -946,7 +973,9 @@ fn session_configured_from_thread_fork_response(
|
||||
response.sandbox.to_core(),
|
||||
response.cwd.clone(),
|
||||
response.reasoning_effort,
|
||||
config,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn review_target_to_app_server(
|
||||
@@ -972,7 +1001,7 @@ fn review_target_to_app_server(
|
||||
clippy::too_many_arguments,
|
||||
reason = "session mapping keeps explicit fields"
|
||||
)]
|
||||
fn session_configured_from_thread_response(
|
||||
async fn thread_session_state_from_thread_response(
|
||||
thread_id: &str,
|
||||
thread_name: Option<String>,
|
||||
rollout_path: Option<PathBuf>,
|
||||
@@ -984,12 +1013,15 @@ fn session_configured_from_thread_response(
|
||||
sandbox_policy: SandboxPolicy,
|
||||
cwd: PathBuf,
|
||||
reasoning_effort: Option<codex_protocol::openai_models::ReasoningEffort>,
|
||||
) -> Result<SessionConfiguredEvent, String> {
|
||||
let session_id = ThreadId::from_string(thread_id)
|
||||
config: &Config,
|
||||
) -> Result<ThreadSessionState, String> {
|
||||
let thread_id = ThreadId::from_string(thread_id)
|
||||
.map_err(|err| format!("thread id `{thread_id}` is invalid: {err}"))?;
|
||||
let (history_log_id, history_entry_count) = message_history::history_metadata(config).await;
|
||||
let history_entry_count = u64::try_from(history_entry_count).unwrap_or(u64::MAX);
|
||||
|
||||
Ok(SessionConfiguredEvent {
|
||||
session_id,
|
||||
Ok(ThreadSessionState {
|
||||
thread_id,
|
||||
forked_from_id: None,
|
||||
thread_name,
|
||||
model,
|
||||
@@ -1000,129 +1032,13 @@ fn session_configured_from_thread_response(
|
||||
sandbox_policy,
|
||||
cwd,
|
||||
reasoning_effort,
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: None,
|
||||
history_log_id,
|
||||
history_entry_count,
|
||||
network_proxy: None,
|
||||
rollout_path,
|
||||
})
|
||||
}
|
||||
|
||||
fn thread_initial_messages(
|
||||
thread_id: &ThreadId,
|
||||
turns: &[codex_app_server_protocol::Turn],
|
||||
show_raw_agent_reasoning: bool,
|
||||
) -> Option<Vec<EventMsg>> {
|
||||
let events: Vec<EventMsg> = turns
|
||||
.iter()
|
||||
.flat_map(|turn| turn_initial_messages(thread_id, turn, show_raw_agent_reasoning))
|
||||
.collect();
|
||||
(!events.is_empty()).then_some(events)
|
||||
}
|
||||
|
||||
fn turn_initial_messages(
|
||||
thread_id: &ThreadId,
|
||||
turn: &codex_app_server_protocol::Turn,
|
||||
show_raw_agent_reasoning: bool,
|
||||
) -> Vec<EventMsg> {
|
||||
turn.items
|
||||
.iter()
|
||||
.cloned()
|
||||
.filter_map(app_server_thread_item_to_core)
|
||||
.flat_map(|item| match item {
|
||||
TurnItem::UserMessage(item) => vec![item.as_legacy_event()],
|
||||
TurnItem::Plan(item) => vec![EventMsg::ItemCompleted(ItemCompletedEvent {
|
||||
thread_id: *thread_id,
|
||||
turn_id: turn.id.clone(),
|
||||
item: TurnItem::Plan(item),
|
||||
})],
|
||||
item => item.as_legacy_events(show_raw_agent_reasoning),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn app_server_thread_item_to_core(item: codex_app_server_protocol::ThreadItem) -> Option<TurnItem> {
|
||||
match item {
|
||||
codex_app_server_protocol::ThreadItem::UserMessage { id, content } => {
|
||||
Some(TurnItem::UserMessage(UserMessageItem {
|
||||
id,
|
||||
content: content
|
||||
.into_iter()
|
||||
.map(codex_app_server_protocol::UserInput::into_core)
|
||||
.collect(),
|
||||
}))
|
||||
}
|
||||
codex_app_server_protocol::ThreadItem::AgentMessage { id, text, phase } => {
|
||||
Some(TurnItem::AgentMessage(AgentMessageItem {
|
||||
id,
|
||||
content: vec![AgentMessageContent::Text { text }],
|
||||
phase,
|
||||
}))
|
||||
}
|
||||
codex_app_server_protocol::ThreadItem::Plan { id, text } => {
|
||||
Some(TurnItem::Plan(PlanItem { id, text }))
|
||||
}
|
||||
codex_app_server_protocol::ThreadItem::Reasoning {
|
||||
id,
|
||||
summary,
|
||||
content,
|
||||
} => Some(TurnItem::Reasoning(ReasoningItem {
|
||||
id,
|
||||
summary_text: summary,
|
||||
raw_content: content,
|
||||
})),
|
||||
codex_app_server_protocol::ThreadItem::WebSearch { id, query, action } => {
|
||||
Some(TurnItem::WebSearch(WebSearchItem {
|
||||
id,
|
||||
query,
|
||||
action: app_server_web_search_action_to_core(action?)?,
|
||||
}))
|
||||
}
|
||||
codex_app_server_protocol::ThreadItem::ImageGeneration {
|
||||
id,
|
||||
status,
|
||||
revised_prompt,
|
||||
result,
|
||||
} => Some(TurnItem::ImageGeneration(ImageGenerationItem {
|
||||
id,
|
||||
status,
|
||||
revised_prompt,
|
||||
result,
|
||||
saved_path: None,
|
||||
})),
|
||||
codex_app_server_protocol::ThreadItem::ContextCompaction { id } => {
|
||||
Some(TurnItem::ContextCompaction(ContextCompactionItem { id }))
|
||||
}
|
||||
codex_app_server_protocol::ThreadItem::CommandExecution { .. }
|
||||
| codex_app_server_protocol::ThreadItem::FileChange { .. }
|
||||
| codex_app_server_protocol::ThreadItem::McpToolCall { .. }
|
||||
| codex_app_server_protocol::ThreadItem::DynamicToolCall { .. }
|
||||
| codex_app_server_protocol::ThreadItem::CollabAgentToolCall { .. }
|
||||
| codex_app_server_protocol::ThreadItem::ImageView { .. }
|
||||
| codex_app_server_protocol::ThreadItem::EnteredReviewMode { .. }
|
||||
| codex_app_server_protocol::ThreadItem::ExitedReviewMode { .. } => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn app_server_web_search_action_to_core(
|
||||
action: codex_app_server_protocol::WebSearchAction,
|
||||
) -> Option<codex_protocol::models::WebSearchAction> {
|
||||
match action {
|
||||
codex_app_server_protocol::WebSearchAction::Search { query, queries } => {
|
||||
Some(codex_protocol::models::WebSearchAction::Search { query, queries })
|
||||
}
|
||||
codex_app_server_protocol::WebSearchAction::OpenPage { url } => {
|
||||
Some(codex_protocol::models::WebSearchAction::OpenPage { url })
|
||||
}
|
||||
codex_app_server_protocol::WebSearchAction::FindInPage { url, pattern } => {
|
||||
Some(codex_protocol::models::WebSearchAction::FindInPage { url, pattern })
|
||||
}
|
||||
codex_app_server_protocol::WebSearchAction::Other => {
|
||||
Some(codex_protocol::models::WebSearchAction::Other)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn app_server_rate_limit_snapshots_to_core(
|
||||
response: GetAccountRateLimitsResponse,
|
||||
) -> Vec<RateLimitSnapshot> {
|
||||
@@ -1219,8 +1135,10 @@ mod tests {
|
||||
assert_eq!(fork.model_provider, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_response_restores_initial_messages_from_turn_items() {
|
||||
#[tokio::test]
|
||||
async fn resume_response_restores_turns_from_thread_items() {
|
||||
let temp_dir = tempfile::tempdir().expect("tempdir");
|
||||
let config = build_config(&temp_dir).await;
|
||||
let thread_id = ThreadId::new();
|
||||
let response = ThreadResumeResponse {
|
||||
thread: codex_app_server_protocol::Thread {
|
||||
@@ -1253,6 +1171,7 @@ mod tests {
|
||||
id: "assistant-1".to_string(),
|
||||
text: "assistant reply".to_string(),
|
||||
phase: None,
|
||||
memory_citation: None,
|
||||
},
|
||||
],
|
||||
status: TurnStatus::Completed,
|
||||
@@ -1269,30 +1188,44 @@ mod tests {
|
||||
reasoning_effort: None,
|
||||
};
|
||||
|
||||
let started =
|
||||
started_thread_from_resume_response(&response, /*show_raw_agent_reasoning*/ false)
|
||||
.expect("resume response should map");
|
||||
let initial_messages = started
|
||||
.session_configured
|
||||
.initial_messages
|
||||
.expect("resume response should restore replay history");
|
||||
let started = started_thread_from_resume_response(response.clone(), &config)
|
||||
.await
|
||||
.expect("resume response should map");
|
||||
assert_eq!(started.turns.len(), 1);
|
||||
assert_eq!(started.turns[0], response.thread.turns[0]);
|
||||
}
|
||||
|
||||
assert_eq!(initial_messages.len(), 2);
|
||||
match &initial_messages[0] {
|
||||
EventMsg::UserMessage(event) => {
|
||||
assert_eq!(event.message, "hello from history");
|
||||
assert_eq!(event.images.as_ref(), Some(&Vec::new()));
|
||||
assert!(event.local_images.is_empty());
|
||||
assert!(event.text_elements.is_empty());
|
||||
}
|
||||
other => panic!("expected replayed user message, got {other:?}"),
|
||||
}
|
||||
match &initial_messages[1] {
|
||||
EventMsg::AgentMessage(event) => {
|
||||
assert_eq!(event.message, "assistant reply");
|
||||
assert_eq!(event.phase, None);
|
||||
}
|
||||
other => panic!("expected replayed agent message, got {other:?}"),
|
||||
}
|
||||
#[tokio::test]
|
||||
async fn session_configured_populates_history_metadata() {
|
||||
let temp_dir = tempfile::tempdir().expect("tempdir");
|
||||
let config = build_config(&temp_dir).await;
|
||||
let thread_id = ThreadId::new();
|
||||
|
||||
message_history::append_entry("older", &thread_id, &config)
|
||||
.await
|
||||
.expect("history append should succeed");
|
||||
message_history::append_entry("newer", &thread_id, &config)
|
||||
.await
|
||||
.expect("history append should succeed");
|
||||
|
||||
let session = thread_session_state_from_thread_response(
|
||||
&thread_id.to_string(),
|
||||
Some("restore".to_string()),
|
||||
None,
|
||||
"gpt-5.4".to_string(),
|
||||
"openai".to_string(),
|
||||
None,
|
||||
AskForApproval::Never,
|
||||
codex_protocol::config_types::ApprovalsReviewer::User,
|
||||
SandboxPolicy::new_read_only_policy(),
|
||||
PathBuf::from("/tmp/project"),
|
||||
None,
|
||||
&config,
|
||||
)
|
||||
.await
|
||||
.expect("session should map");
|
||||
|
||||
assert_ne!(session.history_log_id, 0);
|
||||
assert_eq!(session.history_entry_count, 2);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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()),
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user