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
9 changes: 9 additions & 0 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3434,6 +3434,13 @@ function ChatViewContent(props: ChatViewProps) {
},
[activeThreadRef, diffOpen, onDiffPanelOpen],
);
const reorderRightPanelSurface = useCallback(
(surfaceId: string, targetSurfaceId: string) => {
if (!activeThreadRef) return;
useRightPanelStore.getState().reorderSurface(activeThreadRef, surfaceId, targetSurfaceId);
},
[activeThreadRef],
);
const toggleRightPanel = useCallback(() => {
if (!activeThreadRef) return;
if (rightPanelOpen) {
Expand Down Expand Up @@ -6548,6 +6555,7 @@ function ChatViewContent(props: ChatViewProps) {
onCloseOtherSurfaces={closeOtherRightPanelSurfaces}
onCloseSurfacesToRight={closeRightPanelSurfacesToRight}
onCloseAllSurfaces={closeAllRightPanelSurfaces}
onReorderSurface={reorderRightPanelSurface}
onCopyFilePath={copyRightPanelFilePath}
onAddBrowser={createBrowserSurface}
onAddTerminal={addTerminalSurface}
Expand Down Expand Up @@ -6582,6 +6590,7 @@ function ChatViewContent(props: ChatViewProps) {
onCloseOtherSurfaces={closeOtherRightPanelSurfaces}
onCloseSurfacesToRight={closeRightPanelSurfacesToRight}
onCloseAllSurfaces={closeAllRightPanelSurfaces}
onReorderSurface={reorderRightPanelSurface}
onCopyFilePath={copyRightPanelFilePath}
onAddBrowser={createBrowserSurface}
onAddTerminal={addTerminalSurface}
Expand Down
350 changes: 229 additions & 121 deletions apps/web/src/components/RightPanelTabs.tsx

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions apps/web/src/components/rightPanelTabs.logic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { describe, expect, it, vi } from "vite-plus/test";

import { scrollActiveRightPanelTabIntoView } from "./rightPanelTabs.logic";

describe("scrollActiveRightPanelTabIntoView", () => {
it("does not scroll the active tab into view during a tab drag", () => {
const scrollIntoView = vi.fn();

scrollActiveRightPanelTabIntoView({ scrollIntoView }, true);

expect(scrollIntoView).not.toHaveBeenCalled();
});

it("scrolls the active tab into view outside a tab drag", () => {
const scrollIntoView = vi.fn();

scrollActiveRightPanelTabIntoView({ scrollIntoView }, false);

expect(scrollIntoView).toHaveBeenCalledWith({ block: "nearest", inline: "nearest" });
});
});
11 changes: 11 additions & 0 deletions apps/web/src/components/rightPanelTabs.logic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
interface ScrollableTab {
scrollIntoView: (options: ScrollIntoViewOptions) => void;
}

export function scrollActiveRightPanelTabIntoView(
activeTab: ScrollableTab | null,
tabDragActive: boolean,
): void {
if (tabDragActive) return;
activeTab?.scrollIntoView({ block: "nearest", inline: "nearest" });
}
31 changes: 31 additions & 0 deletions apps/web/src/rightPanelStore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,37 @@ describe("rightPanelStore", () => {
});
});

it("reorders surfaces without changing the active surface", () => {
useRightPanelStore.getState().openBrowser(refA, "tab-a");
useRightPanelStore.getState().openTerminal(refA, "term-1");
useRightPanelStore.getState().openFile(refA, "src/index.ts");

useRightPanelStore.getState().reorderSurface(refA, "file:src/index.ts", "browser:tab-a");

const state = selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA);
expect(state.surfaces.map((surface) => surface.id)).toEqual([
"file:src/index.ts",
"browser:tab-a",
"terminal:term-1",
]);
expect(state.activeSurfaceId).toBe("file:src/index.ts");
});

it("keeps a custom surface order while reconciling browser sessions", () => {
useRightPanelStore.getState().openTerminal(refA, "term-1");
useRightPanelStore.getState().openBrowser(refA, "tab-a");
useRightPanelStore.getState().openBrowser(refA, "tab-b");
useRightPanelStore.getState().reorderSurface(refA, "browser:tab-b", "terminal:term-1");

useRightPanelStore.getState().reconcileBrowserSurfaces(refA, ["tab-a", "tab-b", "tab-c"]);

expect(
selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA).surfaces.map(
(surface) => surface.id,
),
).toEqual(["browser:tab-b", "terminal:term-1", "browser:tab-a", "browser:tab-c"]);
});

it("reconciles browser surfaces without deleting other surface kinds", () => {
useRightPanelStore.getState().openTerminal(refA, "term-1");
useRightPanelStore.getState().openBrowser(refA, "tab-a");
Expand Down
28 changes: 20 additions & 8 deletions apps/web/src/rightPanelStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ interface RightPanelStoreState {
closeOtherSurfaces: (ref: ScopedThreadRef, surfaceId: string) => void;
closeSurfacesToRight: (ref: ScopedThreadRef, surfaceId: string) => void;
closeAllSurfaces: (ref: ScopedThreadRef) => void;
reorderSurface: (ref: ScopedThreadRef, surfaceId: string, targetSurfaceId: string) => void;
reconcileBrowserSurfaces: (ref: ScopedThreadRef, tabIds: readonly string[]) => void;
reconcileFileSurfaces: (ref: ScopedThreadRef, workspaceAvailable: boolean) => void;
show: (ref: ScopedThreadRef) => void;
Expand Down Expand Up @@ -559,22 +560,33 @@ export const useRightPanelStore = create<RightPanelStoreState>()(
: { ...current, isOpen: false, surfaces: [], activeSurfaceId: null },
),
})),
reorderSurface: (ref, surfaceId, targetSurfaceId) =>
set((state) => ({
byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => {
const fromIndex = current.surfaces.findIndex((surface) => surface.id === surfaceId);
const toIndex = current.surfaces.findIndex((surface) => surface.id === targetSurfaceId);
if (fromIndex < 0 || toIndex < 0 || fromIndex === toIndex) return current;
const surfaces = [...current.surfaces];
const [surface] = surfaces.splice(fromIndex, 1);
if (!surface) return current;
surfaces.splice(toIndex, 0, surface);
return { ...current, surfaces };
}),
})),
reconcileBrowserSurfaces: (ref, tabIds) =>
set((state) => ({
byThreadKey: updateThread(state.byThreadKey, scopedThreadKey(ref), (current) => {
const validIds = new Set(tabIds.map((tabId) => `browser:${tabId}`));
const nonBrowser = current.surfaces.filter((surface) => surface.kind !== "preview");
const existingBrowser = current.surfaces.filter(
(surface): surface is Extract<RightPanelSurface, { kind: "preview" }> =>
surface.kind === "preview" &&
surface.id !== "browser:new" &&
validIds.has(surface.id),
const existing = current.surfaces.filter(
(surface) =>
surface.kind !== "preview" ||
(surface.id !== "browser:new" && validIds.has(surface.id)),
);
const knownIds = new Set(existingBrowser.map((surface) => surface.id));
const knownIds = new Set(existing.map((surface) => surface.id));
const added = tabIds
.filter((tabId) => !knownIds.has(`browser:${tabId}`))
.map((tabId) => browserSurface(tabId));
const surfaces = [...nonBrowser, ...existingBrowser, ...added];
const surfaces = [...existing, ...added];
const activeStillExists = surfaces.some(
(surface) => surface.id === current.activeSurfaceId,
);
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/routes/_chat.pull-requests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1511,6 +1511,10 @@ function PullRequestsRouteView() {
useRightPanelStore.getState().closeAllSurfaces(rightPanelRef);
selectSurfaceInUrl(null);
};
const reorderSurface = (surfaceId: string, targetSurfaceId: string) => {
if (rightPanelRef === null) return;
useRightPanelStore.getState().reorderSurface(rightPanelRef, surfaceId, targetSurfaceId);
};

return (
<SidebarInset className="h-dvh min-h-0 overflow-hidden overscroll-y-none bg-background text-foreground">
Expand Down Expand Up @@ -1544,6 +1548,7 @@ function PullRequestsRouteView() {
if (surface.kind === "pull-request") closeSurfacesToRight(surface);
}}
onCloseAllSurfaces={closeAllSurfaces}
onReorderSurface={reorderSurface}
onCopyFilePath={() => undefined}
onAddBrowser={() => undefined}
onAddTerminal={() => undefined}
Expand Down
1 change: 1 addition & 0 deletions docs/user/source-control.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ T3 Code works with the platforms your team already uses:
- Open several reviews from the **Pull requests** page as tabs in the right panel
- While working in a thread, open linked reviews in the same compact right-panel tabs without
leaving the conversation
- Drag right-panel tabs to arrange them in the order that works for you
- Open the review directly in your browser with one click
- Command-click (Control-click on Windows and Linux) a pull request number in the sidebar to open it in your browser instead of in T3 Code
- Check out a teammate's branch to review code locally
Expand Down
Loading