mirror of
https://github.com/zed-industries/zed.git
synced 2026-07-10 00:13:29 +00:00
sidebar: Add terminals to thread switcher (#56388)
Some checks are pending
Congratsbot / congrats (push) Blocked by required conditions
Congratsbot / check-author (push) Waiting to run
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / orchestrate (push) Waiting to run
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
Some checks are pending
Congratsbot / congrats (push) Blocked by required conditions
Congratsbot / check-author (push) Waiting to run
deploy_nightly_docs / deploy_docs (push) Waiting to run
run_tests / extension_tests (push) Blocked by required conditions
run_tests / tests_pass (push) Blocked by required conditions
run_tests / check_scripts (push) Blocked by required conditions
run_tests / orchestrate (push) Waiting to run
run_tests / check_postgres_and_protobuf_migrations (push) Blocked by required conditions
run_tests / check_style (push) Waiting to run
run_tests / clippy_windows (push) Blocked by required conditions
run_tests / clippy_linux (push) Blocked by required conditions
run_tests / clippy_mac (push) Blocked by required conditions
run_tests / clippy_mac_x86_64 (push) Blocked by required conditions
run_tests / run_tests_windows (push) Blocked by required conditions
run_tests / run_tests_linux (push) Blocked by required conditions
run_tests / run_tests_mac (push) Blocked by required conditions
run_tests / doctests (push) Blocked by required conditions
run_tests / check_workspace_binaries (push) Blocked by required conditions
run_tests / build_visual_tests_binary (push) Blocked by required conditions
run_tests / check_wasm (push) Blocked by required conditions
run_tests / check_dependencies (push) Blocked by required conditions
run_tests / check_docs (push) Blocked by required conditions
run_tests / check_licenses (push) Blocked by required conditions
Allow confirming a terminal entry to activate it, track terminal access for ordering, and close selected terminals via the archive action. Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - N/A
This commit is contained in:
parent
27fe33cead
commit
7d3989602d
10 changed files with 595 additions and 238 deletions
|
|
@ -225,24 +225,9 @@
|
|||
"bindings": {
|
||||
"ctrl-n": "agent::NewThread",
|
||||
"ctrl-alt-c": "agent::OpenSettings",
|
||||
"ctrl-alt-p": "agent::ManageProfiles",
|
||||
"ctrl-alt-l": "agent::OpenRulesLibrary",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
"ctrl-alt-/": "agent::ToggleModelSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
// `alt-l` is provided as an alternative to `alt-tab` as the latter breaks on Linux under the `AgentPanel` context
|
||||
"alt-l": "agent::CycleFavoriteModels",
|
||||
"shift-alt-i": "agent::ToggleOptionsMenu",
|
||||
"ctrl-alt-shift-n": "agent::ToggleNewThreadMenu",
|
||||
"shift-alt-escape": "agent::ExpandMessageEditor",
|
||||
"ctrl->": "agent::AddSelectionToThread",
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||
"ctrl-shift-enter": "agent::ContinueThread",
|
||||
"shift-alt-q": "agent::AllowAlways",
|
||||
"shift-alt-a": "agent::AllowOnce",
|
||||
"ctrl-alt-a": "agent::OpenPermissionDropdown",
|
||||
"shift-alt-x": "agent::RejectOnce",
|
||||
"ctrl-tab": "agents_sidebar::ToggleThreadSwitcher",
|
||||
"ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }],
|
||||
},
|
||||
|
|
@ -255,14 +240,6 @@
|
|||
"ctrl-c": "markdown::CopyAsMarkdown",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel && acp_thread",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-n": "agent::NewExternalAgentThread",
|
||||
"ctrl-alt-t": "agent::NewThread",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AgentFeedbackMessageEditor > Editor",
|
||||
"bindings": {
|
||||
|
|
@ -279,8 +256,24 @@
|
|||
},
|
||||
{
|
||||
"context": "AcpThread",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-n": "agent::NewThread",
|
||||
"ctrl--": "pane::GoBack",
|
||||
"ctrl-alt-p": "agent::ManageProfiles",
|
||||
"ctrl-alt-l": "agent::OpenRulesLibrary",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
"ctrl-alt-/": "agent::ToggleModelSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
// `alt-l` is provided as an alternative to `alt-tab` as the latter breaks on Linux under the `AcpThread` context
|
||||
"alt-l": "agent::CycleFavoriteModels",
|
||||
"shift-alt-escape": "agent::ExpandMessageEditor",
|
||||
"ctrl->": "agent::AddSelectionToThread",
|
||||
"shift-alt-q": "agent::AllowAlways",
|
||||
"shift-alt-a": "agent::AllowOnce",
|
||||
"ctrl-alt-a": "agent::OpenPermissionDropdown",
|
||||
"shift-alt-x": "agent::RejectOnce",
|
||||
"pageup": "agent::ScrollOutputPageUp",
|
||||
"pagedown": "agent::ScrollOutputPageDown",
|
||||
"home": "agent::ScrollOutputToTop",
|
||||
|
|
@ -1248,7 +1241,7 @@
|
|||
},
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel && Terminal",
|
||||
"context": "AgentPanel > Terminal",
|
||||
"bindings": {
|
||||
"ctrl-n": "agent::NewThread",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -265,21 +265,9 @@
|
|||
"bindings": {
|
||||
"cmd-n": "agent::NewThread",
|
||||
"cmd-alt-c": "agent::OpenSettings",
|
||||
"cmd-alt-l": "agent::OpenRulesLibrary",
|
||||
"cmd-alt-p": "agent::ManageProfiles",
|
||||
"cmd-i": "agent::ToggleProfileSelector",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
"cmd-alt-/": "agent::ToggleModelSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
"cmd-alt-m": "agent::ToggleOptionsMenu",
|
||||
"cmd-alt-shift-n": "agent::ToggleNewThreadMenu",
|
||||
"shift-alt-escape": "agent::ExpandMessageEditor",
|
||||
"cmd->": "agent::AddSelectionToThread",
|
||||
"cmd-shift-e": "project_panel::ToggleFocus",
|
||||
"cmd-shift-enter": "agent::ContinueThread",
|
||||
"cmd-y": "agent::AllowOnce",
|
||||
"cmd-alt-a": "agent::OpenPermissionDropdown",
|
||||
"cmd-alt-z": "agent::RejectOnce",
|
||||
"ctrl-tab": "agents_sidebar::ToggleThreadSwitcher",
|
||||
"ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }],
|
||||
},
|
||||
|
|
@ -291,14 +279,6 @@
|
|||
"cmd-c": "markdown::CopyAsMarkdown",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel && acp_thread",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "agent::NewExternalAgentThread",
|
||||
"cmd-alt-t": "agent::NewThread",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AgentFeedbackMessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
|
|
@ -322,8 +302,21 @@
|
|||
},
|
||||
{
|
||||
"context": "AcpThread",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-n": "agent::NewThread",
|
||||
"ctrl--": "pane::GoBack",
|
||||
"cmd-alt-l": "agent::OpenRulesLibrary",
|
||||
"cmd-alt-p": "agent::ManageProfiles",
|
||||
"cmd-i": "agent::ToggleProfileSelector",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
"cmd-alt-/": "agent::ToggleModelSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
"shift-alt-escape": "agent::ExpandMessageEditor",
|
||||
"cmd->": "agent::AddSelectionToThread",
|
||||
"cmd-y": "agent::AllowOnce",
|
||||
"cmd-alt-a": "agent::OpenPermissionDropdown",
|
||||
"cmd-alt-z": "agent::RejectOnce",
|
||||
"pageup": "agent::ScrollOutputPageUp",
|
||||
"pagedown": "agent::ScrollOutputPageDown",
|
||||
"home": "agent::ScrollOutputToTop",
|
||||
|
|
|
|||
|
|
@ -226,24 +226,9 @@
|
|||
"bindings": {
|
||||
"ctrl-n": "agent::NewThread",
|
||||
"shift-alt-c": "agent::OpenSettings",
|
||||
"shift-alt-l": "agent::OpenRulesLibrary",
|
||||
"shift-alt-p": "agent::ManageProfiles",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
// `alt-l` is provided as an alternative to `alt-tab` as the latter breaks on Windows under the `AgentPanel` context
|
||||
"alt-l": "agent::CycleFavoriteModels",
|
||||
"shift-alt-/": "agent::ToggleModelSelector",
|
||||
"shift-alt-i": "agent::ToggleOptionsMenu",
|
||||
"ctrl-shift-alt-n": "agent::ToggleNewThreadMenu",
|
||||
"shift-alt-escape": "agent::ExpandMessageEditor",
|
||||
"ctrl-shift-.": "agent::AddSelectionToThread",
|
||||
"ctrl-shift-e": "project_panel::ToggleFocus",
|
||||
"ctrl-shift-enter": "agent::ContinueThread",
|
||||
"shift-alt-q": "agent::AllowAlways",
|
||||
"shift-alt-a": "agent::AllowOnce",
|
||||
"ctrl-alt-a": "agent::OpenPermissionDropdown",
|
||||
"shift-alt-x": "agent::RejectOnce",
|
||||
"ctrl-tab": "agents_sidebar::ToggleThreadSwitcher",
|
||||
"ctrl-shift-tab": ["agents_sidebar::ToggleThreadSwitcher", { "select_last": true }],
|
||||
},
|
||||
|
|
@ -255,14 +240,6 @@
|
|||
"ctrl-c": "markdown::CopyAsMarkdown",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AgentPanel && acp_thread",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-n": "agent::NewExternalAgentThread",
|
||||
"ctrl-alt-t": "agent::NewThread",
|
||||
},
|
||||
},
|
||||
{
|
||||
"context": "AgentFeedbackMessageEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
|
|
@ -280,8 +257,24 @@
|
|||
},
|
||||
{
|
||||
"context": "AcpThread",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-n": "agent::NewThread",
|
||||
"ctrl--": "pane::GoBack",
|
||||
"shift-alt-l": "agent::OpenRulesLibrary",
|
||||
"shift-alt-p": "agent::ManageProfiles",
|
||||
"ctrl-i": "agent::ToggleProfileSelector",
|
||||
"shift-tab": "agent::CycleModeSelector",
|
||||
"shift-alt-/": "agent::ToggleModelSelector",
|
||||
"alt-tab": "agent::CycleFavoriteModels",
|
||||
// `alt-l` is provided as an alternative to `alt-tab` as the latter breaks on Windows under the `AcpThread` context
|
||||
"alt-l": "agent::CycleFavoriteModels",
|
||||
"shift-alt-escape": "agent::ExpandMessageEditor",
|
||||
"ctrl-shift-.": "agent::AddSelectionToThread",
|
||||
"shift-alt-q": "agent::AllowAlways",
|
||||
"shift-alt-a": "agent::AllowOnce",
|
||||
"ctrl-alt-a": "agent::OpenPermissionDropdown",
|
||||
"shift-alt-x": "agent::RejectOnce",
|
||||
"pageup": "agent::ScrollOutputPageUp",
|
||||
"pagedown": "agent::ScrollOutputPageDown",
|
||||
"home": "agent::ScrollOutputToTop",
|
||||
|
|
|
|||
|
|
@ -1310,6 +1310,11 @@ impl AgentPanel {
|
|||
self.new_entry(None, window, cx);
|
||||
}
|
||||
|
||||
fn new_agent_thread(&mut self, agent: Agent, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.selected_agent = agent;
|
||||
self.activate_new_thread(true, "agent_panel", window, cx);
|
||||
}
|
||||
|
||||
pub fn activate_new_thread(
|
||||
&mut self,
|
||||
focus: bool,
|
||||
|
|
@ -1327,10 +1332,7 @@ impl AgentPanel {
|
|||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(agent) = action.agent.clone() {
|
||||
self.selected_agent = agent;
|
||||
}
|
||||
self.activate_new_thread(true, "agent_panel", window, cx);
|
||||
self.new_agent_thread(action.agent.clone().into(), window, cx);
|
||||
}
|
||||
|
||||
pub fn new_terminal(
|
||||
|
|
@ -3366,6 +3368,7 @@ impl AgentPanel {
|
|||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
let showing_terminal = matches!(self.visible_surface(), VisibleSurface::Terminal(_));
|
||||
|
||||
let conversation_view = match &self.base_view {
|
||||
BaseView::AgentThread { conversation_view } => Some(conversation_view.clone()),
|
||||
|
|
@ -3427,21 +3430,26 @@ impl AgentPanel {
|
|||
}
|
||||
}
|
||||
|
||||
if !showing_terminal {
|
||||
menu = menu
|
||||
.header("MCP Servers")
|
||||
.action(
|
||||
"View Server Extensions",
|
||||
Box::new(zed_actions::Extensions {
|
||||
category_filter: Some(
|
||||
zed_actions::ExtensionCategoryFilter::ContextServers,
|
||||
),
|
||||
id: None,
|
||||
}),
|
||||
)
|
||||
.action("Add Custom Server…", Box::new(AddContextServer))
|
||||
.separator()
|
||||
.action("Rules", Box::new(OpenRulesLibrary::default()))
|
||||
.action("Profiles", Box::new(ManageProfiles::default()))
|
||||
.separator();
|
||||
}
|
||||
|
||||
menu = menu
|
||||
.header("MCP Servers")
|
||||
.action(
|
||||
"View Server Extensions",
|
||||
Box::new(zed_actions::Extensions {
|
||||
category_filter: Some(
|
||||
zed_actions::ExtensionCategoryFilter::ContextServers,
|
||||
),
|
||||
id: None,
|
||||
}),
|
||||
)
|
||||
.action("Add Custom Server…", Box::new(AddContextServer))
|
||||
.separator()
|
||||
.action("Rules", Box::new(OpenRulesLibrary::default()))
|
||||
.action("Profiles", Box::new(ManageProfiles::default()))
|
||||
.action("Settings", Box::new(OpenSettings))
|
||||
.separator()
|
||||
.action("Toggle Threads Sidebar", Box::new(ToggleWorkspaceSidebar));
|
||||
|
|
@ -3544,7 +3552,7 @@ impl AgentPanel {
|
|||
.item(
|
||||
ContextMenuEntry::new("Zed Agent")
|
||||
.when(is_agent_selected(Agent::NativeAgent), |this| {
|
||||
this.action(Box::new(NewExternalAgentThread { agent: None }))
|
||||
this.action(Box::new(NewThread))
|
||||
})
|
||||
.icon(IconName::ZedAgent)
|
||||
.icon_color(Color::Muted)
|
||||
|
|
@ -3557,10 +3565,8 @@ impl AgentPanel {
|
|||
workspace.panel::<AgentPanel>(cx)
|
||||
{
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.new_external_agent_thread(
|
||||
&NewExternalAgentThread {
|
||||
agent: Some(Agent::NativeAgent),
|
||||
},
|
||||
panel.new_agent_thread(
|
||||
Agent::NativeAgent,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
|
|
@ -3574,6 +3580,7 @@ impl AgentPanel {
|
|||
.when(supports_terminal, |menu| {
|
||||
menu.item(
|
||||
ContextMenuEntry::new("Terminal")
|
||||
.when(showing_terminal, |this| this.action(Box::new(NewThread)))
|
||||
.icon(IconName::Terminal)
|
||||
.icon_color(Color::Muted)
|
||||
.handler({
|
||||
|
|
@ -3653,11 +3660,7 @@ impl AgentPanel {
|
|||
is_agent_selected(Agent::Custom {
|
||||
id: item.id.clone(),
|
||||
}),
|
||||
|this| {
|
||||
this.action(Box::new(NewExternalAgentThread {
|
||||
agent: None,
|
||||
}))
|
||||
},
|
||||
|this| this.action(Box::new(NewThread)),
|
||||
)
|
||||
.icon_color(Color::Muted)
|
||||
.disabled(is_via_collab)
|
||||
|
|
@ -3673,9 +3676,7 @@ impl AgentPanel {
|
|||
panel.update(cx, |panel, cx| {
|
||||
panel.new_external_agent_thread(
|
||||
&NewExternalAgentThread {
|
||||
agent: Some(Agent::Custom {
|
||||
id: agent_id.clone(),
|
||||
}),
|
||||
agent: agent_id.clone(),
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
|
|
@ -4124,12 +4125,6 @@ impl AgentPanel {
|
|||
fn key_context(&self) -> KeyContext {
|
||||
let mut key_context = KeyContext::new_with_defaults();
|
||||
key_context.add("AgentPanel");
|
||||
match self.visible_surface() {
|
||||
VisibleSurface::AgentThread(_) => key_context.add("acp_thread"),
|
||||
VisibleSurface::Terminal(_)
|
||||
| VisibleSurface::Configuration(_)
|
||||
| VisibleSurface::Uninitialized => {}
|
||||
}
|
||||
key_context
|
||||
}
|
||||
}
|
||||
|
|
@ -6601,7 +6596,7 @@ mod tests {
|
|||
});
|
||||
|
||||
// Press cmd-n (activate_draft again with the same agent).
|
||||
cx.dispatch_action(NewExternalAgentThread { agent: None });
|
||||
cx.dispatch_action(NewThread);
|
||||
cx.run_until_parked();
|
||||
|
||||
// The draft entity should not have changed.
|
||||
|
|
@ -6680,7 +6675,7 @@ mod tests {
|
|||
// Switch to a different agent. ensure_draft should extract the typed
|
||||
// content from the old draft and pre-fill the new one.
|
||||
cx.dispatch_action(NewExternalAgentThread {
|
||||
agent: Some(Agent::Stub),
|
||||
agent: Agent::Stub.id(),
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
|
|
|
|||
|
|
@ -149,8 +149,6 @@ actions!(
|
|||
ResetTrialEndUpsell,
|
||||
/// Opens the "Add Context" menu in the message editor.
|
||||
OpenAddContextMenu,
|
||||
/// Continues the current thread.
|
||||
ContinueThread,
|
||||
/// Interrupts the current generation and sends the message immediately.
|
||||
SendImmediately,
|
||||
/// Sends the next queued message immediately.
|
||||
|
|
@ -246,12 +244,33 @@ pub struct ToggleCommandPattern {
|
|||
pub struct NewThread;
|
||||
|
||||
/// Creates a new external agent conversation thread.
|
||||
#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
|
||||
#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = agent)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct NewExternalAgentThread {
|
||||
/// Which agent to use for the conversation.
|
||||
agent: Option<Agent>,
|
||||
/// The agent id to use for the conversation.
|
||||
#[serde(deserialize_with = "deserialize_external_agent_id")]
|
||||
agent: AgentId,
|
||||
}
|
||||
|
||||
fn deserialize_external_agent_id<'de, D>(deserializer: D) -> Result<AgentId, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum AgentIdOrLegacyAgent {
|
||||
LegacyAgent(Agent),
|
||||
AgentId(AgentId),
|
||||
}
|
||||
|
||||
match AgentIdOrLegacyAgent::deserialize(deserializer)? {
|
||||
AgentIdOrLegacyAgent::AgentId(agent_id) => Ok(agent_id),
|
||||
AgentIdOrLegacyAgent::LegacyAgent(Agent::Custom { id }) => Ok(id),
|
||||
AgentIdOrLegacyAgent::LegacyAgent(Agent::NativeAgent) => Ok(Agent::NativeAgent.id()),
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
AgentIdOrLegacyAgent::LegacyAgent(Agent::Stub) => Ok(Agent::Stub.id()),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Deserialize, JsonSchema, Action)]
|
||||
|
|
@ -280,10 +299,13 @@ pub enum Agent {
|
|||
impl From<AgentId> for Agent {
|
||||
fn from(id: AgentId) -> Self {
|
||||
if id.as_ref() == agent::ZED_AGENT_ID.as_ref() {
|
||||
Self::NativeAgent
|
||||
} else {
|
||||
Self::Custom { id }
|
||||
return Self::NativeAgent;
|
||||
}
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
if id.as_ref() == "stub" {
|
||||
return Self::Stub;
|
||||
}
|
||||
Self::Custom { id }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -910,4 +932,23 @@ mod tests {
|
|||
},
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_new_external_agent_thread() {
|
||||
let action = serde_json::from_str::<NewExternalAgentThread>(r#"{"agent":"gemini"}"#)
|
||||
.expect("should deserialize agent id");
|
||||
assert_eq!(action.agent, AgentId::from("gemini"));
|
||||
|
||||
let action = serde_json::from_str::<NewExternalAgentThread>(
|
||||
r#"{"agent":{"custom":{"name":"gemini"}}}"#,
|
||||
)
|
||||
.expect("should deserialize legacy custom agent payload");
|
||||
assert_eq!(action.agent, AgentId::from("gemini"));
|
||||
|
||||
let action = serde_json::from_str::<NewExternalAgentThread>(r#"{"agent":"NativeAgent"}"#)
|
||||
.expect("should deserialize legacy native agent payload");
|
||||
assert_eq!(action.agent, Agent::NativeAgent.id());
|
||||
|
||||
assert!(serde_json::from_str::<NewExternalAgentThread>(r#"{}"#).is_err());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,7 +66,10 @@ use zed_actions::editor::{MoveDown, MoveUp};
|
|||
|
||||
use zed_actions::agents_sidebar::{FocusSidebarFilter, ToggleThreadSwitcher};
|
||||
|
||||
use crate::thread_switcher::{ThreadSwitcher, ThreadSwitcherEntry, ThreadSwitcherEvent};
|
||||
use crate::thread_switcher::{
|
||||
ThreadSwitcher, ThreadSwitcherEntry, ThreadSwitcherEvent, ThreadSwitcherSelection,
|
||||
ThreadSwitcherTerminalEntry, ThreadSwitcherThreadEntry,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
mod sidebar_tests;
|
||||
|
|
@ -376,6 +379,10 @@ impl SidebarContents {
|
|||
fn is_thread_notified(&self, thread_id: &agent_ui::ThreadId) -> bool {
|
||||
self.notified_threads.contains(thread_id)
|
||||
}
|
||||
|
||||
fn is_terminal_notified(&self, terminal_id: TerminalId) -> bool {
|
||||
self.notified_terminals.contains(&terminal_id)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: The mapping from workspace root paths to git repositories needs a
|
||||
|
|
@ -572,6 +579,7 @@ pub struct Sidebar {
|
|||
/// thread, confirming in the thread switcher, etc.) — never from
|
||||
/// background data changes. Used to sort the thread switcher popup.
|
||||
thread_last_accessed: HashMap<ThreadId, DateTime<Utc>>,
|
||||
terminal_last_accessed: HashMap<TerminalId, DateTime<Utc>>,
|
||||
thread_switcher: Option<Entity<ThreadSwitcher>>,
|
||||
_thread_switcher_subscriptions: Vec<gpui::Subscription>,
|
||||
pending_thread_activation: Option<agent_ui::ThreadId>,
|
||||
|
|
@ -678,6 +686,7 @@ impl Sidebar {
|
|||
hovered_thread_index: None,
|
||||
|
||||
thread_last_accessed: HashMap::new(),
|
||||
terminal_last_accessed: HashMap::new(),
|
||||
thread_switcher: None,
|
||||
_thread_switcher_subscriptions: Vec::new(),
|
||||
pending_thread_activation: None,
|
||||
|
|
@ -1123,6 +1132,7 @@ impl Sidebar {
|
|||
let mut notified_terminals: HashSet<TerminalId> = HashSet::new();
|
||||
let mut current_session_ids: HashSet<acp::SessionId> = HashSet::new();
|
||||
let mut current_thread_ids: HashSet<agent_ui::ThreadId> = HashSet::new();
|
||||
let mut current_terminal_ids: HashSet<TerminalId> = HashSet::new();
|
||||
let mut project_header_indices: Vec<usize> = Vec::new();
|
||||
let mut seen_thread_ids: HashSet<agent_ui::ThreadId> = HashSet::new();
|
||||
|
||||
|
|
@ -1188,6 +1198,7 @@ impl Sidebar {
|
|||
.iter()
|
||||
.flat_map(|workspace| terminal_entries_for_workspace(workspace, cx))
|
||||
.collect();
|
||||
current_terminal_ids.extend(terminals.iter().map(|terminal| terminal.id));
|
||||
notified_terminals.extend(
|
||||
terminals
|
||||
.iter()
|
||||
|
|
@ -1530,6 +1541,8 @@ impl Sidebar {
|
|||
|
||||
self.thread_last_accessed
|
||||
.retain(|id, _| current_thread_ids.contains(id));
|
||||
self.terminal_last_accessed
|
||||
.retain(|id, _| current_terminal_ids.contains(id));
|
||||
|
||||
self.contents = SidebarContents {
|
||||
entries,
|
||||
|
|
@ -3275,6 +3288,7 @@ impl Sidebar {
|
|||
return;
|
||||
};
|
||||
|
||||
self.record_terminal_access(terminal_id);
|
||||
self.active_entry = Some(ActiveEntry::Terminal {
|
||||
terminal_id,
|
||||
workspace: workspace.clone(),
|
||||
|
|
@ -3873,6 +3887,11 @@ impl Sidebar {
|
|||
self.archive_thread(&session_id, window, cx);
|
||||
}
|
||||
}
|
||||
Some(ListEntry::Terminal(terminal)) => {
|
||||
let workspace = terminal.workspace.clone();
|
||||
let terminal_id = terminal.id;
|
||||
self.close_terminal(&workspace, terminal_id, window, cx);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -3881,6 +3900,10 @@ impl Sidebar {
|
|||
self.thread_last_accessed.insert(*id, Utc::now());
|
||||
}
|
||||
|
||||
fn record_terminal_access(&mut self, id: TerminalId) {
|
||||
self.terminal_last_accessed.insert(id, Utc::now());
|
||||
}
|
||||
|
||||
fn record_thread_interacted(&mut self, thread_id: &agent_ui::ThreadId, cx: &mut App) {
|
||||
let store = ThreadMetadataStore::global(cx);
|
||||
store.update(cx, |store, cx| {
|
||||
|
|
@ -3925,20 +3948,30 @@ impl Sidebar {
|
|||
}
|
||||
|
||||
/// The sort order used by the ctrl-tab switcher
|
||||
fn thread_cmp_for_switcher(&self, left: &ThreadMetadata, right: &ThreadMetadata) -> Ordering {
|
||||
let sort_time = |x: &ThreadMetadata| {
|
||||
self.thread_last_accessed
|
||||
.get(&x.thread_id)
|
||||
fn switcher_entry_cmp(
|
||||
&self,
|
||||
left: &ThreadSwitcherEntry,
|
||||
right: &ThreadSwitcherEntry,
|
||||
) -> Ordering {
|
||||
let sort_time = |entry: &ThreadSwitcherEntry| match entry {
|
||||
ThreadSwitcherEntry::Thread(entry) => self
|
||||
.thread_last_accessed
|
||||
.get(&entry.metadata.thread_id)
|
||||
.copied()
|
||||
.or(x.interacted_at)
|
||||
.unwrap_or(x.updated_at)
|
||||
.or(entry.metadata.interacted_at)
|
||||
.unwrap_or(entry.metadata.updated_at),
|
||||
ThreadSwitcherEntry::Terminal(entry) => self
|
||||
.terminal_last_accessed
|
||||
.get(&entry.terminal_id)
|
||||
.copied()
|
||||
.unwrap_or(entry.created_at),
|
||||
};
|
||||
|
||||
// .reverse() = most recent first
|
||||
sort_time(left).cmp(&sort_time(right)).reverse()
|
||||
}
|
||||
|
||||
fn mru_threads_for_switcher(&self, cx: &App) -> Vec<ThreadSwitcherEntry> {
|
||||
fn mru_entries_for_switcher(&self, cx: &App) -> Vec<ThreadSwitcherEntry> {
|
||||
let mut current_header_label: Option<SharedString> = None;
|
||||
let mut current_header_key: Option<ProjectGroupKey> = None;
|
||||
let mut entries: Vec<ThreadSwitcherEntry> = self
|
||||
|
|
@ -3971,7 +4004,7 @@ impl Sidebar {
|
|||
let timestamp: SharedString =
|
||||
format_history_entry_timestamp(Self::thread_display_time(&thread.metadata))
|
||||
.into();
|
||||
Some(ThreadSwitcherEntry {
|
||||
Some(ThreadSwitcherEntry::Thread(ThreadSwitcherThreadEntry {
|
||||
session_id,
|
||||
title: thread.metadata.display_title(),
|
||||
icon: thread.icon,
|
||||
|
|
@ -3993,13 +4026,25 @@ impl Sidebar {
|
|||
is_title_generating: thread.is_title_generating,
|
||||
notified,
|
||||
timestamp,
|
||||
})
|
||||
}))
|
||||
}
|
||||
ListEntry::Terminal(terminal) => {
|
||||
let timestamp: SharedString =
|
||||
format_history_entry_timestamp(terminal.created_at).into();
|
||||
Some(ThreadSwitcherEntry::Terminal(ThreadSwitcherTerminalEntry {
|
||||
terminal_id: terminal.id,
|
||||
title: terminal.title.clone(),
|
||||
workspace: terminal.workspace.clone(),
|
||||
project_name: current_header_label.clone(),
|
||||
created_at: terminal.created_at,
|
||||
notified: self.contents.is_terminal_notified(terminal.id),
|
||||
timestamp,
|
||||
}))
|
||||
}
|
||||
ListEntry::Terminal(_) => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
entries.sort_by(|a, b| self.thread_cmp_for_switcher(&a.metadata, &b.metadata));
|
||||
entries.sort_by(|a, b| self.switcher_entry_cmp(a, b));
|
||||
|
||||
entries
|
||||
}
|
||||
|
|
@ -4023,6 +4068,96 @@ impl Sidebar {
|
|||
self.toggle_thread_switcher_impl(action.select_last, window, cx);
|
||||
}
|
||||
|
||||
fn preview_switcher_selection(
|
||||
&mut self,
|
||||
selection: &ThreadSwitcherSelection,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match selection {
|
||||
ThreadSwitcherSelection::Thread {
|
||||
metadata,
|
||||
workspace,
|
||||
} => {
|
||||
if let Some(multi_workspace) = self.multi_workspace.upgrade() {
|
||||
multi_workspace.update(cx, |multi_workspace, cx| {
|
||||
multi_workspace.activate(workspace.clone(), None, window, cx);
|
||||
});
|
||||
}
|
||||
self.active_entry = Some(ActiveEntry::Thread {
|
||||
thread_id: metadata.thread_id,
|
||||
session_id: metadata.session_id.clone(),
|
||||
workspace: workspace.clone(),
|
||||
});
|
||||
self.update_entries(cx);
|
||||
Self::load_agent_thread_in_workspace(workspace, metadata, false, window, cx);
|
||||
}
|
||||
ThreadSwitcherSelection::Terminal {
|
||||
terminal_id,
|
||||
workspace,
|
||||
} => {
|
||||
if !cx.has_flag::<AgentPanelTerminalFeatureFlag>() {
|
||||
return;
|
||||
}
|
||||
if let Some(multi_workspace) = self.multi_workspace.upgrade() {
|
||||
multi_workspace.update(cx, |multi_workspace, cx| {
|
||||
multi_workspace.activate(workspace.clone(), None, window, cx);
|
||||
});
|
||||
}
|
||||
self.active_entry = Some(ActiveEntry::Terminal {
|
||||
terminal_id: *terminal_id,
|
||||
workspace: workspace.clone(),
|
||||
});
|
||||
self.update_entries(cx);
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.activate_terminal(*terminal_id, false, window, cx);
|
||||
});
|
||||
}
|
||||
workspace.reveal_panel::<AgentPanel>(window, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm_switcher_selection(
|
||||
&mut self,
|
||||
selection: &ThreadSwitcherSelection,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match selection {
|
||||
ThreadSwitcherSelection::Thread {
|
||||
metadata,
|
||||
workspace,
|
||||
} => {
|
||||
if let Some(multi_workspace) = self.multi_workspace.upgrade() {
|
||||
multi_workspace.update(cx, |multi_workspace, cx| {
|
||||
multi_workspace.activate(workspace.clone(), None, window, cx);
|
||||
multi_workspace.retain_active_workspace(cx);
|
||||
});
|
||||
}
|
||||
self.record_thread_access(&metadata.thread_id);
|
||||
self.active_entry = Some(ActiveEntry::Thread {
|
||||
thread_id: metadata.thread_id,
|
||||
session_id: metadata.session_id.clone(),
|
||||
workspace: workspace.clone(),
|
||||
});
|
||||
self.update_entries(cx);
|
||||
self.dismiss_thread_switcher(cx);
|
||||
Self::load_agent_thread_in_workspace(workspace, metadata, true, window, cx);
|
||||
}
|
||||
ThreadSwitcherSelection::Terminal {
|
||||
terminal_id,
|
||||
workspace,
|
||||
} => {
|
||||
self.dismiss_thread_switcher(cx);
|
||||
self.activate_terminal(workspace, *terminal_id, true, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_thread_switcher_impl(
|
||||
&mut self,
|
||||
select_last: bool,
|
||||
|
|
@ -4040,21 +4175,27 @@ impl Sidebar {
|
|||
return;
|
||||
}
|
||||
|
||||
let entries = self.mru_threads_for_switcher(cx);
|
||||
let entries = self.mru_entries_for_switcher(cx);
|
||||
if entries.len() < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
let weak_multi_workspace = self.multi_workspace.clone();
|
||||
|
||||
// Capture the full active entry so dismissal can restore terminal
|
||||
// entries too, not just threads.
|
||||
// Snapshot the active entry (thread or terminal) so dismissal can
|
||||
// restore it.
|
||||
let original_active_entry = self.active_entry.clone();
|
||||
let original_metadata = match &original_active_entry {
|
||||
Some(ActiveEntry::Thread { thread_id, .. }) => entries
|
||||
.iter()
|
||||
.find(|e| *thread_id == e.metadata.thread_id)
|
||||
.map(|e| e.metadata.clone()),
|
||||
Some(ActiveEntry::Thread { thread_id, .. }) => {
|
||||
entries.iter().find_map(|entry| match entry {
|
||||
ThreadSwitcherEntry::Thread(entry)
|
||||
if *thread_id == entry.metadata.thread_id =>
|
||||
{
|
||||
Some(entry.metadata.clone())
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let original_workspace = self
|
||||
|
|
@ -4069,44 +4210,13 @@ impl Sidebar {
|
|||
subscriptions.push(cx.subscribe_in(&thread_switcher, window, {
|
||||
let thread_switcher = thread_switcher.clone();
|
||||
move |this, _emitter, event: &ThreadSwitcherEvent, window, cx| match event {
|
||||
ThreadSwitcherEvent::Preview {
|
||||
metadata,
|
||||
workspace,
|
||||
} => {
|
||||
if let Some(mw) = weak_multi_workspace.upgrade() {
|
||||
mw.update(cx, |mw, cx| {
|
||||
mw.activate(workspace.clone(), None, window, cx);
|
||||
});
|
||||
}
|
||||
this.active_entry = Some(ActiveEntry::Thread {
|
||||
thread_id: metadata.thread_id,
|
||||
session_id: metadata.session_id.clone(),
|
||||
workspace: workspace.clone(),
|
||||
});
|
||||
this.update_entries(cx);
|
||||
Self::load_agent_thread_in_workspace(workspace, metadata, false, window, cx);
|
||||
ThreadSwitcherEvent::Preview(selection) => {
|
||||
this.preview_switcher_selection(selection, window, cx);
|
||||
let focus = thread_switcher.focus_handle(cx);
|
||||
window.focus(&focus, cx);
|
||||
}
|
||||
ThreadSwitcherEvent::Confirmed {
|
||||
metadata,
|
||||
workspace,
|
||||
} => {
|
||||
if let Some(mw) = weak_multi_workspace.upgrade() {
|
||||
mw.update(cx, |mw, cx| {
|
||||
mw.activate(workspace.clone(), None, window, cx);
|
||||
mw.retain_active_workspace(cx);
|
||||
});
|
||||
}
|
||||
this.record_thread_access(&metadata.thread_id);
|
||||
this.active_entry = Some(ActiveEntry::Thread {
|
||||
thread_id: metadata.thread_id,
|
||||
session_id: metadata.session_id.clone(),
|
||||
workspace: workspace.clone(),
|
||||
});
|
||||
this.update_entries(cx);
|
||||
this.dismiss_thread_switcher(cx);
|
||||
Self::load_agent_thread_in_workspace(workspace, metadata, true, window, cx);
|
||||
ThreadSwitcherEvent::Confirmed(selection) => {
|
||||
this.confirm_switcher_selection(selection, window, cx);
|
||||
}
|
||||
ThreadSwitcherEvent::Dismissed => {
|
||||
if let Some(mw) = weak_multi_workspace.upgrade() {
|
||||
|
|
@ -4178,7 +4288,7 @@ impl Sidebar {
|
|||
let initial_preview = thread_switcher
|
||||
.read(cx)
|
||||
.selected_entry()
|
||||
.map(|entry| (entry.metadata.clone(), entry.workspace.clone()));
|
||||
.map(ThreadSwitcherEntry::selection);
|
||||
|
||||
self.thread_switcher = Some(thread_switcher);
|
||||
self._thread_switcher_subscriptions = subscriptions;
|
||||
|
|
@ -4188,19 +4298,8 @@ impl Sidebar {
|
|||
});
|
||||
}
|
||||
|
||||
if let Some((metadata, workspace)) = initial_preview {
|
||||
if let Some(mw) = self.multi_workspace.upgrade() {
|
||||
mw.update(cx, |mw, cx| {
|
||||
mw.activate(workspace.clone(), None, window, cx);
|
||||
});
|
||||
}
|
||||
self.active_entry = Some(ActiveEntry::Thread {
|
||||
thread_id: metadata.thread_id,
|
||||
session_id: metadata.session_id.clone(),
|
||||
workspace: workspace.clone(),
|
||||
});
|
||||
self.update_entries(cx);
|
||||
Self::load_agent_thread_in_workspace(&workspace, &metadata, false, window, cx);
|
||||
if let Some(selection) = initial_preview {
|
||||
self.preview_switcher_selection(&selection, window, cx);
|
||||
}
|
||||
|
||||
window.focus(&focus, cx);
|
||||
|
|
@ -4359,6 +4458,7 @@ impl Sidebar {
|
|||
.blend(color.panel_background.opacity(0.25));
|
||||
let terminal_id = terminal.id;
|
||||
let workspace = terminal.workspace.clone();
|
||||
let focus_handle = self.focus_handle.clone();
|
||||
|
||||
ThreadItem::new(id, terminal.title.clone())
|
||||
.base_bg(sidebar_bg)
|
||||
|
|
@ -4382,7 +4482,17 @@ impl Sidebar {
|
|||
IconButton::new("close-terminal", IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Close Terminal"))
|
||||
.tooltip({
|
||||
let focus_handle = focus_handle.clone();
|
||||
move |_window, cx| {
|
||||
Tooltip::for_action_in(
|
||||
"Close Terminal",
|
||||
&ArchiveSelectedThread,
|
||||
&focus_handle,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.close_terminal(&workspace, terminal_id, window, cx);
|
||||
})),
|
||||
|
|
|
|||
|
|
@ -1522,6 +1522,123 @@ async fn test_agent_panel_terminal_notifications_update_sidebar(cx: &mut TestApp
|
|||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_thread_switcher_can_activate_agent_panel_terminal(cx: &mut TestAppContext) {
|
||||
let project = init_test_project_with_agent_panel("/my-project", cx).await;
|
||||
enable_agent_panel_terminal(cx);
|
||||
let (multi_workspace, cx) =
|
||||
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||
let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
|
||||
|
||||
let build_terminal_id = panel
|
||||
.update_in(cx, |panel, window, cx| {
|
||||
panel.insert_test_terminal("Build", true, window, cx)
|
||||
})
|
||||
.expect("build test terminal should be inserted");
|
||||
let server_terminal_id = panel
|
||||
.update_in(cx, |panel, window, cx| {
|
||||
panel.insert_test_terminal("Server", true, window, cx)
|
||||
})
|
||||
.expect("server test terminal should be inserted");
|
||||
cx.run_until_parked();
|
||||
|
||||
focus_sidebar(&sidebar, cx);
|
||||
sidebar.update_in(cx, |sidebar, window, cx| {
|
||||
sidebar.on_toggle_thread_switcher(&ToggleThreadSwitcher::default(), window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
let (entry_terminal_ids, selected_terminal_id) = sidebar.read_with(cx, |sidebar, cx| {
|
||||
let switcher = sidebar
|
||||
.thread_switcher
|
||||
.as_ref()
|
||||
.expect("switcher should be open");
|
||||
let switcher = switcher.read(cx);
|
||||
let entry_terminal_ids = switcher
|
||||
.entries()
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
entry
|
||||
.terminal_id()
|
||||
.expect("expected terminal switcher entry")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let selected_terminal_id = switcher
|
||||
.selected_entry()
|
||||
.expect("switcher should have selected entry")
|
||||
.terminal_id()
|
||||
.expect("expected selected terminal switcher entry");
|
||||
(entry_terminal_ids, selected_terminal_id)
|
||||
});
|
||||
|
||||
assert_eq!(entry_terminal_ids.len(), 2);
|
||||
assert!(entry_terminal_ids.contains(&build_terminal_id));
|
||||
assert!(entry_terminal_ids.contains(&server_terminal_id));
|
||||
|
||||
sidebar.update_in(cx, |sidebar, window, cx| {
|
||||
let switcher = sidebar
|
||||
.thread_switcher
|
||||
.as_ref()
|
||||
.expect("switcher should be open");
|
||||
let focus = switcher.focus_handle(cx);
|
||||
focus.dispatch_action(&menu::Confirm, window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
panel.read_with(cx, |panel, _cx| {
|
||||
assert_eq!(panel.active_terminal_id(), Some(selected_terminal_id));
|
||||
});
|
||||
sidebar.read_with(cx, |sidebar, _cx| {
|
||||
assert!(
|
||||
matches!(&sidebar.active_entry, Some(ActiveEntry::Terminal { terminal_id, .. }) if *terminal_id == selected_terminal_id),
|
||||
"expected selected terminal to become active, got {:?}",
|
||||
sidebar.active_entry,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_archive_selected_thread_closes_selected_agent_panel_terminal(
|
||||
cx: &mut TestAppContext,
|
||||
) {
|
||||
let project = init_test_project_with_agent_panel("/my-project", cx).await;
|
||||
enable_agent_panel_terminal(cx);
|
||||
let (multi_workspace, cx) =
|
||||
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
|
||||
let (sidebar, panel) = setup_sidebar_with_agent_panel(&multi_workspace, cx);
|
||||
|
||||
let terminal_id = panel
|
||||
.update_in(cx, |panel, window, cx| {
|
||||
panel.insert_test_terminal("Dev Server", true, window, cx)
|
||||
})
|
||||
.expect("test terminal should be inserted");
|
||||
cx.run_until_parked();
|
||||
|
||||
focus_sidebar(&sidebar, cx);
|
||||
let terminal_index = sidebar.read_with(cx, |sidebar, _cx| {
|
||||
sidebar
|
||||
.contents
|
||||
.entries
|
||||
.iter()
|
||||
.position(|entry| matches!(entry, ListEntry::Terminal(terminal) if terminal.id == terminal_id))
|
||||
.expect("terminal should be visible in sidebar")
|
||||
});
|
||||
sidebar.update_in(cx, |sidebar, _window, _cx| {
|
||||
sidebar.selection = Some(terminal_index);
|
||||
});
|
||||
cx.dispatch_action(ArchiveSelectedThread);
|
||||
cx.run_until_parked();
|
||||
|
||||
panel.read_with(cx, |panel, _cx| {
|
||||
assert!(!panel.has_terminal(terminal_id));
|
||||
});
|
||||
sidebar.read_with(cx, |sidebar, _cx| {
|
||||
assert!(sidebar.contents.entries.iter().all(|entry| {
|
||||
!matches!(entry, ListEntry::Terminal(terminal) if terminal.id == terminal_id)
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_closing_active_agent_panel_terminal_activates_neighbor(cx: &mut TestAppContext) {
|
||||
let project = init_test_project_with_agent_panel("/my-project", cx).await;
|
||||
|
|
@ -5944,7 +6061,7 @@ async fn test_thread_switcher_ordering(cx: &mut TestAppContext) {
|
|||
.read(cx)
|
||||
.entries()
|
||||
.iter()
|
||||
.map(|e| e.metadata.thread_id)
|
||||
.map(|entry| entry.thread_id().expect("expected thread switcher entry"))
|
||||
.collect()
|
||||
})
|
||||
};
|
||||
|
|
@ -5959,8 +6076,8 @@ async fn test_thread_switcher_ordering(cx: &mut TestAppContext) {
|
|||
let s = switcher.read(cx);
|
||||
s.selected_entry()
|
||||
.expect("should have selection")
|
||||
.metadata
|
||||
.thread_id
|
||||
.thread_id()
|
||||
.expect("expected selected thread entry")
|
||||
})
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use action_log::DiffStats;
|
||||
use agent_client_protocol::schema as acp;
|
||||
use agent_ui::thread_metadata_store::ThreadMetadata;
|
||||
use agent_ui::{TerminalId, thread_metadata_store::ThreadMetadata};
|
||||
use gpui::{
|
||||
Action as _, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Modifiers,
|
||||
ModifiersChangedEvent, Render, ScrollHandle, SharedString, prelude::*,
|
||||
|
|
@ -9,7 +9,8 @@ use ui::{AgentThreadStatus, ThreadItem, ThreadItemWorktreeInfo, WithScrollbar, p
|
|||
use workspace::{ModalView, Workspace};
|
||||
use zed_actions::agents_sidebar::ToggleThreadSwitcher;
|
||||
|
||||
pub(crate) struct ThreadSwitcherEntry {
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ThreadSwitcherThreadEntry {
|
||||
pub session_id: acp::SessionId,
|
||||
pub title: SharedString,
|
||||
pub icon: IconName,
|
||||
|
|
@ -25,15 +26,150 @@ pub(crate) struct ThreadSwitcherEntry {
|
|||
pub timestamp: SharedString,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ThreadSwitcherTerminalEntry {
|
||||
pub terminal_id: TerminalId,
|
||||
pub title: SharedString,
|
||||
pub workspace: Entity<Workspace>,
|
||||
pub project_name: Option<SharedString>,
|
||||
pub created_at: chrono::DateTime<chrono::Utc>,
|
||||
pub notified: bool,
|
||||
pub timestamp: SharedString,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum ThreadSwitcherEntry {
|
||||
Thread(ThreadSwitcherThreadEntry),
|
||||
Terminal(ThreadSwitcherTerminalEntry),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum ThreadSwitcherSelection {
|
||||
Thread {
|
||||
metadata: ThreadMetadata,
|
||||
workspace: Entity<Workspace>,
|
||||
},
|
||||
Terminal {
|
||||
terminal_id: TerminalId,
|
||||
workspace: Entity<Workspace>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ThreadSwitcherEntry {
|
||||
pub(crate) fn selection(&self) -> ThreadSwitcherSelection {
|
||||
match self {
|
||||
Self::Thread(entry) => ThreadSwitcherSelection::Thread {
|
||||
metadata: entry.metadata.clone(),
|
||||
workspace: entry.workspace.clone(),
|
||||
},
|
||||
Self::Terminal(entry) => ThreadSwitcherSelection::Terminal {
|
||||
terminal_id: entry.terminal_id,
|
||||
workspace: entry.workspace.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn element_id(&self) -> SharedString {
|
||||
match self {
|
||||
Self::Thread(entry) => {
|
||||
SharedString::from(format!("thread-switcher-thread-{}", entry.session_id))
|
||||
}
|
||||
Self::Terminal(entry) => {
|
||||
SharedString::from(format!("thread-switcher-terminal-{}", entry.terminal_id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn title(&self) -> SharedString {
|
||||
match self {
|
||||
Self::Thread(entry) => entry.title.clone(),
|
||||
Self::Terminal(entry) => entry.title.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
match self {
|
||||
Self::Thread(entry) => entry.icon,
|
||||
Self::Terminal(_) => IconName::Terminal,
|
||||
}
|
||||
}
|
||||
|
||||
fn icon_from_external_svg(&self) -> Option<SharedString> {
|
||||
match self {
|
||||
Self::Thread(entry) => entry.icon_from_external_svg.clone(),
|
||||
Self::Terminal(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn status(&self) -> AgentThreadStatus {
|
||||
match self {
|
||||
Self::Thread(entry) => entry.status,
|
||||
Self::Terminal(_) => AgentThreadStatus::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn project_name(&self) -> Option<SharedString> {
|
||||
match self {
|
||||
Self::Thread(entry) => entry.project_name.clone(),
|
||||
Self::Terminal(entry) => entry.project_name.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn worktrees(&self) -> Vec<ThreadItemWorktreeInfo> {
|
||||
match self {
|
||||
Self::Thread(entry) => entry.worktrees.clone(),
|
||||
Self::Terminal(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn timestamp(&self) -> SharedString {
|
||||
match self {
|
||||
Self::Thread(entry) => entry.timestamp.clone(),
|
||||
Self::Terminal(entry) => entry.timestamp.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
fn is_title_generating(&self) -> bool {
|
||||
match self {
|
||||
Self::Thread(entry) => entry.is_title_generating,
|
||||
Self::Terminal(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn notified(&self) -> bool {
|
||||
match self {
|
||||
Self::Thread(entry) => entry.notified,
|
||||
Self::Terminal(entry) => entry.notified,
|
||||
}
|
||||
}
|
||||
|
||||
fn diff_stats(&self) -> DiffStats {
|
||||
match self {
|
||||
Self::Thread(entry) => entry.diff_stats,
|
||||
Self::Terminal(_) => DiffStats::default(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn thread_id(&self) -> Option<agent_ui::ThreadId> {
|
||||
match self {
|
||||
Self::Thread(entry) => Some(entry.metadata.thread_id),
|
||||
Self::Terminal(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn terminal_id(&self) -> Option<TerminalId> {
|
||||
match self {
|
||||
Self::Thread(_) => None,
|
||||
Self::Terminal(entry) => Some(entry.terminal_id),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum ThreadSwitcherEvent {
|
||||
Preview {
|
||||
metadata: ThreadMetadata,
|
||||
workspace: Entity<Workspace>,
|
||||
},
|
||||
Confirmed {
|
||||
metadata: ThreadMetadata,
|
||||
workspace: Entity<Workspace>,
|
||||
},
|
||||
Preview(ThreadSwitcherSelection),
|
||||
Confirmed(ThreadSwitcherSelection),
|
||||
Dismissed,
|
||||
}
|
||||
|
||||
|
|
@ -62,10 +198,7 @@ impl ThreadSwitcher {
|
|||
};
|
||||
|
||||
if let Some(entry) = entries.get(selected_index) {
|
||||
cx.emit(ThreadSwitcherEvent::Preview {
|
||||
metadata: entry.metadata.clone(),
|
||||
workspace: entry.workspace.clone(),
|
||||
});
|
||||
cx.emit(ThreadSwitcherEvent::Preview(entry.selection()));
|
||||
}
|
||||
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
|
@ -124,10 +257,7 @@ impl ThreadSwitcher {
|
|||
fn emit_preview(&mut self, cx: &mut Context<Self>) {
|
||||
self.scroll_handle.scroll_to_item(self.selected_index);
|
||||
if let Some(entry) = self.entries.get(self.selected_index) {
|
||||
cx.emit(ThreadSwitcherEvent::Preview {
|
||||
metadata: entry.metadata.clone(),
|
||||
workspace: entry.workspace.clone(),
|
||||
});
|
||||
cx.emit(ThreadSwitcherEvent::Preview(entry.selection()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -137,10 +267,7 @@ impl ThreadSwitcher {
|
|||
|
||||
fn confirm_selected(&mut self, cx: &mut Context<Self>) {
|
||||
if let Some(entry) = self.entries.get(self.selected_index) {
|
||||
cx.emit(ThreadSwitcherEvent::Confirmed {
|
||||
metadata: entry.metadata.clone(),
|
||||
workspace: entry.workspace.clone(),
|
||||
});
|
||||
cx.emit(ThreadSwitcherEvent::Confirmed(entry.selection()));
|
||||
}
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
|
@ -233,28 +360,25 @@ impl Render for ThreadSwitcher {
|
|||
.overflow_y_scroll()
|
||||
.track_scroll(&self.scroll_handle)
|
||||
.children(self.entries.iter().enumerate().map(|(ix, entry)| {
|
||||
let id =
|
||||
SharedString::from(format!("thread-switcher-{}", entry.session_id));
|
||||
let diff_stats = entry.diff_stats();
|
||||
|
||||
ThreadItem::new(id, entry.title.clone())
|
||||
ThreadItem::new(entry.element_id(), entry.title())
|
||||
.rounded(true)
|
||||
.icon(entry.icon)
|
||||
.status(entry.status)
|
||||
.when_some(entry.icon_from_external_svg.clone(), |this, svg| {
|
||||
.icon(entry.icon())
|
||||
.status(entry.status())
|
||||
.when_some(entry.icon_from_external_svg(), |this, svg| {
|
||||
this.custom_icon_from_external_svg(svg)
|
||||
})
|
||||
.when_some(entry.project_name.clone(), |this, name| {
|
||||
this.project_name(name)
|
||||
.when_some(entry.project_name(), |this, name| this.project_name(name))
|
||||
.worktrees(entry.worktrees())
|
||||
.timestamp(entry.timestamp())
|
||||
.title_generating(entry.is_title_generating())
|
||||
.notified(entry.notified())
|
||||
.when(diff_stats.lines_added > 0, |this| {
|
||||
this.added(diff_stats.lines_added as usize)
|
||||
})
|
||||
.worktrees(entry.worktrees.clone())
|
||||
.timestamp(entry.timestamp.clone())
|
||||
.title_generating(entry.is_title_generating)
|
||||
.notified(entry.notified)
|
||||
.when(entry.diff_stats.lines_added > 0, |this| {
|
||||
this.added(entry.diff_stats.lines_added as usize)
|
||||
})
|
||||
.when(entry.diff_stats.lines_removed > 0, |this| {
|
||||
this.removed(entry.diff_stats.lines_removed as usize)
|
||||
.when(diff_stats.lines_removed > 0, |this| {
|
||||
this.removed(diff_stats.lines_removed as usize)
|
||||
})
|
||||
.selected(ix == selected_index)
|
||||
.base_bg(cx.theme().colors().elevated_surface_background)
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ From the "New Thread…" menu you can:
|
|||
- Pick **Zed Agent** or any installed [external agent](./external-agents.md) to start a new thread with that agent.
|
||||
- Choose **New From Summary** to start a fresh Zed Agent thread seeded with a summary of the current conversation — useful for compacting long threads as you approach the context window limit.
|
||||
|
||||
{#action agent::NewExternalAgentThread} creates another thread with the currently selected agent.
|
||||
{#action agent::NewExternalAgentThread} creates a new thread with the specified external agent id.
|
||||
|
||||
You can also start a new thread from the [Threads Sidebar](./parallel-agents.md#threads-sidebar), scoped to a specific project — see [Running Multiple Threads](./parallel-agents.md#running-multiple-threads).
|
||||
|
||||
|
|
|
|||
|
|
@ -29,10 +29,7 @@ If you'd like to bind this to a keyboard shortcut, you can do so by editing your
|
|||
[
|
||||
{
|
||||
"bindings": {
|
||||
"cmd-alt-g": [
|
||||
"agent::NewExternalAgentThread",
|
||||
{ "agent": { "custom": { "name": "gemini" } } }
|
||||
]
|
||||
"cmd-alt-g": ["agent::NewExternalAgentThread", { "agent": "gemini" }]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -75,10 +72,7 @@ If you'd like to bind this to a keyboard shortcut, you can do so by editing your
|
|||
[
|
||||
{
|
||||
"bindings": {
|
||||
"cmd-alt-c": [
|
||||
"agent::NewExternalAgentThread",
|
||||
{ "agent": { "custom": { "name": "claude-acp" } } }
|
||||
]
|
||||
"cmd-alt-c": ["agent::NewExternalAgentThread", { "agent": "claude-acp" }]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
@ -150,10 +144,7 @@ If you'd like to bind this to a keyboard shortcut, you can do so by editing your
|
|||
[
|
||||
{
|
||||
"bindings": {
|
||||
"cmd-alt-c": [
|
||||
"agent::NewExternalAgentThread",
|
||||
{ "agent": { "custom": { "name": "codex-acp" } } }
|
||||
]
|
||||
"cmd-alt-c": ["agent::NewExternalAgentThread", { "agent": "codex-acp" }]
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue