Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,16 @@ describe("tab close/navigation shortcuts live in the always-mounted controller",
it("registers the tab shortcuts in WorkspaceChromeController", () => {
expect(controllerSource).toMatch(/shortcuts\.next_tab/)
expect(controllerSource).toMatch(/shortcuts\.prev_tab/)
expect(controllerSource).toMatch(/numberedTabIndexFromEvent/)
expect(controllerSource).toMatch(/pickNumberedTabId/)
expect(controllerSource).toMatch(/shortcuts\.close_current_tab/)
expect(controllerSource).toMatch(/shortcuts\.reopen_last_closed_tab/)
expect(controllerSource).toMatch(/popClosedTab/)
expect(controllerSource).toMatch(/shortcuts\.close_all_file_tabs/)
// ...and actually drives the tab / file-tab actions. The e.preventDefault()
// calls next to these are what stop mod+w reaching the window-close default.
expect(controllerSource).toMatch(/switchTab\(/)
expect(controllerSource).toMatch(/switchFileTab\(/)
expect(controllerSource).toMatch(/closeTab\(/)
expect(controllerSource).toMatch(/closeFileTab\(/)
expect(controllerSource).toMatch(/closeAllFileTabs\(/)
Expand Down
79 changes: 75 additions & 4 deletions src/components/layout/workspace-chrome-controller.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,13 @@ import {
import { useWorkbenchRoute } from "@/contexts/workbench-route-context"
import { useSearchDialog } from "@/contexts/search-dialog-context"
import { useShortcutSettings } from "@/hooks/use-shortcut-settings"
import { matchShortcutEvent } from "@/lib/keyboard-shortcuts"
import { useAppWorkspaceStore } from "@/stores/app-workspace-store"
import { popClosedTab } from "@/lib/closed-tab-stack"
import {
matchShortcutEvent,
numberedTabIndexFromEvent,
pickNumberedTabId,
} from "@/lib/keyboard-shortcuts"
import { SearchCommandDialog } from "@/components/conversations/search-command-dialog"
import { WorkspaceFolderDialog } from "@/components/layout/workspace-folder-dialog"

Expand All @@ -34,15 +40,17 @@ export function WorkspaceChromeController() {
const { toggle } = useSidebarContext()
const { toggle: toggleAuxPanel } = useAuxPanelContext()
const { toggle: toggleTerminal } = useTerminalContext()
const { openNewConversationTab, switchTab, closeTab } = useTabActions()
const { openNewConversationTab, openTab, switchTab, closeTab } =
useTabActions()
const tabs = useTabStore((s) => s.tabs)
const activeTabId = useTabStore((s) => s.activeTabId)
// Tab-close/navigation shortcuts used to live in the visible tab strips.
// Mobile no longer mounts those strips, so this always-mounted controller now
// owns them too (see the keydown handler below).
const { mode, activePane, filesMaximized } = useWorkspaceView()
const { activeFileTabId } = useWorkspaceFileTabs()
const { closeFileTab, closeAllFileTabs } = useWorkspaceActions()
const { activeFileTabId, fileTabs } = useWorkspaceFileTabs()
const { closeFileTab, closeAllFileTabs, switchFileTab, openFilePreview } =
useWorkspaceActions()
const { openConversations } = useWorkbenchRoute()
const { shortcuts } = useShortcutSettings()
// Search open-state is shared (see search-dialog-context): the trigger lives
Expand Down Expand Up @@ -132,6 +140,31 @@ export function WorkspaceChromeController() {
return
}

const numberedIndex = numberedTabIndexFromEvent(e, shortcuts)
if (numberedIndex !== null) {
if (conversationPaneActive) {
const tabId = pickNumberedTabId(
tabs.map((tab) => tab.id),
numberedIndex
)
if (!tabId) return
e.preventDefault()
switchTab(tabId)
return
}
if (filesPaneActive) {
const tabId = pickNumberedTabId(
fileTabs.map((tab) => tab.id),
numberedIndex
)
if (!tabId) return
e.preventDefault()
switchFileTab(tabId)
return
}
return
}

if (matchShortcutEvent(e, shortcuts.close_all_file_tabs)) {
if (!filesPaneActive) return
e.preventDefault()
Expand All @@ -149,6 +182,40 @@ export function WorkspaceChromeController() {
e.preventDefault()
closeFileTab(activeFileTabId)
}
return
}

if (matchShortcutEvent(e, shortcuts.reopen_last_closed_tab)) {
e.preventDefault()
while (true) {
const closed = popClosedTab()
if (!closed) return
if (closed.kind === "file") {
void openFilePreview(closed.path, {
folderId: closed.folderId ?? undefined,
})
return
}
if (closed.conversationId != null) {
openConversations()
openTab(
closed.folderId,
closed.conversationId,
closed.agentType,
closed.isPinned,
closed.title
)
return
}
const folder = useAppWorkspaceStore
.getState()
.getFolder(closed.folderId)
const workingDir = closed.workingDir ?? folder?.path
if (!workingDir) continue
openConversations()
openNewConversationTab(closed.folderId, workingDir)
return
}
}
}
document.addEventListener("keydown", handleKeyDown)
Expand All @@ -159,6 +226,8 @@ export function WorkspaceChromeController() {
handleOpenSettings,
openConversations,
openNewConversationTab,
openTab,
openFilePreview,
setSearchOpen,
shortcuts,
toggle,
Expand All @@ -172,9 +241,11 @@ export function WorkspaceChromeController() {
mode,
activePane,
filesMaximized,
fileTabs,
activeFileTabId,
closeFileTab,
closeAllFileTabs,
switchFileTab,
])

return (
Expand Down
9 changes: 9 additions & 0 deletions src/contexts/workspace-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
splitAbsPath,
} from "@/lib/file-open-target"
import { isAbsoluteFilePath } from "@/lib/file-path-display"
import { pushClosedTab, snapshotFileTab } from "@/lib/closed-tab-stack"
import {
isHtmlPreviewable,
isImageFile,
Expand Down Expand Up @@ -2139,6 +2140,9 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
if (!confirmed) return prev
}

const closed = snapshotFileTab(tab)
if (closed) pushClosedTab(closed)

const next = prev.filter((candidate) => candidate.id !== tabId)

setActiveFileTabId((current) => {
Expand Down Expand Up @@ -2204,6 +2208,11 @@ export function WorkspaceProvider({ children }: WorkspaceProviderProps) {
if (!confirmed) return prev
}

for (const tab of prev) {
const closed = snapshotFileTab(tab)
if (closed) pushClosedTab(closed)
}

inFlightLoadsRef.current.clear()
setActiveFileTabId(null)
setPreviewFileTabIds(new Set())
Expand Down
40 changes: 40 additions & 0 deletions src/i18n/messages/ar.json
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,10 @@
"title": "إغلاق التبويب الحالي",
"description": "إغلاق المحادثة الحالية أو تبويب الملف الحالي"
},
"reopen_last_closed_tab": {
"title": "إعادة فتح التبويب المغلق",
"description": "إعادة فتح آخر تبويب محادثة أو ملف تم إغلاقه"
},
"close_all_file_tabs": {
"title": "إغلاق جميع تبويبات الملفات",
"description": "إغلاق جميع تبويبات الملفات المفتوحة عندما تكون لوحة الملفات نشطة"
Expand All @@ -404,6 +408,42 @@
"title": "علامة التبويب السابقة",
"description": "التبديل إلى علامة تبويب المحادثة أو الملف السابقة"
},
"switch_tab_1": {
"title": "التبديل إلى التبويب 1",
"description": "الانتقال إلى تبويب المحادثة أو الملف رقم 1"
},
"switch_tab_2": {
"title": "التبديل إلى التبويب 2",
"description": "الانتقال إلى تبويب المحادثة أو الملف رقم 2"
},
"switch_tab_3": {
"title": "التبديل إلى التبويب 3",
"description": "الانتقال إلى تبويب المحادثة أو الملف رقم 3"
},
"switch_tab_4": {
"title": "التبديل إلى التبويب 4",
"description": "الانتقال إلى تبويب المحادثة أو الملف رقم 4"
},
"switch_tab_5": {
"title": "التبديل إلى التبويب 5",
"description": "الانتقال إلى تبويب المحادثة أو الملف رقم 5"
},
"switch_tab_6": {
"title": "التبديل إلى التبويب 6",
"description": "الانتقال إلى تبويب المحادثة أو الملف رقم 6"
},
"switch_tab_7": {
"title": "التبديل إلى التبويب 7",
"description": "الانتقال إلى تبويب المحادثة أو الملف رقم 7"
},
"switch_tab_8": {
"title": "التبديل إلى التبويب 8",
"description": "الانتقال إلى تبويب المحادثة أو الملف رقم 8"
},
"switch_tab_9": {
"title": "التبديل إلى التبويب 9",
"description": "الانتقال إلى تبويب المحادثة أو الملف رقم 9"
},
"send_message": {
"title": "إرسال الرسالة",
"description": "إرسال الرسالة الحالية في مربع الإدخال"
Expand Down
40 changes: 40 additions & 0 deletions src/i18n/messages/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,10 @@
"title": "Aktuellen Tab schließen",
"description": "Schließt den aktuellen Konversations- oder Dateitab"
},
"reopen_last_closed_tab": {
"title": "Geschlossenen Tab wiederöffnen",
"description": "Den zuletzt geschlossenen Konversations- oder Datei-Tab wieder öffnen"
},
"close_all_file_tabs": {
"title": "Alle Dateitabs schließen",
"description": "Schließt alle geöffneten Dateitabs, wenn der Dateibereich aktiv ist"
Expand All @@ -404,6 +408,42 @@
"title": "Vorheriger Tab",
"description": "Zum vorherigen Konversations- oder Datei-Tab wechseln"
},
"switch_tab_1": {
"title": "Zu Tab 1 wechseln",
"description": "Zum 1. Konversations- oder Datei-Tab springen"
},
"switch_tab_2": {
"title": "Zu Tab 2 wechseln",
"description": "Zum 2. Konversations- oder Datei-Tab springen"
},
"switch_tab_3": {
"title": "Zu Tab 3 wechseln",
"description": "Zum 3. Konversations- oder Datei-Tab springen"
},
"switch_tab_4": {
"title": "Zu Tab 4 wechseln",
"description": "Zum 4. Konversations- oder Datei-Tab springen"
},
"switch_tab_5": {
"title": "Zu Tab 5 wechseln",
"description": "Zum 5. Konversations- oder Datei-Tab springen"
},
"switch_tab_6": {
"title": "Zu Tab 6 wechseln",
"description": "Zum 6. Konversations- oder Datei-Tab springen"
},
"switch_tab_7": {
"title": "Zu Tab 7 wechseln",
"description": "Zum 7. Konversations- oder Datei-Tab springen"
},
"switch_tab_8": {
"title": "Zu Tab 8 wechseln",
"description": "Zum 8. Konversations- oder Datei-Tab springen"
},
"switch_tab_9": {
"title": "Zu Tab 9 wechseln",
"description": "Zum 9. Konversations- oder Datei-Tab springen"
},
"send_message": {
"title": "Nachricht senden",
"description": "Die aktuelle Nachricht im Eingabefeld senden"
Expand Down
40 changes: 40 additions & 0 deletions src/i18n/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,10 @@
"title": "Close Current Tab",
"description": "Close current conversation or file tab"
},
"reopen_last_closed_tab": {
"title": "Reopen Closed Tab",
"description": "Reopen the most recently closed conversation or file tab"
},
"close_all_file_tabs": {
"title": "Close All File Tabs",
"description": "Close all open file tabs when the file pane is active"
Expand All @@ -404,6 +408,42 @@
"title": "Previous Tab",
"description": "Switch to the previous conversation or file tab"
},
"switch_tab_1": {
"title": "Switch to Tab 1",
"description": "Jump to conversation or file tab 1"
},
"switch_tab_2": {
"title": "Switch to Tab 2",
"description": "Jump to conversation or file tab 2"
},
"switch_tab_3": {
"title": "Switch to Tab 3",
"description": "Jump to conversation or file tab 3"
},
"switch_tab_4": {
"title": "Switch to Tab 4",
"description": "Jump to conversation or file tab 4"
},
"switch_tab_5": {
"title": "Switch to Tab 5",
"description": "Jump to conversation or file tab 5"
},
"switch_tab_6": {
"title": "Switch to Tab 6",
"description": "Jump to conversation or file tab 6"
},
"switch_tab_7": {
"title": "Switch to Tab 7",
"description": "Jump to conversation or file tab 7"
},
"switch_tab_8": {
"title": "Switch to Tab 8",
"description": "Jump to conversation or file tab 8"
},
"switch_tab_9": {
"title": "Switch to Tab 9",
"description": "Jump to conversation or file tab 9"
},
"send_message": {
"title": "Send Message",
"description": "Send the current message in the input box"
Expand Down
40 changes: 40 additions & 0 deletions src/i18n/messages/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,10 @@
"title": "Cerrar pestaña actual",
"description": "Cierra la conversación o pestaña de archivo actual"
},
"reopen_last_closed_tab": {
"title": "Reabrir pestaña cerrada",
"description": "Reabrir la última pestaña de conversación o archivo cerrada"
},
"close_all_file_tabs": {
"title": "Cerrar todas las pestañas de archivos",
"description": "Cierra todas las pestañas de archivos abiertas cuando el panel de archivos está activo"
Expand All @@ -404,6 +408,42 @@
"title": "Pestaña anterior",
"description": "Cambiar a la pestaña anterior de conversación o archivo"
},
"switch_tab_1": {
"title": "Ir a la pestaña 1",
"description": "Saltar a la pestaña de conversación o archivo 1"
},
"switch_tab_2": {
"title": "Ir a la pestaña 2",
"description": "Saltar a la pestaña de conversación o archivo 2"
},
"switch_tab_3": {
"title": "Ir a la pestaña 3",
"description": "Saltar a la pestaña de conversación o archivo 3"
},
"switch_tab_4": {
"title": "Ir a la pestaña 4",
"description": "Saltar a la pestaña de conversación o archivo 4"
},
"switch_tab_5": {
"title": "Ir a la pestaña 5",
"description": "Saltar a la pestaña de conversación o archivo 5"
},
"switch_tab_6": {
"title": "Ir a la pestaña 6",
"description": "Saltar a la pestaña de conversación o archivo 6"
},
"switch_tab_7": {
"title": "Ir a la pestaña 7",
"description": "Saltar a la pestaña de conversación o archivo 7"
},
"switch_tab_8": {
"title": "Ir a la pestaña 8",
"description": "Saltar a la pestaña de conversación o archivo 8"
},
"switch_tab_9": {
"title": "Ir a la pestaña 9",
"description": "Saltar a la pestaña de conversación o archivo 9"
},
"send_message": {
"title": "Enviar mensaje",
"description": "Enviar el mensaje actual en el cuadro de entrada"
Expand Down
Loading