Skip to content

Commit 65417fb

Browse files
committed
Tabs: ⌘⇧T reopens closed tabs, dblclick bar = new tab
- Per-pane in-session closed-tab stack on `TabManager`. Restores path, view mode, sort, pin, cursor, history, original index. Capped via the new `fileExplorer.tabs.closedTabHistorySize` setting (default 10, range 1–50). Lowering the cap trims both panes' stacks immediately. - `closeOtherTabs` pushes right-to-left so pressing ⌘⇧T N times rebuilds the exact pre-close arrangement. - Native menu item "Reopen closed tab" (⌘⇧T) under Tab, disabled when the focused pane's stack is empty (mirrors the `pin_tab` enable-state pattern via a new `set_reopen_closed_tab_enabled` Tauri command). - Command palette + shortcut registry entries for `tab.reopen`. New `reopen` action on the MCP `tab` tool (per-pane, no `tab_id`). - Double-click on empty tab bar (padding, trailing flex space, top spacer) creates a new tab. Skipped on `.tab`, `.close-btn`, `.new-tab-btn`. - Tests: 12 new in `tab-state-manager.test.ts` (push order on close-others, original-index restore, cap behavior, pinned-survives-closeOthers, trim), 5 in `TabBar.a11y.test.ts`, 1 MCP schema assertion, plus a `setReopenClosedTabEnabled` mock for `DualPaneExplorer.test.ts`. - Docs updated in `tabs/CLAUDE.md` and `menu/CLAUDE.md`.
1 parent 6b6c933 commit 65417fb

25 files changed

Lines changed: 708 additions & 52 deletions

apps/desktop/src-tauri/src/commands/ui.rs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
use crate::ignore_poison::IgnorePoison;
22
use crate::menu::{
3-
CLOSE_TAB_ID, CommandScope, FileContextInfo, MenuState, ViewMode, build_breadcrumb_context_menu,
4-
build_context_menu, build_network_host_context_menu, build_tab_context_menu, frontend_shortcut_to_accelerator,
5-
menu_id_to_command, rebuild_view_mode_items, sync_view_mode_check_states,
3+
CLOSE_TAB_ID, CommandScope, FileContextInfo, MenuState, REOPEN_CLOSED_TAB_ID, ViewMode,
4+
build_breadcrumb_context_menu, build_context_menu, build_network_host_context_menu, build_tab_context_menu,
5+
frontend_shortcut_to_accelerator, menu_id_to_command, rebuild_view_mode_items, sync_view_mode_check_states,
66
};
77
#[cfg(any(target_os = "macos", target_os = "linux"))]
88
use std::process::Command;
@@ -401,6 +401,20 @@ pub fn update_pin_tab_menu<R: Runtime>(app: AppHandle<R>, is_pinned: bool) -> Re
401401
item.set_text(label).map_err(|e| e.to_string())
402402
}
403403

404+
/// Enables or disables the Tab menu "Reopen closed tab" item based on whether the
405+
/// focused pane's closed-tab stack has entries. Mirrors the dynamic-label pattern
406+
/// used by `update_pin_tab_menu`.
407+
#[tauri::command]
408+
#[specta::specta]
409+
pub fn set_reopen_closed_tab_enabled<R: Runtime>(app: AppHandle<R>, enabled: bool) -> Result<(), String> {
410+
let menu_state = app.state::<MenuState<R>>();
411+
let guard = menu_state.reopen_closed_tab.lock_ignore_poison();
412+
let Some(item) = guard.as_ref() else {
413+
return Err("Menu not initialized".to_string());
414+
};
415+
item.set_enabled(enabled).map_err(|e| e.to_string())
416+
}
417+
404418
/// Enables or disables explorer-scoped menu items based on the current context.
405419
/// - `"explorer"`: all menu items enabled (main file explorer has focus)
406420
/// - `"other"`: all non-App items disabled except Close tab (⌘W), which doubles as
@@ -417,6 +431,12 @@ pub fn set_menu_context<R: Runtime>(app: AppHandle<R>, context: String) -> Resul
417431
if id == CLOSE_TAB_ID {
418432
continue;
419433
}
434+
// Reopen closed tab is managed exclusively by `set_reopen_closed_tab_enabled` —
435+
// skip it here so an "explorer" context switch doesn't enable it while the
436+
// focused pane's closed-tab stack is empty.
437+
if id == REOPEN_CLOSED_TAB_ID {
438+
continue;
439+
}
420440
let is_app = matches!(menu_id_to_command(id), Some((_, CommandScope::App)));
421441
if !is_app {
422442
let _ = entry.item.set_enabled(enabled);

apps/desktop/src-tauri/src/ipc.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,9 @@ fn collect_cross_platform_types(types: &mut Types) -> Vec<Function> {
129129
crate::commands::icons::clear_extension_icon_cache,
130130
crate::commands::icons::clear_directory_icon_cache,
131131
// show_file_context_menu, show_breadcrumb_context_menu, update_pin_tab_menu,
132-
// show_main_window, update_menu_context, set_menu_context, toggle_hidden_files,
133-
// update_view_mode_menu, copy_to_clipboard are generic (<R: Runtime>) — excluded
132+
// set_reopen_closed_tab_enabled, show_main_window, update_menu_context,
133+
// set_menu_context, toggle_hidden_files, update_view_mode_menu, copy_to_clipboard
134+
// are generic (<R: Runtime>) — excluded
134135
crate::commands::ui::show_tab_context_menu,
135136
crate::commands::ui::show_network_host_context_menu,
136137
crate::commands::ui::show_in_finder,
@@ -586,6 +587,7 @@ pub fn builder() -> Builder<tauri::Wry> {
586587
crate::commands::ui::show_tab_context_menu,
587588
crate::commands::ui::show_network_host_context_menu,
588589
crate::commands::ui::update_pin_tab_menu,
590+
crate::commands::ui::set_reopen_closed_tab_enabled,
589591
crate::commands::ui::show_main_window,
590592
crate::commands::ui::update_menu_context,
591593
crate::commands::ui::set_menu_context,

apps/desktop/src-tauri/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,7 @@ pub fn run() {
537537
*menu_state.view_left_pane_submenu.lock_ignore_poison() = Some(menu_items.view_left_pane_submenu);
538538
*menu_state.view_right_pane_submenu.lock_ignore_poison() = Some(menu_items.view_right_pane_submenu);
539539
*menu_state.pin_tab.lock_ignore_poison() = Some(menu_items.pin_tab);
540+
*menu_state.reopen_closed_tab.lock_ignore_poison() = Some(menu_items.reopen_closed_tab);
540541
*menu_state.items.lock_ignore_poison() = menu_items.items;
541542
*menu_state.sort_submenu.lock_ignore_poison() = Some(menu_items.sort_submenu);
542543
app.manage(menu_state);

apps/desktop/src-tauri/src/mcp/executor/app.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ pub fn execute_tab<R: Runtime>(app: &AppHandle<R>, params: &Value) -> ToolResult
5959
"activate" => tab_id
6060
.ok_or_else(|| ToolError::invalid_params("'tab_id' is required for activate"))?
6161
.to_string(),
62-
"new" => String::new(), // not used
62+
"new" | "reopen" => String::new(), // not used
6363
_ => {
6464
// close, close_others, set_pinned: default to active tab
6565
if let Some(id) = tab_id {
@@ -84,6 +84,7 @@ pub fn execute_tab<R: Runtime>(app: &AppHandle<R>, params: &Value) -> ToolResult
8484

8585
// Validate tab_id exists (for actions that need it)
8686
if action != "new"
87+
&& action != "reopen"
8788
&& !resolved_tab_id.is_empty()
8889
&& let Some(store) = app.try_state::<PaneStateStore>()
8990
{
@@ -108,6 +109,10 @@ pub fn execute_tab<R: Runtime>(app: &AppHandle<R>, params: &Value) -> ToolResult
108109
app.emit("mcp-tab", json!({"action": "new", "pane": pane}))?;
109110
Ok(json!(format!("OK: Creating new tab in {} pane", pane)))
110111
}
112+
"reopen" => {
113+
app.emit("mcp-tab", json!({"action": "reopen", "pane": pane}))?;
114+
Ok(json!(format!("OK: Reopening last closed tab in {} pane", pane)))
115+
}
111116
"close" => {
112117
app.emit(
113118
"mcp-tab",

apps/desktop/src-tauri/src/mcp/tools.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,13 @@ impl Tool {
3535
fn get_tab_tools() -> Vec<Tool> {
3636
vec![Tool {
3737
name: "tab".to_string(),
38-
description: "Create, close, activate, or pin tabs".to_string(),
38+
description: "Create, close, activate, pin, or reopen tabs".to_string(),
3939
input_schema: json!({
4040
"type": "object",
4141
"properties": {
4242
"action": {
4343
"type": "string",
44-
"enum": ["new", "close", "close_others", "activate", "set_pinned"],
44+
"enum": ["new", "close", "close_others", "activate", "set_pinned", "reopen"],
4545
"description": "Action to perform on the tab"
4646
},
4747
"pane": {
@@ -51,7 +51,7 @@ fn get_tab_tools() -> Vec<Tool> {
5151
},
5252
"tab_id": {
5353
"type": "string",
54-
"description": "Tab ID. Defaults to active tab for close, close_others, set_pinned. Required for activate."
54+
"description": "Tab ID. Defaults to active tab for close, close_others, set_pinned. Required for activate. Not used for new or reopen."
5555
},
5656
"pinned": {
5757
"type": "boolean",
@@ -625,6 +625,7 @@ mod tests {
625625
assert!(action_enum.contains(&json!("close_others")));
626626
assert!(action_enum.contains(&json!("activate")));
627627
assert!(action_enum.contains(&json!("set_pinned")));
628+
assert!(action_enum.contains(&json!("reopen")));
628629

629630
let pane_enum = props.get("pane").unwrap().get("enum").unwrap().as_array().unwrap();
630631
assert!(pane_enum.contains(&json!("left")));

apps/desktop/src-tauri/src/menu/CLAUDE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,3 +201,9 @@ also Window and Help.
201201
conflict with the custom MenuItem.
202202
- **Pin tab label**: `pin_tab` in MenuState is updated dynamically by the frontend to show
203203
"Pin tab" or "Unpin tab" based on the active tab's state.
204+
- **Reopen closed tab item**: The Tab submenu includes "Reopen closed tab" (⌘⇧T on macOS) between
205+
Close tab and the Next/Previous tab pair. The item is created **disabled** and toggled live via
206+
`set_reopen_closed_tab_enabled(enabled: bool)` — same dynamic-state pattern as `pin_tab`'s label.
207+
`MenuState.reopen_closed_tab` holds the `MenuItem` reference. The frontend pushes enable state
208+
after every close, reopen, and focus change so the menu always reflects the focused pane's
209+
closed-tab stack.

apps/desktop/src-tauri/src/menu/linux.rs

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ use super::{
1010
COPY_PATH_ID, DESELECT_ALL_ID, EDIT_COPY_ID, EDIT_CUT_ID, EDIT_ID, EDIT_PASTE_ID, EDIT_PASTE_MOVE_ID,
1111
ENTER_LICENSE_KEY_ID, FILE_COPY_ID, FILE_DELETE_ID, FILE_DELETE_PERMANENTLY_ID, FILE_MOVE_ID, FILE_NEW_FOLDER_ID,
1212
FILE_VIEW_ID, GET_INFO_ID, GO_BACK_ID, GO_FORWARD_ID, GO_PARENT_ID, HELP_SEND_ERROR_REPORT_ID, MenuItems,
13-
NEW_TAB_ID, NEXT_TAB_ID, OPEN_ID, PIN_TAB_MENU_ID, PREV_TAB_ID, QUICK_LOOK_ID, RENAME_ID, SEARCH_FILES_ID,
14-
SELECT_ALL_ID, SETTINGS_ID, SHOW_HIDDEN_FILES_ID, SHOW_IN_FINDER_ID, SORT_BY_EXTENSION_ID, SORT_BY_MODIFIED_ID,
15-
SORT_BY_NAME_ID, SORT_BY_SIZE_ID, SWAP_PANES_ID, SWITCH_PANE_ID, VIEW_MODE_BRIEF_LEFT_ID, VIEW_MODE_BRIEF_RIGHT_ID,
16-
VIEW_MODE_FULL_LEFT_ID, VIEW_MODE_FULL_RIGHT_ID, ViewMode, brief_view_label, build_sort_submenu,
17-
build_zoom_submenu, copy_path_accelerator, full_view_label, register_item, show_in_file_manager_accelerator,
18-
show_in_file_manager_label,
13+
NEW_TAB_ID, NEXT_TAB_ID, OPEN_ID, PIN_TAB_MENU_ID, PREV_TAB_ID, QUICK_LOOK_ID, RENAME_ID, REOPEN_CLOSED_TAB_ID,
14+
SEARCH_FILES_ID, SELECT_ALL_ID, SETTINGS_ID, SHOW_HIDDEN_FILES_ID, SHOW_IN_FINDER_ID, SORT_BY_EXTENSION_ID,
15+
SORT_BY_MODIFIED_ID, SORT_BY_NAME_ID, SORT_BY_SIZE_ID, SWAP_PANES_ID, SWITCH_PANE_ID, VIEW_MODE_BRIEF_LEFT_ID,
16+
VIEW_MODE_BRIEF_RIGHT_ID, VIEW_MODE_FULL_LEFT_ID, VIEW_MODE_FULL_RIGHT_ID, ViewMode, brief_view_label,
17+
build_sort_submenu, build_zoom_submenu, copy_path_accelerator, full_view_label, register_item,
18+
show_in_file_manager_accelerator, show_in_file_manager_label,
1919
};
2020

2121
/// Linux menu: builds all menus from scratch, matching the macOS menu structure.
@@ -260,6 +260,14 @@ pub(crate) fn build_menu_linux<R: Runtime>(
260260
// --- Tab menu ---
261261
let new_tab_item = MenuItem::with_id(app, NEW_TAB_ID, "&New tab", true, Some("Cmd+T"))?;
262262
let close_tab_item = MenuItem::with_id(app, CLOSE_TAB_ID, "&Close tab", true, Some("Cmd+W"))?;
263+
// Disabled initially; frontend enables it after the first close.
264+
let reopen_closed_tab_item = MenuItem::with_id(
265+
app,
266+
REOPEN_CLOSED_TAB_ID,
267+
"&Reopen closed tab",
268+
false,
269+
Some("Cmd+Shift+T"),
270+
)?;
263271
let next_tab_item = MenuItem::with_id(app, NEXT_TAB_ID, "Ne&xt tab", true, Some("Ctrl+Tab"))?;
264272
let prev_tab_item = MenuItem::with_id(app, PREV_TAB_ID, "&Previous tab", true, Some("Ctrl+Shift+Tab"))?;
265273
let pin_tab_item = MenuItem::with_id(app, PIN_TAB_MENU_ID, "P&in tab", true, None::<&str>)?;
@@ -272,6 +280,7 @@ pub(crate) fn build_menu_linux<R: Runtime>(
272280
&[
273281
&new_tab_item,
274282
&close_tab_item,
283+
&reopen_closed_tab_item,
275284
&PredefinedMenuItem::separator(app)?,
276285
&next_tab_item,
277286
&prev_tab_item,
@@ -379,12 +388,13 @@ pub(crate) fn build_menu_linux<R: Runtime>(
379388
register_item(&mut items, GO_FORWARD_ID, &go_forward_item, &go_menu, 1);
380389
register_item(&mut items, GO_PARENT_ID, &go_parent_item, &go_menu, 3);
381390

382-
// Tab menu positions: new(0), close(1), sep(2), next(3), prev(4), sep(5), pin(6), close_others(7)
391+
// Tab menu positions: new(0), close(1), reopen(2), sep(3), next(4), prev(5), sep(6), pin(7), close_others(8)
383392
register_item(&mut items, NEW_TAB_ID, &new_tab_item, &tab_menu, 0);
384393
register_item(&mut items, CLOSE_TAB_ID, &close_tab_item, &tab_menu, 1);
385-
register_item(&mut items, NEXT_TAB_ID, &next_tab_item, &tab_menu, 3);
386-
register_item(&mut items, PREV_TAB_ID, &prev_tab_item, &tab_menu, 4);
387-
register_item(&mut items, CLOSE_OTHER_TABS_ID, &close_other_tabs_item, &tab_menu, 7);
394+
register_item(&mut items, REOPEN_CLOSED_TAB_ID, &reopen_closed_tab_item, &tab_menu, 2);
395+
register_item(&mut items, NEXT_TAB_ID, &next_tab_item, &tab_menu, 4);
396+
register_item(&mut items, PREV_TAB_ID, &prev_tab_item, &tab_menu, 5);
397+
register_item(&mut items, CLOSE_OTHER_TABS_ID, &close_other_tabs_item, &tab_menu, 8);
388398

389399
// Help menu: about(0), separator(1), send_error_report(2)
390400
register_item(&mut items, ABOUT_ID, &about_item, &help_menu, 0);
@@ -406,6 +416,7 @@ pub(crate) fn build_menu_linux<R: Runtime>(
406416
view_left_pane_submenu,
407417
view_right_pane_submenu,
408418
pin_tab: pin_tab_item,
419+
reopen_closed_tab: reopen_closed_tab_item,
409420
items,
410421
sort_submenu,
411422
})

apps/desktop/src-tauri/src/menu/macos.rs

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ use super::{
1313
COPY_PATH_ID, DESELECT_ALL_ID, EDIT_COPY_ID, EDIT_CUT_ID, EDIT_ID, EDIT_PASTE_ID, EDIT_PASTE_MOVE_ID,
1414
ENTER_LICENSE_KEY_ID, FILE_COPY_ID, FILE_DELETE_ID, FILE_DELETE_PERMANENTLY_ID, FILE_MOVE_ID, FILE_NEW_FOLDER_ID,
1515
FILE_VIEW_ID, GET_INFO_ID, GO_BACK_ID, GO_FORWARD_ID, GO_PARENT_ID, HELP_SEND_ERROR_REPORT_ID, MenuItems,
16-
NEW_TAB_ID, NEXT_TAB_ID, OPEN_ID, PIN_TAB_MENU_ID, PREV_TAB_ID, QUICK_LOOK_ID, RENAME_ID, SEARCH_FILES_ID,
17-
SELECT_ALL_ID, SETTINGS_ID, SHOW_HIDDEN_FILES_ID, SHOW_IN_FINDER_ID, SORT_BY_EXTENSION_ID, SORT_BY_MODIFIED_ID,
18-
SORT_BY_NAME_ID, SORT_BY_SIZE_ID, SWAP_PANES_ID, SWITCH_PANE_ID, VIEW_MODE_BRIEF_LEFT_ID, VIEW_MODE_BRIEF_RIGHT_ID,
19-
VIEW_MODE_FULL_LEFT_ID, VIEW_MODE_FULL_RIGHT_ID, ViewMode, brief_view_label, build_sort_submenu,
20-
build_zoom_submenu, copy_path_accelerator, full_view_label, register_item, show_in_file_manager_accelerator,
21-
show_in_file_manager_label,
16+
NEW_TAB_ID, NEXT_TAB_ID, OPEN_ID, PIN_TAB_MENU_ID, PREV_TAB_ID, QUICK_LOOK_ID, RENAME_ID, REOPEN_CLOSED_TAB_ID,
17+
SEARCH_FILES_ID, SELECT_ALL_ID, SETTINGS_ID, SHOW_HIDDEN_FILES_ID, SHOW_IN_FINDER_ID, SORT_BY_EXTENSION_ID,
18+
SORT_BY_MODIFIED_ID, SORT_BY_NAME_ID, SORT_BY_SIZE_ID, SWAP_PANES_ID, SWITCH_PANE_ID, VIEW_MODE_BRIEF_LEFT_ID,
19+
VIEW_MODE_BRIEF_RIGHT_ID, VIEW_MODE_FULL_LEFT_ID, VIEW_MODE_FULL_RIGHT_ID, ViewMode, brief_view_label,
20+
build_sort_submenu, build_zoom_submenu, copy_path_accelerator, full_view_label, register_item,
21+
show_in_file_manager_accelerator, show_in_file_manager_label,
2222
};
2323

2424
pub(crate) fn build_menu_macos<R: Runtime>(
@@ -276,6 +276,14 @@ pub(crate) fn build_menu_macos<R: Runtime>(
276276
// --- Tab menu ---
277277
let new_tab_item = MenuItem::with_id(app, NEW_TAB_ID, "New tab", true, Some("Cmd+T"))?;
278278
let close_tab_item = MenuItem::with_id(app, CLOSE_TAB_ID, "Close tab", true, Some("Cmd+W"))?;
279+
// Disabled initially; frontend enables it after the first close via `set_reopen_closed_tab_enabled`.
280+
let reopen_closed_tab_item = MenuItem::with_id(
281+
app,
282+
REOPEN_CLOSED_TAB_ID,
283+
"Reopen closed tab",
284+
false,
285+
Some("Cmd+Shift+T"),
286+
)?;
279287
let next_tab_item = MenuItem::with_id(app, NEXT_TAB_ID, "Next tab", true, Some("Ctrl+Tab"))?;
280288
let prev_tab_item = MenuItem::with_id(app, PREV_TAB_ID, "Previous tab", true, Some("Ctrl+Shift+Tab"))?;
281289
let pin_tab_item = MenuItem::with_id(app, PIN_TAB_MENU_ID, "Pin tab", true, None::<&str>)?;
@@ -288,6 +296,7 @@ pub(crate) fn build_menu_macos<R: Runtime>(
288296
&[
289297
&new_tab_item,
290298
&close_tab_item,
299+
&reopen_closed_tab_item,
291300
&PredefinedMenuItem::separator(app)?,
292301
&next_tab_item,
293302
&prev_tab_item,
@@ -392,12 +401,13 @@ pub(crate) fn build_menu_macos<R: Runtime>(
392401
register_item(&mut items, GO_FORWARD_ID, &go_forward_item, &go_menu, 1);
393402
register_item(&mut items, GO_PARENT_ID, &go_parent_item, &go_menu, 3);
394403

395-
// Tab menu positions: new(0), close(1), sep(2), next(3), prev(4), sep(5), pin(6), close_others(7)
404+
// Tab menu positions: new(0), close(1), reopen(2), sep(3), next(4), prev(5), sep(6), pin(7), close_others(8)
396405
register_item(&mut items, NEW_TAB_ID, &new_tab_item, &tab_menu, 0);
397406
register_item(&mut items, CLOSE_TAB_ID, &close_tab_item, &tab_menu, 1);
398-
register_item(&mut items, NEXT_TAB_ID, &next_tab_item, &tab_menu, 3);
399-
register_item(&mut items, PREV_TAB_ID, &prev_tab_item, &tab_menu, 4);
400-
register_item(&mut items, CLOSE_OTHER_TABS_ID, &close_other_tabs_item, &tab_menu, 7);
407+
register_item(&mut items, REOPEN_CLOSED_TAB_ID, &reopen_closed_tab_item, &tab_menu, 2);
408+
register_item(&mut items, NEXT_TAB_ID, &next_tab_item, &tab_menu, 4);
409+
register_item(&mut items, PREV_TAB_ID, &prev_tab_item, &tab_menu, 5);
410+
register_item(&mut items, CLOSE_OTHER_TABS_ID, &close_other_tabs_item, &tab_menu, 8);
401411

402412
// Help menu positions: send_error_report(0)
403413
register_item(
@@ -422,6 +432,7 @@ pub(crate) fn build_menu_macos<R: Runtime>(
422432
view_left_pane_submenu,
423433
view_right_pane_submenu,
424434
pin_tab: pin_tab_item,
435+
reopen_closed_tab: reopen_closed_tab_item,
425436
items,
426437
sort_submenu,
427438
})

apps/desktop/src-tauri/src/menu/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ pub fn menu_id_to_command(menu_id: &str) -> Option<(&'static str, CommandScope)>
131131
// Tab commands (file-scoped)
132132
NEW_TAB_ID => Some(("tab.new", CommandScope::FileScoped)),
133133
CLOSE_TAB_ID => Some(("tab.close", CommandScope::FileScoped)),
134+
REOPEN_CLOSED_TAB_ID => Some(("tab.reopen", CommandScope::FileScoped)),
134135
NEXT_TAB_ID => Some(("tab.next", CommandScope::FileScoped)),
135136
PREV_TAB_ID => Some(("tab.prev", CommandScope::FileScoped)),
136137
PIN_TAB_MENU_ID => Some(("tab.togglePin", CommandScope::FileScoped)),
@@ -211,6 +212,7 @@ pub fn command_id_to_menu_id(command_id: &str) -> Option<&'static str> {
211212
"nav.parent" => Some(GO_PARENT_ID),
212213
"tab.new" => Some(NEW_TAB_ID),
213214
"tab.close" => Some(CLOSE_TAB_ID),
215+
"tab.reopen" => Some(REOPEN_CLOSED_TAB_ID),
214216
"tab.next" => Some(NEXT_TAB_ID),
215217
"tab.prev" => Some(PREV_TAB_ID),
216218
"tab.togglePin" => Some(PIN_TAB_MENU_ID),
@@ -311,6 +313,8 @@ pub struct MenuState<R: Runtime> {
311313
pub view_mode_brief_accel: Mutex<Option<String>>,
312314
/// Pin/unpin tab menu item (label toggles based on active tab state)
313315
pub pin_tab: Mutex<Option<MenuItem<R>>>,
316+
/// Reopen closed tab menu item (enabled when the focused pane's closed-tab stack is non-empty)
317+
pub reopen_closed_tab: Mutex<Option<MenuItem<R>>>,
314318
/// Generic menu items keyed by menu item ID, for accelerator and enable/disable updates.
315319
pub items: Mutex<HashMap<String, MenuItemEntry<R>>>,
316320
/// Sort by submenu (disabled when not in explorer context)
@@ -336,6 +340,7 @@ impl<R: Runtime> Default for MenuState<R> {
336340
view_mode_full_accel: Mutex::new(Some("Cmd+1".to_string())),
337341
view_mode_brief_accel: Mutex::new(Some("Cmd+2".to_string())),
338342
pin_tab: Mutex::new(None),
343+
reopen_closed_tab: Mutex::new(None),
339344
items: Mutex::new(HashMap::new()),
340345
sort_submenu: Mutex::new(None),
341346
network_host_context: Mutex::new(NetworkHostMenuContext::default()),
@@ -361,6 +366,8 @@ pub struct MenuItems<R: Runtime> {
361366
pub view_right_pane_submenu: Submenu<R>,
362367
/// Pin/unpin tab menu item (label updated dynamically by frontend)
363368
pub pin_tab: MenuItem<R>,
369+
/// Reopen closed tab menu item (enable state synced from frontend)
370+
pub reopen_closed_tab: MenuItem<R>,
364371
/// Generic menu items for accelerator updates, keyed by menu item ID.
365372
pub items: HashMap<String, MenuItemEntry<R>>,
366373
/// Sort by submenu (disabled when not in explorer context)
@@ -382,6 +389,7 @@ pub const VIEWER_WORD_WRAP_ID: &str = "viewer_word_wrap";
382389
pub const NEW_TAB_ID: &str = "new_tab";
383390
pub const PIN_TAB_MENU_ID: &str = "pin_tab_menu";
384391
pub const CLOSE_TAB_ID: &str = "close_tab";
392+
pub const REOPEN_CLOSED_TAB_ID: &str = "reopen_closed_tab";
385393
pub const NEXT_TAB_ID: &str = "next_tab";
386394
pub const PREV_TAB_ID: &str = "prev_tab";
387395
pub const CLOSE_OTHER_TABS_ID: &str = "close_other_tabs";
@@ -1364,6 +1372,7 @@ mod tests {
13641372
"nav.parent",
13651373
"tab.new",
13661374
"tab.close",
1375+
"tab.reopen",
13671376
"tab.next",
13681377
"tab.prev",
13691378
"tab.togglePin",

0 commit comments

Comments
 (0)