agent_ui: Only create ThreadHistory if agent supports it (#51759)

Rather than always having a ThreadHistory with an internal option,
changed it to be an Option<ThreadHistory> so we can better distingiuish
between an empty list and unsupported

Release Notes:

- N/A
This commit is contained in:
Ben Brandt authored and GitHub committed 2026-03-17 13:58:11 +00:00
1 parent 95c96ad9a1
commit ef40f2144d
12 files changed
+197 -244

No files matched your search

@@ -24,7 +24,7 @@ pub enum AgentConnectionEntry {
#[derive(Clone)]
pub struct AgentConnectedState {
pub connection: Rc<dyn AgentConnection>,
pub history: Entity<ThreadHistory>,
pub history: Option<Entity<ThreadHistory>>,
}
impl AgentConnectionEntry {
@@ -38,7 +38,7 @@ impl AgentConnectionEntry {
pub fn history(&self) -> Option<&Entity<ThreadHistory>> {
match self {
AgentConnectionEntry::Connected(state) => Some(&state.history),
AgentConnectionEntry::Connected(state) => state.history.as_ref(),
_ => None,
}
}
@@ -163,7 +163,9 @@ impl AgentConnectionStore {
let connect_task = server.connect(delegate, cx);
let connect_task = cx.spawn(async move |_this, cx| match connect_task.await {
Ok(connection) => cx.update(|cx| {
let history = cx.new(|cx| ThreadHistory::new(connection.session_list(cx), cx));
let history = connection
.session_list(cx)
.map(|session_list| cx.new(|cx| ThreadHistory::new(session_list, cx)));
Ok(AgentConnectedState {
connection,
history,
+6 -13
View File
@@ -1461,13 +1461,9 @@ impl AgentPanel {
.read(cx)
.history()?
.clone();
if history.read(cx).has_session_list() {
Some(History::AgentThreads {
view: self.create_thread_history_view(agent, history, window, cx),
})
} else {
None
}
Some(History::AgentThreads {
view: self.create_thread_history_view(agent, history, window, cx),
})
}
}
}
@@ -4650,16 +4646,13 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) else {
return;
};
let Some(history) = panel
let history = panel
.read(cx)
.connection_store()
.read(cx)
.entry(&crate::Agent::NativeAgent)
.and_then(|s| s.read(cx).history())
else {
log::error!("No connection entry found for native agent");
return;
};
.map(|h| h.downgrade());
let project = workspace.read(cx).project().downgrade();
let panel = panel.read(cx);
let thread_store = panel.thread_store().clone();
@@ -4669,7 +4662,7 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
project,
thread_store,
None,
history.downgrade(),
history,
initial_prompt,
window,
cx,
+4 -4
View File
@@ -223,7 +223,7 @@ pub struct PromptCompletionProvider<T: PromptCompletionProviderDelegate> {
source: Arc<T>,
editor: WeakEntity<Editor>,
mention_set: Entity<MentionSet>,
history: WeakEntity<ThreadHistory>,
history: Option<WeakEntity<ThreadHistory>>,
prompt_store: Option<Entity<PromptStore>>,
workspace: WeakEntity<Workspace>,
}
@@ -233,7 +233,7 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
source: T,
editor: WeakEntity<Editor>,
mention_set: Entity<MentionSet>,
history: WeakEntity<ThreadHistory>,
history: Option<WeakEntity<ThreadHistory>>,
prompt_store: Option<Entity<PromptStore>>,
workspace: WeakEntity<Workspace>,
) -> Self {
@@ -920,7 +920,7 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
}
Some(PromptContextType::Thread) => {
if let Some(history) = self.history.upgrade() {
if let Some(history) = self.history.as_ref().and_then(|h| h.upgrade()) {
let sessions = history
.read(cx)
.sessions()
@@ -1146,7 +1146,7 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
return Task::ready(recent);
}
if let Some(history) = self.history.upgrade() {
if let Some(history) = self.history.as_ref().and_then(|h| h.upgrade()) {
const RECENT_COUNT: usize = 2;
recent.extend(
history
+20 -72
View File
@@ -421,7 +421,7 @@ pub struct ConnectedServerState {
active_id: Option<acp::SessionId>,
threads: HashMap<acp::SessionId, Entity<ThreadView>>,
connection: Rc<dyn AgentConnection>,
history: Entity<ThreadHistory>,
history: Option<Entity<ThreadHistory>>,
conversation: Entity<Conversation>,
_connection_entry_subscription: Subscription,
}
@@ -816,7 +816,7 @@ impl ConversationView {
conversation: Entity<Conversation>,
resumed_without_history: bool,
initial_content: Option<AgentInitialContent>,
history: Entity<ThreadHistory>,
history: Option<Entity<ThreadHistory>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Entity<ThreadView> {
@@ -833,7 +833,7 @@ impl ConversationView {
self.workspace.clone(),
self.project.downgrade(),
self.thread_store.clone(),
history.downgrade(),
history.as_ref().map(|h| h.downgrade()),
self.prompt_store.clone(),
prompt_capabilities.clone(),
available_commands.clone(),
@@ -1082,7 +1082,7 @@ impl ConversationView {
threads: HashMap::default(),
connection,
conversation: cx.new(|_cx| Conversation::default()),
history: cx.new(|cx| ThreadHistory::new(None, cx)),
history: None,
_connection_entry_subscription: Subscription::new(|| {}),
}),
cx,
@@ -2213,7 +2213,7 @@ impl ConversationView {
let Some(connected) = self.as_connected() else {
return;
};
let history = connected.history.downgrade();
let history = connected.history.as_ref().map(|h| h.downgrade());
let Some(thread) = connected.active_view() else {
return;
};
@@ -2601,7 +2601,7 @@ impl ConversationView {
}
pub fn history(&self) -> Option<&Entity<ThreadHistory>> {
self.as_connected().map(|c| &c.history)
self.as_connected().and_then(|c| c.history.as_ref())
}
pub fn delete_history_entry(&mut self, session_id: &acp::SessionId, cx: &mut Context<Self>) {
@@ -2609,9 +2609,10 @@ impl ConversationView {
return;
};
let task = connected
.history
.update(cx, |history, cx| history.delete_session(&session_id, cx));
let Some(history) = &connected.history else {
return;
};
let task = history.update(cx, |history, cx| history.delete_session(&session_id, cx));
task.detach_and_log_err(cx);
if let Some(store) = ThreadMetadataStore::try_global(cx) {
@@ -2902,60 +2903,14 @@ pub(crate) mod tests {
let session_a = AgentSessionInfo::new(SessionId::new("session-a"));
let session_b = AgentSessionInfo::new(SessionId::new("session-b"));
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await;
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let thread_store = cx.update(|_window, cx| cx.new(|cx| ThreadStore::new(cx)));
let connection_store =
cx.update(|_window, cx| cx.new(|cx| AgentConnectionStore::new(project.clone(), cx)));
let conversation_view = cx.update(|window, cx| {
cx.new(|cx| {
ConversationView::new(
Rc::new(StubAgentServer::default_response()),
connection_store,
Agent::Custom { id: "Test".into() },
None,
None,
None,
None,
workspace.downgrade(),
project,
Some(thread_store),
None,
window,
cx,
)
})
});
// Wait for connection to establish
cx.run_until_parked();
let history = cx.update(|_window, cx| {
conversation_view
.read(cx)
.history()
.expect("Missing history")
.clone()
});
// Initially empty because StubAgentConnection.session_list() returns None
active_thread(&conversation_view, cx).read_with(cx, |view, _cx| {
assert_eq!(view.recent_history_entries.len(), 0);
});
// Now set the session list - this simulates external agents providing their history
let list_a: Rc<dyn AgentSessionList> =
Rc::new(StubSessionList::new(vec![session_a.clone()]));
history.update(cx, |history, cx| {
history.set_session_list(Some(list_a), cx);
});
cx.run_until_parked();
// Use a connection that provides a session list so ThreadHistory is created
let (conversation_view, history, cx) = setup_thread_view_with_history(
StubAgentServer::new(SessionHistoryConnection::new(vec![session_a.clone()])),
cx,
)
.await;
// Initially has session_a from the connection's session list
active_thread(&conversation_view, cx).read_with(cx, |view, _cx| {
assert_eq!(view.recent_history_entries.len(), 1);
assert_eq!(
@@ -2964,11 +2919,11 @@ pub(crate) mod tests {
);
});
// Update to a different session list
// Swap to a different session list
let list_b: Rc<dyn AgentSessionList> =
Rc::new(StubSessionList::new(vec![session_b.clone()]));
history.update(cx, |history, cx| {
history.set_session_list(Some(list_b), cx);
history.set_session_list(list_b, cx);
});
cx.run_until_parked();
@@ -2986,19 +2941,12 @@ pub(crate) mod tests {
init_test(cx);
let session = AgentSessionInfo::new(SessionId::new("history-session"));
let (conversation_view, history, cx) = setup_thread_view_with_history(
let (conversation_view, _history, cx) = setup_thread_view_with_history(
StubAgentServer::new(SessionHistoryConnection::new(vec![session.clone()])),
cx,
)
.await;
history.read_with(cx, |history, _cx| {
assert!(
history.has_session_list(),
"session list should be attached after thread creation"
);
});
active_thread(&conversation_view, cx).read_with(cx, |view, _cx| {
assert_eq!(view.recent_history_entries.len(), 1);
assert_eq!(
@@ -228,8 +228,8 @@ pub struct ThreadView {
pub hovered_recent_history_item: Option<usize>,
pub show_external_source_prompt_warning: bool,
pub show_codex_windows_warning: bool,
pub history: Entity<ThreadHistory>,
pub _history_subscription: Subscription,
pub history: Option<Entity<ThreadHistory>>,
pub _history_subscription: Option<Subscription>,
}
impl Focusable for ThreadView {
fn focus_handle(&self, cx: &App) -> FocusHandle {
@@ -273,7 +273,7 @@ impl ThreadView {
resumed_without_history: bool,
project: WeakEntity<Project>,
thread_store: Option<Entity<ThreadStore>>,
history: Entity<ThreadHistory>,
history: Option<Entity<ThreadHistory>>,
prompt_store: Option<Entity<PromptStore>>,
initial_content: Option<AgentInitialContent>,
mut subscriptions: Vec<Subscription>,
@@ -284,8 +284,10 @@ impl ThreadView {
let placeholder = placeholder_text(agent_display_name.as_ref(), false);
let history_subscription = cx.observe(&history, |this, history, cx| {
this.update_recent_history_from_cache(&history, cx);
let history_subscription = history.as_ref().map(|h| {
cx.observe(h, |this, history, cx| {
this.update_recent_history_from_cache(&history, cx);
})
});
let mut should_auto_submit = false;
@@ -296,7 +298,7 @@ impl ThreadView {
workspace.clone(),
project.clone(),
thread_store,
history.downgrade(),
history.as_ref().map(|h| h.downgrade()),
prompt_store,
prompt_capabilities.clone(),
available_commands.clone(),
@@ -392,7 +394,10 @@ impl ThreadView {
}));
}));
let recent_history_entries = history.read(cx).get_recent_sessions(3);
let recent_history_entries = history
.as_ref()
.map(|h| h.read(cx).get_recent_sessions(3))
.unwrap_or_default();
let mut this = Self {
id,
@@ -7502,7 +7507,10 @@ impl ThreadView {
),
)
.child(v_flex().p_1().pr_1p5().gap_1().children({
let supports_delete = self.history.read(cx).supports_delete();
let supports_delete = self
.history
.as_ref()
.map_or(false, |h| h.read(cx).supports_delete());
recent_history
.into_iter()
.enumerate()
+4 -4
View File
@@ -26,7 +26,7 @@ pub struct EntryViewState {
workspace: WeakEntity<Workspace>,
project: WeakEntity<Project>,
thread_store: Option<Entity<ThreadStore>>,
history: WeakEntity<ThreadHistory>,
history: Option<WeakEntity<ThreadHistory>>,
prompt_store: Option<Entity<PromptStore>>,
entries: Vec<Entry>,
prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
@@ -39,7 +39,7 @@ impl EntryViewState {
workspace: WeakEntity<Workspace>,
project: WeakEntity<Project>,
thread_store: Option<Entity<ThreadStore>>,
history: WeakEntity<ThreadHistory>,
history: Option<WeakEntity<ThreadHistory>>,
prompt_store: Option<Entity<PromptStore>>,
prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
@@ -510,14 +510,14 @@ mod tests {
});
let thread_store = None;
let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
let history: Option<gpui::WeakEntity<crate::ThreadHistory>> = None;
let view_state = cx.new(|_cx| {
EntryViewState::new(
workspace.downgrade(),
project.downgrade(),
thread_store,
history.downgrade(),
history,
None,
Default::default(),
Default::default(),
+11 -17
View File
@@ -278,15 +278,11 @@ impl InlineAssistant {
let prompt_store = agent_panel.prompt_store().as_ref().cloned();
let thread_store = agent_panel.thread_store().clone();
let Some(history) = agent_panel
let history = agent_panel
.connection_store()
.read(cx)
.entry(&crate::Agent::NativeAgent)
.and_then(|s| s.read(cx).history().cloned())
else {
log::error!("No connection entry found for native agent");
return;
};
.and_then(|s| s.read(cx).history().cloned());
let handle_assist =
|window: &mut Window, cx: &mut Context<Workspace>| match inline_assist_target {
@@ -298,7 +294,7 @@ impl InlineAssistant {
workspace.project().downgrade(),
thread_store,
prompt_store,
history.downgrade(),
history.as_ref().map(|h| h.downgrade()),
action.prompt.clone(),
window,
cx,
@@ -313,7 +309,7 @@ impl InlineAssistant {
workspace.project().downgrade(),
thread_store,
prompt_store,
history.downgrade(),
history.as_ref().map(|h| h.downgrade()),
action.prompt.clone(),
window,
cx,
@@ -495,7 +491,7 @@ impl InlineAssistant {
project: WeakEntity<Project>,
thread_store: Entity<ThreadStore>,
prompt_store: Option<Entity<PromptStore>>,
history: WeakEntity<ThreadHistory>,
history: Option<WeakEntity<ThreadHistory>>,
initial_prompt: Option<String>,
window: &mut Window,
codegen_ranges: &[Range<Anchor>],
@@ -634,7 +630,7 @@ impl InlineAssistant {
project: WeakEntity<Project>,
thread_store: Entity<ThreadStore>,
prompt_store: Option<Entity<PromptStore>>,
history: WeakEntity<ThreadHistory>,
history: Option<WeakEntity<ThreadHistory>>,
initial_prompt: Option<String>,
window: &mut Window,
cx: &mut App,
@@ -679,7 +675,7 @@ impl InlineAssistant {
workspace: Entity<Workspace>,
thread_store: Entity<ThreadStore>,
prompt_store: Option<Entity<PromptStore>>,
history: WeakEntity<ThreadHistory>,
history: Option<WeakEntity<ThreadHistory>>,
window: &mut Window,
cx: &mut App,
) -> InlineAssistId {
@@ -1983,8 +1979,7 @@ impl CodeActionProvider for AssistantCodeActionProvider {
.read(cx)
.entry(&crate::Agent::NativeAgent)
.and_then(|e| e.read(cx).history())
.context("no history found for native agent")?
.downgrade();
.map(|h| h.downgrade());
anyhow::Ok((panel.thread_store().clone(), history))
})??;
@@ -2155,7 +2150,7 @@ pub mod test {
setup(cx);
let (_editor, buffer, _history) = cx.update(|window, cx| {
let (_editor, buffer) = cx.update(|window, cx| {
let buffer = cx.new(|cx| Buffer::local("", cx));
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
let editor = cx.new(|cx| Editor::for_multibuffer(multibuffer, None, window, cx));
@@ -2172,7 +2167,6 @@ pub mod test {
});
let thread_store = cx.new(|cx| ThreadStore::new(cx));
let history = cx.new(|cx| crate::ThreadHistory::new(None, cx));
// Add editor to workspace
workspace.update(cx, |workspace, cx| {
@@ -2188,7 +2182,7 @@ pub mod test {
project.downgrade(),
thread_store,
None,
history.downgrade(),
None,
Some(prompt),
window,
cx,
@@ -2198,7 +2192,7 @@ pub mod test {
inline_assistant.start_assist(assist_id, window, cx);
});
(editor, buffer, history)
(editor, buffer)
});
cx.run_until_parked();
+3 -3
View File
@@ -64,7 +64,7 @@ pub struct PromptEditor<T> {
pub editor: Entity<Editor>,
mode: PromptEditorMode,
mention_set: Entity<MentionSet>,
history: WeakEntity<ThreadHistory>,
history: Option<WeakEntity<ThreadHistory>>,
prompt_store: Option<Entity<PromptStore>>,
workspace: WeakEntity<Workspace>,
model_selector: Entity<AgentModelSelector>,
@@ -1227,7 +1227,7 @@ impl PromptEditor<BufferCodegen> {
fs: Arc<dyn Fs>,
thread_store: Entity<ThreadStore>,
prompt_store: Option<Entity<PromptStore>>,
history: WeakEntity<ThreadHistory>,
history: Option<WeakEntity<ThreadHistory>>,
project: WeakEntity<Project>,
workspace: WeakEntity<Workspace>,
window: &mut Window,
@@ -1386,7 +1386,7 @@ impl PromptEditor<TerminalCodegen> {
fs: Arc<dyn Fs>,
thread_store: Entity<ThreadStore>,
prompt_store: Option<Entity<PromptStore>>,
history: WeakEntity<ThreadHistory>,
history: Option<WeakEntity<ThreadHistory>>,
project: WeakEntity<Project>,
workspace: WeakEntity<Workspace>,
window: &mut Window,
+17 -33
View File
@@ -110,7 +110,7 @@ impl MessageEditor {
workspace: WeakEntity<Workspace>,
project: WeakEntity<Project>,
thread_store: Option<Entity<ThreadStore>>,
history: WeakEntity<ThreadHistory>,
history: Option<WeakEntity<ThreadHistory>>,
prompt_store: Option<Entity<PromptStore>>,
prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
@@ -1789,7 +1789,6 @@ mod tests {
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let thread_store = None;
let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
let message_editor = cx.update(|window, cx| {
cx.new(|cx| {
@@ -1797,7 +1796,7 @@ mod tests {
workspace.downgrade(),
project.downgrade(),
thread_store.clone(),
history.downgrade(),
None,
None,
Default::default(),
Default::default(),
@@ -1902,7 +1901,6 @@ mod tests {
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
let workspace_handle = workspace.downgrade();
let message_editor = workspace.update_in(cx, |_, window, cx| {
cx.new(|cx| {
@@ -1910,7 +1908,7 @@ mod tests {
workspace_handle.clone(),
project.downgrade(),
thread_store.clone(),
history.downgrade(),
None,
None,
prompt_capabilities.clone(),
available_commands.clone(),
@@ -2057,7 +2055,6 @@ mod tests {
let mut cx = VisualTestContext::from_window(window.into(), cx);
let thread_store = None;
let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
let available_commands = Rc::new(RefCell::new(vec![
acp::AvailableCommand::new("quick-math", "2 + 2 = 4 - 1 = 3"),
@@ -2075,7 +2072,7 @@ mod tests {
workspace_handle,
project.downgrade(),
thread_store.clone(),
history.downgrade(),
None,
None,
prompt_capabilities.clone(),
available_commands.clone(),
@@ -2291,7 +2288,6 @@ mod tests {
}
let thread_store = cx.new(|cx| ThreadStore::new(cx));
let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
@@ -2301,7 +2297,7 @@ mod tests {
workspace_handle,
project.downgrade(),
Some(thread_store),
history.downgrade(),
None,
None,
prompt_capabilities.clone(),
Default::default(),
@@ -2786,7 +2782,6 @@ mod tests {
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
let message_editor = cx.update(|window, cx| {
cx.new(|cx| {
@@ -2794,7 +2789,7 @@ mod tests {
workspace.downgrade(),
project.downgrade(),
thread_store.clone(),
history.downgrade(),
None,
None,
Default::default(),
Default::default(),
@@ -2886,7 +2881,6 @@ mod tests {
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
let session_id = acp::SessionId::new("thread-123");
let title = Some("Previous Conversation".into());
@@ -2897,7 +2891,7 @@ mod tests {
workspace.downgrade(),
project.downgrade(),
thread_store.clone(),
history.downgrade(),
None,
None,
Default::default(),
Default::default(),
@@ -2961,7 +2955,6 @@ mod tests {
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let thread_store = None;
let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
let message_editor = cx.update(|window, cx| {
cx.new(|cx| {
@@ -2969,7 +2962,7 @@ mod tests {
workspace.downgrade(),
project.downgrade(),
thread_store.clone(),
history.downgrade(),
None,
None,
Default::default(),
Default::default(),
@@ -3017,7 +3010,6 @@ mod tests {
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let thread_store = None;
let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
let message_editor = cx.update(|window, cx| {
cx.new(|cx| {
@@ -3025,7 +3017,7 @@ mod tests {
workspace.downgrade(),
project.downgrade(),
thread_store.clone(),
history.downgrade(),
None,
None,
Default::default(),
Default::default(),
@@ -3071,7 +3063,6 @@ mod tests {
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
let message_editor = cx.update(|window, cx| {
cx.new(|cx| {
@@ -3079,7 +3070,7 @@ mod tests {
workspace.downgrade(),
project.downgrade(),
thread_store.clone(),
history.downgrade(),
None,
None,
Default::default(),
Default::default(),
@@ -3126,7 +3117,6 @@ mod tests {
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
let message_editor = cx.update(|window, cx| {
cx.new(|cx| {
@@ -3134,7 +3124,7 @@ mod tests {
workspace.downgrade(),
project.downgrade(),
thread_store.clone(),
history.downgrade(),
None,
None,
Default::default(),
Default::default(),
@@ -3190,7 +3180,6 @@ mod tests {
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
let (message_editor, editor) = workspace.update_in(cx, |workspace, window, cx| {
let workspace_handle = cx.weak_entity();
@@ -3199,7 +3188,7 @@ mod tests {
workspace_handle,
project.downgrade(),
thread_store.clone(),
history.downgrade(),
None,
None,
Default::default(),
Default::default(),
@@ -3349,7 +3338,6 @@ mod tests {
});
let thread_store = Some(cx.new(|cx| ThreadStore::new(cx)));
let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
// Create a new `MessageEditor`. The `EditorMode::full()` has to be used
// to ensure we have a fixed viewport, so we can eventually actually
@@ -3361,7 +3349,7 @@ mod tests {
workspace_handle,
project.downgrade(),
thread_store.clone(),
history.downgrade(),
None,
None,
Default::default(),
Default::default(),
@@ -3469,7 +3457,6 @@ mod tests {
let mut cx = VisualTestContext::from_window(window.into(), cx);
let thread_store = cx.new(|cx| ThreadStore::new(cx));
let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
let workspace_handle = cx.weak_entity();
@@ -3478,7 +3465,7 @@ mod tests {
workspace_handle,
project.downgrade(),
Some(thread_store),
history.downgrade(),
None,
None,
Default::default(),
Default::default(),
@@ -3551,7 +3538,6 @@ mod tests {
let mut cx = VisualTestContext::from_window(window.into(), cx);
let thread_store = cx.new(|cx| ThreadStore::new(cx));
let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
let workspace_handle = cx.weak_entity();
@@ -3560,7 +3546,7 @@ mod tests {
workspace_handle,
project.downgrade(),
Some(thread_store),
history.downgrade(),
None,
None,
Default::default(),
Default::default(),
@@ -3635,7 +3621,6 @@ mod tests {
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
let message_editor = cx.update(|window, cx| {
cx.new(|cx| {
@@ -3643,7 +3628,7 @@ mod tests {
workspace.downgrade(),
project.downgrade(),
None,
history.downgrade(),
None,
None,
Default::default(),
Default::default(),
@@ -3787,7 +3772,6 @@ mod tests {
let (multi_workspace, cx) =
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
let history = cx.update(|_window, cx| cx.new(|cx| crate::ThreadHistory::new(None, cx)));
let message_editor = cx.update(|window, cx| {
cx.new(|cx| {
@@ -3795,7 +3779,7 @@ mod tests {
workspace.downgrade(),
project.downgrade(),
None,
history.downgrade(),
None,
None,
Default::default(),
Default::default(),
@@ -64,7 +64,7 @@ impl TerminalInlineAssistant {
project: WeakEntity<Project>,
thread_store: Entity<ThreadStore>,
prompt_store: Option<Entity<PromptStore>>,
history: WeakEntity<ThreadHistory>,
history: Option<WeakEntity<ThreadHistory>>,
initial_prompt: Option<String>,
window: &mut Window,
cx: &mut App,
+29 -60
View File
@@ -5,59 +5,48 @@ use std::rc::Rc;
use ui::prelude::*;
pub struct ThreadHistory {
session_list: Option<Rc<dyn AgentSessionList>>,
session_list: Rc<dyn AgentSessionList>,
sessions: Vec<AgentSessionInfo>,
_refresh_task: Task<()>,
_watch_task: Option<Task<()>>,
}
impl ThreadHistory {
pub fn new(session_list: Option<Rc<dyn AgentSessionList>>, cx: &mut Context<Self>) -> Self {
pub fn new(session_list: Rc<dyn AgentSessionList>, cx: &mut Context<Self>) -> Self {
let mut this = Self {
session_list: None,
session_list,
sessions: Vec::new(),
_refresh_task: Task::ready(()),
_watch_task: None,
};
this.set_session_list_impl(session_list, cx);
this.start_watching(cx);
this
}
#[cfg(any(test, feature = "test-support"))]
pub fn set_session_list(
&mut self,
session_list: Option<Rc<dyn AgentSessionList>>,
session_list: Rc<dyn AgentSessionList>,
cx: &mut Context<Self>,
) {
self.set_session_list_impl(session_list, cx);
}
fn set_session_list_impl(
&mut self,
session_list: Option<Rc<dyn AgentSessionList>>,
cx: &mut Context<Self>,
) {
if let (Some(current), Some(next)) = (&self.session_list, &session_list)
&& Rc::ptr_eq(current, next)
{
if Rc::ptr_eq(&self.session_list, &session_list) {
return;
}
self.session_list = session_list;
self.sessions.clear();
self._refresh_task = Task::ready(());
self.start_watching(cx);
}
let Some(session_list) = self.session_list.as_ref() else {
self._watch_task = None;
cx.notify();
return;
};
let Some(rx) = session_list.watch(cx) else {
fn start_watching(&mut self, cx: &mut Context<Self>) {
let Some(rx) = self.session_list.watch(cx) else {
self._watch_task = None;
self.refresh_sessions(false, cx);
return;
};
session_list.notify_refresh();
self.session_list.notify_refresh();
self._watch_task = Some(cx.spawn(async move |this, cx| {
while let Ok(first_update) = rx.recv().await {
@@ -132,10 +121,7 @@ impl ThreadHistory {
}
fn refresh_sessions(&mut self, load_all_pages: bool, cx: &mut Context<Self>) {
let Some(session_list) = self.session_list.clone() else {
cx.notify();
return;
};
let session_list = self.session_list.clone();
self._refresh_task = cx.spawn(async move |this, cx| {
let mut cursor: Option<String> = None;
@@ -196,14 +182,8 @@ impl ThreadHistory {
self.sessions.is_empty()
}
pub fn has_session_list(&self) -> bool {
self.session_list.is_some()
}
pub fn refresh(&mut self, _cx: &mut Context<Self>) {
if let Some(session_list) = &self.session_list {
session_list.notify_refresh();
}
self.session_list.notify_refresh();
}
pub fn session_for_id(&self, session_id: &acp::SessionId) -> Option<AgentSessionInfo> {
@@ -222,10 +202,7 @@ impl ThreadHistory {
}
pub fn supports_delete(&self) -> bool {
self.session_list
.as_ref()
.map(|sl| sl.supports_delete())
.unwrap_or(false)
self.session_list.supports_delete()
}
pub(crate) fn delete_session(
@@ -233,19 +210,11 @@ impl ThreadHistory {
session_id: &acp::SessionId,
cx: &mut App,
) -> Task<anyhow::Result<()>> {
if let Some(session_list) = self.session_list.as_ref() {
session_list.delete_session(session_id, cx)
} else {
Task::ready(Ok(()))
}
self.session_list.delete_session(session_id, cx)
}
pub(crate) fn delete_sessions(&self, cx: &mut App) -> Task<anyhow::Result<()>> {
if let Some(session_list) = self.session_list.as_ref() {
session_list.delete_sessions(cx)
} else {
Task::ready(Ok(()))
}
self.session_list.delete_sessions(cx)
}
}
@@ -425,7 +394,7 @@ mod tests {
vec![test_session("session-2", "Second")],
));
let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx));
let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx));
cx.run_until_parked();
history.update(cx, |history, _cx| {
@@ -447,7 +416,7 @@ mod tests {
vec![test_session("session-2", "Second")],
));
let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx));
let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx));
cx.run_until_parked();
session_list.clear_requested_cursors();
@@ -482,7 +451,7 @@ mod tests {
vec![test_session("session-2", "Second")],
));
let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx));
let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx));
cx.run_until_parked();
history.update(cx, |history, cx| history.refresh_full_history(cx));
@@ -513,7 +482,7 @@ mod tests {
vec![test_session("session-2", "Second")],
));
let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx));
let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx));
cx.run_until_parked();
history.update(cx, |history, cx| history.refresh_full_history(cx));
@@ -542,7 +511,7 @@ mod tests {
vec![test_session("session-2", "Second")],
));
let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx));
let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx));
cx.run_until_parked();
history.update(cx, |history, cx| history.refresh_full_history(cx));
@@ -585,7 +554,7 @@ mod tests {
.with_async_responses(),
);
let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx));
let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx));
cx.run_until_parked();
session_list.clear_requested_cursors();
@@ -616,7 +585,7 @@ mod tests {
}];
let session_list = Rc::new(TestSessionList::new(sessions));
let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx));
let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx));
cx.run_until_parked();
session_list.send_update(SessionListUpdate::SessionInfo {
@@ -649,7 +618,7 @@ mod tests {
}];
let session_list = Rc::new(TestSessionList::new(sessions));
let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx));
let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx));
cx.run_until_parked();
session_list.send_update(SessionListUpdate::SessionInfo {
@@ -679,7 +648,7 @@ mod tests {
}];
let session_list = Rc::new(TestSessionList::new(sessions));
let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx));
let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx));
cx.run_until_parked();
session_list.send_update(SessionListUpdate::SessionInfo {
@@ -712,7 +681,7 @@ mod tests {
}];
let session_list = Rc::new(TestSessionList::new(sessions));
let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx));
let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx));
cx.run_until_parked();
session_list.send_update(SessionListUpdate::SessionInfo {
@@ -749,7 +718,7 @@ mod tests {
}];
let session_list = Rc::new(TestSessionList::new(sessions));
let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx));
let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx));
cx.run_until_parked();
session_list.send_update(SessionListUpdate::SessionInfo {
@@ -783,7 +752,7 @@ mod tests {
}];
let session_list = Rc::new(TestSessionList::new(sessions));
let history = cx.new(|cx| ThreadHistory::new(Some(session_list.clone()), cx));
let history = cx.new(|cx| ThreadHistory::new(session_list.clone(), cx));
cx.run_until_parked();
session_list.send_update(SessionListUpdate::SessionInfo {
+81 -26
View File
@@ -91,6 +91,22 @@ fn fuzzy_match_positions(query: &str, text: &str) -> Option<Vec<usize>> {
}
}
fn archive_empty_state_message(
has_history: bool,
is_empty: bool,
has_query: bool,
) -> Option<&'static str> {
if !is_empty {
None
} else if !has_history {
Some("This agent does not support viewing archived threads.")
} else if has_query {
Some("No threads match your search.")
} else {
Some("No archived threads yet.")
}
}
pub enum ThreadsArchiveViewEvent {
Close,
OpenThread {
@@ -171,6 +187,7 @@ impl ThreadsArchiveView {
fn set_selected_agent(&mut self, agent: Agent, window: &mut Window, cx: &mut Context<Self>) {
self.selected_agent = agent.clone();
self.is_loading = true;
self.reset_history_subscription();
self.history = None;
self.items.clear();
self.selection = None;
@@ -193,25 +210,33 @@ impl ThreadsArchiveView {
cx.notify();
}
fn set_history(&mut self, history: Entity<ThreadHistory>, cx: &mut Context<Self>) {
self._history_subscription = cx.observe(&history, |this, _, cx| {
this.update_items(cx);
});
history.update(cx, |history, cx| {
history.refresh_full_history(cx);
});
self.history = Some(history);
fn reset_history_subscription(&mut self) {
self._history_subscription = Subscription::new(|| {});
}
fn set_history(&mut self, history: Option<Entity<ThreadHistory>>, cx: &mut Context<Self>) {
self.reset_history_subscription();
if let Some(history) = &history {
self._history_subscription = cx.observe(history, |this, _, cx| {
this.update_items(cx);
});
history.update(cx, |history, cx| {
history.refresh_full_history(cx);
});
}
self.history = history;
self.is_loading = false;
self.update_items(cx);
cx.notify();
}
fn update_items(&mut self, cx: &mut Context<Self>) {
let Some(history) = self.history.as_ref() else {
return;
};
let sessions = history.read(cx).sessions().to_vec();
let sessions = self
.history
.as_ref()
.map(|h| h.read(cx).sessions().to_vec())
.unwrap_or_default();
let query = self.filter_editor.read(cx).text(cx).to_lowercase();
let today = Local::now().naive_local().date();
@@ -696,6 +721,12 @@ impl Focusable for ThreadsArchiveView {
}
}
impl ThreadsArchiveView {
fn empty_state_message(&self, is_empty: bool, has_query: bool) -> Option<&'static str> {
archive_empty_state_message(self.history.is_some(), is_empty, has_query)
}
}
impl Render for ThreadsArchiveView {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let is_empty = self.items.is_empty();
@@ -713,24 +744,13 @@ impl Render for ThreadsArchiveView {
.with_rotate_animation(2),
)
.into_any_element()
} else if is_empty && has_query {
} else if let Some(message) = self.empty_state_message(is_empty, has_query) {
v_flex()
.flex_1()
.justify_center()
.items_center()
.child(
Label::new("No threads match your search.")
.size(LabelSize::Small)
.color(Color::Muted),
)
.into_any_element()
} else if is_empty {
v_flex()
.flex_1()
.justify_center()
.items_center()
.child(
Label::new("No archived threads yet.")
Label::new(message)
.size(LabelSize::Small)
.color(Color::Muted),
)
@@ -768,3 +788,38 @@ impl Render for ThreadsArchiveView {
.child(content)
}
}
#[cfg(test)]
mod tests {
use super::archive_empty_state_message;
#[test]
fn empty_state_message_returns_none_when_archive_has_items() {
assert_eq!(archive_empty_state_message(false, false, false), None);
assert_eq!(archive_empty_state_message(true, false, true), None);
}
#[test]
fn empty_state_message_distinguishes_unsupported_history() {
assert_eq!(
archive_empty_state_message(false, true, false),
Some("This agent does not support viewing archived threads.")
);
assert_eq!(
archive_empty_state_message(false, true, true),
Some("This agent does not support viewing archived threads.")
);
}
#[test]
fn empty_state_message_distinguishes_empty_history_and_search_results() {
assert_eq!(
archive_empty_state_message(true, true, false),
Some("No archived threads yet.")
);
assert_eq!(
archive_empty_state_message(true, true, true),
Some("No threads match your search.")
);
}
}