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
18 changes: 17 additions & 1 deletion apps/web/src/components/CommandPalette.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
FileSearchIcon,
FolderIcon,
FolderPlusIcon,
LayersIcon,
LinkIcon,
MessageSquareIcon,
MonitorIcon,
Expand Down Expand Up @@ -76,7 +77,7 @@ import { useDesktopLocalBootstraps } from "../connection/useDesktopLocalBootstra
import { useHandleNewThread } from "../hooks/useHandleNewThread";
import { useOpenPanelPullRequestUrl } from "../hooks/useOpenPanelPullRequestUrl";
import { writeTextToClipboard } from "../hooks/useCopyToClipboard";
import { useClientSettings } from "../hooks/useSettings";
import { useClientSettings, useUpdateClientSettings } from "../hooks/useSettings";
import { useTheme } from "../hooks/useTheme";
import { useCustomThemes } from "../hooks/useCustomThemes";
import { useEnvironmentThemeDefinitions } from "../hooks/useEnvironmentTheme";
Expand Down Expand Up @@ -705,6 +706,7 @@ function OpenCommandPaletteDialog(props: {
const isActionsOnly = deferredQuery.startsWith(">");
const [highlightedItemValue, setHighlightedItemValue] = useState<string | null>(null);
const clientSettings = useClientSettings();
const updateClientSettings = useUpdateClientSettings();
const createProject = useAtomCommand(projectEnvironment.create, {
reportFailure: false,
});
Expand Down Expand Up @@ -2010,6 +2012,20 @@ function OpenCommandPaletteDialog(props: {
},
});

if (!clientSettings.legacySidebarEnabled) {
actionItems.push({
kind: "action",
value: "action:sidebar-tabs",
searchTerms: ["tabs", "sidebar", "chat tabs", "group", "show", "hide"],
title: clientSettings.sidebarShowTabs ? "Hide tabs in sidebar" : "Show tabs in sidebar",
icon: <LayersIcon className={ITEM_ICON_CLASS} />,
shortcutCommand: "sidebar.toggleTabs",
run: async () => {
await updateClientSettings({ sidebarShowTabs: !clientSettings.sidebarShowTabs });
},
});
}

if (
environments.some(
(environment) => environment.serverConfig?.environment.capabilities.pullRequests === true,
Expand Down
34 changes: 34 additions & 0 deletions apps/web/src/components/Sidebar.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
deleteSelectedThreadEntries,
filterSidebarProjectScopeItems,
getSidebarThreadIdsToPrewarm,
groupSidebarTabThreads,
resolveAdjacentThreadId,
reduceSidebarProjectScopeMenuState,
getFallbackThreadIdAfterDelete,
Expand Down Expand Up @@ -45,6 +46,7 @@ import {
shouldCreateNewThreadInCurrentProject,
shouldNavigateAfterThreadPark,
THREAD_JUMP_HINT_SHOW_DELAY_MS,
withSidebarTabThreads,
type SidebarListItem,
type SidebarListMarker,
type SidebarSection,
Expand Down Expand Up @@ -598,6 +600,38 @@ describe("shouldCreateNewThreadInCurrentProject", () => {
});
});

describe("sidebar tab rows", () => {
const threadByKey = new Map(
["local:a", "local:a2", "local:a3", "local:b", "local:c", "local:c2"].map((key) => [key, key]),
);
const hiddenTabThreads = new Map([
["local:a3", "local:a"],
["local:c2", "local:c"],
["local:a2", "local:a"],
["local:gone", "local:c"],
]);

it("lists each group's tabs under its row in tab order, skipping unknown threads", () => {
const tabsByRowKey = groupSidebarTabThreads(threadByKey, hiddenTabThreads);
expect([...tabsByRowKey]).toEqual([
["local:a", ["local:a3", "local:a2"]],
["local:c", ["local:c2"]],
]);
expect(
withSidebarTabThreads(["local:b", "local:a", "local:c"], (key) => key, tabsByRowKey),
).toEqual(["local:b", "local:a", "local:a3", "local:a2", "local:c", "local:c2"]);
});

it("leaves tabs out when their group's row is not listed", () => {
const tabsByRowKey = groupSidebarTabThreads(threadByKey, hiddenTabThreads);
expect(withSidebarTabThreads(["local:b", "local:c"], (key) => key, tabsByRowKey)).toEqual([
"local:b",
"local:c",
"local:c2",
]);
});
});

describe("orderItemsByPreferredIds", () => {
it("keeps preferred ids first, skips stale ids, and preserves the relative order of remaining items", () => {
const ordered = orderItemsByPreferredIds({
Expand Down
28 changes: 28 additions & 0 deletions apps/web/src/components/Sidebar.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,34 @@ export function orderItemsByPreferredIds<TItem, TId>(input: {
return [...ordered, ...remaining];
}

/**
* The tabs listed under each tab group's sidebar row, in tab order. `hiddenTabThreads` maps a
* tab's thread key to the key of the row that stands for its group.
*/
export function groupSidebarTabThreads<T>(
threadByKey: ReadonlyMap<string, T>,
hiddenTabThreads: ReadonlyMap<string, string>,
): ReadonlyMap<string, readonly T[]> {
const tabsByRowKey = new Map<string, T[]>();
for (const [threadKey, rowKey] of hiddenTabThreads) {
const thread = threadByKey.get(threadKey);
if (thread === undefined) continue;
const tabs = tabsByRowKey.get(rowKey);
if (tabs) tabs.push(thread);
else tabsByRowKey.set(rowKey, [thread]);
}
return tabsByRowKey;
}

/** Rows in the order the sidebar shows them: each group's row followed by its tabs. */
export function withSidebarTabThreads<T>(
rows: readonly T[],
getKey: (row: T) => string,
tabsByRowKey: ReadonlyMap<string, readonly T[]>,
): T[] {
return rows.flatMap((row) => [row, ...(tabsByRowKey.get(getKey(row)) ?? [])]);
}

export function getSidebarThreadIdsToPrewarm<TThreadId>(
visibleThreadIds: readonly TThreadId[],
limit = SIDEBAR_THREAD_PREWARM_LIMIT,
Expand Down
Loading