From 93095a275e600610248ca3de383be26c3ef496ab Mon Sep 17 00:00:00 2001 From: Marve10s Date: Thu, 9 Apr 2026 21:48:00 +0300 Subject: [PATCH 01/11] configurable sidebar preview count --- apps/web/src/components/Sidebar.tsx | 125 +++++++++++++++++- .../components/settings/SettingsPanels.tsx | 4 + apps/web/vite.config.ts | 2 +- packages/contracts/src/settings.ts | 13 ++ 4 files changed, 136 insertions(+), 8 deletions(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 9939833a951..38873367c46 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -4,6 +4,7 @@ import { ChevronRightIcon, CloudIcon, GitPullRequestIcon, + MinusIcon, PlusIcon, SearchIcon, SettingsIcon, @@ -54,7 +55,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"; @@ -178,7 +182,6 @@ import { type SidebarProjectGroupMember, type SidebarProjectSnapshot, } from "../sidebarProjectGrouping"; -const THREAD_PREVIEW_LIMIT = 6; const SIDEBAR_SORT_LABELS: Record = { updated_at: "Last user message", created_at: "Created at", @@ -199,6 +202,13 @@ const PROJECT_GROUPING_MODE_LABELS: Record = separate: "Keep separate", }; +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, right: ReadonlyMap, @@ -942,6 +952,9 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec sidebarProjectGroupingOverrides: settings.sidebarProjectGroupingOverrides, })); const { updateSettings } = useUpdateSettings(); + const sidebarThreadPreviewCount = useSettings( + (settings) => settings.sidebarThreadPreviewCount, + ); const router = useRouter(); const markThreadUnread = useUiStateStore((state) => state.markThreadUnread); const toggleProject = useUiStateStore((state) => state.toggleProject); @@ -1165,11 +1178,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)), @@ -1198,6 +1211,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec pinnedCollapsedThread, projectExpanded, projectThreads, + sidebarThreadPreviewCount, threadLastVisitedAts, visibleProjectThreads, ]); @@ -2164,17 +2178,44 @@ function ProjectSortMenu({ projectSortOrder, threadSortOrder, projectGroupingMode, + threadPreviewCount, onProjectSortOrderChange, onThreadSortOrderChange, onProjectGroupingModeChange, + onThreadPreviewCountChange, }: { projectSortOrder: SidebarProjectSortOrder; threadSortOrder: SidebarThreadSortOrder; projectGroupingMode: SidebarProjectGroupingMode; + threadPreviewCount: SidebarThreadPreviewCount; onProjectSortOrderChange: (sortOrder: SidebarProjectSortOrder) => void; onThreadSortOrderChange: (sortOrder: SidebarThreadSortOrder) => void; onProjectGroupingModeChange: (mode: SidebarProjectGroupingMode) => 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 ( @@ -2185,9 +2226,9 @@ function ProjectSortMenu({ > - Sort projects + Sidebar options - +
Sort projects @@ -2226,6 +2267,63 @@ function ProjectSortMenu({ ))} + +
+ Visible threads +
+
+ + { + 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); + } + }} + /> + +
+
@@ -2368,6 +2466,7 @@ interface SidebarProjectsContentProps { projectSortOrder: SidebarProjectSortOrder; threadSortOrder: SidebarThreadSortOrder; projectGroupingMode: SidebarProjectGroupingMode; + threadPreviewCount: SidebarThreadPreviewCount; updateSettings: ReturnType["updateSettings"]; openAddProject: () => void; isManualProjectSorting: boolean; @@ -2408,6 +2507,7 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( projectSortOrder, threadSortOrder, projectGroupingMode, + threadPreviewCount, updateSettings, openAddProject, isManualProjectSorting, @@ -2454,6 +2554,12 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( }, [updateSettings], ); + const handleThreadPreviewCountChange = useCallback( + (count: SidebarThreadPreviewCount) => { + updateSettings({ sidebarThreadPreviewCount: count }); + }, + [updateSettings], + ); return ( @@ -2513,9 +2619,11 @@ const SidebarProjectsContent = memo(function SidebarProjectsContent( projectSortOrder={projectSortOrder} threadSortOrder={threadSortOrder} projectGroupingMode={projectGroupingMode} + threadPreviewCount={threadPreviewCount} onProjectSortOrderChange={handleProjectSortOrderChange} onThreadSortOrderChange={handleThreadSortOrderChange} onProjectGroupingModeChange={handleProjectGroupingModeChange} + onThreadPreviewCountChange={handleThreadPreviewCountChange} /> s.sidebarThreadPreviewCount); const { updateSettings } = useUpdateSettings(); const { handleNewThread } = useNewThreadHandler(); const { archiveThread, deleteThread } = useThreadActions(); @@ -2923,11 +3032,11 @@ 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)), @@ -2935,6 +3044,7 @@ export default function Sidebar() { }), [ sidebarThreadSortOrder, + sidebarThreadPreviewCount, expandedThreadListsByProject, projectExpandedById, routeThreadKey, @@ -3299,6 +3409,7 @@ export default function Sidebar() { projectSortOrder={sidebarProjectSortOrder} threadSortOrder={sidebarThreadSortOrder} projectGroupingMode={sidebarProjectGroupingMode} + threadPreviewCount={sidebarThreadPreviewCount} updateSettings={updateSettings} openAddProject={openAddProjectCommandPalette} isManualProjectSorting={isManualProjectSorting} diff --git a/apps/web/src/components/settings/SettingsPanels.tsx b/apps/web/src/components/settings/SettingsPanels.tsx index f76c69581d8..a87c3c26986 100644 --- a/apps/web/src/components/settings/SettingsPanels.tsx +++ b/apps/web/src/components/settings/SettingsPanels.tsx @@ -435,6 +435,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"] : []), @@ -465,6 +468,7 @@ export function useSettingsRestore(onRestored?: () => void) { settings.defaultThreadEnvMode, settings.diffWordWrap, settings.enableAssistantStreaming, + settings.sidebarThreadPreviewCount, settings.timestampFormat, theme, ], diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 01b50766952..8b2ca530daa 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -72,7 +72,7 @@ export default defineConfig({ port, strictPort: true, ...(devProxyTarget - ? { + ? { proxy: { "/.well-known": { target: devProxyTarget, diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index fada38eb1ca..cd1fd2aa9d5 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -30,6 +30,16 @@ export const SidebarProjectGroupingMode = Schema.Literals([ ]); export type SidebarProjectGroupingMode = typeof SidebarProjectGroupingMode.Type; export const DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE: SidebarProjectGroupingMode = "repository"; +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))), @@ -48,6 +58,9 @@ export const ClientSettingsSchema = Schema.Struct({ sidebarThreadSortOrder: SidebarThreadSortOrder.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_THREAD_SORT_ORDER)), ), + sidebarThreadPreviewCount: SidebarThreadPreviewCount.pipe( + Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_THREAD_PREVIEW_COUNT)), + ), timestampFormat: TimestampFormat.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_TIMESTAMP_FORMAT)), ), From 321a395390c178263005bba3e867a72418bd3137 Mon Sep 17 00:00:00 2001 From: Marve10s Date: Thu, 9 Apr 2026 22:14:33 +0300 Subject: [PATCH 02/11] fix(web): stop forcing default primary http url --- apps/web/vite.config.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 8b2ca530daa..8eb8caa21c9 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -7,7 +7,6 @@ import pkg from "./package.json" with { type: "json" }; const port = Number(process.env.PORT ?? 5733); const host = process.env.HOST?.trim() || "localhost"; -const configuredHttpUrl = process.env.VITE_HTTP_URL?.trim(); const configuredWsUrl = process.env.VITE_WS_URL?.trim(); const sourcemapEnv = process.env.T3CODE_WEB_SOURCEMAP?.trim().toLowerCase(); @@ -40,7 +39,6 @@ function resolveDevProxyTarget(wsUrl: string | undefined): string | undefined { } const devProxyTarget = resolveDevProxyTarget(configuredWsUrl); - export default defineConfig({ plugins: [ tanstackRouter(), @@ -59,7 +57,6 @@ export default defineConfig({ include: ["@pierre/diffs", "@pierre/diffs/react", "@pierre/diffs/worker/worker.js"], }, define: { - "import.meta.env.VITE_HTTP_URL": JSON.stringify(configuredHttpUrl ?? ""), // In dev mode, tell the web app where the WebSocket server lives "import.meta.env.VITE_WS_URL": JSON.stringify(configuredWsUrl ?? ""), "import.meta.env.APP_VERSION": JSON.stringify(pkg.version), From e4c541a3030e4c2f82d6c8acb9e7add166a75574 Mon Sep 17 00:00:00 2001 From: Marve10s Date: Thu, 9 Apr 2026 22:35:34 +0300 Subject: [PATCH 03/11] revert unrelated vite.config.ts change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the blank-line removal that was left over from the dev bootstrap fix commits — not in scope for this PR. --- apps/web/vite.config.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 8eb8caa21c9..5d6fe2de405 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -39,6 +39,7 @@ function resolveDevProxyTarget(wsUrl: string | undefined): string | undefined { } const devProxyTarget = resolveDevProxyTarget(configuredWsUrl); + export default defineConfig({ plugins: [ tanstackRouter(), From 9573ebf9a909480dd57f81d97bdad149b3797efb Mon Sep 17 00:00:00 2001 From: Marve10s Date: Fri, 10 Apr 2026 19:10:28 +0300 Subject: [PATCH 04/11] fix(desktop): add sidebarThreadPreviewCount to test fixture The clientPersistence test was missing the new field after merge. --- apps/desktop/src/clientPersistence.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/desktop/src/clientPersistence.test.ts b/apps/desktop/src/clientPersistence.test.ts index 27f1e1d91ae..43ca897e099 100644 --- a/apps/desktop/src/clientPersistence.test.ts +++ b/apps/desktop/src/clientPersistence.test.ts @@ -57,6 +57,7 @@ const clientSettings: ClientSettings = { "environment-1:/tmp/project-a": "separate", }, sidebarProjectSortOrder: "manual", + sidebarThreadPreviewCount: 6, sidebarThreadSortOrder: "created_at", timestampFormat: "24-hour", }; From ccf24c118ecd38da498890f3a2668e1954e56c1e Mon Sep 17 00:00:00 2001 From: Marve10s Date: Fri, 10 Apr 2026 19:19:02 +0300 Subject: [PATCH 05/11] Fix client settings type coverage for thread preview count --- apps/desktop/src/clientPersistence.test.ts | 2 +- apps/web/src/localApi.test.ts | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/clientPersistence.test.ts b/apps/desktop/src/clientPersistence.test.ts index 43ca897e099..0c2933efe06 100644 --- a/apps/desktop/src/clientPersistence.test.ts +++ b/apps/desktop/src/clientPersistence.test.ts @@ -57,8 +57,8 @@ const clientSettings: ClientSettings = { "environment-1:/tmp/project-a": "separate", }, sidebarProjectSortOrder: "manual", - sidebarThreadPreviewCount: 6, sidebarThreadSortOrder: "created_at", + sidebarThreadPreviewCount: 6, timestampFormat: "24-hour", }; diff --git a/apps/web/src/localApi.test.ts b/apps/web/src/localApi.test.ts index 4258ccb3802..746ee59844b 100644 --- a/apps/web/src/localApi.test.ts +++ b/apps/web/src/localApi.test.ts @@ -538,6 +538,7 @@ describe("wsApi", () => { }, sidebarProjectSortOrder: "manual" as const, sidebarThreadSortOrder: "created_at" as const, + sidebarThreadPreviewCount: 6, timestampFormat: "24-hour" as const, }; const getClientSettings = vi.fn().mockResolvedValue({ @@ -595,6 +596,7 @@ describe("wsApi", () => { }, sidebarProjectSortOrder: "manual" as const, sidebarThreadSortOrder: "created_at" as const, + sidebarThreadPreviewCount: 6, timestampFormat: "24-hour" as const, }; From 9dc6c1ba1331272b2f4d9462e1ade7976dad9d2b Mon Sep 17 00:00:00 2001 From: Marve10s Date: Fri, 10 Apr 2026 19:22:15 +0300 Subject: [PATCH 06/11] Fix duplicate sidebar thread preview count in desktop test --- apps/desktop/src/clientPersistence.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/desktop/src/clientPersistence.test.ts b/apps/desktop/src/clientPersistence.test.ts index 0c2933efe06..27f1e1d91ae 100644 --- a/apps/desktop/src/clientPersistence.test.ts +++ b/apps/desktop/src/clientPersistence.test.ts @@ -58,7 +58,6 @@ const clientSettings: ClientSettings = { }, sidebarProjectSortOrder: "manual", sidebarThreadSortOrder: "created_at", - sidebarThreadPreviewCount: 6, timestampFormat: "24-hour", }; From fdba6ccf43437161c124a6895cd353546c8c23b3 Mon Sep 17 00:00:00 2001 From: Marve10s Date: Thu, 16 Apr 2026 12:27:25 +0300 Subject: [PATCH 07/11] Fix sidebar preview stepper stale input handling --- apps/web/src/components/Sidebar.tsx | 34 ++++++++++++++++++----------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 38873367c46..ceb962e34c2 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -2199,21 +2199,25 @@ function ProjectSortMenu({ setThreadPreviewInput(String(threadPreviewCount)); }, [threadPreviewCount]); - const commitThreadPreviewCount = useCallback( + const resolveThreadPreviewInputValue = useCallback( (nextValue: string) => { const parsedValue = Number.parseInt(nextValue, 10); - if (!Number.isInteger(parsedValue)) { - setThreadPreviewInput(String(threadPreviewCount)); - return; - } + return Number.isInteger(parsedValue) + ? clampSidebarThreadPreviewCount(parsedValue) + : threadPreviewCount; + }, + [threadPreviewCount], + ); - const clampedValue = clampSidebarThreadPreviewCount(parsedValue); + const commitThreadPreviewCount = useCallback( + (nextValue: string) => { + const clampedValue = resolveThreadPreviewInputValue(nextValue); setThreadPreviewInput(String(clampedValue)); if (clampedValue !== threadPreviewCount) { onThreadPreviewCountChange(clampedValue); } }, - [onThreadPreviewCountChange, threadPreviewCount], + [onThreadPreviewCountChange, resolveThreadPreviewInputValue, threadPreviewCount], ); return ( @@ -2278,9 +2282,11 @@ function ProjectSortMenu({ className="size-7 shrink-0" aria-label="Decrease visible thread count" disabled={threadPreviewCount <= MIN_SIDEBAR_THREAD_PREVIEW_COUNT} - onClick={() => - onThreadPreviewCountChange(clampSidebarThreadPreviewCount(threadPreviewCount - 1)) - } + onClick={() => { + commitThreadPreviewCount( + String(resolveThreadPreviewInputValue(threadPreviewInput) - 1), + ); + }} > @@ -2316,9 +2322,11 @@ function ProjectSortMenu({ className="size-7 shrink-0" aria-label="Increase visible thread count" disabled={threadPreviewCount >= MAX_SIDEBAR_THREAD_PREVIEW_COUNT} - onClick={() => - onThreadPreviewCountChange(clampSidebarThreadPreviewCount(threadPreviewCount + 1)) - } + onClick={() => { + commitThreadPreviewCount( + String(resolveThreadPreviewInputValue(threadPreviewInput) + 1), + ); + }} > From 93be670a9530bd277c7d0ceadc6d00fe35d12dbb Mon Sep 17 00:00:00 2001 From: Marve10s Date: Fri, 17 Apr 2026 13:02:29 +0300 Subject: [PATCH 08/11] Fix rebase fallout for preview count branch --- apps/desktop/src/clientPersistence.test.ts | 1 + apps/web/vite.config.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/clientPersistence.test.ts b/apps/desktop/src/clientPersistence.test.ts index 27f1e1d91ae..0c2933efe06 100644 --- a/apps/desktop/src/clientPersistence.test.ts +++ b/apps/desktop/src/clientPersistence.test.ts @@ -58,6 +58,7 @@ const clientSettings: ClientSettings = { }, sidebarProjectSortOrder: "manual", sidebarThreadSortOrder: "created_at", + sidebarThreadPreviewCount: 6, timestampFormat: "24-hour", }; diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts index 5d6fe2de405..7c123b4fed2 100644 --- a/apps/web/vite.config.ts +++ b/apps/web/vite.config.ts @@ -70,7 +70,7 @@ export default defineConfig({ port, strictPort: true, ...(devProxyTarget - ? { + ? { proxy: { "/.well-known": { target: devProxyTarget, From 272bf9d17fb224099310ddda0e29fa4b1517a29f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 7 May 2026 16:22:28 -0700 Subject: [PATCH 09/11] feat(web): use number field for thread preview limit Co-authored-by: codex --- apps/web/package.json | 2 +- apps/web/src/components/Sidebar.tsx | 115 +++++---------- apps/web/src/components/ui/input.tsx | 41 +++-- apps/web/src/components/ui/number-field.tsx | 156 ++++++++++++++++++++ bun.lock | 18 ++- packages/contracts/src/settings.test.ts | 22 ++- packages/contracts/src/settings.ts | 4 +- 7 files changed, 258 insertions(+), 100 deletions(-) create mode 100644 apps/web/src/components/ui/number-field.tsx diff --git a/apps/web/package.json b/apps/web/package.json index 7fa8818109b..b61d529ab1c 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -13,7 +13,7 @@ "test:browser:install": "playwright install --with-deps chromium" }, "dependencies": { - "@base-ui/react": "^1.2.0", + "@base-ui/react": "^1.4.1", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index c79a36c39c8..b14bc5b9d2b 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -4,8 +4,6 @@ import { ChevronRightIcon, CloudIcon, FolderPlusIcon, - MinusIcon, - PlusIcon, SearchIcon, SettingsIcon, SquarePenIcon, @@ -133,6 +131,13 @@ import { MenuSeparator, MenuTrigger, } from "./ui/menu"; +import { + NumberField, + NumberFieldDecrement, + NumberFieldGroup, + NumberFieldIncrement, + NumberFieldInput, +} from "./ui/number-field"; import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "./ui/select"; import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip"; import { @@ -2283,31 +2288,18 @@ function ProjectSortMenu({ onProjectGroupingModeChange: (mode: SidebarProjectGroupingMode) => void; onThreadPreviewCountChange: (count: SidebarThreadPreviewCount) => void; }) { - const [threadPreviewInput, setThreadPreviewInput] = useState(() => String(threadPreviewCount)); - - useEffect(() => { - setThreadPreviewInput(String(threadPreviewCount)); - }, [threadPreviewCount]); - - const resolveThreadPreviewInputValue = useCallback( - (nextValue: string) => { - const parsedValue = Number.parseInt(nextValue, 10); - return Number.isInteger(parsedValue) - ? clampSidebarThreadPreviewCount(parsedValue) - : threadPreviewCount; - }, - [threadPreviewCount], - ); + const handleThreadPreviewCountChange = useCallback( + (nextValue: number | null) => { + if (nextValue === null) { + return; + } - const commitThreadPreviewCount = useCallback( - (nextValue: string) => { - const clampedValue = resolveThreadPreviewInputValue(nextValue); - setThreadPreviewInput(String(clampedValue)); + const clampedValue = clampSidebarThreadPreviewCount(nextValue); if (clampedValue !== threadPreviewCount) { onThreadPreviewCountChange(clampedValue); } }, - [onThreadPreviewCountChange, resolveThreadPreviewInputValue, threadPreviewCount], + [onThreadPreviewCountChange, threadPreviewCount], ); return ( @@ -2365,61 +2357,36 @@ function ProjectSortMenu({
Visible threads
-
- - + { - 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); - } - }} - /> - + + + { + event.stopPropagation(); + }} + /> + + +
diff --git a/apps/web/src/components/ui/input.tsx b/apps/web/src/components/ui/input.tsx index 001a240ddfd..7ab7eb6cea0 100644 --- a/apps/web/src/components/ui/input.tsx +++ b/apps/web/src/components/ui/input.tsx @@ -27,6 +27,31 @@ function Input({ props.type === "file" && "text-muted-foreground file:me-3 file:bg-transparent file:font-medium file:text-foreground file:text-sm", ); + let inputElement: React.ReactElement; + + if (nativeInput) { + const { style, onValueChange: _onValueChange, ...nativeInputProps } = props; + const nativeStyle = typeof style === "function" ? undefined : style; + + inputElement = ( + )} + /> + ); + } else { + inputElement = ( + + ); + } return ( - {nativeInput ? ( - - ) : ( - - )} + {inputElement} ); } diff --git a/apps/web/src/components/ui/number-field.tsx b/apps/web/src/components/ui/number-field.tsx new file mode 100644 index 00000000000..2d627757d65 --- /dev/null +++ b/apps/web/src/components/ui/number-field.tsx @@ -0,0 +1,156 @@ +"use client"; + +import { NumberField as NumberFieldPrimitive } from "@base-ui/react/number-field"; +import { MinusIcon, PlusIcon } from "lucide-react"; +import * as React from "react"; +import { cn } from "~/lib/utils"; +import { Label } from "~/components/ui/label"; + +export const NumberFieldContext: React.Context<{ + fieldId: string; +} | null> = React.createContext<{ + fieldId: string; +} | null>(null); + +export function NumberField({ + id, + className, + size = "default", + ...props +}: NumberFieldPrimitive.Root.Props & { + size?: "sm" | "default" | "lg"; +}): React.ReactElement { + const generatedId = React.useId(); + const fieldId = id ?? generatedId; + const contextValue = React.useMemo(() => ({ fieldId }), [fieldId]); + + return ( + + + + ); +} + +export function NumberFieldGroup({ + className, + ...props +}: NumberFieldPrimitive.Group.Props): React.ReactElement { + return ( + + ); +} + +export function NumberFieldDecrement({ + className, + ...props +}: NumberFieldPrimitive.Decrement.Props): React.ReactElement { + return ( + + + + ); +} + +export function NumberFieldIncrement({ + className, + ...props +}: NumberFieldPrimitive.Increment.Props): React.ReactElement { + return ( + + + + ); +} + +export function NumberFieldInput({ + className, + ...props +}: NumberFieldPrimitive.Input.Props): React.ReactElement { + return ( + + ); +} + +export function NumberFieldScrubArea({ + className, + label, + ...props +}: NumberFieldPrimitive.ScrubArea.Props & { + label: string; +}): React.ReactElement { + const context = React.useContext(NumberFieldContext); + + if (!context) { + throw new Error( + "NumberFieldScrubArea must be used within a NumberField component for accessibility.", + ); + } + + return ( + + + + + + + ); +} + +export function CursorGrowIcon(props: React.ComponentProps<"svg">): React.ReactElement { + return ( + + ); +} + +export { NumberFieldPrimitive }; diff --git a/bun.lock b/bun.lock index a87ac77094b..c1c52a5b837 100644 --- a/bun.lock +++ b/bun.lock @@ -16,7 +16,7 @@ }, "apps/desktop": { "name": "@t3tools/desktop", - "version": "0.0.21", + "version": "0.0.22", "dependencies": { "@effect/platform-node": "catalog:", "effect": "catalog:", @@ -49,7 +49,7 @@ }, "apps/server": { "name": "t3", - "version": "0.0.21", + "version": "0.0.22", "bin": { "t3": "./dist/bin.mjs", }, @@ -82,9 +82,9 @@ }, "apps/web": { "name": "@t3tools/web", - "version": "0.0.21", + "version": "0.0.22", "dependencies": { - "@base-ui/react": "^1.2.0", + "@base-ui/react": "^1.4.1", "@dnd-kit/core": "^6.3.1", "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", @@ -148,7 +148,7 @@ }, "packages/contracts": { "name": "@t3tools/contracts", - "version": "0.0.21", + "version": "0.0.22", "dependencies": { "effect": "catalog:", }, @@ -350,9 +350,9 @@ "@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=="], - "@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=="], + "@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=="], - "@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=="], + "@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=="], "@blazediff/core": ["@blazediff/core@1.9.1", "", {}, "sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA=="], @@ -2122,6 +2122,10 @@ "@babel/types/@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@base-ui/react/@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], + + "@base-ui/utils/@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], + "@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=="], "@electron/get/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], diff --git a/packages/contracts/src/settings.test.ts b/packages/contracts/src/settings.test.ts index d2b73f567a7..10378cd0bf7 100644 --- a/packages/contracts/src/settings.test.ts +++ b/packages/contracts/src/settings.test.ts @@ -2,10 +2,30 @@ import { describe, expect, it } from "vitest"; import { Schema } from "effect"; import { ProviderInstanceId } from "./providerInstance.ts"; -import { DEFAULT_SERVER_SETTINGS, ServerSettings, ServerSettingsPatch } from "./settings.ts"; +import { + MAX_SIDEBAR_THREAD_PREVIEW_COUNT, + MIN_SIDEBAR_THREAD_PREVIEW_COUNT, + SidebarThreadPreviewCount, + DEFAULT_SERVER_SETTINGS, + ServerSettings, + ServerSettingsPatch, +} from "./settings.ts"; const decodeServerSettings = Schema.decodeUnknownSync(ServerSettings); const decodeServerSettingsPatch = Schema.decodeUnknownSync(ServerSettingsPatch); +const decodeSidebarThreadPreviewCount = Schema.decodeUnknownSync(SidebarThreadPreviewCount); + +describe("SidebarThreadPreviewCount", () => { + it("accepts the configured 1-15 range", () => { + expect(decodeSidebarThreadPreviewCount(MIN_SIDEBAR_THREAD_PREVIEW_COUNT)).toBe(1); + expect(decodeSidebarThreadPreviewCount(MAX_SIDEBAR_THREAD_PREVIEW_COUNT)).toBe(15); + }); + + it("rejects values outside the configured range", () => { + expect(() => decodeSidebarThreadPreviewCount(MIN_SIDEBAR_THREAD_PREVIEW_COUNT - 1)).toThrow(); + expect(() => decodeSidebarThreadPreviewCount(MAX_SIDEBAR_THREAD_PREVIEW_COUNT + 1)).toThrow(); + }); +}); describe("ServerSettings.providerInstances (slice-2 invariant)", () => { it("defaults to an empty record so legacy configs without the key still decode", () => { diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 14edc8d5d31..15f13e539ea 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -27,8 +27,8 @@ export const SidebarProjectGroupingMode = Schema.Literals([ ]); export type SidebarProjectGroupingMode = typeof SidebarProjectGroupingMode.Type; export const DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE: SidebarProjectGroupingMode = "repository"; -export const MIN_SIDEBAR_THREAD_PREVIEW_COUNT = 2; -export const MAX_SIDEBAR_THREAD_PREVIEW_COUNT = 10; +export const MIN_SIDEBAR_THREAD_PREVIEW_COUNT = 1; +export const MAX_SIDEBAR_THREAD_PREVIEW_COUNT = 15; export const SidebarThreadPreviewCount = Schema.Int.check( Schema.isBetween({ minimum: MIN_SIDEBAR_THREAD_PREVIEW_COUNT, From 9a569808ba675fdfc7f7fe9bf15d71cbc3dcee52 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 7 May 2026 16:26:06 -0700 Subject: [PATCH 10/11] Discard changes to packages/contracts/src/settings.ts --- packages/contracts/src/settings.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index 15f13e539ea..a4805494dfb 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -27,16 +27,6 @@ export const SidebarProjectGroupingMode = Schema.Literals([ ]); export type SidebarProjectGroupingMode = typeof SidebarProjectGroupingMode.Type; export const DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE: SidebarProjectGroupingMode = "repository"; -export const MIN_SIDEBAR_THREAD_PREVIEW_COUNT = 1; -export const MAX_SIDEBAR_THREAD_PREVIEW_COUNT = 15; -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({ autoOpenPlanSidebar: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), @@ -85,9 +75,6 @@ export const ClientSettingsSchema = Schema.Struct({ sidebarThreadSortOrder: SidebarThreadSortOrder.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_THREAD_SORT_ORDER)), ), - sidebarThreadPreviewCount: SidebarThreadPreviewCount.pipe( - Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_THREAD_PREVIEW_COUNT)), - ), timestampFormat: TimestampFormat.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_TIMESTAMP_FORMAT)), ), From 5dd1acf973ff76fdf7ab1dc2c463a98dda1214d7 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 7 May 2026 16:28:09 -0700 Subject: [PATCH 11/11] fix(contracts): restore sidebar preview setting Co-authored-by: codex --- packages/contracts/src/settings.test.ts | 22 +--------------------- packages/contracts/src/settings.ts | 13 +++++++++++++ 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/packages/contracts/src/settings.test.ts b/packages/contracts/src/settings.test.ts index 10378cd0bf7..d2b73f567a7 100644 --- a/packages/contracts/src/settings.test.ts +++ b/packages/contracts/src/settings.test.ts @@ -2,30 +2,10 @@ import { describe, expect, it } from "vitest"; import { Schema } from "effect"; import { ProviderInstanceId } from "./providerInstance.ts"; -import { - MAX_SIDEBAR_THREAD_PREVIEW_COUNT, - MIN_SIDEBAR_THREAD_PREVIEW_COUNT, - SidebarThreadPreviewCount, - DEFAULT_SERVER_SETTINGS, - ServerSettings, - ServerSettingsPatch, -} from "./settings.ts"; +import { DEFAULT_SERVER_SETTINGS, ServerSettings, ServerSettingsPatch } from "./settings.ts"; const decodeServerSettings = Schema.decodeUnknownSync(ServerSettings); const decodeServerSettingsPatch = Schema.decodeUnknownSync(ServerSettingsPatch); -const decodeSidebarThreadPreviewCount = Schema.decodeUnknownSync(SidebarThreadPreviewCount); - -describe("SidebarThreadPreviewCount", () => { - it("accepts the configured 1-15 range", () => { - expect(decodeSidebarThreadPreviewCount(MIN_SIDEBAR_THREAD_PREVIEW_COUNT)).toBe(1); - expect(decodeSidebarThreadPreviewCount(MAX_SIDEBAR_THREAD_PREVIEW_COUNT)).toBe(15); - }); - - it("rejects values outside the configured range", () => { - expect(() => decodeSidebarThreadPreviewCount(MIN_SIDEBAR_THREAD_PREVIEW_COUNT - 1)).toThrow(); - expect(() => decodeSidebarThreadPreviewCount(MAX_SIDEBAR_THREAD_PREVIEW_COUNT + 1)).toThrow(); - }); -}); describe("ServerSettings.providerInstances (slice-2 invariant)", () => { it("defaults to an empty record so legacy configs without the key still decode", () => { diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index a4805494dfb..15f13e539ea 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -27,6 +27,16 @@ export const SidebarProjectGroupingMode = Schema.Literals([ ]); export type SidebarProjectGroupingMode = typeof SidebarProjectGroupingMode.Type; export const DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE: SidebarProjectGroupingMode = "repository"; +export const MIN_SIDEBAR_THREAD_PREVIEW_COUNT = 1; +export const MAX_SIDEBAR_THREAD_PREVIEW_COUNT = 15; +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({ autoOpenPlanSidebar: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), @@ -75,6 +85,9 @@ export const ClientSettingsSchema = Schema.Struct({ sidebarThreadSortOrder: SidebarThreadSortOrder.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_THREAD_SORT_ORDER)), ), + sidebarThreadPreviewCount: SidebarThreadPreviewCount.pipe( + Schema.withDecodingDefault(Effect.succeed(DEFAULT_SIDEBAR_THREAD_PREVIEW_COUNT)), + ), timestampFormat: TimestampFormat.pipe( Schema.withDecodingDefault(Effect.succeed(DEFAULT_TIMESTAMP_FORMAT)), ),