Showing 6 changed files with 80 additions and 35 deletions.
| @@ -224,9 +224,13 @@ pub struct Workspace { | ||
| 224 | 224 | search: Entity<InputState>, |
| 225 | 225 | sidebar_open: bool, |
| 226 | 226 | panel_open: bool, |
| 227 | - /// What the right panel shows, in the project panel's place: a surface | |
| 228 | - /// (subagents, browser, terminal) or the list to pick one from. | |
| 229 | - right_panel: Option<surfaces::Surface>, | |
| 227 | + /// What each thread's right panel shows, in the project panel's place: a | |
| 228 | + /// surface (subagents, browser, device, terminal) or the list to pick one | |
| 229 | + /// from. Each thread has its own, so another thread's device or browser | |
| 230 | + /// doesn't follow you around. | |
| 231 | + right_panels: HashMap<EntityId, surfaces::Surface>, | |
| 232 | + /// The right panel while no thread is open. | |
| 233 | + idle_right_panel: Option<surfaces::Surface>, | |
| 230 | 234 | /// The surface picker's focus, for its keys. |
| 231 | 235 | surface_focus: FocusHandle, |
| 232 | 236 | settled_open: bool, |
| @@ -385,7 +389,8 @@ impl Workspace { | ||
| 385 | 389 | search, |
| 386 | 390 | sidebar_open: true, |
| 387 | 391 | panel_open: true, |
| 388 | - right_panel: None, | |
| 392 | + right_panels: HashMap::new(), | |
| 393 | + idle_right_panel: None, | |
| 389 | 394 | surface_focus: cx.focus_handle(), |
| 390 | 395 | settled_open: false, |
| 391 | 396 | snoozed_open: false, |
| @@ -1549,6 +1554,7 @@ impl Workspace { | ||
| 1549 | 1554 | let id = session.entity_id(); |
| 1550 | 1555 | self.chats.remove(&id); |
| 1551 | 1556 | self.browsers.remove(&id); |
| 1557 | + self.right_panels.remove(&id); | |
| 1552 | 1558 | let thread_id = session.read(cx).id.clone(); |
| 1553 | 1559 | self.forget_thread_devices(&thread_id, cx); |
| 1554 | 1560 | self.drop_terminals(id); |
| @@ -1785,8 +1791,8 @@ impl Workspace { | ||
| 1785 | 1791 | |
| 1786 | 1792 | fn toggle_project_panel(&mut self, cx: &mut Context<Self>) { |
| 1787 | 1793 | // With a surface in its place, the project panel comes back. |
| 1788 | - if self.right_panel.is_some() { | |
| 1789 | - self.right_panel = None; | |
| 1794 | + if self.right_panel().is_some() { | |
| 1795 | + self.set_right_panel(None); | |
| 1790 | 1796 | self.panel_open = true; |
| 1791 | 1797 | } else { |
| 1792 | 1798 | self.panel_open = !self.panel_open; |
| @@ -1805,9 +1811,11 @@ impl Workspace { | ||
| 1805 | 1811 | } |
| 1806 | 1812 | let thread_id = session.read(cx).id.clone(); |
| 1807 | 1813 | let panel = cx.new(|cx| BrowserPanel::new(&thread_id, window, cx)); |
| 1808 | - let subscription = cx.subscribe(&panel, |this, _, event: &BrowserPanelEvent, cx| match event { | |
| 1814 | + let subscription = cx.subscribe(&panel, move |this, _, event: &BrowserPanelEvent, cx| match event { | |
| 1809 | 1815 | BrowserPanelEvent::Close => { |
| 1810 | - this.right_panel = None; | |
| 1816 | + if this.right_panels.get(&id) == Some(&surfaces::Surface::Browser) { | |
| 1817 | + this.right_panels.remove(&id); | |
| 1818 | + } | |
| 1811 | 1819 | cx.notify(); |
| 1812 | 1820 | } |
| 1813 | 1821 | }); |
| @@ -2111,7 +2119,7 @@ impl Render for Workspace { | ||
| 2111 | 2119 | let docked_terminal = |
| 2112 | 2120 | if self.surface_open(surfaces::Surface::Terminal) { self.render_terminal_drawer(true, window, cx) } else { None }; |
| 2113 | 2121 | let panel_visible = self.panel_open |
| 2114 | - && self.right_panel.is_none() | |
| 2122 | + && self.right_panel().is_none() | |
| 2115 | 2123 | && self.active_project().is_some() |
| 2116 | 2124 | && window.viewport_size().width >= px(PANEL_MIN_WINDOW); |
| 2117 | 2125 | // The browser gives way before the conversation gets too narrow. |
| @@ -113,9 +113,15 @@ impl Workspace { | ||
| 113 | 113 | let active = self.active.as_ref().is_some_and(|session| session.read(cx).id == thread); |
| 114 | 114 | if active && !float { |
| 115 | 115 | self.open_surface(Surface::Device, window, cx); |
| 116 | - } else if active && self.surface_open(Surface::Device) { | |
| 117 | - // Floating closes the panel, as the browser's floating preview does. | |
| 118 | - self.right_panel = None; | |
| 116 | + } else if let Some(session) = self.session_by_id(thread, cx) { | |
| 117 | + let id = session.entity_id(); | |
| 118 | + if !float { | |
| 119 | + // It's there when you open the thread. | |
| 120 | + self.set_thread_surface(id, Some(Surface::Device)); | |
| 121 | + } else if self.thread_surface(id) == Some(Surface::Device) { | |
| 122 | + // Floating closes the panel, as the browser's floating preview does. | |
| 123 | + self.set_thread_surface(id, None); | |
| 124 | + } | |
| 119 | 125 | } |
| 120 | 126 | cx.notify(); |
| 121 | 127 | } |
| @@ -236,21 +242,19 @@ impl Workspace { | ||
| 236 | 242 | let thread_id = thread.to_string(); |
| 237 | 243 | cx.subscribe_in(&panel, window, move |this, _, event: &DevicePanelEvent, window, cx| { |
| 238 | 244 | let active = this.active.as_ref().is_some_and(|session| session.read(cx).id == thread_id); |
| 245 | + let session = this.session_by_id(&thread_id, cx).map(|session| session.entity_id()); | |
| 239 | 246 | match event { |
| 240 | - DevicePanelEvent::Close => { | |
| 241 | - if active && this.surface_open(Surface::Device) { | |
| 242 | - this.right_panel = None; | |
| 243 | - } | |
| 244 | - } | |
| 245 | 247 | // Floating closes the panel, as the browser's floating preview does. |
| 246 | - DevicePanelEvent::Floated => { | |
| 247 | - if active && this.surface_open(Surface::Device) { | |
| 248 | - this.right_panel = None; | |
| 248 | + DevicePanelEvent::Close | DevicePanelEvent::Floated => { | |
| 249 | + if let Some(session) = session.filter(|session| this.thread_surface(*session) == Some(Surface::Device)) { | |
| 250 | + this.set_thread_surface(session, None); | |
| 249 | 251 | } |
| 250 | 252 | } |
| 251 | 253 | DevicePanelEvent::Docked => { |
| 252 | 254 | if active { |
| 253 | 255 | this.open_surface(Surface::Device, window, cx); |
| 256 | + } else if let Some(session) = session { | |
| 257 | + this.set_thread_surface(session, Some(Surface::Device)); | |
| 254 | 258 | } |
| 255 | 259 | } |
| 256 | 260 | } |
| @@ -291,7 +295,7 @@ impl Workspace { | ||
| 291 | 295 | Some(move |_: &mut Window, cx: &mut App| { |
| 292 | 296 | let _ = workspace.update(cx, |this, cx| { |
| 293 | 297 | if this.surface_open(Surface::Device) { |
| 294 | - this.right_panel = None; | |
| 298 | + this.set_right_panel(None); | |
| 295 | 299 | cx.notify(); |
| 296 | 300 | } |
| 297 | 301 | }); |
| @@ -280,7 +280,7 @@ impl Workspace { | ||
| 280 | 280 | .ghost() |
| 281 | 281 | .mini() |
| 282 | 282 | .icon(IconName::PanelRight) |
| 283 | - .selected(self.right_panel.is_some()) | |
| 283 | + .selected(self.right_panel().is_some()) | |
| 284 | 284 | .tooltip("Toggle right panel (Ctrl+Alt+B)") |
| 285 | 285 | .on_click(cx.listener(|this, _, window, cx| this.toggle_right_panel(window, cx))), |
| 286 | 286 | ) |
| @@ -145,7 +145,7 @@ impl Workspace { | ||
| 145 | 145 | .icon(IconName::Close) |
| 146 | 146 | .tooltip("Close subagents") |
| 147 | 147 | .on_click(cx.listener(|this, _, _, cx| { |
| 148 | - this.right_panel = None; | |
| 148 | + this.set_right_panel(None); | |
| 149 | 149 | cx.notify(); |
| 150 | 150 | })), |
| 151 | 151 | ); |
| @@ -33,16 +33,49 @@ const SURFACES: [(Surface, &str, &str); 4] = [ | ||
| 33 | 33 | pub(super) const TERMINAL_PANEL_WIDTH: f32 = 520.; |
| 34 | 34 | |
| 35 | 35 | impl Workspace { |
| 36 | + /// What the open thread's right panel shows. | |
| 37 | + pub(super) fn right_panel(&self) -> Option<Surface> { | |
| 38 | + match &self.active { | |
| 39 | + Some(session) => self.right_panels.get(&session.entity_id()).copied(), | |
| 40 | + None => self.idle_right_panel, | |
| 41 | + } | |
| 42 | + } | |
| 43 | + | |
| 44 | + /// Sets what the open thread's right panel shows. Other threads keep theirs. | |
| 45 | + pub(super) fn set_right_panel(&mut self, surface: Option<Surface>) { | |
| 46 | + match self.active.as_ref().map(|session| session.entity_id()) { | |
| 47 | + Some(id) => self.set_thread_surface(id, surface), | |
| 48 | + None => self.idle_right_panel = surface, | |
| 49 | + } | |
| 50 | + } | |
| 51 | + | |
| 52 | + /// What a thread's right panel shows, open or not. | |
| 53 | + pub(super) fn thread_surface(&self, thread: EntityId) -> Option<Surface> { | |
| 54 | + self.right_panels.get(&thread).copied() | |
| 55 | + } | |
| 56 | + | |
| 57 | + /// Sets what a thread's right panel shows, open or not. | |
| 58 | + pub(super) fn set_thread_surface(&mut self, thread: EntityId, surface: Option<Surface>) { | |
| 59 | + match surface { | |
| 60 | + Some(surface) => { | |
| 61 | + self.right_panels.insert(thread, surface); | |
| 62 | + } | |
| 63 | + None => { | |
| 64 | + self.right_panels.remove(&thread); | |
| 65 | + } | |
| 66 | + } | |
| 67 | + } | |
| 68 | + | |
| 36 | 69 | pub(super) fn surface_open(&self, surface: Surface) -> bool { |
| 37 | - self.right_panel == Some(surface) | |
| 70 | + self.right_panel() == Some(surface) | |
| 38 | 71 | } |
| 39 | 72 | |
| 40 | 73 | /// The right panel's button: opens "Open a surface", or closes the panel. |
| 41 | 74 | pub(super) fn toggle_right_panel(&mut self, window: &mut Window, cx: &mut Context<Self>) { |
| 42 | - if self.right_panel.is_some() { | |
| 43 | - self.right_panel = None; | |
| 75 | + if self.right_panel().is_some() { | |
| 76 | + self.set_right_panel(None); | |
| 44 | 77 | } else { |
| 45 | - self.right_panel = Some(Surface::Picker); | |
| 78 | + self.set_right_panel(Some(Surface::Picker)); | |
| 46 | 79 | self.surface_focus.focus(window, cx); |
| 47 | 80 | } |
| 48 | 81 | cx.notify(); |
| @@ -51,8 +84,8 @@ impl Workspace { | ||
| 51 | 84 | /// Shows a surface in the right panel, or closes the panel if it's |
| 52 | 85 | /// showing it already. |
| 53 | 86 | pub(super) fn toggle_surface(&mut self, surface: Surface, window: &mut Window, cx: &mut Context<Self>) { |
| 54 | - if self.right_panel == Some(surface) { | |
| 55 | - self.right_panel = None; | |
| 87 | + if self.right_panel() == Some(surface) { | |
| 88 | + self.set_right_panel(None); | |
| 56 | 89 | cx.notify(); |
| 57 | 90 | return; |
| 58 | 91 | } |
| @@ -67,7 +100,7 @@ impl Workspace { | ||
| 67 | 100 | // The terminals move over from the drawer, if it's open. |
| 68 | 101 | self.dock_terminal(window, cx); |
| 69 | 102 | } |
| 70 | - self.right_panel = Some(surface); | |
| 103 | + self.set_right_panel(Some(surface)); | |
| 71 | 104 | if surface == Surface::Picker { |
| 72 | 105 | self.surface_focus.focus(window, cx); |
| 73 | 106 | } |
| @@ -81,7 +114,7 @@ impl Workspace { | ||
| 81 | 114 | return; |
| 82 | 115 | } |
| 83 | 116 | if keystroke.key == "escape" { |
| 84 | - self.right_panel = None; | |
| 117 | + self.set_right_panel(None); | |
| 85 | 118 | cx.notify(); |
| 86 | 119 | return; |
| 87 | 120 | } |
| @@ -190,7 +190,7 @@ impl Workspace { | ||
| 190 | 190 | } |
| 191 | 191 | // In the right panel, it hides from there. |
| 192 | 192 | if self.surface_open(Surface::Terminal) { |
| 193 | - self.right_panel = None; | |
| 193 | + self.set_right_panel(None); | |
| 194 | 194 | self.focus_active_composer(window, cx); |
| 195 | 195 | cx.notify(); |
| 196 | 196 | return; |
| @@ -224,7 +224,7 @@ impl Workspace { | ||
| 224 | 224 | let drawer = self.terminals.entry(session).or_default(); |
| 225 | 225 | drawer.open = false; |
| 226 | 226 | let empty = drawer.layout.is_empty(); |
| 227 | - self.right_panel = Some(Surface::Terminal); | |
| 227 | + self.set_right_panel(Some(Surface::Terminal)); | |
| 228 | 228 | if empty { |
| 229 | 229 | self.new_terminal(window, cx); |
| 230 | 230 | } else { |
| @@ -339,8 +339,8 @@ impl Workspace { | ||
| 339 | 339 | let empty = drawer.layout.is_empty(); |
| 340 | 340 | if empty { |
| 341 | 341 | self.terminals.remove(&session); |
| 342 | - if self.surface_open(Surface::Terminal) && self.active.as_ref().is_some_and(|active| active.entity_id() == session) { | |
| 343 | - self.right_panel = None; | |
| 342 | + if self.thread_surface(session) == Some(Surface::Terminal) { | |
| 343 | + self.set_thread_surface(session, None); | |
| 344 | 344 | } |
| 345 | 345 | } |
| 346 | 346 | if self.active.as_ref().is_some_and(|active| active.entity_id() == session) { |