Vectrum
Sign In

Right panel: Open a surface, with subagents, browser and terminal

The right panel's button opens T3 Code's surface picker: Subagents (S),
Browser (B) and Terminal (T). The terminal can sit in the right panel
instead of the drawer. The subagents button leaves the top bar; the right
panel's button lights up while subagents work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
JjohannSocommitted 1 parent 4bc1f4dcommit1d43ce0

Showing 6 changed files with 275 additions and 78 deletions.

+6−3README.mdView File
@@ -104,11 +104,14 @@ Gruvbox, Rosé Pine, Solarized and Dracula.
104104 Sublime Text, Kate or the file manager, whichever are installed), the branch with a
105105 switcher, **Commit** (asks the agent to commit, or commit and push), the size of the
106106 uncommitted changes, and the thread's cost.
107- - A subagents panel (Ctrl+Alt+S, or the robot in the top bar), in the project panel's
107+ - A right panel, as T3 Code has it: its button in the top bar (Ctrl+Alt+B) opens "Open a
108+ surface", which lists the subagents, the browser and a terminal, keyed S, B and T. A
109+ terminal there holds the thread's terminals instead of the drawer under the conversation.
110+ - A subagents panel (Ctrl+Alt+S, or S in the right panel), in the project panel's
108111 place: every subagent the thread started, the working ones first with what each is
109112 doing right now (its latest tool call and progress summary), how long it has run, its
110113 kind, tool uses and tokens, then the finished and failed ones. Every working one has a Stop button. Click one to jump to its call in the conversation. While the
111- panel is closed, a light on the robot says subagents are working. It covers Claude's
114+ panel is closed, a light on the right panel's button says subagents are working. It covers Claude's
112115 Task and Agent calls; Pi's [`subagent` extension](https://github.com/earendil-works/pi/tree/main/packages/coding-agent/examples/extensions/subagent)
113116 (a row per task, single, parallel or chained); and jcode's swarm, whose agents keep
114117 working in sessions of their own after the turn ends: the app checks on them every few
@@ -274,7 +277,7 @@ The shortcuts follow T3 Code's default keymap.
274277 | Ctrl+D / Ctrl+Shift+D | Split the terminal side by side / stacked (terminal focused) |
275278 | Ctrl+N / Ctrl+W | New terminal / close the terminal (terminal focused) |
276279 | Ctrl+Shift+C / Ctrl+Shift+V | Copy / paste in the terminal |
277- | Ctrl+Alt+B | Show or hide the right panel (the browser) |
280+ | Ctrl+Alt+B | Show or hide the right panel: Open a surface |
278281 | Ctrl+Shift+B | Show or hide the thread details (project panel) |
279282 | Ctrl+Alt+S | Show or hide the subagents panel |
280283 | Ctrl+O | Add a project folder |
+29−26src/ui/workspace.rsView File
@@ -53,6 +53,7 @@ mod palette;
5353 mod remote;
5454 mod sidebar;
5555 mod subagents;
56+ mod surfaces;
5657 mod terminal_drawer;
5758
5859 use actions::UndoEntry;
@@ -219,10 +220,11 @@ pub struct Workspace {
219220 search: Entity<InputState>,
220221 sidebar_open: bool,
221222 panel_open: bool,
222- /// The browser takes the project panel's place while open.
223- browser_open: bool,
224- /// The subagents panel, in the project panel's place.
225- subagents_open: bool,
223+ /// What the right panel shows, in the project panel's place: a surface
224+ /// (subagents, browser, terminal) or the list to pick one from.
225+ right_panel: Option<surfaces::Surface>,
226+ /// The surface picker's focus, for its keys.
227+ surface_focus: FocusHandle,
226228 settled_open: bool,
227229 snoozed_open: bool,
228230 working_open: bool,
@@ -376,8 +378,8 @@ impl Workspace {
376378 search,
377379 sidebar_open: true,
378380 panel_open: true,
379- browser_open: false,
380- subagents_open: false,
381+ right_panel: None,
382+ surface_focus: cx.focus_handle(),
381383 settled_open: false,
382384 snoozed_open: false,
383385 working_open: false,
@@ -604,7 +606,7 @@ impl Workspace {
604606 }
605607 // Working threads show how long they've worked.
606608 let working = self.projects.iter().flat_map(|project| &project.sessions).any(|session| session.read(cx).is_working());
607- if working && (self.sidebar_open || self.subagents_open) {
609+ if working && (self.sidebar_open || self.surface_open(surfaces::Surface::Subagents)) {
608610 cx.notify();
609611 }
610612 // Repaint about once a minute so thread ages stay current.
@@ -1761,10 +1763,9 @@ impl Workspace {
17611763 }
17621764
17631765 fn toggle_project_panel(&mut self, cx: &mut Context<Self>) {
1764- // With the browser in its place, the project panel comes back.
1765- if self.browser_open || self.subagents_open {
1766- self.browser_open = false;
1767- self.subagents_open = false;
1766+ // With a surface in its place, the project panel comes back.
1767+ if self.right_panel.is_some() {
1768+ self.right_panel = None;
17681769 self.panel_open = true;
17691770 } else {
17701771 self.panel_open = !self.panel_open;
@@ -1775,7 +1776,7 @@ impl Workspace {
17751776 /// The open thread's browser panel while it shows, made on first use.
17761777 /// The others let go of their streams.
17771778 fn shown_browser(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Option<Entity<BrowserPanel>> {
1778- let session = self.active.clone().filter(|_| self.browser_open && !self.settings_open);
1779+ let session = self.active.clone().filter(|_| self.surface_open(surfaces::Surface::Browser) && !self.settings_open);
17791780 let panel = session.map(|session| {
17801781 let id = session.entity_id();
17811782 if let Some(panel) = self.browsers.get(&id) {
@@ -1785,7 +1786,7 @@ impl Workspace {
17851786 let panel = cx.new(|cx| BrowserPanel::new(&thread_id, window, cx));
17861787 let subscription = cx.subscribe(&panel, |this, _, event: &BrowserPanelEvent, cx| match event {
17871788 BrowserPanelEvent::Close => {
1788- this.browser_open = false;
1789+ this.right_panel = None;
17891790 cx.notify();
17901791 }
17911792 });
@@ -2082,14 +2083,16 @@ impl Render for Workspace {
20822083 cx.set_global(directory);
20832084 let browser = self.shown_browser(window, cx);
20842085 let p = metal::palette(cx);
2085- let subagents_visible = self.subagents_open && browser.is_none();
2086+ let subagents_visible = self.surface_open(surfaces::Surface::Subagents);
2087+ let picker_visible = self.surface_open(surfaces::Surface::Picker) && !self.settings_open;
2088+ let docked_terminal =
2089+ if self.surface_open(surfaces::Surface::Terminal) { self.render_terminal_drawer(true, window, cx) } else { None };
20862090 let panel_visible = self.panel_open
2087- && !subagents_visible
2088- && browser.is_none()
2091+ && self.right_panel.is_none()
20892092 && self.active_project().is_some()
20902093 && window.viewport_size().width >= px(PANEL_MIN_WINDOW);
20912094 // The browser gives way before the conversation gets too narrow.
2092- let chat_min = if browser.is_some() { px(360.) } else { px(0.) };
2095+ let chat_min = if browser.is_some() || docked_terminal.is_some() { px(360.) } else { px(0.) };
20932096
20942097 h_flex()
20952098 .key_context("Workspace")
@@ -2134,10 +2137,7 @@ impl Render for Workspace {
21342137 }
21352138 }))
21362139 .on_action(cx.listener(|this, _: &OpenUsage, window, cx| this.open_settings_page("Usage", window, cx)))
2137- .on_action(cx.listener(|this, _: &ToggleRightPanel, _, cx| {
2138- this.browser_open = !this.browser_open;
2139- cx.notify();
2140- }))
2140+ .on_action(cx.listener(|this, _: &ToggleRightPanel, window, cx| this.toggle_right_panel(window, cx)))
21412141 .on_action(cx.listener(|this, _: &SelectTheme, window, cx| {
21422142 this.open_palette(palette::PaletteView::Themes, window, cx)
21432143 }))
@@ -2182,10 +2182,11 @@ impl Render for Workspace {
21822182 cx.notify();
21832183 }))
21842184 .on_action(cx.listener(|this, _: &ToggleProjectPanel, _, cx| this.toggle_project_panel(cx)))
2185- .on_action(cx.listener(|this, _: &ToggleSubagents, _, cx| this.toggle_subagents(cx)))
2186- .on_action(cx.listener(|this, _: &ToggleBrowser, _, cx| {
2187- this.browser_open = !this.browser_open;
2188- cx.notify();
2185+ .on_action(cx.listener(|this, _: &ToggleSubagents, window, cx| {
2186+ this.toggle_surface(surfaces::Surface::Subagents, window, cx)
2187+ }))
2188+ .on_action(cx.listener(|this, _: &ToggleBrowser, window, cx| {
2189+ this.toggle_surface(surfaces::Surface::Browser, window, cx)
21892190 }))
21902191 .on_action(cx.listener(|this, _: &FocusSearch, window, cx| {
21912192 this.settings_open = false;
@@ -2230,10 +2231,12 @@ impl Render for Workspace {
22302231 Some(chat) => this.child(chat),
22312232 None => this.child(self.render_empty_sheet(cx)),
22322233 }))
2233- .children(self.render_terminal_drawer(window, cx)),
2234+ .children(self.render_terminal_drawer(false, window, cx)),
22342235 )
22352236 .when(panel_visible, |this| this.child(self.render_panel(cx)))
22362237 .when(subagents_visible, |this| this.child(self.render_subagents_panel(cx)))
2238+ .when(picker_visible, |this| this.child(self.render_surface_picker(cx)))
2239+ .children(docked_terminal)
22372240 .when_some(browser, |this, browser| this.child(browser)),
22382241 ),
22392242 ),
+12−26src/ui/workspace/header.rsView File
@@ -9,6 +9,7 @@ use gpui_kit::prelude::FluentBuilder as _;
99 use gpui_kit::*;
1010
1111 use super::Workspace;
12+ use super::surfaces::Surface;
1213 use crate::Dismiss;
1314 use crate::ui::browser::BrowserWatch;
1415 use crate::ui::icons::Lucide;
@@ -236,10 +237,12 @@ impl Workspace {
236237 /// drawer, and the right panel (the browser), plus the subagents panel.
237238 fn render_layout_controls(&self, panel_visible: bool, cx: &mut Context<Self>) -> impl IntoElement {
238239 let p = metal::palette(cx);
239- let subagents_shown = self.subagents_open && !self.browser_open;
240- let subagents_working = !subagents_shown && self.working_subagents(cx) > 0;
241- let live = !self.browser_open
240+ // While the panel shows something else, its light says subagents are
241+ // working, or the agent is using the browser.
242+ let subagents_working = !self.surface_open(Surface::Subagents) && self.working_subagents(cx) > 0;
243+ let browser_live = !self.surface_open(Surface::Browser)
242244 && self.active.as_ref().is_some_and(|session| BrowserWatch::is_live(&session.read(cx).id, cx));
245+ let live = subagents_working || browser_live;
243246 h_flex()
244247 .flex_none()
245248 .gap(px(4.))
@@ -252,28 +255,12 @@ impl Workspace {
252255 .tooltip("Toggle thread details (Ctrl+Shift+B)")
253256 .on_click(cx.listener(|this, _, _, cx| this.toggle_project_panel(cx))),
254257 )
255- .child(
256- div()
257- .relative()
258- .child(
259- MetalButton::new("toggle-subagents")
260- .ghost()
261- .mini()
262- .icon(Lucide::Bot)
263- .selected(subagents_shown)
264- .tooltip("Toggle subagents (Ctrl+Alt+S)")
265- .on_click(cx.listener(|this, _, _, cx| this.toggle_subagents(cx))),
266- )
267- .when(subagents_working, |this| {
268- this.child(div().absolute().top(px(1.)).right(px(1.)).child(metal::led("subagents-live", Led::Live, true, p)))
269- }),
270- )
271258 .child(if self.terminal_available(cx) {
272259 MetalButton::new("toggle-terminal-drawer")
273260 .ghost()
274261 .mini()
275262 .icon(IconName::PanelBottom)
276- .selected(self.terminal_open())
263+ .selected(self.terminal_open() || self.surface_open(Surface::Terminal))
277264 .tooltip("Toggle terminal drawer (Ctrl+J)")
278265 .on_click(cx.listener(|this, _, window, cx| this.toggle_terminal(window, cx)))
279266 .into_any_element()
@@ -293,15 +280,14 @@ impl Workspace {
293280 .ghost()
294281 .mini()
295282 .icon(IconName::PanelRight)
296- .selected(self.browser_open)
283+ .selected(self.right_panel.is_some())
297284 .tooltip("Toggle right panel (Ctrl+Alt+B)")
298- .on_click(cx.listener(|this, _, _, cx| {
299- this.browser_open = !this.browser_open;
300- cx.notify();
301- })),
285+ .on_click(cx.listener(|this, _, window, cx| this.toggle_right_panel(window, cx))),
302286 )
303287 .when(live, |this| {
304- this.child(div().absolute().top(px(1.)).right(px(1.)).child(metal::led("browser-live", Led::Live, false, p)))
288+ this.child(
289+ div().absolute().top(px(1.)).right(px(1.)).child(metal::led("right-panel-live", Led::Live, subagents_working, p)),
290+ )
305291 }),
306292 )
307293 }
+1−12src/ui/workspace/subagents.rsView File
@@ -92,17 +92,6 @@ impl Workspace {
9292 })
9393 }
9494
95- pub(super) fn toggle_subagents(&mut self, cx: &mut Context<Self>) {
96- // With the browser in its place, the subagents come back.
97- if self.browser_open {
98- self.browser_open = false;
99- self.subagents_open = true;
100- } else {
101- self.subagents_open = !self.subagents_open;
102- }
103- cx.notify();
104- }
105-
10695 /// Opens a subagent's thread, if it isn't open, and shows its call; or
10796 /// for a task delegated to the app, the thread it runs in.
10897 fn reveal_subagent(
@@ -156,7 +145,7 @@ impl Workspace {
156145 .icon(IconName::Close)
157146 .tooltip("Close subagents")
158147 .on_click(cx.listener(|this, _, _, cx| {
159- this.subagents_open = false;
148+ this.right_panel = None;
160149 cx.notify();
161150 })),
162151 );
+160−0src/ui/workspace/surfaces.rsaddedView File
@@ -0,0 +1,160 @@
1+ //! The right panel, as T3 Code has it: it shows one surface at a time (the
2+ //! subagents, the browser or a terminal), and opens on "Open a surface", a
3+ //! short list to pick one from, each with a key of its own.
4+
5+ use gpui_kit::component::{Icon, IconName, h_flex, v_flex};
6+ use gpui_kit::prelude::FluentBuilder as _;
7+ use gpui_kit::*;
8+
9+ use super::Workspace;
10+ use crate::ui::icons::Lucide;
11+ use crate::ui::metal::{self, MetalStyled as _};
12+
13+ /// What the right panel shows.
14+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
15+ pub(super) enum Surface {
16+ /// "Open a surface": the list of the others.
17+ Picker,
18+ Subagents,
19+ Browser,
20+ Terminal,
21+ }
22+
23+ /// The surfaces the picker lists: each with its icon, name and key.
24+ const SURFACES: [(Surface, &str, &str); 3] =
25+ [(Surface::Subagents, "Subagents", "S"), (Surface::Browser, "Browser", "B"), (Surface::Terminal, "Terminal", "T")];
26+
27+ /// How wide a terminal in the right panel is.
28+ pub(super) const TERMINAL_PANEL_WIDTH: f32 = 520.;
29+
30+ impl Workspace {
31+ pub(super) fn surface_open(&self, surface: Surface) -> bool {
32+ self.right_panel == Some(surface)
33+ }
34+
35+ /// The right panel's button: opens "Open a surface", or closes the panel.
36+ pub(super) fn toggle_right_panel(&mut self, window: &mut Window, cx: &mut Context<Self>) {
37+ if self.right_panel.is_some() {
38+ self.right_panel = None;
39+ } else {
40+ self.right_panel = Some(Surface::Picker);
41+ self.surface_focus.focus(window, cx);
42+ }
43+ cx.notify();
44+ }
45+
46+ /// Shows a surface in the right panel, or closes the panel if it's
47+ /// showing it already.
48+ pub(super) fn toggle_surface(&mut self, surface: Surface, window: &mut Window, cx: &mut Context<Self>) {
49+ if self.right_panel == Some(surface) {
50+ self.right_panel = None;
51+ cx.notify();
52+ return;
53+ }
54+ self.open_surface(surface, window, cx);
55+ }
56+
57+ pub(super) fn open_surface(&mut self, surface: Surface, window: &mut Window, cx: &mut Context<Self>) {
58+ if surface == Surface::Terminal {
59+ if !self.terminal_available(cx) {
60+ return;
61+ }
62+ // The terminals move over from the drawer, if it's open.
63+ self.dock_terminal(window, cx);
64+ }
65+ self.right_panel = Some(surface);
66+ if surface == Surface::Picker {
67+ self.surface_focus.focus(window, cx);
68+ }
69+ cx.notify();
70+ }
71+
72+ /// The picker's keys: S, B and T open their surfaces; Escape closes it.
73+ fn surface_key(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context<Self>) {
74+ let keystroke = &event.keystroke;
75+ if keystroke.modifiers.modified() {
76+ return;
77+ }
78+ if keystroke.key == "escape" {
79+ self.right_panel = None;
80+ cx.notify();
81+ return;
82+ }
83+ if let Some((surface, _, _)) = SURFACES.iter().find(|(_, _, key)| key.eq_ignore_ascii_case(&keystroke.key)) {
84+ cx.stop_propagation();
85+ self.open_surface(*surface, window, cx);
86+ }
87+ }
88+
89+ pub(super) fn render_surface_picker(&self, cx: &mut Context<Self>) -> impl IntoElement {
90+ let p = metal::palette(cx);
91+ let terminal = self.terminal_available(cx);
92+ let rows = SURFACES.iter().map(|(surface, name, key)| {
93+ let surface = *surface;
94+ let enabled = surface != Surface::Terminal || terminal;
95+ let icon = match surface {
96+ Surface::Subagents => Icon::new(Lucide::Bot),
97+ Surface::Browser => Icon::new(IconName::Globe),
98+ _ => Icon::new(Lucide::SquareTerminal),
99+ };
100+ h_flex()
101+ .id(SharedString::from(format!("surface-{name}")))
102+ .h(px(31.))
103+ .px(px(10.))
104+ .gap(px(10.))
105+ .rounded(px(8.))
106+ .text_size(px(13.5))
107+ .text_color(if enabled { p.ink } else { p.ink3 })
108+ .when(enabled, |this| {
109+ this.cursor_pointer()
110+ .hover(|style| style.bg(p.row_hover))
111+ .on_click(cx.listener(move |this, _, window, cx| this.open_surface(surface, window, cx)))
112+ })
113+ .child(icon.size(px(16.)).text_color(if enabled { p.ink2 } else { p.ink3 }))
114+ .child(div().flex_1().child(*name))
115+ .child(
116+ div()
117+ .flex_none()
118+ .min_w(px(18.))
119+ .h(px(18.))
120+ .px(px(5.))
121+ .flex()
122+ .items_center()
123+ .justify_center()
124+ .rounded(px(4.))
125+ .bg(p.row_hover)
126+ .font_family(metal::MONO)
127+ .text_size(px(10.5))
128+ .text_color(p.ink3)
129+ .child(*key),
130+ )
131+ });
132+ v_flex()
133+ .id("surface-picker")
134+ .key_context("SurfacePicker")
135+ .track_focus(&self.surface_focus)
136+ .on_key_down(cx.listener(Self::surface_key))
137+ .w(px(super::PANEL_WIDTH))
138+ .flex_none()
139+ .h_full()
140+ .p(px(12.))
141+ .pl(px(0.))
142+ .child(
143+ v_flex()
144+ .flex_1()
145+ .justify_center()
146+ .px(px(16.))
147+ .rounded(px(18.))
148+ .raised(p)
149+ .child(
150+ div()
151+ .pb(px(14.))
152+ .text_center()
153+ .text_size(px(14.))
154+ .font_weight(FontWeight::MEDIUM)
155+ .child("Open a surface"),
156+ )
157+ .child(v_flex().gap(px(2.)).children(rows)),
158+ )
159+ }
160+ }
+67−11src/ui/workspace/terminal_drawer.rsView File
@@ -13,6 +13,7 @@ use gpui_kit::component::{Icon, h_flex, tooltip::Tooltip, v_flex};
1313 use gpui_kit::prelude::FluentBuilder as _;
1414 use gpui_kit::*;
1515
16+ use super::surfaces::{Surface, TERMINAL_PANEL_WIDTH};
1617 use super::{Workspace, actions};
1718 use crate::ui::icons::Lucide;
1819 use crate::ui::metal::{self, MetalButton, Palette};
@@ -174,6 +175,13 @@ impl Workspace {
174175 if self.settings_open {
175176 return;
176177 }
178+ // In the right panel, it hides from there.
179+ if self.surface_open(Surface::Terminal) {
180+ self.right_panel = None;
181+ self.focus_active_composer(window, cx);
182+ cx.notify();
183+ return;
184+ }
177185 let Some(session) = self.active.as_ref().map(|session| session.entity_id()) else { return };
178186 if self.terminal_open() {
179187 if let Some(drawer) = self.terminals.get_mut(&session) {
@@ -196,10 +204,29 @@ impl Workspace {
196204 cx.notify();
197205 }
198206
207+ /// Shows the open thread's terminals in the right panel instead of the
208+ /// drawer, starting one if it has none.
209+ pub(super) fn dock_terminal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
210+ let Some(session) = self.active.as_ref().map(|session| session.entity_id()) else { return };
211+ let drawer = self.terminals.entry(session).or_default();
212+ drawer.open = false;
213+ let empty = drawer.layout.is_empty();
214+ self.right_panel = Some(Surface::Terminal);
215+ if empty {
216+ self.new_terminal(window, cx);
217+ } else {
218+ self.focus_terminal(window, cx);
219+ }
220+ }
221+
199222 /// T3's terminal.new: a terminal in a group of its own.
200223 pub(super) fn new_terminal(&mut self, window: &mut Window, cx: &mut Context<Self>) {
201224 let Some(session) = self.active.as_ref().map(|session| session.entity_id()) else { return };
202- let Some(drawer) = self.terminals.get_mut(&session).filter(|drawer| drawer.open) else { return };
225+ let docked = self.surface_open(Surface::Terminal);
226+ if docked {
227+ self.terminals.entry(session).or_default();
228+ }
229+ let Some(drawer) = self.terminals.get_mut(&session).filter(|drawer| drawer.open || docked) else { return };
203230 let id = drawer.layout.new_terminal();
204231 self.start_terminal(session, id, window, cx);
205232 }
@@ -207,7 +234,8 @@ impl Workspace {
207234 /// T3's terminal.split and terminal.splitVertical.
208235 pub(super) fn split_terminal(&mut self, vertical: bool, window: &mut Window, cx: &mut Context<Self>) {
209236 let Some(session) = self.active.as_ref().map(|session| session.entity_id()) else { return };
210- let Some(drawer) = self.terminals.get_mut(&session).filter(|drawer| drawer.open) else { return };
237+ let docked = self.surface_open(Surface::Terminal);
238+ let Some(drawer) = self.terminals.get_mut(&session).filter(|drawer| drawer.open || docked) else { return };
211239 let Some(id) = drawer.layout.split(vertical) else { return };
212240 self.start_terminal(session, id, window, cx);
213241 }
@@ -284,6 +312,9 @@ impl Workspace {
284312 let empty = drawer.layout.is_empty();
285313 if empty {
286314 self.terminals.remove(&session);
315+ if self.surface_open(Surface::Terminal) && self.active.as_ref().is_some_and(|active| active.entity_id() == session) {
316+ self.right_panel = None;
317+ }
287318 }
288319 if self.active.as_ref().is_some_and(|active| active.entity_id() == session) {
289320 let refocus = move |this: &mut Self, window: &mut Window, cx: &mut Context<Self>| {
@@ -328,9 +359,19 @@ impl Workspace {
328359 }
329360
330361 /// The open thread's drawer, when it shows.
331- pub(super) fn render_terminal_drawer(&self, window: &mut Window, cx: &mut Context<Self>) -> Option<AnyElement> {
362+ /// The open thread's terminals: in the drawer under the conversation,
363+ /// or `docked` in the right panel.
364+ pub(super) fn render_terminal_drawer(&self, docked: bool, window: &mut Window, cx: &mut Context<Self>) -> Option<AnyElement> {
332365 let session = self.active.as_ref()?.entity_id();
333- let drawer = self.terminals.get(&session).filter(|drawer| drawer.open)?;
366+ let no_terminals = TerminalDrawer::default();
367+ let drawer = match self.terminals.get(&session) {
368+ Some(drawer) if docked || drawer.open => drawer,
369+ None if docked => &no_terminals,
370+ _ => return None,
371+ };
372+ if !docked && self.surface_open(Surface::Terminal) {
373+ return None;
374+ }
334375 let p = metal::palette(cx);
335376 let height = clamp_height(self.terminal_height, window.viewport_size().height.as_f32());
336377 let layout = &drawer.layout;
@@ -351,11 +392,26 @@ impl Workspace {
351392 cx.notify();
352393 }),
353394 );
354- let base = v_flex().relative().flex_none().w_full().h(px(height)).overflow_hidden().child(metal::rule(p));
395+ let base = v_flex().relative().overflow_hidden().map(|this| {
396+ if docked {
397+ this.size_full().rounded(px(18.)).border_1().border_color(p.rule)
398+ } else {
399+ this.flex_none().w_full().h(px(height)).child(metal::rule(p))
400+ }
401+ });
402+ // Docked, it fills the panel and has no edge to drag.
403+ let handle = (!docked).then_some(handle);
404+ let finish = move |body: AnyElement| -> AnyElement {
405+ if docked {
406+ div().w(px(TERMINAL_PANEL_WIDTH)).flex_none().h_full().p(px(12.)).pl(px(0.)).child(body).into_any_element()
407+ } else {
408+ body
409+ }
410+ };
355411
356412 if layout.is_empty() {
357- return Some(
358- base.child(handle)
413+ return Some(finish(
414+ base.children(handle)
359415 .child(
360416 v_flex()
361417 .flex_1()
@@ -376,7 +432,7 @@ impl Workspace {
376432 ),
377433 )
378434 .into_any_element(),
379- );
435+ ));
380436 }
381437
382438 let active = layout.active;
@@ -423,8 +479,8 @@ impl Workspace {
423479 .child(div().flex_1().min_w_0().h_full().child(grid))
424480 .when(has_sidebar, |this| this.child(self.render_terminal_sidebar(drawer, at_limit, cx)));
425481
426- Some(
427- base.child(handle)
482+ Some(finish(
483+ base.children(handle)
428484 .child(body)
429485 .when(!has_sidebar, |this| {
430486 this.child(
@@ -450,7 +506,7 @@ impl Workspace {
450506 )
451507 })
452508 .into_any_element(),
453- )
509+ ))
454510 }
455511
456512 /// The list of groups beside the terminals once there's more than one.