authorgravatar for 126423069+Marve10s@users.noreply.github.comIbrahim Elkamali <126423069+Marve10s@users.noreply.github.com> 2026-05-08 03:12:59+03:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-05-07 17:12:59-07:00
log466d8ee5b0a003b3f735dd25e21273539b515316
tree7eac4914d8a5fdc0506875d1504bd3466afa7a75
parent11f40556de8a37c7f16486c23fb6bce058275557
signature Signed by PGP key B5690EEEBB952194

feat(web): add configurable sidebar thread preview count (#1856)

Co-authored-by: Julius Marminge <julius0216@outlook.com> Co-authored-by: codex <codex@users.noreply.github.com> Co-authored-by: Julius Marminge <jmarminge@gmail.com>

11 files changed, 308 insertions(+), 33 deletions(-)

.plans/19-remote-endpoints-hosted-static.md-1
......@@ -347,4 +347,3 @@ Each implementation PR should run:
347347- `bun typecheck`
348348- focused tests for changed backend/web behavior
349349- backend tests for any server-side endpoint discovery or auth changes using `bun run test`, never `bun test`
350
apps/desktop/src/clientPersistence.test.ts+1
......@@ -63,6 +63,7 @@ const clientSettings: ClientSettings = {
6363 },
6464 sidebarProjectSortOrder: "manual",
6565 sidebarThreadSortOrder: "created_at",
66 sidebarThreadPreviewCount: 6,
6667 timestampFormat: "24-hour",
6768};
6869
apps/web/package.json+1-1
......@@ -13,7 +13,7 @@
1313 "test:browser:install": "playwright install --with-deps chromium"
1414 },
1515 "dependencies": {
16 "@base-ui/react": "^1.2.0",
16 "@base-ui/react": "^1.4.1",
1717 "@dnd-kit/core": "^6.3.1",
1818 "@dnd-kit/modifiers": "^9.0.0",
1919 "@dnd-kit/sortable": "^10.0.0",
apps/web/src/components/Sidebar.tsx+94-7
......@@ -54,7 +54,10 @@ import {
5454} from "@t3tools/client-runtime";
5555import { Link, useLocation, useNavigate, useParams, useRouter } from "@tanstack/react-router";
5656import {
57 MAX_SIDEBAR_THREAD_PREVIEW_COUNT,
58 MIN_SIDEBAR_THREAD_PREVIEW_COUNT,
5759 type SidebarProjectSortOrder,
60 type SidebarThreadPreviewCount,
5861 type SidebarThreadSortOrder,
5962} from "@t3tools/contracts/settings";
6063import { usePrimaryEnvironmentId } from "../environments/primary";
......@@ -128,6 +131,13 @@ import {
128131 MenuSeparator,
129132 MenuTrigger,
130133} from "./ui/menu";
134import {
135 NumberField,
136 NumberFieldDecrement,
137 NumberFieldGroup,
138 NumberFieldIncrement,
139 NumberFieldInput,
140} from "./ui/number-field";
131141import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "./ui/select";
132142import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";
133143import {
......@@ -186,7 +196,6 @@ import {
186196 type SidebarProjectSnapshot,
187197} from "../sidebarProjectGrouping";
188198import { SidebarProviderUpdatePill } from "./sidebar/SidebarProviderUpdatePill";
189const THREAD_PREVIEW_LIMIT = 6;
190199const SIDEBAR_SORT_LABELS: Record<SidebarProjectSortOrder, string> = {
191200 updated_at: "Last user message",
192201 created_at: "Created at",
......@@ -207,6 +216,13 @@ const PROJECT_GROUPING_MODE_LABELS: Record<SidebarProjectGroupingMode, string> =
207216 separate: "Keep separate",
208217};
209218
219function clampSidebarThreadPreviewCount(value: number): SidebarThreadPreviewCount {
220 return Math.min(
221 MAX_SIDEBAR_THREAD_PREVIEW_COUNT,
222 Math.max(MIN_SIDEBAR_THREAD_PREVIEW_COUNT, value),
223 ) as SidebarThreadPreviewCount;
224}
225
210226function formatProjectMemberActionLabel(
211227 member: SidebarProjectGroupMember,
212228 groupedProjectCount: number,
......@@ -936,6 +952,9 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
936952 sidebarProjectGroupingOverrides: settings.sidebarProjectGroupingOverrides,
937953 }));
938954 const { updateSettings } = useUpdateSettings();
955 const sidebarThreadPreviewCount = useSettings<SidebarThreadPreviewCount>(
956 (settings) => settings.sidebarThreadPreviewCount,
957 );
939958 const router = useRouter();
940959 const { isMobile, setOpenMobile } = useSidebar();
941960 const markThreadUnread = useUiStateStore((state) => state.markThreadUnread);
......@@ -1159,11 +1178,11 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
11591178 },
11601179 });
11611180 };
1162 const hasOverflowingThreads = visibleProjectThreads.length > THREAD_PREVIEW_LIMIT;
1181 const hasOverflowingThreads = visibleProjectThreads.length > sidebarThreadPreviewCount;
11631182 const previewThreads =
11641183 isThreadListExpanded || !hasOverflowingThreads
11651184 ? visibleProjectThreads
1166 : visibleProjectThreads.slice(0, THREAD_PREVIEW_LIMIT);
1185 : visibleProjectThreads.slice(0, sidebarThreadPreviewCount);
11671186 const visibleThreadKeys = new Set(
11681187 [...previewThreads, ...(pinnedCollapsedThread ? [pinnedCollapsedThread] : [])].map((thread) =>
11691188 scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)),
......@@ -1192,6 +1211,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
11921211 pinnedCollapsedThread,
11931212 projectExpanded,
11941213 projectThreads,
1214 sidebarThreadPreviewCount,
11951215 threadLastVisitedAts,
11961216 visibleProjectThreads,
11971217 ]);
......@@ -2253,17 +2273,35 @@ function ProjectSortMenu({
22532273 projectSortOrder,
22542274 threadSortOrder,
22552275 projectGroupingMode,
2276 threadPreviewCount,
22562277 onProjectSortOrderChange,
22572278 onThreadSortOrderChange,
22582279 onProjectGroupingModeChange,
2280 onThreadPreviewCountChange,
22592281}: {
22602282 projectSortOrder: SidebarProjectSortOrder;
22612283 threadSortOrder: SidebarThreadSortOrder;
22622284 projectGroupingMode: SidebarProjectGroupingMode;
2285 threadPreviewCount: SidebarThreadPreviewCount;
22632286 onProjectSortOrderChange: (sortOrder: SidebarProjectSortOrder) => void;
22642287 onThreadSortOrderChange: (sortOrder: SidebarThreadSortOrder) => void;
22652288 onProjectGroupingModeChange: (mode: SidebarProjectGroupingMode) => void;
2289 onThreadPreviewCountChange: (count: SidebarThreadPreviewCount) => void;
22662290}) {
2291 const handleThreadPreviewCountChange = useCallback(
2292 (nextValue: number | null) => {
2293 if (nextValue === null) {
2294 return;
2295 }
2296
2297 const clampedValue = clampSidebarThreadPreviewCount(nextValue);
2298 if (clampedValue !== threadPreviewCount) {
2299 onThreadPreviewCountChange(clampedValue);
2300 }
2301 },
2302 [onThreadPreviewCountChange, threadPreviewCount],
2303 );
2304
22672305 return (
22682306 <Menu>
22692307 <Tooltip>
......@@ -2274,9 +2312,9 @@ function ProjectSortMenu({
22742312 >
22752313 <ArrowUpDownIcon className="size-3.5" />
22762314 </TooltipTrigger>
2277 <TooltipPopup side="right">Sort projects</TooltipPopup>
2315 <TooltipPopup side="right">Sidebar options</TooltipPopup>
22782316 </Tooltip>
2279 <MenuPopup align="end" side="bottom" className="min-w-44">
2317 <MenuPopup align="end" side="bottom" className="min-w-52">
22802318 <MenuGroup>
22812319 <div className="px-2 py-1 sm:text-xs font-medium text-muted-foreground">
22822320 Sort projects
......@@ -2315,6 +2353,42 @@ function ProjectSortMenu({
23152353 ))}
23162354 </MenuRadioGroup>
23172355 </MenuGroup>
2356 <MenuGroup>
2357 <div className="px-2 pt-2 pb-1 text-muted-foreground sm:text-xs font-medium">
2358 Visible threads
2359 </div>
2360 <div className="px-2 py-1">
2361 <NumberField
2362 aria-label="Visible thread count"
2363 className="w-28 gap-0"
2364 max={MAX_SIDEBAR_THREAD_PREVIEW_COUNT}
2365 min={MIN_SIDEBAR_THREAD_PREVIEW_COUNT}
2366 onValueChange={handleThreadPreviewCountChange}
2367 size="sm"
2368 step={1}
2369 value={threadPreviewCount}
2370 >
2371 <NumberFieldGroup className="h-7 rounded-md sm:h-6.5">
2372 <NumberFieldDecrement
2373 aria-label="Decrease visible thread count"
2374 className="px-2 sm:px-2 [&_svg]:size-3.5"
2375 />
2376 <NumberFieldInput
2377 aria-label="Visible thread count"
2378 className="h-7 w-9 grow-0 px-0 text-xs leading-7 sm:h-6.5 sm:leading-6.5"
2379 inputMode="numeric"
2380 onKeyDownCapture={(event) => {
2381 event.stopPropagation();
2382 }}
2383 />
2384 <NumberFieldIncrement
2385 aria-label="Increase visible thread count"
2386 className="px-2 sm:px-2 [&_svg]:size-3.5"
2387 />
2388 </NumberFieldGroup>
2389 </NumberField>
2390 </div>
2391 </MenuGroup>
23182392 <MenuSeparator />
23192393 <MenuGroup>
23202394 <div className="px-2 pt-2 pb-1 font-medium text-muted-foreground sm:text-xs">
......@@ -2462,6 +2536,7 @@ interface SidebarProjectsContentProps {
24622536 projectSortOrder: SidebarProjectSortOrder;
24632537 threadSortOrder: SidebarThreadSortOrder;
24642538 projectGroupingMode: SidebarProjectGroupingMode;
2539 threadPreviewCount: SidebarThreadPreviewCount;
24652540 updateSettings: ReturnType<typeof useUpdateSettings>["updateSettings"];
24662541 openAddProject: () => void;
24672542 isManualProjectSorting: boolean;
......@@ -2502,6 +2577,7 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
25022577 projectSortOrder,
25032578 threadSortOrder,
25042579 projectGroupingMode,
2580 threadPreviewCount,
25052581 updateSettings,
25062582 openAddProject,
25072583 isManualProjectSorting,
......@@ -2548,6 +2624,12 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
25482624 },
25492625 [updateSettings],
25502626 );
2627 const handleThreadPreviewCountChange = useCallback(
2628 (count: SidebarThreadPreviewCount) => {
2629 updateSettings({ sidebarThreadPreviewCount: count });
2630 },
2631 [updateSettings],
2632 );
25512633
25522634 return (
25532635 <SidebarContent className="gap-0">
......@@ -2607,9 +2689,11 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent(
26072689 projectSortOrder={projectSortOrder}
26082690 threadSortOrder={threadSortOrder}
26092691 projectGroupingMode={projectGroupingMode}
2692 threadPreviewCount={threadPreviewCount}
26102693 onProjectSortOrderChange={handleProjectSortOrderChange}
26112694 onThreadSortOrderChange={handleThreadSortOrderChange}
26122695 onProjectGroupingModeChange={handleProjectGroupingModeChange}
2696 onThreadPreviewCountChange={handleThreadPreviewCountChange}
26132697 />
26142698 <Tooltip>
26152699 <TooltipTrigger
......@@ -2729,6 +2813,7 @@ export default function Sidebar() {
27292813 sidebarProjectGroupingMode: settings.sidebarProjectGroupingMode,
27302814 sidebarProjectGroupingOverrides: settings.sidebarProjectGroupingOverrides,
27312815 }));
2816 const sidebarThreadPreviewCount = useSettings((s) => s.sidebarThreadPreviewCount);
27322817 const { updateSettings } = useUpdateSettings();
27332818 const { handleNewThread } = useNewThreadHandler();
27342819 const { archiveThread, deleteThread } = useThreadActions();
......@@ -3024,11 +3109,11 @@ export default function Sidebar() {
30243109 return [];
30253110 }
30263111 const isThreadListExpanded = expandedThreadListsByProject.has(project.projectKey);
3027 const hasOverflowingThreads = projectThreads.length > THREAD_PREVIEW_LIMIT;
3112 const hasOverflowingThreads = projectThreads.length > sidebarThreadPreviewCount;
30283113 const previewThreads =
30293114 isThreadListExpanded || !hasOverflowingThreads
30303115 ? projectThreads
3031 : projectThreads.slice(0, THREAD_PREVIEW_LIMIT);
3116 : projectThreads.slice(0, sidebarThreadPreviewCount);
30323117 const renderedThreads = pinnedCollapsedThread ? [pinnedCollapsedThread] : previewThreads;
30333118 return renderedThreads.map((thread) =>
30343119 scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)),
......@@ -3036,6 +3121,7 @@ export default function Sidebar() {
30363121 }),
30373122 [
30383123 sidebarThreadSortOrder,
3124 sidebarThreadPreviewCount,
30393125 expandedThreadListsByProject,
30403126 projectExpandedById,
30413127 routeThreadKey,
......@@ -3358,6 +3444,7 @@ export default function Sidebar() {
33583444 projectSortOrder={sidebarProjectSortOrder}
33593445 threadSortOrder={sidebarThreadSortOrder}
33603446 projectGroupingMode={sidebarProjectGroupingMode}
3447 threadPreviewCount={sidebarThreadPreviewCount}
33613448 updateSettings={updateSettings}
33623449 openAddProject={openAddProjectCommandPalette}
33633450 isManualProjectSorting={isManualProjectSorting}
apps/web/src/components/settings/SettingsPanels.tsx+4
......@@ -364,6 +364,9 @@ export function useSettingsRestore(onRestored?: () => void) {
364364 ...(settings.timestampFormat !== DEFAULT_UNIFIED_SETTINGS.timestampFormat
365365 ? ["Time format"]
366366 : []),
367 ...(settings.sidebarThreadPreviewCount !== DEFAULT_UNIFIED_SETTINGS.sidebarThreadPreviewCount
368 ? ["Visible threads"]
369 : []),
367370 ...(settings.diffWordWrap !== DEFAULT_UNIFIED_SETTINGS.diffWordWrap
368371 ? ["Diff line wrapping"]
369372 : []),
......@@ -400,6 +403,7 @@ export function useSettingsRestore(onRestored?: () => void) {
400403 settings.diffIgnoreWhitespace,
401404 settings.diffWordWrap,
402405 settings.enableAssistantStreaming,
406 settings.sidebarThreadPreviewCount,
403407 settings.timestampFormat,
404408 theme,
405409 ],
apps/web/src/components/ui/input.tsx+26-15
......@@ -27,6 +27,31 @@ function Input({
2727 props.type === "file" &&
2828 "text-muted-foreground file:me-3 file:bg-transparent file:font-medium file:text-foreground file:text-sm",
2929 );
30 let inputElement: React.ReactElement;
31
32 if (nativeInput) {
33 const { style, onValueChange: _onValueChange, ...nativeInputProps } = props;
34 const nativeStyle = typeof style === "function" ? undefined : style;
35
36 inputElement = (
37 <input
38 className={inputClassName}
39 data-slot="input"
40 size={typeof size === "number" ? size : undefined}
41 style={nativeStyle}
42 {...(nativeInputProps as React.ComponentProps<"input">)}
43 />
44 );
45 } else {
46 inputElement = (
47 <InputPrimitive
48 className={inputClassName}
49 data-slot="input"
50 size={typeof size === "number" ? size : undefined}
51 {...props}
52 />
53 );
54 }
3055
3156 return (
3257 <span
......@@ -40,21 +65,7 @@ function Input({
4065 data-size={size}
4166 data-slot="input-control"
4267 >
43 {nativeInput ? (
44 <input
45 className={inputClassName}
46 data-slot="input"
47 size={typeof size === "number" ? size : undefined}
48 {...props}
49 />
50 ) : (
51 <InputPrimitive
52 className={inputClassName}
53 data-slot="input"
54 size={typeof size === "number" ? size : undefined}
55 {...props}
56 />
57 )}
68 {inputElement}
5869 </span>
5970 );
6071}
apps/web/src/components/ui/number-field.tsx created+156
......@@ -0,0 +1,156 @@
1"use client";
2
3import { NumberField as NumberFieldPrimitive } from "@base-ui/react/number-field";
4import { MinusIcon, PlusIcon } from "lucide-react";
5import * as React from "react";
6import { cn } from "~/lib/utils";
7import { Label } from "~/components/ui/label";
8
9export const NumberFieldContext: React.Context<{
10 fieldId: string;
11} | null> = React.createContext<{
12 fieldId: string;
13} | null>(null);
14
15export function NumberField({
16 id,
17 className,
18 size = "default",
19 ...props
20}: NumberFieldPrimitive.Root.Props & {
21 size?: "sm" | "default" | "lg";
22}): React.ReactElement {
23 const generatedId = React.useId();
24 const fieldId = id ?? generatedId;
25 const contextValue = React.useMemo(() => ({ fieldId }), [fieldId]);
26
27 return (
28 <NumberFieldContext.Provider value={contextValue}>
29 <NumberFieldPrimitive.Root
30 className={cn("flex w-full flex-col items-start gap-2", className)}
31 data-size={size}
32 data-slot="number-field"
33 id={fieldId}
34 {...props}
35 />
36 </NumberFieldContext.Provider>
37 );
38}
39
40export function NumberFieldGroup({
41 className,
42 ...props
43}: NumberFieldPrimitive.Group.Props): React.ReactElement {
44 return (
45 <NumberFieldPrimitive.Group
46 className={cn(
47 "relative flex w-full justify-between rounded-lg border border-input bg-background not-dark:bg-clip-padding text-base text-foreground shadow-xs/5 ring-ring/24 transition-shadow before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] not-data-disabled:not-focus-within:not-aria-invalid:before:shadow-[0_1px_--theme(--color-black/4%)] focus-within:border-ring focus-within:ring-[3px] has-aria-invalid:border-destructive/36 has-autofill:bg-foreground/4 focus-within:has-aria-invalid:border-destructive/64 focus-within:has-aria-invalid:ring-destructive/48 data-disabled:pointer-events-none data-disabled:opacity-64 sm:text-sm dark:bg-input/32 dark:has-autofill:bg-foreground/8 dark:has-aria-invalid:ring-destructive/24 dark:not-data-disabled:not-focus-within:not-aria-invalid:before:shadow-[0_-1px_--theme(--color-white/6%)] [&_svg:not([class*='size-'])]:size-4.5 sm:[&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0 [[data-disabled],:focus-within,[aria-invalid]]:shadow-none",
48 className,
49 )}
50 data-slot="number-field-group"
51 {...props}
52 />
53 );
54}
55
56export function NumberFieldDecrement({
57 className,
58 ...props
59}: NumberFieldPrimitive.Decrement.Props): React.ReactElement {
60 return (
61 <NumberFieldPrimitive.Decrement
62 className={cn(
63 "relative flex shrink-0 cursor-pointer items-center justify-center rounded-s-[calc(var(--radius-lg)-1px)] in-data-[size=sm]:px-[calc(--spacing(2.5)-1px)] px-[calc(--spacing(3)-1px)] transition-colors pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:bg-accent",
64 className,
65 )}
66 data-slot="number-field-decrement"
67 {...props}
68 >
69 <MinusIcon />
70 </NumberFieldPrimitive.Decrement>
71 );
72}
73
74export function NumberFieldIncrement({
75 className,
76 ...props
77}: NumberFieldPrimitive.Increment.Props): React.ReactElement {
78 return (
79 <NumberFieldPrimitive.Increment
80 className={cn(
81 "relative flex shrink-0 cursor-pointer items-center justify-center rounded-e-[calc(var(--radius-lg)-1px)] in-data-[size=sm]:px-[calc(--spacing(2.5)-1px)] px-[calc(--spacing(3)-1px)] transition-colors pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:bg-accent",
82 className,
83 )}
84 data-slot="number-field-increment"
85 {...props}
86 >
87 <PlusIcon />
88 </NumberFieldPrimitive.Increment>
89 );
90}
91
92export function NumberFieldInput({
93 className,
94 ...props
95}: NumberFieldPrimitive.Input.Props): React.ReactElement {
96 return (
97 <NumberFieldPrimitive.Input
98 className={cn(
99 "h-8.5 in-data-[size=lg]:h-9.5 in-data-[size=sm]:h-7.5 w-full min-w-0 grow bg-transparent in-data-[size=sm]:px-[calc(--spacing(2.5)-1px)] px-[calc(--spacing(3)-1px)] text-center tabular-nums in-data-[size=lg]:leading-9.5 in-data-[size=sm]:leading-7.5 leading-8.5 outline-none [transition:background-color_5000000s_ease-in-out_0s] sm:h-7.5 sm:in-data-[size=lg]:h-8.5 sm:in-data-[size=sm]:h-6.5 sm:in-data-[size=lg]:leading-8.5 sm:in-data-[size=sm]:leading-8.5 sm:leading-7.5",
100 className,
101 )}
102 data-slot="number-field-input"
103 {...props}
104 />
105 );
106}
107
108export function NumberFieldScrubArea({
109 className,
110 label,
111 ...props
112}: NumberFieldPrimitive.ScrubArea.Props & {
113 label: string;
114}): React.ReactElement {
115 const context = React.useContext(NumberFieldContext);
116
117 if (!context) {
118 throw new Error(
119 "NumberFieldScrubArea must be used within a NumberField component for accessibility.",
120 );
121 }
122
123 return (
124 <NumberFieldPrimitive.ScrubArea
125 className={cn("flex cursor-ew-resize", className)}
126 data-slot="number-field-scrub-area"
127 {...props}
128 >
129 <Label className="cursor-ew-resize" htmlFor={context.fieldId}>
130 {label}
131 </Label>
132 <NumberFieldPrimitive.ScrubAreaCursor className="drop-shadow-[0_1px_1px_#0008] filter">
133 <CursorGrowIcon />
134 </NumberFieldPrimitive.ScrubAreaCursor>
135 </NumberFieldPrimitive.ScrubArea>
136 );
137}
138
139export function CursorGrowIcon(props: React.ComponentProps<"svg">): React.ReactElement {
140 return (
141 <svg
142 aria-hidden="true"
143 fill="black"
144 height="14"
145 stroke="white"
146 viewBox="0 0 24 14"
147 width="26"
148 xmlns="http://www.w3.org/2000/svg"
149 {...props}
150 >
151 <path d="M19.5 5.5L6.49737 5.51844V2L1 6.9999L6.5 12L6.49737 8.5L19.5 8.5V12L25 6.9999L19.5 2V5.5Z" />
152 </svg>
153 );
154}
155
156export { NumberFieldPrimitive };
apps/web/src/localApi.test.ts+2
......@@ -613,6 +613,7 @@ describe("wsApi", () => {
613613 },
614614 sidebarProjectSortOrder: "manual" as const,
615615 sidebarThreadSortOrder: "created_at" as const,
616 sidebarThreadPreviewCount: 6,
616617 timestampFormat: "24-hour" as const,
617618 };
618619 const getClientSettings = vi.fn().mockResolvedValue({
......@@ -675,6 +676,7 @@ describe("wsApi", () => {
675676 },
676677 sidebarProjectSortOrder: "manual" as const,
677678 sidebarThreadSortOrder: "created_at" as const,
679 sidebarThreadPreviewCount: 6,
678680 timestampFormat: "24-hour" as const,
679681 };
680682
apps/web/vite.config.ts-2
......@@ -7,7 +7,6 @@ import pkg from "./package.json" with { type: "json" };
77
88const port = Number(process.env.PORT ?? 5733);
99const host = process.env.HOST?.trim() || "localhost";
10const configuredHttpUrl = process.env.VITE_HTTP_URL?.trim();
1110const configuredWsUrl = process.env.VITE_WS_URL?.trim();
1211const configuredHostedAppUrl = (() => {
1312 if (process.env.VERCEL_ENV === "production" && process.env.VERCEL_PROJECT_PRODUCTION_URL) {
......@@ -74,7 +73,6 @@ export default defineConfig({
7473 ],
7574 },
7675 define: {
77 "import.meta.env.VITE_HTTP_URL": JSON.stringify(configuredHttpUrl ?? ""),
7876 // In dev mode, tell the web app where the WebSocket server lives
7977 "import.meta.env.VITE_WS_URL": JSON.stringify(configuredWsUrl ?? ""),
8078 "import.meta.env.VITE_HOSTED_APP_URL": JSON.stringify(configuredHostedAppUrl ?? ""),
bun.lock+11-7
......@@ -16,7 +16,7 @@
1616 },
1717 "apps/desktop": {
1818 "name": "@t3tools/desktop",
19 "version": "0.0.21",
19 "version": "0.0.22",
2020 "dependencies": {
2121 "@effect/platform-node": "catalog:",
2222 "effect": "catalog:",
......@@ -49,7 +49,7 @@
4949 },
5050 "apps/server": {
5151 "name": "t3",
52 "version": "0.0.21",
52 "version": "0.0.22",
5353 "bin": {
5454 "t3": "./dist/bin.mjs",
5555 },
......@@ -82,9 +82,9 @@
8282 },
8383 "apps/web": {
8484 "name": "@t3tools/web",
85 "version": "0.0.21",
85 "version": "0.0.22",
8686 "dependencies": {
87 "@base-ui/react": "^1.2.0",
87 "@base-ui/react": "^1.4.1",
8888 "@dnd-kit/core": "^6.3.1",
8989 "@dnd-kit/modifiers": "^9.0.0",
9090 "@dnd-kit/sortable": "^10.0.0",
......@@ -148,7 +148,7 @@
148148 },
149149 "packages/contracts": {
150150 "name": "@t3tools/contracts",
151 "version": "0.0.21",
151 "version": "0.0.22",
152152 "dependencies": {
153153 "effect": "catalog:",
154154 },
......@@ -350,9 +350,9 @@
350350
351351 "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
352352
353 "@base-ui/react": ["@base-ui/react@1.3.0", "", { "dependencies": { "@babel/runtime": "^7.28.6", "@base-ui/utils": "0.2.6", "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "tabbable": "^6.4.0", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-FwpKqZbPz14AITp1CVgf4AjhKPe1OeeVKSBMdgD10zbFlj3QSWelmtCMLi2+/PFZZcIm3l87G7rwtCZJwHyXWA=="],
353 "@base-ui/react": ["@base-ui/react@1.4.1", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@base-ui/utils": "0.2.8", "@floating-ui/react-dom": "^2.1.8", "@floating-ui/utils": "^0.2.11", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@date-fns/tz": "^1.2.0", "@types/react": "^17 || ^18 || ^19", "date-fns": "^4.0.0", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@date-fns/tz", "@types/react", "date-fns"] }, "sha512-Ab5/LIhcmL8BQcsBUYiOfkSDRdLpvgUBzMK30cu684JPcLclYlztharvCZyNNgzJtbAiREzI9q0pI5erHCMgCw=="],
354354
355 "@base-ui/utils": ["@base-ui/utils@0.2.6", "", { "dependencies": { "@babel/runtime": "^7.28.6", "@floating-ui/utils": "^0.2.11", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-yQ+qeuqohwhsNpoYDqqXaLllYAkPCP4vYdDrVo8FQXaAPfHWm1pG/Vm+jmGTA5JFS0BAIjookyapuJFY8F9PIw=="],
355 "@base-ui/utils": ["@base-ui/utils@0.2.8", "", { "dependencies": { "@babel/runtime": "^7.29.2", "@floating-ui/utils": "^0.2.11", "reselect": "^5.1.1", "use-sync-external-store": "^1.6.0" }, "peerDependencies": { "@types/react": "^17 || ^18 || ^19", "react": "^17 || ^18 || ^19", "react-dom": "^17 || ^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-jvOi+c+ftGlGotNcKnzPVg2IhCaDTB6/6R3JeqdjdXktuAJi3wKH9T7+svuaKh1mmfVU11UWzUZVH74JDfi/wQ=="],
356356
357357 "@blazediff/core": ["@blazediff/core@1.9.1", "", {}, "sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA=="],
358358
......@@ -2122,6 +2122,10 @@
21222122
21232123 "@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="],
21242124
2125 "@base-ui/react/@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
2126
2127 "@base-ui/utils/@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="],
2128
21252129 "@electron/get/fs-extra": ["fs-extra@8.1.0", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g=="],
21262130
21272131 "@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
packages/contracts/src/settings.ts+13
......@@ -27,6 +27,16 @@ export const SidebarProjectGroupingMode = Schema.Literals([
2727]);
2828export type SidebarProjectGroupingMode = typeof SidebarProjectGroupingMode.Type;
2929export const DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE: SidebarProjectGroupingMode = "repository";
30export const MIN_SIDEBAR_THREAD_PREVIEW_COUNT = 1;
31export const MAX_SIDEBAR_THREAD_PREVIEW_COUNT = 15;
32export const SidebarThreadPreviewCount = Schema.Int.check(
33 Schema.isBetween({
34 minimum: MIN_SIDEBAR_THREAD_PREVIEW_COUNT,
35 maximum: MAX_SIDEBAR_THREAD_PREVIEW_COUNT,
36 }),
37);
38export type SidebarThreadPreviewCount = typeof SidebarThreadPreviewCount.Type;
39export const DEFAULT_SIDEBAR_THREAD_PREVIEW_COUNT: SidebarThreadPreviewCount = 6;
3040
3141export const ClientSettingsSchema = Schema.Struct({
3242 autoOpenPlanSidebar: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))),
......@@ -75,6 +85,9 @@ export const ClientSettingsSchema = Schema.Struct({
7585 sidebarThreadSortOrder: SidebarThreadSortOrder.pipe(
7686 Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_THREAD_SORT_ORDER)),
7787 ),
88 sidebarThreadPreviewCount: SidebarThreadPreviewCount.pipe(
89 Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_THREAD_PREVIEW_COUNT)),
90 ),
7891 timestampFormat: TimestampFormat.pipe(
7992 Schema.withDecodingDefault(Effect.succeed(DEFAULT_TIMESTAMP_FORMAT)),
8093 ),