From 16d65e977a5aeba98bb6701554c1f1da651e34e2 Mon Sep 17 00:00:00 2001 From: Harshith Mullapudi <17528887+harshithmullapudi@users.noreply.github.com> Date: Sat, 31 Oct 2026 16:57:35 +0000 Subject: [PATCH 1/3] feat(desktop): offer the Files changed list as a right-pane option The changes-list pane (a list-only Changes view) was registered but nothing could open it. Add it to RIGHT_PANE_OPTIONS so it shows in the + menu and the right-pane new tab, for a narrow, persistent file list beside the agent terminal without an open diff (GH #9198, SUPER-3078). --- .../components/RightPaneNewTab/paneOptions.test.ts | 5 +++++ .../components/RightPaneNewTab/paneOptions.ts | 8 +++++++- .../$workspaceId/components/RightPaneNewTab/types.ts | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightPaneNewTab/paneOptions.test.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightPaneNewTab/paneOptions.test.ts index 9598bddd90b..d46fa7436a3 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightPaneNewTab/paneOptions.test.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightPaneNewTab/paneOptions.test.ts @@ -10,6 +10,7 @@ describe("getRightPaneOptions", () => { "terminal", "browser", "diff", + "changes-list", "files", "pages-list", "desktop", @@ -34,11 +35,15 @@ describe("createRightPane", () => { "terminal", "browser", "diff", + "changes-list", "files", "pages-list", "desktop", "mobile", ]); + expect(panes.find((pane) => pane.kind === "changes-list")?.data).toEqual({ + kind: "changes-list", + }); expect(panes.find((pane) => pane.kind === "browser")?.data).toEqual({ url: "https://example.com", }); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightPaneNewTab/paneOptions.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightPaneNewTab/paneOptions.ts index af2f65a50eb..726f9d5aa71 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightPaneNewTab/paneOptions.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightPaneNewTab/paneOptions.ts @@ -3,7 +3,7 @@ import { msg } from "@lingui/core/macro"; import type { CreatePaneInput } from "@superset/panes"; import type { IconType } from "react-icons"; import { BsTerminalPlus } from "react-icons/bs"; -import { LuFileDiff, LuFileText, LuFolderTree } from "react-icons/lu"; +import { LuFileDiff, LuFiles, LuFileText, LuFolderTree } from "react-icons/lu"; import { TbDeviceDesktop, TbDeviceMobile, TbWorld } from "react-icons/tb"; import type { HotkeyId } from "renderer/hotkeys"; import type { @@ -46,6 +46,11 @@ const RIGHT_PANE_OPTIONS: RightPaneOption[] = [ Icon: LuFileDiff, hotkeyId: "OPEN_DIFF_VIEWER", }, + { + kind: "changes-list", + label: msg({ message: "Files changed" }), + Icon: LuFiles, + }, { kind: "files", label: msg({ message: "Files" }), Icon: LuFolderTree }, { kind: "pages-list", label: msg({ message: "Pages" }), Icon: LuFileText }, { @@ -77,6 +82,7 @@ export function createRightPane( ): CreatePaneInput { switch (kind) { case "files": + case "changes-list": case "pages-list": case "desktop": case "mobile": diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightPaneNewTab/types.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightPaneNewTab/types.ts index 60ba063611c..c8ebcbe4a63 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightPaneNewTab/types.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/RightPaneNewTab/types.ts @@ -1,6 +1,7 @@ export type RightPaneKind = | "files" | "diff" + | "changes-list" | "pages-list" | "browser" | "desktop" From d4b03993f0e190b726159dfd0e93601b03188a80 Mon Sep 17 00:00:00 2001 From: Harshith Mullapudi <17528887+harshithmullapudi@users.noreply.github.com> Date: Sat, 31 Oct 2026 16:57:35 +0000 Subject: [PATCH 2/3] fix(desktop): open beside-panes beside their anchor in the right pane area openBesidePane delegated to store.openPane, and the right pane area's placeInTabs ignores `beside` and opens a new tab. A file clicked in the Files changed list then opened a Changes tab that hid the list. Reuse an unpinned pane of the same kind in the anchor's tab, else split it in to the left, in openBesidePane itself. The center store behaves as before. --- .../openBesidePane/openBesidePane.test.ts | 18 +++++++++++-- .../utils/openBesidePane/openBesidePane.ts | 25 ++++++++++++++++++- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/utils/openBesidePane/openBesidePane.test.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/utils/openBesidePane/openBesidePane.test.ts index ce5ad28138a..4049d490a54 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/utils/openBesidePane/openBesidePane.test.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/utils/openBesidePane/openBesidePane.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "bun:test"; import { createWorkspaceStore } from "@superset/panes"; +import { placeInTabs } from "../../hooks/useV2WorkspacePaneLayout/utils/placeInTabs"; import type { FilePaneData, PaneViewerData } from "../../types"; import { openBesidePane } from "./openBesidePane"; @@ -12,8 +13,8 @@ function file(filePath: string) { }; } -function makeStore() { - const store = createWorkspaceStore(); +function makeStore(placePane?: typeof placeInTabs) { + const store = createWorkspaceStore({ placePane }); store.getState().addTab({ id: "t1", panes: [tree] }); return store; } @@ -59,6 +60,19 @@ describe("openBesidePane", () => { ).toHaveLength(3); }); + it("splits beside the anchor in a store that places panes in tabs", () => { + const store = makeStore(placeInTabs); + + openBesidePane(store, "tree", file("/a.ts")); + openBesidePane(store, "tree", file("/b.ts")); + + expect(store.getState().tabs).toHaveLength(1); + const panes = Object.values(store.getState().getTab("t1")?.panes ?? {}); + expect(panes).toHaveLength(2); + const filePane = panes.find((pane) => pane.kind === "file"); + expect((filePane?.data as FilePaneData).filePath).toBe("/b.ts"); + }); + it("opens a new tab when asked", () => { const store = makeStore(); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/utils/openBesidePane/openBesidePane.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/utils/openBesidePane/openBesidePane.ts index 17c7f62bbbb..191a38c0c05 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/utils/openBesidePane/openBesidePane.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/utils/openBesidePane/openBesidePane.ts @@ -13,5 +13,28 @@ export function openBesidePane( state.addTab({ panes: [pane] }); return; } - state.openPane({ pane, beside: anchorPaneId }); + const anchor = state.getPane(anchorPaneId); + const tab = anchor ? state.getTab(anchor.tabId) : null; + if (!anchor || !tab) { + state.openPane({ pane }); + return; + } + // Placed here, not by openPane: the right pane area's placePane opens every pane as a tab. + const reusable = Object.values(tab.panes).find( + (candidate) => + candidate.kind === pane.kind && + !candidate.pinned && + candidate.id !== anchorPaneId, + ); + if (reusable) { + state.setPaneData({ paneId: reusable.id, data: pane.data }); + state.focusPane(reusable.id); + return; + } + state.splitPane({ + tabId: tab.id, + paneId: anchorPaneId, + position: "left", + newPane: pane, + }); } From fcd8eabdcadb332ed6f7e78b5889520eab3edca8 Mon Sep 17 00:00:00 2001 From: Harshith Mullapudi <17528887+harshithmullapudi@users.noreply.github.com> Date: Sat, 31 Oct 2026 17:21:40 +0000 Subject: [PATCH 3/3] fix(desktop): place beside-panes in placeInTabs, stacked above the anchor Review feedback: openBesidePane copied the beside branch of the panes store and skipped getPaneKey reuse. Revert it to delegate to openPane, and make the right pane area's placeInTabs honor `beside` instead: reuse an unpinned pane of the kind in the anchor's tab, else split it in above the anchor. Stacking keeps a ~280 px Files changed list on screen, where a side-by-side split pushed it past the window (panes are >= 260 px). --- .../utils/placeInTabs/placeInTabs.test.ts | 24 ++++++++++++++++- .../utils/placeInTabs/placeInTabs.ts | 27 ++++++++++++++++++- .../openBesidePane/openBesidePane.test.ts | 18 ++----------- .../utils/openBesidePane/openBesidePane.ts | 25 +---------------- 4 files changed, 52 insertions(+), 42 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2WorkspacePaneLayout/utils/placeInTabs/placeInTabs.test.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2WorkspacePaneLayout/utils/placeInTabs/placeInTabs.test.ts index 9e90322a728..1086731f12e 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2WorkspacePaneLayout/utils/placeInTabs/placeInTabs.test.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2WorkspacePaneLayout/utils/placeInTabs/placeInTabs.test.ts @@ -18,7 +18,7 @@ describe("placeInTabs", () => { store.getState().addTab({ id: "t1", panes: [pane("p1")] }); store.getState().addTab({ id: "t2", panes: [pane("list", "list")] }); - store.getState().openPane({ pane: pane("p2"), beside: "list" }); + store.getState().openPane({ pane: pane("p2") }); expect(paneIdsByTab()).toEqual([["p2"], ["list"]]); expect(store.getState().activeTabId).toBe("t1"); @@ -27,6 +27,28 @@ describe("placeInTabs", () => { expect(paneIdsByTab()).toEqual([["p2"], ["list"], ["p3"]]); }); + it("stacks a beside pane above its anchor, then reuses it", () => { + const store = createWorkspaceStore({ placePane: placeInTabs }); + store.getState().addTab({ id: "t1", panes: [pane("p1")] }); + store.getState().addTab({ id: "t2", panes: [pane("list", "list")] }); + + store.getState().openPane({ pane: pane("d1", "diff"), beside: "list" }); + store.getState().openPane({ pane: pane("d2", "diff"), beside: "list" }); + + const tab = store.getState().getTab("t2"); + expect(store.getState().tabs).toHaveLength(2); + expect(tab?.layout).toMatchObject({ + type: "split", + direction: "vertical", + second: { type: "pane", paneId: "list" }, + }); + expect(Object.values(tab?.panes ?? {}).map((p) => p.data.label)).toEqual([ + "list", + "d2", + ]); + expect(store.getState().activeTabId).toBe("t2"); + }); + it("leaves focus alone when openPane is told not to focus", () => { const store = createWorkspaceStore({ placePane: placeInTabs }); store diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2WorkspacePaneLayout/utils/placeInTabs/placeInTabs.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2WorkspacePaneLayout/utils/placeInTabs/placeInTabs.ts index 873e4387062..5c45b628a2b 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2WorkspacePaneLayout/utils/placeInTabs/placeInTabs.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/hooks/useV2WorkspacePaneLayout/utils/placeInTabs/placeInTabs.ts @@ -2,8 +2,33 @@ import type { OpenPaneArgs, WorkspaceStore } from "@superset/panes"; export function placeInTabs( store: WorkspaceStore, - { pane, focus, reuse }: OpenPaneArgs, + { pane, beside, focus, reuse }: OpenPaneArgs, ): void { + const anchor = beside ? store.getPane(beside) : null; + const anchorTab = anchor ? store.getTab(anchor.tabId) : null; + if (anchor && anchorTab) { + const reusable = Object.values(anchorTab.panes).find( + (candidate) => + candidate.kind === pane.kind && + !candidate.pinned && + candidate.id !== anchor.pane.id, + ); + if (reusable) { + store.setPaneData({ paneId: reusable.id, data: pane.data }); + store.setActivePane({ tabId: anchorTab.id, paneId: reusable.id }); + return; + } + // Panes are at least 260px wide, so a side-by-side split pushes a narrow anchor off screen. + store.splitPane({ + tabId: anchorTab.id, + paneId: anchor.pane.id, + position: "top", + newPane: pane, + focus: false, + }); + return; + } + const tabs = [ ...store.tabs.filter((tab) => tab.id === store.activeTabId), ...store.tabs.filter((tab) => tab.id !== store.activeTabId), diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/utils/openBesidePane/openBesidePane.test.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/utils/openBesidePane/openBesidePane.test.ts index 4049d490a54..ce5ad28138a 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/utils/openBesidePane/openBesidePane.test.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/utils/openBesidePane/openBesidePane.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "bun:test"; import { createWorkspaceStore } from "@superset/panes"; -import { placeInTabs } from "../../hooks/useV2WorkspacePaneLayout/utils/placeInTabs"; import type { FilePaneData, PaneViewerData } from "../../types"; import { openBesidePane } from "./openBesidePane"; @@ -13,8 +12,8 @@ function file(filePath: string) { }; } -function makeStore(placePane?: typeof placeInTabs) { - const store = createWorkspaceStore({ placePane }); +function makeStore() { + const store = createWorkspaceStore(); store.getState().addTab({ id: "t1", panes: [tree] }); return store; } @@ -60,19 +59,6 @@ describe("openBesidePane", () => { ).toHaveLength(3); }); - it("splits beside the anchor in a store that places panes in tabs", () => { - const store = makeStore(placeInTabs); - - openBesidePane(store, "tree", file("/a.ts")); - openBesidePane(store, "tree", file("/b.ts")); - - expect(store.getState().tabs).toHaveLength(1); - const panes = Object.values(store.getState().getTab("t1")?.panes ?? {}); - expect(panes).toHaveLength(2); - const filePane = panes.find((pane) => pane.kind === "file"); - expect((filePane?.data as FilePaneData).filePath).toBe("/b.ts"); - }); - it("opens a new tab when asked", () => { const store = makeStore(); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/utils/openBesidePane/openBesidePane.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/utils/openBesidePane/openBesidePane.ts index 191a38c0c05..17c7f62bbbb 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/utils/openBesidePane/openBesidePane.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/utils/openBesidePane/openBesidePane.ts @@ -13,28 +13,5 @@ export function openBesidePane( state.addTab({ panes: [pane] }); return; } - const anchor = state.getPane(anchorPaneId); - const tab = anchor ? state.getTab(anchor.tabId) : null; - if (!anchor || !tab) { - state.openPane({ pane }); - return; - } - // Placed here, not by openPane: the right pane area's placePane opens every pane as a tab. - const reusable = Object.values(tab.panes).find( - (candidate) => - candidate.kind === pane.kind && - !candidate.pinned && - candidate.id !== anchorPaneId, - ); - if (reusable) { - state.setPaneData({ paneId: reusable.id, data: pane.data }); - state.focusPane(reusable.id); - return; - } - state.splitPane({ - tabId: tab.id, - paneId: anchorPaneId, - position: "left", - newPane: pane, - }); + state.openPane({ pane, beside: anchorPaneId }); }