From 93095a275e600610248ca3de383be26c3ef496ab Mon Sep 17 00:00:00 2001 From: Marve10s Date: Thu, 9 Apr 2026 21:48:00 +0300 Subject: [PATCH 1/8] 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 9939833a95..38873367c4 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 f76c69581d..a87c3c2698 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 01b5076695..8b2ca530da 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 fada38eb1c..cd1fd2aa9d 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 2/8] 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 8b2ca530da..8eb8caa21c 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 3/8] 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 8eb8caa21c..5d6fe2de40 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 4/8] 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 27f1e1d91a..43ca897e09 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 5/8] 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 43ca897e09..0c2933efe0 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 4258ccb380..746ee59844 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 6/8] 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 0c2933efe0..27f1e1d91a 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 7/8] 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 38873367c4..ceb962e34c 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 8/8] 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 27f1e1d91a..0c2933efe0 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 5d6fe2de40..7c123b4fed 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,