From a146b2b281a06323d93bf0fea6594d50a1fbbfb1 Mon Sep 17 00:00:00 2001 From: Serhii Vecherenko Date: Thu, 30 Jul 2026 01:13:33 -0700 Subject: [PATCH] feat(dev-terminal): add resizable bottom sidebar and scoped headers - Preserve terminal scope when locking the right panel - Show project/worktree labels in auxiliary and terminal panels --- .../hooks/useRightPanelThreadLock.test.tsx | 70 +++++++++++- src/renderer/hooks/useRightPanelThreadLock.ts | 24 ++-- src/renderer/locales/de/messages.po | 1 + src/renderer/locales/en/messages.po | 1 + src/renderer/locales/es/messages.po | 1 + src/renderer/locales/fr/messages.po | 1 + src/renderer/locales/ja/messages.po | 1 + src/renderer/locales/ko/messages.po | 1 + src/renderer/locales/pl/messages.po | 1 + src/renderer/locales/pt-BR/messages.po | 1 + src/renderer/locales/ru/messages.po | 1 + src/renderer/locales/tr/messages.po | 1 + src/renderer/locales/uk/messages.po | 1 + src/renderer/locales/vi/messages.po | 1 + src/renderer/locales/zh-CN/messages.po | 1 + src/renderer/state/devTerminalStore.ts | 20 ++++ src/renderer/utils/projectScopeLabel.ts | 8 ++ .../MainView/parts/ProjectAuxiliaryPanel.tsx | 7 ++ .../DevTerminalPanel.test.tsx | 33 +++++- .../DevTerminalPanel/DevTerminalPanel.tsx | 8 +- .../parts/BottomTerminalLayout.test.tsx | 67 ++++++++++++ .../parts/BottomTerminalLayout.tsx | 39 ++++++- .../parts/RightTerminalLayout.tsx | 10 +- .../parts/useBottomTerminalSidebarResize.ts | 103 ++++++++++++++++++ 24 files changed, 375 insertions(+), 27 deletions(-) create mode 100644 src/renderer/utils/projectScopeLabel.ts create mode 100644 src/renderer/views/MainView/parts/RightPanel/parts/DevTerminalPanel/parts/BottomTerminalLayout.test.tsx create mode 100644 src/renderer/views/MainView/parts/RightPanel/parts/DevTerminalPanel/parts/useBottomTerminalSidebarResize.ts diff --git a/src/renderer/hooks/useRightPanelThreadLock.test.tsx b/src/renderer/hooks/useRightPanelThreadLock.test.tsx index 1270b313..beaba655 100644 --- a/src/renderer/hooks/useRightPanelThreadLock.test.tsx +++ b/src/renderer/hooks/useRightPanelThreadLock.test.tsx @@ -2,7 +2,9 @@ import { renderHook } from "@testing-library/react"; import { beforeEach, describe, expect, it } from "vitest"; import type { Thread } from "@/shared/contracts"; import { useAppStore } from "@/renderer/state/appStore"; +import { useDevTerminalStore } from "@/renderer/state/devTerminalStore"; import { usePanelStore } from "@/renderer/state/panelStore"; +import { useSharedSettings } from "@/renderer/state/sharedSettingsStore"; import { useRightPanelThreadLock } from "./useRightPanelThreadLock"; function makeThread(input: Partial = {}): Thread { @@ -26,10 +28,11 @@ function makeThread(input: Partial = {}): Thread { } const threadA = makeThread({ id: "thread-a", projectId: "project-a" }); +const threadBWorktreePath = "/repo-b/.poracode/worktrees/feature"; const threadB = makeThread({ id: "thread-b", projectId: "project-b", - worktreePath: "/repo-b/.poracode/worktrees/feature", + worktreePath: threadBWorktreePath, }); function focusThread(threadId: string) { @@ -51,6 +54,17 @@ describe("useRightPanelThreadLock", () => { filesPanelContext: null, rightPanelTab: "git", }); + useDevTerminalStore.setState({ + isOpen: false, + activeProjectId: null, + activeWorktreePath: null, + tabs: [], + activeTabId: null, + focusRequestId: 0, + tabActivity: {}, + streamingTabs: {}, + }); + useSharedSettings.setState({ terminalPosition: "bottom" }); focusThread("thread-a"); }); @@ -98,4 +112,58 @@ describe("useRightPanelThreadLock", () => { expect(usePanelStore.getState().gitReviewContext).toBeNull(); expect(usePanelStore.getState().filesPanelContext).toBeNull(); }); + + it("re-scopes the open bottom terminal to the focused thread", () => { + usePanelStore.setState({ gitReviewContext: null, gitReviewAsPanel: false }); + useDevTerminalStore.setState({ + isOpen: true, + activeProjectId: "project-a", + tabs: [ + { + id: "terminal-a", + projectId: "project-a", + title: "Project A", + createdAt: "2026-03-22T00:00:00.000Z", + }, + { + id: "terminal-b", + projectId: "project-b", + worktreePath: threadBWorktreePath, + title: "Feature", + createdAt: "2026-03-22T00:00:00.000Z", + }, + ], + activeTabId: "terminal-a", + }); + const { rerender } = renderHook(() => useRightPanelThreadLock()); + + focusThread("thread-b"); + rerender(); + + expect(useDevTerminalStore.getState()).toMatchObject({ + isOpen: true, + activeProjectId: "project-b", + activeWorktreePath: threadBWorktreePath, + activeTabId: "terminal-b", + }); + }); + + it("leaves a right-docked terminal on its existing scope", () => { + usePanelStore.setState({ gitReviewContext: null, gitReviewAsPanel: false }); + useSharedSettings.setState({ terminalPosition: "right" }); + useDevTerminalStore.setState({ + isOpen: true, + activeProjectId: "project-a", + activeWorktreePath: null, + }); + const { rerender } = renderHook(() => useRightPanelThreadLock()); + + focusThread("thread-b"); + rerender(); + + expect(useDevTerminalStore.getState()).toMatchObject({ + activeProjectId: "project-a", + activeWorktreePath: null, + }); + }); }); diff --git a/src/renderer/hooks/useRightPanelThreadLock.ts b/src/renderer/hooks/useRightPanelThreadLock.ts index 72b287af..019de3e5 100644 --- a/src/renderer/hooks/useRightPanelThreadLock.ts +++ b/src/renderer/hooks/useRightPanelThreadLock.ts @@ -3,21 +3,26 @@ import { isHomeProjectId } from "@/shared/homeScope"; import { resolveActivePaneId } from "@/renderer/actions/currentProject"; import { showFilesPanel, showGitReviewPanel } from "@/renderer/actions/panelActions"; import { useAppStore } from "@/renderer/state/appStore"; +import { useDevTerminalStore } from "@/renderer/state/devTerminalStore"; import { hasDirtyEditorBuffers } from "@/renderer/state/fileEditorSelectors"; import { usePanelStore } from "@/renderer/state/panelStore"; +import { useSharedSettings } from "@/renderer/state/sharedSettingsStore"; /** * Keeps the right panel pinned to the focused thread while * `rightPanelFollowsThread` is on: whichever scope-bearing tools are already - * open (git, files) re-target that thread's project + worktree on every thread - * switch. Panels the user has closed stay closed — the lock re-scopes, it does - * not open anything. + * open (git, files, bottom terminal) re-target that thread's project + + * worktree on every thread switch. Panels the user has closed stay closed — + * the lock re-scopes, it does not open anything or spawn a new shell. * - * The dev terminal is deliberately left alone: re-targeting it would spawn a - * shell process for every thread visited. + * A right-docked terminal is deliberately left alone. It shares the right + * panel itself, while the bottom terminal is a separate surface that should + * follow the panel lock. */ export function useRightPanelThreadLock(): void { const enabled = usePanelStore((s) => s.rightPanelFollowsThread); + const terminalOpen = useDevTerminalStore((s) => s.isOpen); + const terminalPosition = useSharedSettings((s) => s.terminalPosition); const projectId = useAppStore((state) => { if (state.view.kind !== "thread") return null; const paneId = resolveActivePaneId(state.view.panes, state.focusedPaneId); @@ -38,7 +43,8 @@ export function useRightPanelThreadLock(): void { const panel = usePanelStore.getState(); const gitPanelOpen = panel.gitReviewContext !== null && panel.gitReviewAsPanel; const filesPanelOpen = panel.filesPanelContext !== null; - if (!gitPanelOpen && !filesPanelOpen) return; + const bottomTerminalOpen = terminalOpen && terminalPosition === "bottom"; + if (!gitPanelOpen && !filesPanelOpen && !bottomTerminalOpen) return; // Both `show*Panel` helpers force their own tab; remember what the user was // actually looking at and restore it after re-scoping. @@ -53,8 +59,12 @@ export function useRightPanelThreadLock(): void { showFilesPanel(projectId, scopedWorktree); } } + if (bottomTerminalOpen) { + const terminal = useDevTerminalStore.getState(); + terminal.setPanelScope(projectId, scopedWorktree); + } if (usePanelStore.getState().rightPanelTab !== activeTab) { panel.setRightPanelTab(activeTab); } - }, [enabled, projectId, worktreePath]); + }, [enabled, projectId, terminalOpen, terminalPosition, worktreePath]); } diff --git a/src/renderer/locales/de/messages.po b/src/renderer/locales/de/messages.po index e164d110..422e0f57 100644 --- a/src/renderer/locales/de/messages.po +++ b/src/renderer/locales/de/messages.po @@ -8554,6 +8554,7 @@ msgstr "Größe der Zeile ändern" #: src/mobile/WideShell.tsx #: src/renderer/views/MainView/parts/AppShell/AppShell.tsx +#: src/renderer/views/MainView/parts/RightPanel/parts/DevTerminalPanel/parts/BottomTerminalLayout.tsx msgid "Resize sidebar" msgstr "Größe der Seitenleiste ändern" diff --git a/src/renderer/locales/en/messages.po b/src/renderer/locales/en/messages.po index 7061abc2..595bbab8 100644 --- a/src/renderer/locales/en/messages.po +++ b/src/renderer/locales/en/messages.po @@ -8554,6 +8554,7 @@ msgstr "Resize row" #: src/mobile/WideShell.tsx #: src/renderer/views/MainView/parts/AppShell/AppShell.tsx +#: src/renderer/views/MainView/parts/RightPanel/parts/DevTerminalPanel/parts/BottomTerminalLayout.tsx msgid "Resize sidebar" msgstr "Resize sidebar" diff --git a/src/renderer/locales/es/messages.po b/src/renderer/locales/es/messages.po index 632171dd..89f8dc90 100644 --- a/src/renderer/locales/es/messages.po +++ b/src/renderer/locales/es/messages.po @@ -8554,6 +8554,7 @@ msgstr "Redimensionar fila" #: src/mobile/WideShell.tsx #: src/renderer/views/MainView/parts/AppShell/AppShell.tsx +#: src/renderer/views/MainView/parts/RightPanel/parts/DevTerminalPanel/parts/BottomTerminalLayout.tsx msgid "Resize sidebar" msgstr "Redimensionar barra lateral" diff --git a/src/renderer/locales/fr/messages.po b/src/renderer/locales/fr/messages.po index 2d0f37a5..0023bb75 100644 --- a/src/renderer/locales/fr/messages.po +++ b/src/renderer/locales/fr/messages.po @@ -8553,6 +8553,7 @@ msgstr "Redimensionner la ligne" #: src/mobile/WideShell.tsx #: src/renderer/views/MainView/parts/AppShell/AppShell.tsx +#: src/renderer/views/MainView/parts/RightPanel/parts/DevTerminalPanel/parts/BottomTerminalLayout.tsx msgid "Resize sidebar" msgstr "Redimensionner la barre latérale" diff --git a/src/renderer/locales/ja/messages.po b/src/renderer/locales/ja/messages.po index 488f804c..d107104b 100644 --- a/src/renderer/locales/ja/messages.po +++ b/src/renderer/locales/ja/messages.po @@ -8552,6 +8552,7 @@ msgstr "行のサイズを変更する" #: src/mobile/WideShell.tsx #: src/renderer/views/MainView/parts/AppShell/AppShell.tsx +#: src/renderer/views/MainView/parts/RightPanel/parts/DevTerminalPanel/parts/BottomTerminalLayout.tsx msgid "Resize sidebar" msgstr "サイドバーのサイズを変更する" diff --git a/src/renderer/locales/ko/messages.po b/src/renderer/locales/ko/messages.po index 2531e6d7..d1744064 100644 --- a/src/renderer/locales/ko/messages.po +++ b/src/renderer/locales/ko/messages.po @@ -8554,6 +8554,7 @@ msgstr "행 크기 조정" #: src/mobile/WideShell.tsx #: src/renderer/views/MainView/parts/AppShell/AppShell.tsx +#: src/renderer/views/MainView/parts/RightPanel/parts/DevTerminalPanel/parts/BottomTerminalLayout.tsx msgid "Resize sidebar" msgstr "사이드바 크기 조정" diff --git a/src/renderer/locales/pl/messages.po b/src/renderer/locales/pl/messages.po index 0dc27ff0..3bb54c77 100644 --- a/src/renderer/locales/pl/messages.po +++ b/src/renderer/locales/pl/messages.po @@ -8554,6 +8554,7 @@ msgstr "Zmień rozmiar wiersza" #: src/mobile/WideShell.tsx #: src/renderer/views/MainView/parts/AppShell/AppShell.tsx +#: src/renderer/views/MainView/parts/RightPanel/parts/DevTerminalPanel/parts/BottomTerminalLayout.tsx msgid "Resize sidebar" msgstr "Zmień rozmiar paska bocznego" diff --git a/src/renderer/locales/pt-BR/messages.po b/src/renderer/locales/pt-BR/messages.po index 0e08ca1c..e65f6284 100644 --- a/src/renderer/locales/pt-BR/messages.po +++ b/src/renderer/locales/pt-BR/messages.po @@ -8554,6 +8554,7 @@ msgstr "Redimensionar linha" #: src/mobile/WideShell.tsx #: src/renderer/views/MainView/parts/AppShell/AppShell.tsx +#: src/renderer/views/MainView/parts/RightPanel/parts/DevTerminalPanel/parts/BottomTerminalLayout.tsx msgid "Resize sidebar" msgstr "Redimensionar barra lateral" diff --git a/src/renderer/locales/ru/messages.po b/src/renderer/locales/ru/messages.po index 6e4ac415..f3ce1fd1 100644 --- a/src/renderer/locales/ru/messages.po +++ b/src/renderer/locales/ru/messages.po @@ -8554,6 +8554,7 @@ msgstr "Изменить размер строки" #: src/mobile/WideShell.tsx #: src/renderer/views/MainView/parts/AppShell/AppShell.tsx +#: src/renderer/views/MainView/parts/RightPanel/parts/DevTerminalPanel/parts/BottomTerminalLayout.tsx msgid "Resize sidebar" msgstr "Изменить размер боковой панели" diff --git a/src/renderer/locales/tr/messages.po b/src/renderer/locales/tr/messages.po index af397ae3..a0d1c963 100644 --- a/src/renderer/locales/tr/messages.po +++ b/src/renderer/locales/tr/messages.po @@ -8554,6 +8554,7 @@ msgstr "Satırı yeniden boyutlandır" #: src/mobile/WideShell.tsx #: src/renderer/views/MainView/parts/AppShell/AppShell.tsx +#: src/renderer/views/MainView/parts/RightPanel/parts/DevTerminalPanel/parts/BottomTerminalLayout.tsx msgid "Resize sidebar" msgstr "Kenar çubuğunu yeniden boyutlandır" diff --git a/src/renderer/locales/uk/messages.po b/src/renderer/locales/uk/messages.po index fc8477bc..52d1eadd 100644 --- a/src/renderer/locales/uk/messages.po +++ b/src/renderer/locales/uk/messages.po @@ -8554,6 +8554,7 @@ msgstr "Змінити розмір рядка" #: src/mobile/WideShell.tsx #: src/renderer/views/MainView/parts/AppShell/AppShell.tsx +#: src/renderer/views/MainView/parts/RightPanel/parts/DevTerminalPanel/parts/BottomTerminalLayout.tsx msgid "Resize sidebar" msgstr "Змінити розмір бічної панелі" diff --git a/src/renderer/locales/vi/messages.po b/src/renderer/locales/vi/messages.po index 01d3bdb7..373b6e1e 100644 --- a/src/renderer/locales/vi/messages.po +++ b/src/renderer/locales/vi/messages.po @@ -8554,6 +8554,7 @@ msgstr "Thay đổi kích thước hàng" #: src/mobile/WideShell.tsx #: src/renderer/views/MainView/parts/AppShell/AppShell.tsx +#: src/renderer/views/MainView/parts/RightPanel/parts/DevTerminalPanel/parts/BottomTerminalLayout.tsx msgid "Resize sidebar" msgstr "Thay đổi kích thước thanh bên" diff --git a/src/renderer/locales/zh-CN/messages.po b/src/renderer/locales/zh-CN/messages.po index 1e2b287f..17b03613 100644 --- a/src/renderer/locales/zh-CN/messages.po +++ b/src/renderer/locales/zh-CN/messages.po @@ -8553,6 +8553,7 @@ msgstr "调整行大小" #: src/mobile/WideShell.tsx #: src/renderer/views/MainView/parts/AppShell/AppShell.tsx +#: src/renderer/views/MainView/parts/RightPanel/parts/DevTerminalPanel/parts/BottomTerminalLayout.tsx msgid "Resize sidebar" msgstr "调整侧边栏大小" diff --git a/src/renderer/state/devTerminalStore.ts b/src/renderer/state/devTerminalStore.ts index 2eeeb8a4..bfa39894 100644 --- a/src/renderer/state/devTerminalStore.ts +++ b/src/renderer/state/devTerminalStore.ts @@ -33,6 +33,8 @@ interface DevTerminalActions { openWorktreePanel: (projectId: string, worktreePath: string) => void; closePanel: () => void; setActiveProject: (projectId: string) => void; + /** Re-scope an open panel without opening it or spawning a shell. */ + setPanelScope: (projectId: string, worktreePath?: string) => void; addTab: (projectId: string, projectName: string, worktreePath?: string) => DevTerminalTab; removeTab: (tabId: string) => void; setActiveTab: (tabId: string) => void; @@ -105,6 +107,24 @@ export const useDevTerminalStore = create }); }, + setPanelScope: (projectId, worktreePath) => + set((state) => { + if ( + state.activeProjectId === projectId && + (state.activeWorktreePath ?? undefined) === worktreePath + ) { + return {}; + } + const scopedTab = state.tabs.find( + (tab) => tab.projectId === projectId && (tab.worktreePath ?? undefined) === worktreePath, + ); + return { + activeProjectId: projectId, + activeWorktreePath: worktreePath ?? null, + activeTabId: scopedTab?.id ?? null, + }; + }), + addTab: (projectId, projectName, worktreePath?) => { const tab: DevTerminalTab = { id: `shell:${crypto.randomUUID()}`, diff --git a/src/renderer/utils/projectScopeLabel.ts b/src/renderer/utils/projectScopeLabel.ts new file mode 100644 index 00000000..2548287e --- /dev/null +++ b/src/renderer/utils/projectScopeLabel.ts @@ -0,0 +1,8 @@ +import { getBasename } from "@/shared/pathUtils"; + +/** Compact project/worktree scope label for docked panel headers. */ +export function formatProjectScopeLabel(projectName: string, worktreePath?: string): string { + if (!worktreePath) return projectName; + const worktreeName = getBasename(worktreePath); + return worktreeName ? `${projectName} / ${worktreeName}` : projectName; +} diff --git a/src/renderer/views/MainView/parts/ProjectAuxiliaryPanel.tsx b/src/renderer/views/MainView/parts/ProjectAuxiliaryPanel.tsx index 4320586b..b4e66bcf 100644 --- a/src/renderer/views/MainView/parts/ProjectAuxiliaryPanel.tsx +++ b/src/renderer/views/MainView/parts/ProjectAuxiliaryPanel.tsx @@ -45,6 +45,7 @@ import { import { showTerminalPanel } from "@/renderer/actions/terminalActions"; import { getCurrentProjectId, resolveActivePaneId } from "@/renderer/actions/currentProject"; import { buildFileEditorContext } from "@/renderer/utils/gitHelpers"; +import { formatProjectScopeLabel } from "@/renderer/utils/projectScopeLabel"; import { GitReviewPanelContent } from "./RightPanel/parts/GitReviewPanelContent"; interface PanelProjectScope { @@ -246,6 +247,12 @@ export function ProjectAuxiliaryPanel(props: { includeTerminal: boolean; visible return t`Usage`; case "notes": return notesProjectId ? projectNameForScope({ projectId: notesProjectId }) : t`Notes`; + case "terminal": { + const terminalProjectName = projectNameForScope(terminalScope); + return terminalProjectName + ? formatProjectScopeLabel(terminalProjectName, terminalWorktreePath ?? undefined) + : undefined; + } case "subagent": case "plan": return undefined; diff --git a/src/renderer/views/MainView/parts/RightPanel/parts/DevTerminalPanel/DevTerminalPanel.test.tsx b/src/renderer/views/MainView/parts/RightPanel/parts/DevTerminalPanel/DevTerminalPanel.test.tsx index ee56258f..4c991379 100644 --- a/src/renderer/views/MainView/parts/RightPanel/parts/DevTerminalPanel/DevTerminalPanel.test.tsx +++ b/src/renderer/views/MainView/parts/RightPanel/parts/DevTerminalPanel/DevTerminalPanel.test.tsx @@ -22,22 +22,30 @@ vi.mock("@/renderer/bridge", () => ({ vi.mock("./parts/RightTerminalLayout", () => ({ RightTerminalLayout: (props: { projectTabs: DevTerminalTab[]; + activeScopeLabel: string | undefined; handleCloseTab: (tab: DevTerminalTab) => void; }) => ( - + <> + {props.activeScopeLabel} + + ), })); vi.mock("./parts/BottomTerminalLayout", () => ({ BottomTerminalLayout: (props: { projectTabs: DevTerminalTab[]; + activeScopeLabel: string | undefined; handleCloseTab: (tab: DevTerminalTab) => void; }) => ( - + <> + {props.activeScopeLabel} + + ), })); @@ -109,6 +117,19 @@ describe("DevTerminalPanel", () => { expect(usePanelStore.getState().filesPanelContext?.projectId).toBe(project.id); }); + it("shows the project and worktree in the terminal scope label", () => { + const worktreePath = "/repo/.poracode/worktrees/feature"; + useSharedSettings.setState({ terminalPosition: "bottom" }); + useDevTerminalStore.setState({ + activeWorktreePath: worktreePath, + tabs: [{ ...tab, worktreePath }], + }); + + render(); + + expect(screen.getByText("Poracode / feature")).toBeInTheDocument(); + }); + it("can force the right layout for an embedded host without changing saved settings", () => { useSharedSettings.setState({ terminalPosition: "bottom" }); const onEmpty = vi.fn<() => void>(); diff --git a/src/renderer/views/MainView/parts/RightPanel/parts/DevTerminalPanel/DevTerminalPanel.tsx b/src/renderer/views/MainView/parts/RightPanel/parts/DevTerminalPanel/DevTerminalPanel.tsx index a7070735..64b30264 100644 --- a/src/renderer/views/MainView/parts/RightPanel/parts/DevTerminalPanel/DevTerminalPanel.tsx +++ b/src/renderer/views/MainView/parts/RightPanel/parts/DevTerminalPanel/DevTerminalPanel.tsx @@ -11,6 +11,7 @@ import { useDevTerminalStore, type DevTerminalTab } from "@/renderer/state/devTe import { useSharedSettings } from "@/renderer/state/sharedSettingsStore"; import { closeAllPanels } from "@/renderer/actions/panelActions"; import { clearEagerShellStart, wasShellStartedEagerly } from "@/renderer/utils/shellUtils"; +import { formatProjectScopeLabel } from "@/renderer/utils/projectScopeLabel"; import type { TerminalSize } from "@/shared/contracts"; import type { TerminalFeedListener } from "@/shared/remote/terminalFeed"; import { buildWorktreeLocation } from "@/shared/worktree"; @@ -48,6 +49,9 @@ export function DevTerminalPanel(props: { return !tab.worktreePath; }); const activeProject = projects.find((p) => p.id === activeProjectId); + const activeScopeLabel = activeProject + ? formatProjectScopeLabel(activeProject.name, activeWorktreePath ?? undefined) + : undefined; const selectedTabId = projectTabs.find((tab) => tab.id === activeTabId)?.id ?? projectTabs.at(-1)?.id ?? "__add__"; const activeTab = projectTabs.find((tab) => tab.id === selectedTabId); @@ -207,7 +211,7 @@ export function DevTerminalPanel(props: { ({ + TerminalSurfaces: () =>
, +})); + +function renderLayout() { + return render( + void>()} + updateTabTitle={vi.fn<() => void>()} + fadeStyle={{ opacity: 1, transition: "none" }} + emptyState={null} + handleCloseTab={vi.fn<() => void>()} + handleCloseSplit={vi.fn<() => void>()} + handleSelectionChange={vi.fn<() => void>()} + getTabContextItems={() => []} + handleTabContextAction={vi.fn<() => void>()} + />, + ); +} + +describe("BottomTerminalLayout", () => { + beforeEach(() => { + localStorage.clear(); + }); + + it("shows the project and worktree scope in the header", () => { + renderLayout(); + + expect(screen.getByText("Poracode / feature")).toBeInTheDocument(); + }); + + it("resizes and persists the terminal sidebar with the keyboard", () => { + renderLayout(); + const separator = screen.getByRole("separator", { name: "Resize sidebar" }); + const sidebar = separator.previousElementSibling; + expect(sidebar).toHaveStyle({ width: "140px" }); + + fireEvent.keyDown(separator, { key: "ArrowRight" }); + + expect(sidebar).toHaveStyle({ width: "164px" }); + expect(localStorage.getItem("poracode-bottom-terminal-sidebar-width")).toBe("164"); + }); + + it("resizes and persists the terminal sidebar by dragging", () => { + renderLayout(); + const separator = screen.getByRole("separator", { name: "Resize sidebar" }); + const sidebar = separator.previousElementSibling; + + fireEvent.pointerDown(separator, { clientX: 100 }); + fireEvent.pointerMove(window, { clientX: 170 }); + fireEvent.pointerUp(window); + + expect(sidebar).toHaveStyle({ width: "210px" }); + expect(localStorage.getItem("poracode-bottom-terminal-sidebar-width")).toBe("210"); + }); +}); diff --git a/src/renderer/views/MainView/parts/RightPanel/parts/DevTerminalPanel/parts/BottomTerminalLayout.tsx b/src/renderer/views/MainView/parts/RightPanel/parts/DevTerminalPanel/parts/BottomTerminalLayout.tsx index 55e181d0..526f1584 100644 --- a/src/renderer/views/MainView/parts/RightPanel/parts/DevTerminalPanel/parts/BottomTerminalLayout.tsx +++ b/src/renderer/views/MainView/parts/RightPanel/parts/DevTerminalPanel/parts/BottomTerminalLayout.tsx @@ -1,7 +1,7 @@ import { Columns2, PanelBottomClose, Plus, Trash2 } from "lucide-react"; import { Tabs } from "@heroui/react"; import { useLingui } from "@lingui/react/macro"; -import type { Project, TerminalSize } from "@/shared/contracts"; +import type { TerminalSize } from "@/shared/contracts"; import type { TerminalFeedListener } from "@/shared/remote/terminalFeed"; import { useDevTerminalStore, type DevTerminalTab } from "@/renderer/state/devTerminalStore"; import { ContextMenu } from "@/renderer/components/common/ContextMenu"; @@ -11,11 +11,16 @@ import { panelHeaderRowClass, } from "@/renderer/components/layout/sidebarChrome"; import { TerminalSurfaces } from "./TerminalSurfaces"; +import { + BOTTOM_TERMINAL_SIDEBAR_MAX_WIDTH, + BOTTOM_TERMINAL_SIDEBAR_MIN_WIDTH, + useBottomTerminalSidebarResize, +} from "./useBottomTerminalSidebarResize"; export function BottomTerminalLayout(props: { tabs: DevTerminalTab[]; projectTabs: DevTerminalTab[]; - activeProject: Project | undefined; + activeScopeLabel: string | undefined; selectedTabId: string; activeTab: DevTerminalTab | undefined; focusRequestId: number; @@ -37,7 +42,7 @@ export function BottomTerminalLayout(props: { const { tabs, projectTabs, - activeProject, + activeScopeLabel, selectedTabId, activeTab, focusRequestId, @@ -53,6 +58,8 @@ export function BottomTerminalLayout(props: { onTerminalResize, watchTerminal, } = props; + const { sidebarRef, sidebarWidth, handleResizeStart, handleResizeKeyDown } = + useBottomTerminalSidebarResize(); // Build flat entries: primary tabs + their split children type TabRow = { id: string; tab: DevTerminalTab; isSplit: boolean }; @@ -64,10 +71,17 @@ export function BottomTerminalLayout(props: { return (
-
- {activeProject && ( +
+ {activeScopeLabel && (
- +
+
+
- {!hideHeader && activeProject && ( + {!hideHeader && activeScopeLabel && (
- +