Skip to content
Merged
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
70 changes: 69 additions & 1 deletion src/renderer/hooks/useRightPanelThreadLock.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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> = {}): Thread {
Expand All @@ -26,10 +28,11 @@ function makeThread(input: Partial<Thread> = {}): 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) {
Expand All @@ -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");
});

Expand Down Expand Up @@ -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,
});
});
});
24 changes: 17 additions & 7 deletions src/renderer/hooks/useRightPanelThreadLock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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.
Expand All @@ -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]);
}
1 change: 1 addition & 0 deletions src/renderer/locales/de/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
1 change: 1 addition & 0 deletions src/renderer/locales/en/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
1 change: 1 addition & 0 deletions src/renderer/locales/es/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
1 change: 1 addition & 0 deletions src/renderer/locales/fr/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
1 change: 1 addition & 0 deletions src/renderer/locales/ja/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -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 "サイドバーのサイズを変更する"

Expand Down
1 change: 1 addition & 0 deletions src/renderer/locales/ko/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -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 "사이드바 크기 조정"

Expand Down
1 change: 1 addition & 0 deletions src/renderer/locales/pl/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
1 change: 1 addition & 0 deletions src/renderer/locales/pt-BR/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
1 change: 1 addition & 0 deletions src/renderer/locales/ru/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -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 "Изменить размер боковой панели"

Expand Down
1 change: 1 addition & 0 deletions src/renderer/locales/tr/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
1 change: 1 addition & 0 deletions src/renderer/locales/uk/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -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 "Змінити розмір бічної панелі"

Expand Down
1 change: 1 addition & 0 deletions src/renderer/locales/vi/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
1 change: 1 addition & 0 deletions src/renderer/locales/zh-CN/messages.po
Original file line number Diff line number Diff line change
Expand Up @@ -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 "调整侧边栏大小"

Expand Down
20 changes: 20 additions & 0 deletions src/renderer/state/devTerminalStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -105,6 +107,24 @@ export const useDevTerminalStore = create<DevTerminalState & DevTerminalActions>
});
},

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()}`,
Expand Down
8 changes: 8 additions & 0 deletions src/renderer/utils/projectScopeLabel.ts
Original file line number Diff line number Diff line change
@@ -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;
}
7 changes: 7 additions & 0 deletions src/renderer/views/MainView/parts/ProjectAuxiliaryPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,22 +22,30 @@ vi.mock("@/renderer/bridge", () => ({
vi.mock("./parts/RightTerminalLayout", () => ({
RightTerminalLayout: (props: {
projectTabs: DevTerminalTab[];
activeScopeLabel: string | undefined;
handleCloseTab: (tab: DevTerminalTab) => void;
}) => (
<button type="button" onClick={() => props.handleCloseTab(props.projectTabs[0]!)}>
close right tab
</button>
<>
<span>{props.activeScopeLabel}</span>
<button type="button" onClick={() => props.handleCloseTab(props.projectTabs[0]!)}>
close right tab
</button>
</>
),
}));

vi.mock("./parts/BottomTerminalLayout", () => ({
BottomTerminalLayout: (props: {
projectTabs: DevTerminalTab[];
activeScopeLabel: string | undefined;
handleCloseTab: (tab: DevTerminalTab) => void;
}) => (
<button type="button" onClick={() => props.handleCloseTab(props.projectTabs[0]!)}>
close bottom tab
</button>
<>
<span>{props.activeScopeLabel}</span>
<button type="button" onClick={() => props.handleCloseTab(props.projectTabs[0]!)}>
close bottom tab
</button>
</>
),
}));

Expand Down Expand Up @@ -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(<DevTerminalPanel hideHeader />);

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>();
Expand Down
Loading