From db4378e820cc74a7846307c1da0452e684bc4295 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Thu, 30 Apr 2026 19:00:01 -0700 Subject: [PATCH 1/5] fix(desktop): expand collapsed sections when cycling workspaces via keyboard (#3848) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Meta+Alt+Up/Down (and the ⌘1..9 jump shortcuts) cycle through every workspace in the sidebar regardless of collapse state, but the destination row stayed hidden when its parent project or section was collapsed. Toggle the collapsed parents open before navigating so the focused workspace is visible. --- .../useDashboardSidebarShortcuts.ts | 61 ++++++++++++++++++- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts index 7660739882c..82413df4232 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/DashboardSidebar/hooks/useDashboardSidebarShortcuts/useDashboardSidebarShortcuts.ts @@ -2,9 +2,17 @@ import { useMatchRoute, useNavigate } from "@tanstack/react-router"; import { useCallback, useMemo, useRef } from "react"; import { useHotkey } from "renderer/hotkeys"; import { navigateToV2Workspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; +import { useDashboardSidebarState } from "renderer/routes/_authenticated/hooks/useDashboardSidebarState"; import type { DashboardSidebarProject } from "../../types"; import { getProjectChildrenWorkspaces } from "../../utils/projectChildren"; +interface WorkspaceLocation { + projectId: string; + projectIsCollapsed: boolean; + sectionId: string | null; + sectionIsCollapsed: boolean; +} + const MAX_SHORTCUT_COUNT = 9; function haveSameIds(left: string[], right: string[]): boolean { @@ -43,6 +51,8 @@ export function useDashboardSidebarShortcuts( groups: DashboardSidebarProject[], ) { const navigate = useNavigate(); + const { toggleProjectCollapsed, toggleSectionCollapsed } = + useDashboardSidebarState(); const flattenedWorkspaces = useMemo( () => groups @@ -53,14 +63,55 @@ export function useDashboardSidebarShortcuts( const workspaceShortcutLabels = useStableWorkspaceShortcutLabels(flattenedWorkspaces); + const workspaceLocations = useMemo(() => { + const map = new Map(); + for (const project of groups) { + for (const child of project.children) { + if (child.type === "workspace") { + map.set(child.workspace.id, { + projectId: project.id, + projectIsCollapsed: project.isCollapsed, + sectionId: null, + sectionIsCollapsed: false, + }); + continue; + } + for (const workspace of child.section.workspaces) { + map.set(workspace.id, { + projectId: project.id, + projectIsCollapsed: project.isCollapsed, + sectionId: child.section.id, + sectionIsCollapsed: child.section.isCollapsed, + }); + } + } + } + return map; + }, [groups]); + + const revealWorkspace = useCallback( + (workspaceId: string) => { + const location = workspaceLocations.get(workspaceId); + if (!location) return; + if (location.projectIsCollapsed) { + toggleProjectCollapsed(location.projectId); + } + if (location.sectionId && location.sectionIsCollapsed) { + toggleSectionCollapsed(location.sectionId); + } + }, + [workspaceLocations, toggleProjectCollapsed, toggleSectionCollapsed], + ); + const switchToWorkspace = useCallback( (index: number) => { const workspace = flattenedWorkspaces[index]; if (workspace) { + revealWorkspace(workspace.id); navigateToV2Workspace(workspace.id, navigate); } }, - [flattenedWorkspaces, navigate], + [flattenedWorkspaces, navigate, revealWorkspace], ); useHotkey("JUMP_TO_WORKSPACE_1", () => switchToWorkspace(0)); @@ -88,7 +139,9 @@ export function useDashboardSidebarShortcuts( ); if (index === -1) return; const prevIndex = index <= 0 ? flattenedWorkspaces.length - 1 : index - 1; - navigateToV2Workspace(flattenedWorkspaces[prevIndex].id, navigate); + const target = flattenedWorkspaces[prevIndex]; + revealWorkspace(target.id); + navigateToV2Workspace(target.id, navigate); }); useHotkey("NEXT_WORKSPACE", () => { @@ -98,7 +151,9 @@ export function useDashboardSidebarShortcuts( ); if (index === -1) return; const nextIndex = index >= flattenedWorkspaces.length - 1 ? 0 : index + 1; - navigateToV2Workspace(flattenedWorkspaces[nextIndex].id, navigate); + const target = flattenedWorkspaces[nextIndex]; + revealWorkspace(target.id); + navigateToV2Workspace(target.id, navigate); }); return workspaceShortcutLabels; From d13a509fa82e70742892e72ee191dd25d03ce698 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Wed, 29 Apr 2026 20:36:21 -0700 Subject: [PATCH 2/5] fix(api): jwtProcedure accepts x-api-key sessions, not just Bearer (#3895) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Better-auth's apiKey plugin (`enableSessionForAPIKeys: true`) populates ctx.session for x-api-key requests, but jwtProcedure was throwing on the very first line if the request didn't carry an Authorization Bearer header — so any procedure marked jwtProcedure rejected api-key auth before reaching the session fallback. The CLI's `superset hosts`, `workspaces.list`, etc. all 401'd with `--api-key sk_live_…`. Accept any of: a verifiable Bearer JWT, a successful Bearer JWT fallback, or an x-api-key-derived session. Throw only if none of those produce identity. The TRPCError re-throw on explicit JWT rejection is preserved. Trailing error message updated to reflect the broader contract. --- packages/trpc/src/trpc.ts | 63 ++++++++++++++++++++++++++------------- 1 file changed, 42 insertions(+), 21 deletions(-) diff --git a/packages/trpc/src/trpc.ts b/packages/trpc/src/trpc.ts index b971aff2c94..125c39f4164 100644 --- a/packages/trpc/src/trpc.ts +++ b/packages/trpc/src/trpc.ts @@ -77,36 +77,57 @@ export const protectedProcedure = t.procedure export const jwtProcedure = t.procedure.use(async ({ ctx, next }) => { const authHeader = ctx.headers.get("authorization"); - if (!authHeader?.startsWith("Bearer ")) { - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "JWT bearer token required", - }); - } + const bearer = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null; - const token = authHeader.slice(7); - try { - const { payload } = await ctx.auth.api.verifyJWT({ body: { token } }); - if (!payload?.sub) { - throw new TRPCError({ code: "UNAUTHORIZED", message: "Invalid JWT" }); + if (bearer) { + try { + const { payload } = await ctx.auth.api.verifyJWT({ + body: { token: bearer }, + }); + if (payload?.sub) { + const organizationIds = (payload.organizationIds as string[]) ?? []; + return next({ + ctx: { + userId: payload.sub, + email: (payload.email as string) ?? "", + organizationIds, + activeOrganizationId: organizationIds[0] ?? null, + }, + }); + } + } catch (error) { + // A live session is the legit fallback for an unverifiable token + // (expired/missing). A TRPCError from verifyJWT is an explicit + // rejection (revoked/forged) — surface it instead of laundering + // it into session auth. + if (error instanceof TRPCError) throw error; } + } - const organizationIds = (payload.organizationIds as string[]) ?? []; + if (ctx.session) { + const userId = ctx.session.user.id; + const memberRows = await db.query.members.findMany({ + where: eq(members.userId, userId), + columns: { organizationId: true }, + }); + const organizationIds = memberRows.map((row) => row.organizationId); return next({ ctx: { - userId: payload.sub, - email: (payload.email as string) ?? "", + userId, + email: ctx.session.user.email ?? "", organizationIds, - activeOrganizationId: organizationIds[0] ?? null, + activeOrganizationId: + ctx.session.session.activeOrganizationId ?? + organizationIds[0] ?? + null, }, }); - } catch (error) { - if (error instanceof TRPCError) throw error; - throw new TRPCError({ - code: "UNAUTHORIZED", - message: "JWT verification failed", - }); } + + throw new TRPCError({ + code: "UNAUTHORIZED", + message: "Not authenticated. Provide a bearer JWT, x-api-key, or session.", + }); }); export const adminProcedure = protectedProcedure.use(async ({ ctx, next }) => { From a741f514e255aa692d0fa450797e0638b04ab5ad Mon Sep 17 00:00:00 2001 From: Kiet <31864905+Kitenite@users.noreply.github.com> Date: Wed, 29 Apr 2026 21:57:48 -0700 Subject: [PATCH 3/5] feat(desktop): add hover dropdown actions to changes sidebar rows (#3897) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each file in the v2 changes sidebar now reveals a more-actions dropdown on hover, with Open Diff / Open Diff in New Tab / Open File / Open File in New Tab / Open in Editor (mirrors the right-click menu, plus new Open File entries that route through the existing file-open pane). Click-modifier shortcut hints (⇧/⌘) are shown next to the actions that have them. --- .../WorkspaceSidebar/WorkspaceSidebar.tsx | 1 + .../ChangesFileList/ChangesFileList.tsx | 3 + .../components/FileRow/FileRow.tsx | 142 +++++++++++++----- .../ChangesTabContent/ChangesTabContent.tsx | 3 + .../hooks/useChangesTab/useChangesTab.tsx | 3 + 5 files changed, 114 insertions(+), 38 deletions(-) diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx index bac06261b80..12e90ced38b 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/WorkspaceSidebar.tsx @@ -131,6 +131,7 @@ export function WorkspaceSidebar({ workspaceId, gitStatus, onSelectFile: onSelectDiffFile, + onOpenFile: onSelectFile, }); const changesTab: SidebarTabDefinition = { ...changesTabDef, diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/ChangesFileList.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/ChangesFileList.tsx index 36c976d31b0..8a622d02e91 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/ChangesFileList.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/ChangesFileList.tsx @@ -7,6 +7,7 @@ interface ChangesFileListProps { isLoading?: boolean; worktreePath?: string; onSelectFile?: (path: string, openInNewTab?: boolean) => void; + onOpenFile?: (absolutePath: string, openInNewTab?: boolean) => void; onOpenInEditor?: (path: string) => void; } @@ -15,6 +16,7 @@ export const ChangesFileList = memo(function ChangesFileList({ isLoading, worktreePath, onSelectFile, + onOpenFile, onOpenInEditor, }: ChangesFileListProps) { if (isLoading) { @@ -41,6 +43,7 @@ export const ChangesFileList = memo(function ChangesFileList({ file={file} worktreePath={worktreePath} onSelect={onSelectFile} + onOpenFile={onOpenFile} onOpenInEditor={onOpenInEditor} /> ))} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/components/FileRow/FileRow.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/components/FileRow/FileRow.tsx index b77d344d761..a1d90d2bbd0 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/components/FileRow/FileRow.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesFileList/components/FileRow/FileRow.tsx @@ -6,7 +6,15 @@ import { ContextMenuShortcut, ContextMenuTrigger, } from "@superset/ui/context-menu"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuShortcut, + DropdownMenuTrigger, +} from "@superset/ui/dropdown-menu"; import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; +import { ChevronDown } from "lucide-react"; import { memo } from "react"; import { StatusIndicator } from "renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/StatusIndicator"; import { PathActionsMenuItems } from "renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/components/FilesTab/components/WorkspaceFilesTreeItem/components/PathActionsMenuItems"; @@ -33,6 +41,7 @@ interface FileRowProps { file: ChangesetFile; worktreePath?: string; onSelect?: (path: string, openInNewTab?: boolean) => void; + onOpenFile?: (absolutePath: string, openInNewTab?: boolean) => void; onOpenInEditor?: (path: string) => void; } @@ -40,6 +49,7 @@ export const FileRow = memo(function FileRow({ file, worktreePath, onSelect, + onOpenFile, onOpenInEditor, }: FileRowProps) { const { dir, basename } = splitPath(file.path); @@ -52,46 +62,90 @@ export const FileRow = memo(function FileRow({ : undefined; const rowButton = ( - + + {(file.additions > 0 || file.deletions > 0) && ( + + {file.additions > 0 && ( + +{file.additions} + )} + {file.additions > 0 && file.deletions > 0 && " "} + {file.deletions > 0 && ( + -{file.deletions} + )} + + )} + + + +
+ + + + + + onSelect?.(file.path)}> + Open Diff + + onSelect?.(file.path, true)}> + Open Diff in New Tab + {SHIFT_CLICK_LABEL} + + absolutePath && onOpenFile?.(absolutePath)} + disabled={!onOpenFile || !absolutePath} + > + Open File + + absolutePath && onOpenFile?.(absolutePath, true)} + disabled={!onOpenFile || !absolutePath} + > + Open File in New Tab + + onOpenInEditor?.(file.path)} + disabled={!onOpenInEditor} + > + Open in Editor + {MOD_CLICK_LABEL} + + + +
+ ); return ( @@ -110,6 +164,18 @@ export const FileRow = memo(function FileRow({ Open Diff in New Tab {SHIFT_CLICK_LABEL} + absolutePath && onOpenFile?.(absolutePath)} + disabled={!onOpenFile || !absolutePath} + > + Open File + + absolutePath && onOpenFile?.(absolutePath, true)} + disabled={!onOpenFile || !absolutePath} + > + Open File in New Tab + onOpenInEditor?.(file.path)} disabled={!onOpenInEditor} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesTabContent/ChangesTabContent.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesTabContent/ChangesTabContent.tsx index 33442519c15..211fb8500cb 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesTabContent/ChangesTabContent.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/components/ChangesTabContent/ChangesTabContent.tsx @@ -24,6 +24,7 @@ interface ChangesTabContentProps { totalDeletions: number; worktreePath?: string; onSelectFile?: (path: string, openInNewTab?: boolean) => void; + onOpenFile?: (absolutePath: string, openInNewTab?: boolean) => void; onOpenInEditor?: (path: string) => void; onFilterChange: (filter: ChangesFilter) => void; onBaseBranchChange: (branchName: string) => void; @@ -44,6 +45,7 @@ export const ChangesTabContent = memo(function ChangesTabContent({ totalDeletions, worktreePath, onSelectFile, + onOpenFile, onOpenInEditor, onFilterChange, onBaseBranchChange, @@ -92,6 +94,7 @@ export const ChangesTabContent = memo(function ChangesTabContent({ isLoading={isLoading} worktreePath={worktreePath} onSelectFile={onSelectFile} + onOpenFile={onOpenFile} onOpenInEditor={onOpenInEditor} /> diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx index 809df197830..4660bf8cb31 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/v2-workspace/$workspaceId/components/WorkspaceSidebar/hooks/useChangesTab/useChangesTab.tsx @@ -22,12 +22,14 @@ interface UseChangesTabParams { workspaceId: string; gitStatus: ReturnType; onSelectFile?: (path: string, openInNewTab?: boolean) => void; + onOpenFile?: (absolutePath: string, openInNewTab?: boolean) => void; } export function useChangesTab({ workspaceId, gitStatus: status, onSelectFile, + onOpenFile, }: UseChangesTabParams): SidebarTabDefinition { const collections = useCollections(); const utils = workspaceTrpc.useUtils(); @@ -192,6 +194,7 @@ export function useChangesTab({ totalDeletions={totalDeletions} worktreePath={worktreePath} onSelectFile={onSelectFile} + onOpenFile={onOpenFile} onOpenInEditor={handleOpenInEditor} onFilterChange={setFilter} onBaseBranchChange={setBaseBranch} From b6d7b09da2d1e9f502fd83c5f0d56dcdede60938 Mon Sep 17 00:00:00 2001 From: Satya Patel Date: Thu, 30 Apr 2026 21:57:30 -0700 Subject: [PATCH 4/5] fix(docs): redirect docs root to /overview after install+overview merge (#3935) --- apps/docs/next.config.mjs | 4 ++-- apps/docs/src/app/sitemap.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/docs/next.config.mjs b/apps/docs/next.config.mjs index 2b0fcb13ec8..c656a361dd4 100644 --- a/apps/docs/next.config.mjs +++ b/apps/docs/next.config.mjs @@ -29,12 +29,12 @@ const config = { return [ { source: "/", - destination: "/installation", + destination: "/overview", permanent: false, }, { source: "/docs", - destination: "/installation", + destination: "/overview", permanent: false, }, ]; diff --git a/apps/docs/src/app/sitemap.ts b/apps/docs/src/app/sitemap.ts index c78151903f6..7072ac1cbfc 100644 --- a/apps/docs/src/app/sitemap.ts +++ b/apps/docs/src/app/sitemap.ts @@ -11,6 +11,6 @@ export default function sitemap(): MetadataRoute.Sitemap { url: `${baseUrl}${page.url}`, lastModified: new Date(), changeFrequency: "weekly" as const, - priority: page.url === "/installation" ? 1.0 : 0.8, + priority: page.url === "/overview" ? 1.0 : 0.8, })); } From 478d7c07bc2d871408b25111e4d09cd020c4ba99 Mon Sep 17 00:00:00 2001 From: Avi Peltz Date: Thu, 30 Apr 2026 23:38:02 -0700 Subject: [PATCH 5/5] style(panes): match v2 tab + add-button styling to v1 (#3939) Active tab uses bg-border/30 instead of bg-muted, and the add-tab button is a 28px chip with a border and bg-muted/30 fill, sitting centered inside its 40px cell, matching the v1 GroupStrip look. --- .../components/Workspace/components/TabBar/TabBar.tsx | 10 +++++----- .../components/TabBar/components/TabItem/TabItem.tsx | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/panes/src/react/components/Workspace/components/TabBar/TabBar.tsx b/packages/panes/src/react/components/Workspace/components/TabBar/TabBar.tsx index c6fb91f210f..0f99f630b01 100644 --- a/packages/panes/src/react/components/Workspace/components/TabBar/TabBar.tsx +++ b/packages/panes/src/react/components/Workspace/components/TabBar/TabBar.tsx @@ -45,8 +45,8 @@ function AddTabButton<_TData>({ }) { const button = (