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
1 change: 1 addition & 0 deletions apps/desktop/src/clientPersistence.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ const clientSettings: ClientSettings = {
confirmThreadDelete: false,
diffWordWrap: true,
sidebarProjectSortOrder: "manual",
sidebarThreadPreviewCount: 6,
sidebarThreadSortOrder: "created_at",
timestampFormat: "24-hour",
};
Expand Down
126 changes: 119 additions & 7 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
CloudIcon,
FolderIcon,
GitPullRequestIcon,
MinusIcon,
PlusIcon,
SearchIcon,
SettingsIcon,
Expand Down Expand Up @@ -50,7 +51,10 @@ import {
} from "@t3tools/client-runtime";
import { Link, useLocation, useNavigate, useParams, useRouter } from "@tanstack/react-router";
import {
MAX_SIDEBAR_THREAD_PREVIEW_COUNT,
MIN_SIDEBAR_THREAD_PREVIEW_COUNT,
type SidebarProjectSortOrder,
type SidebarThreadPreviewCount,
type SidebarThreadSortOrder,
} from "@t3tools/contracts/settings";
import { usePrimaryEnvironmentId } from "../environments/primary";
Expand Down Expand Up @@ -103,6 +107,7 @@ import {
} from "./desktopUpdate.logic";
import { Alert, AlertAction, AlertDescription, AlertTitle } from "./ui/alert";
import { Button } from "./ui/button";
import { Input } from "./ui/input";
import { Menu, MenuGroup, MenuPopup, MenuRadioGroup, MenuRadioItem, MenuTrigger } from "./ui/menu";
import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
import {
Expand Down Expand Up @@ -148,7 +153,6 @@ import {
useSavedEnvironmentRuntimeStore,
} from "../environments/runtime";
import type { Project, SidebarThreadSummary } from "../types";
const THREAD_PREVIEW_LIMIT = 6;
const SIDEBAR_SORT_LABELS: Record<SidebarProjectSortOrder, string> = {
updated_at: "Last user message",
created_at: "Created at",
Expand All @@ -164,6 +168,13 @@ const SIDEBAR_LIST_ANIMATION_OPTIONS = {
} as const;
const EMPTY_THREAD_JUMP_LABELS = new Map<string, string>();

function clampSidebarThreadPreviewCount(value: number): SidebarThreadPreviewCount {
return Math.min(
MAX_SIDEBAR_THREAD_PREVIEW_COUNT,
Math.max(MIN_SIDEBAR_THREAD_PREVIEW_COUNT, value),
) as SidebarThreadPreviewCount;
}

function threadJumpLabelMapsEqual(
left: ReadonlyMap<string, string>,
right: ReadonlyMap<string, string>,
Expand Down Expand Up @@ -980,6 +991,9 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
const defaultThreadEnvMode = useSettings<ThreadEnvMode>(
(settings) => settings.defaultThreadEnvMode,
);
const sidebarThreadPreviewCount = useSettings<SidebarThreadPreviewCount>(
(settings) => settings.sidebarThreadPreviewCount,
);
const router = useRouter();
const markThreadUnread = useUiStateStore((state) => state.markThreadUnread);
const toggleProject = useUiStateStore((state) => state.toggleProject);
Expand Down Expand Up @@ -1195,11 +1209,11 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
},
});
};
const hasOverflowingThreads = visibleProjectThreads.length > THREAD_PREVIEW_LIMIT;
const hasOverflowingThreads = visibleProjectThreads.length > sidebarThreadPreviewCount;
const previewThreads =
isThreadListExpanded || !hasOverflowingThreads
? visibleProjectThreads
: visibleProjectThreads.slice(0, THREAD_PREVIEW_LIMIT);
: visibleProjectThreads.slice(0, sidebarThreadPreviewCount);
const visibleThreadKeys = new Set(
[...previewThreads, ...(pinnedCollapsedThread ? [pinnedCollapsedThread] : [])].map((thread) =>
scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)),
Expand Down Expand Up @@ -1228,6 +1242,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
pinnedCollapsedThread,
projectExpanded,
projectThreads,
sidebarThreadPreviewCount,
threadLastVisitedAts,
visibleProjectThreads,
]);
Expand Down Expand Up @@ -1839,14 +1854,41 @@ type SortableProjectHandleProps = Pick<
function ProjectSortMenu({
projectSortOrder,
threadSortOrder,
threadPreviewCount,
onProjectSortOrderChange,
onThreadSortOrderChange,
onThreadPreviewCountChange,
}: {
projectSortOrder: SidebarProjectSortOrder;
threadSortOrder: SidebarThreadSortOrder;
threadPreviewCount: SidebarThreadPreviewCount;
onProjectSortOrderChange: (sortOrder: SidebarProjectSortOrder) => void;
onThreadSortOrderChange: (sortOrder: SidebarThreadSortOrder) => void;
onThreadPreviewCountChange: (count: SidebarThreadPreviewCount) => void;
}) {
const [threadPreviewInput, setThreadPreviewInput] = useState(() => String(threadPreviewCount));

useEffect(() => {
setThreadPreviewInput(String(threadPreviewCount));
}, [threadPreviewCount]);

const commitThreadPreviewCount = useCallback(
(nextValue: string) => {
const parsedValue = Number.parseInt(nextValue, 10);
if (!Number.isInteger(parsedValue)) {
setThreadPreviewInput(String(threadPreviewCount));
return;
}

const clampedValue = clampSidebarThreadPreviewCount(parsedValue);
setThreadPreviewInput(String(clampedValue));
if (clampedValue !== threadPreviewCount) {
onThreadPreviewCountChange(clampedValue);
}
},
[onThreadPreviewCountChange, threadPreviewCount],
);

return (
<Menu>
<Tooltip>
Expand All @@ -1857,9 +1899,9 @@ function ProjectSortMenu({
>
<ArrowUpDownIcon className="size-3.5" />
</TooltipTrigger>
<TooltipPopup side="right">Sort projects</TooltipPopup>
<TooltipPopup side="right">Sidebar options</TooltipPopup>
</Tooltip>
<MenuPopup align="end" side="bottom" className="min-w-44">
<MenuPopup align="end" side="bottom" className="min-w-52">
<MenuGroup>
<div className="px-2 py-1 sm:text-xs font-medium text-muted-foreground">
Sort projects
Expand Down Expand Up @@ -1898,6 +1940,63 @@ function ProjectSortMenu({
))}
</MenuRadioGroup>
</MenuGroup>
<MenuGroup>
<div className="px-2 pt-2 pb-1 text-muted-foreground sm:text-xs font-medium">
Visible threads
</div>
<div className="flex items-center gap-2 px-2 py-1">
<Button
size="icon-xs"
variant="outline"
className="size-7 shrink-0"
aria-label="Decrease visible thread count"
disabled={threadPreviewCount <= MIN_SIDEBAR_THREAD_PREVIEW_COUNT}
onClick={() =>
onThreadPreviewCountChange(clampSidebarThreadPreviewCount(threadPreviewCount - 1))
}
>
<MinusIcon className="size-3.5" />
</Button>
<Input
nativeInput
type="text"
inputMode="numeric"
min={MIN_SIDEBAR_THREAD_PREVIEW_COUNT}
max={MAX_SIDEBAR_THREAD_PREVIEW_COUNT}
pattern="[0-9]*"
size="sm"
className="w-14"
aria-label="Visible thread count"
value={threadPreviewInput}
onKeyDownCapture={(event) => {
event.stopPropagation();
}}
onChange={(event) => {
setThreadPreviewInput(event.currentTarget.value.replace(/[^0-9]/g, ""));
}}
onBlur={(event) => {
commitThreadPreviewCount(event.currentTarget.value);
}}
onKeyDown={(event) => {
if (event.key === "Enter") {
commitThreadPreviewCount(event.currentTarget.value);
}
}}
/>
<Button
size="icon-xs"
variant="outline"
className="size-7 shrink-0"
aria-label="Increase visible thread count"
disabled={threadPreviewCount >= MAX_SIDEBAR_THREAD_PREVIEW_COUNT}
onClick={() =>
onThreadPreviewCountChange(clampSidebarThreadPreviewCount(threadPreviewCount + 1))
}
>
<PlusIcon className="size-3.5" />
</Button>
</div>
</MenuGroup>
</MenuPopup>
</Menu>
);
Expand Down Expand Up @@ -2015,6 +2114,7 @@ interface SidebarProjectsContentProps {
handleDesktopUpdateButtonClick: () => void;
projectSortOrder: SidebarProjectSortOrder;
threadSortOrder: SidebarThreadSortOrder;
threadPreviewCount: SidebarThreadPreviewCount;
updateSettings: ReturnType<typeof useUpdateSettings>["updateSettings"];
shouldShowProjectPathEntry: boolean;
handleStartAddProject: () => void;
Expand Down Expand Up @@ -2067,6 +2167,7 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
handleDesktopUpdateButtonClick,
projectSortOrder,
threadSortOrder,
threadPreviewCount,
updateSettings,
shouldShowProjectPathEntry,
handleStartAddProject,
Expand Down Expand Up @@ -2120,6 +2221,12 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
},
[updateSettings],
);
const handleThreadPreviewCountChange = useCallback(
(count: SidebarThreadPreviewCount) => {
updateSettings({ sidebarThreadPreviewCount: count });
},
[updateSettings],
);
const handleAddProjectInputChange = useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
setNewCwd(event.target.value);
Expand Down Expand Up @@ -2198,8 +2305,10 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
<ProjectSortMenu
projectSortOrder={projectSortOrder}
threadSortOrder={threadSortOrder}
threadPreviewCount={threadPreviewCount}
onProjectSortOrderChange={handleProjectSortOrderChange}
onThreadSortOrderChange={handleThreadSortOrderChange}
onThreadPreviewCountChange={handleThreadPreviewCountChange}
/>
<Tooltip>
<TooltipTrigger
Expand Down Expand Up @@ -2364,6 +2473,7 @@ export default function Sidebar() {
const isOnSettings = pathname.startsWith("/settings");
const sidebarThreadSortOrder = useSettings((s) => s.sidebarThreadSortOrder);
const sidebarProjectSortOrder = useSettings((s) => s.sidebarProjectSortOrder);
const sidebarThreadPreviewCount = useSettings((s) => s.sidebarThreadPreviewCount);
const defaultThreadEnvMode = useSettings((s) => s.defaultThreadEnvMode);
const { updateSettings } = useUpdateSettings();
const { handleNewThread } = useNewThreadHandler();
Expand Down Expand Up @@ -2819,18 +2929,19 @@ export default function Sidebar() {
return [];
}
const isThreadListExpanded = expandedThreadListsByProject.has(project.projectKey);
const hasOverflowingThreads = projectThreads.length > THREAD_PREVIEW_LIMIT;
const hasOverflowingThreads = projectThreads.length > sidebarThreadPreviewCount;
const previewThreads =
isThreadListExpanded || !hasOverflowingThreads
? projectThreads
: projectThreads.slice(0, THREAD_PREVIEW_LIMIT);
: projectThreads.slice(0, sidebarThreadPreviewCount);
const renderedThreads = pinnedCollapsedThread ? [pinnedCollapsedThread] : previewThreads;
return renderedThreads.map((thread) =>
scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)),
);
}),
[
sidebarThreadSortOrder,
sidebarThreadPreviewCount,
expandedThreadListsByProject,
projectExpandedById,
routeThreadKey,
Expand Down Expand Up @@ -3170,6 +3281,7 @@ export default function Sidebar() {
handleDesktopUpdateButtonClick={handleDesktopUpdateButtonClick}
projectSortOrder={sidebarProjectSortOrder}
threadSortOrder={sidebarThreadSortOrder}
threadPreviewCount={sidebarThreadPreviewCount}
updateSettings={updateSettings}
shouldShowProjectPathEntry={shouldShowProjectPathEntry}
handleStartAddProject={handleStartAddProject}
Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/components/settings/SettingsPanels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,9 @@ export function useSettingsRestore(onRestored?: () => void) {
...(settings.timestampFormat !== DEFAULT_UNIFIED_SETTINGS.timestampFormat
? ["Time format"]
: []),
...(settings.sidebarThreadPreviewCount !== DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount
? ["Visible threads"]
: []),
...(settings.diffWordWrap !== DEFAULT_UNIFIED_SETTINGS.diffWordWrap
? ["Diff line wrapping"]
: []),
Expand All @@ -394,6 +397,7 @@ export function useSettingsRestore(onRestored?: () => void) {
settings.defaultThreadEnvMode,
settings.diffWordWrap,
settings.enableAssistantStreaming,
settings.sidebarThreadPreviewCount,
settings.timestampFormat,
theme,
],
Expand Down
5 changes: 5 additions & 0 deletions apps/web/src/localApi.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,6 +505,7 @@ describe("wsApi", () => {
diffWordWrap: true,
sidebarProjectSortOrder: "manual",
sidebarThreadSortOrder: "created_at",
sidebarThreadPreviewCount: 6,
timestampFormat: "24-hour",
});
const setClientSettings = vi.fn().mockResolvedValue(undefined);
Expand Down Expand Up @@ -533,6 +534,7 @@ describe("wsApi", () => {
diffWordWrap: true,
sidebarProjectSortOrder: "manual",
sidebarThreadSortOrder: "created_at",
sidebarThreadPreviewCount: 6,
timestampFormat: "24-hour",
});
await api.persistence.getSavedEnvironmentRegistry();
Expand All @@ -551,6 +553,7 @@ describe("wsApi", () => {
diffWordWrap: true,
sidebarProjectSortOrder: "manual",
sidebarThreadSortOrder: "created_at",
sidebarThreadPreviewCount: 6,
timestampFormat: "24-hour",
});
expect(getSavedEnvironmentRegistry).toHaveBeenCalledWith();
Expand All @@ -570,6 +573,7 @@ describe("wsApi", () => {
diffWordWrap: true,
sidebarProjectSortOrder: "manual",
sidebarThreadSortOrder: "created_at",
sidebarThreadPreviewCount: 6,
timestampFormat: "24-hour",
});
await api.persistence.setSavedEnvironmentRegistry([
Expand All @@ -593,6 +597,7 @@ describe("wsApi", () => {
diffWordWrap: true,
sidebarProjectSortOrder: "manual",
sidebarThreadSortOrder: "created_at",
sidebarThreadPreviewCount: 6,
timestampFormat: "24-hour",
});
await expect(api.persistence.getSavedEnvironmentRegistry()).resolves.toEqual([
Expand Down
14 changes: 14 additions & 0 deletions packages/contracts/src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ export const SidebarThreadSortOrder = Schema.Literals(["updated_at", "created_at
export type SidebarThreadSortOrder = typeof SidebarThreadSortOrder.Type;
export const DEFAULT_SIDEBAR_THREAD_SORT_ORDER: SidebarThreadSortOrder = "updated_at";

export const MIN_SIDEBAR_THREAD_PREVIEW_COUNT = 2;
export const MAX_SIDEBAR_THREAD_PREVIEW_COUNT = 10;
export const SidebarThreadPreviewCount = Schema.Int.check(
Schema.isBetween({
minimum: MIN_SIDEBAR_THREAD_PREVIEW_COUNT,
maximum: MAX_SIDEBAR_THREAD_PREVIEW_COUNT,
}),
);
export type SidebarThreadPreviewCount = typeof SidebarThreadPreviewCount.Type;
export const DEFAULT_SIDEBAR_THREAD_PREVIEW_COUNT: SidebarThreadPreviewCount = 6;

export const ClientSettingsSchema = Schema.Struct({
confirmThreadArchive: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
confirmThreadDelete: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
Expand All @@ -36,6 +47,9 @@ export const ClientSettingsSchema = Schema.Struct({
timestampFormat: TimestampFormat.pipe(
Schema.withDecodingDefault(Effect.succeed(DEFAULT_TIMESTAMP_FORMAT)),
),
sidebarThreadPreviewCount: SidebarThreadPreviewCount.pipe(
Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_THREAD_PREVIEW_COUNT)),
),
});
export type ClientSettings = typeof ClientSettingsSchema.Type;

Expand Down
Loading