From 4a39cade982926f607b4606d0f8d92a807f4b0bf Mon Sep 17 00:00:00 2001 From: shivam <91240327+shivamhwp@users.noreply.github.com> Date: Sun, 13 Sep 2026 21:25:17 +0530 Subject: [PATCH 01/72] fix(web): offer recovery from missing pages (#11314) --- apps/web/src/routes/__root.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index 0831a922325e..63495ce5cd65 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -3,6 +3,7 @@ import { scopedProjectKey, scopeProjectRef } from "@t3tools/client-runtime/envir import { squashAtomCommandFailure } from "@t3tools/client-runtime/state/runtime"; import { Outlet, + Link, redirect, createRootRoute, type ErrorComponentProps, @@ -104,11 +105,27 @@ export const Route = createRootRoute({ }, component: RootRouteView, errorComponent: RootRouteErrorView, + notFoundComponent: RootRouteNotFoundView, head: () => ({ meta: [{ name: "title", content: APP_DISPLAY_NAME }], }), }); +function RootRouteNotFoundView() { + return ( +
+
+

Page not found

+

+ This link doesn't point to a page in {APP_DISPLAY_NAME}. Go home to choose a project or + start a thread. +

+ +
+
+ ); +} + function RootRouteView() { useEffect(() => installDesktopPasteAsText(window.desktopBridge, window), []); const pathname = useLocation({ select: (location) => location.pathname }); From e6286839369a29d829aee3c64dd64ce39c626d2f Mon Sep 17 00:00:00 2001 From: shivam <91240327+shivamhwp@users.noreply.github.com> Date: Sun, 13 Sep 2026 21:26:03 +0530 Subject: [PATCH 02/72] fix(web): retry startup after the server recovers (#11291) --- apps/web/src/routes/__root.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx index 63495ce5cd65..982d81420445 100644 --- a/apps/web/src/routes/__root.tsx +++ b/apps/web/src/routes/__root.tsx @@ -9,6 +9,7 @@ import { type ErrorComponentProps, useLocation, useNavigate, + useRouter, } from "@tanstack/react-router"; import { CheckIcon, CopyIcon } from "lucide-react"; import { useEffect, useEffectEvent, useMemo, useRef, useState } from "react"; @@ -353,7 +354,8 @@ function HostedStaticEnvironmentBootstrap() { return null; } -function RootRouteErrorView({ error, reset }: ErrorComponentProps) { +function RootRouteErrorView({ error }: ErrorComponentProps) { + const router = useRouter(); const message = errorMessage(error); // Router pathname rather than window.location: desktop uses hash history, where the window path is always "/". const pathname = useLocation({ select: (location) => location.pathname }); @@ -376,7 +378,7 @@ function RootRouteErrorView({ error, reset }: ErrorComponentProps) {

{message}

- + + + } + > + {content} + + {props.label} + ); } @@ -704,6 +752,9 @@ const SidebarDraftRow = memo(function SidebarDraftRow(props: { onDiscard: (draftId: DraftId) => void; }) { const { composer, draftId, onDiscard, onNavigate, session } = props; + const compactEnabled = useCompactSidebarEnabled(); + const { state, isMobile } = useSidebar(); + const compact = compactEnabled && state === "collapsed" && !isMobile; const promptPreview = replaceComposerContextReferences(composer.prompt, (occurrence) => occurrence.label) .trim() @@ -742,6 +793,34 @@ const SidebarDraftRow = memo(function SidebarDraftRow(props: { }, [draftId, onDiscard], ); + if (compact) { + return ( +
  • + + + } + > + + + +
    {props.projectDisplayName}
    +
    {preview}
    +
    +
    +
  • + ); + } return (
  • scopeThreadRef(thread.environmentId, thread.id), [thread.environmentId, thread.id], @@ -1208,6 +1290,16 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { branchMismatch={branchMismatch} terminalStatus={terminalStatus} terminalProcessCount={terminalProcessCount} + compactStatus={ + compact + ? (topStatus?.label ?? + (variantAction === "unsnooze" + ? "Snoozed" + : variantAction === "unsettle" + ? "Settled" + : "Ready")) + : undefined + } /> ); @@ -1254,6 +1346,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { [isRenaming, onStartRename, thread.title, threadRef], ); const [isFileDragOver, setIsFileDragOver] = useState(false); + const [tooltipOpen, setTooltipOpen] = useState(false); const fileDropHandlers = useMemo( () => onFileDropThreads @@ -1416,7 +1509,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { // A zero-height boundary also makes dnd-kit scale the source to // zero. Only projected peers use scaleY as a visibility sentinel. visibility: - !sortable.isDragging && sortable.transform?.scaleY === 0 + sortable.hidden || (!sortable.isDragging && sortable.transform?.scaleY === 0) ? ("hidden" as const) : undefined, }, @@ -1557,6 +1650,77 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { ) ) : null; + if (compact) { + return ( +
  • + + + } + > + {props.project ? ( + + ) : driverKind ? ( + + ) : ( + + )} + {topStatus ? ( + + ) : hasUnsentDraft ? ( + + ) : null} + {props.jumpLabel ? : null} + + {sortable?.isDragging ? ( + {dragDestination} + ) : ( + detailsTooltip + )} + +
  • + ); + } + if (variant === "slim") { return (
  • - + - + void; }) { const { thread } = props; + const compactEnabled = useCompactSidebarEnabled(); + const { state, isMobile } = useSidebar(); + const compact = compactEnabled && state === "collapsed" && !isMobile; const threadRef = useMemo( () => scopeThreadRef(thread.environmentId, thread.id), [thread.environmentId, thread.id], @@ -2074,6 +2249,7 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: { onClick={props.onSelect} className={cn( "flex h-9 w-full cursor-pointer items-center gap-2.5 rounded-md px-2.5 text-left text-sm outline-none", + compact && "justify-center px-0", props.isHighlighted || props.isRouteActive ? "bg-sidebar-row-active text-sidebar-foreground" : "text-sidebar-muted-foreground/75 hover:bg-sidebar-row-hover hover:text-sidebar-foreground", @@ -2085,9 +2261,15 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: { > {props.project ? ( + ) : compact ? ( + ) : null} - {thread.title} - + {thread.title} + {threadTimeLabel(thread)} @@ -2115,7 +2297,10 @@ export default function Sidebar() { const projectOrder = useUiStateStore((store) => store.projectOrder); const threads = useThreadShells(); const router = useRouter(); - const { isMobile, setOpenMobile } = useSidebar(); + const { isMobile, setOpenMobile, setOpen, state: sidebarState } = useSidebar(); + const compactEnabled = useCompactSidebarEnabled(); + const compact = compactEnabled && sidebarState === "collapsed" && !isMobile; + const [snoozedFooter, setSnoozedFooter] = useState(null); const keybindings = useAtomValue(primaryServerKeybindingsAtom); const confirmThreadDelete = useClientSettings((s) => s.confirmThreadDelete); const confirmThreadArchive = useClientSettings((s) => s.confirmThreadArchive); @@ -2687,6 +2872,7 @@ export default function Sidebar() { [setSettledShelfExpanded], ); const renderedSettledThreads = useMemo(() => { + if (compact) return EMPTY_THREADS; if (settledShelfExpanded) return visibleSettledThreads; if (routeThreadKey === null) return EMPTY_THREADS; const routeThread = visibleSettledThreads.find( @@ -2694,7 +2880,7 @@ export default function Sidebar() { scopedThreadKey(scopeThreadRef(thread.environmentId, thread.id)) === routeThreadKey, ); return routeThread === undefined ? EMPTY_THREADS : [routeThread]; - }, [routeThreadKey, settledShelfExpanded, visibleSettledThreads]); + }, [compact, routeThreadKey, settledShelfExpanded, visibleSettledThreads]); // The snoozed shelf is collapsed by default: out of the way, never gone. // Collapsed threads don't render (and so don't participate in jump @@ -2922,10 +3108,14 @@ export default function Sidebar() { const [renamingThreadKey, setRenamingThreadKey] = useState(null); const [renamingTitle, setRenamingTitle] = useState(""); - const startThreadRename = useCallback((threadRef: ScopedThreadRef, title: string) => { - setRenamingThreadKey(scopedThreadKey(threadRef)); - setRenamingTitle(title); - }, []); + const startThreadRename = useCallback( + (threadRef: ScopedThreadRef, title: string) => { + if (compact) setOpen(true); + setRenamingThreadKey(scopedThreadKey(threadRef)); + setRenamingTitle(title); + }, + [compact, setOpen], + ); const cancelThreadRename = useCallback(() => setRenamingThreadKey(null), []); const commitThreadRename = useCallback( (threadRef: ScopedThreadRef, title: string, originalTitle: string) => { @@ -3087,8 +3277,18 @@ export default function Sidebar() { const threadListRef = useRef(null); const dragLabelOffsetRef = useRef(0); const restrictBelowPins = useCallback( - (args) => restrictBelowSidebarLabel(args, dragLabelOffsetRef.current), - [], + (args) => + restrictBelowSidebarLabel( + { + ...args, + // The fixed snoozed shelf shares the main list's drag boundary. + containerNodeRect: compact + ? (threadListRef.current?.getBoundingClientRect() ?? args.containerNodeRect) + : args.containerNodeRect, + }, + dragLabelOffsetRef.current, + ), + [compact], ); const listMotionRef = useRef | null>(null); const attachListMotionRef = useCallback((node: HTMLUListElement | null) => { @@ -3310,7 +3510,7 @@ export default function Sidebar() { pinnedThreads.length + activeThreads.length + snoozedThreads.length + - settledThreads.length === + (compact ? 0 : settledThreads.length) === 0 ) { return []; @@ -3326,13 +3526,15 @@ export default function Sidebar() { items.push({ kind: "marker", marker: "snoozed-header" }); items.push(...rowsOf(visibleSnoozedThreads, "snoozed")); } - items.push({ kind: "marker", marker: "settled-header" }); - const settledRows = rowsOf(renderedSettledThreads, "settled"); - items.push({ kind: "marker", marker: "settled-placeholder" }); - items.push(...settledRows); + if (!compact) { + items.push({ kind: "marker", marker: "settled-header" }); + items.push({ kind: "marker", marker: "settled-placeholder" }); + items.push(...rowsOf(renderedSettledThreads, "settled")); + } return items; }, [ activeThreads, + compact, pinnedThreads, renderedSettledThreads, settledThreads.length, @@ -3402,6 +3604,7 @@ export default function Sidebar() { () => createSidebarSortingStrategy({ items: sidebarListItems, + compact, boundaryLabelHeight: SIDEBAR_DRAG_LABEL_HEIGHT, settledOrder: draggedSettledOrder, settledExpanded: settledShelfExpanded, @@ -3410,6 +3613,7 @@ export default function Sidebar() { snoozedThreadCount: snoozedThreads.length, }), [ + compact, draggedSettledOrder, routeThreadKey, settledShelfExpanded, @@ -3418,6 +3622,22 @@ export default function Sidebar() { snoozedThreads.length, ], ); + const draggingCompactSnoozed = compact && dragState?.activeSection === "snoozed"; + const compactSnoozedDragThread = + draggingCompactSnoozed && dragState ? threadByKey.get(dragState.activeKey) : undefined; + const compactSidebarSortingStrategy = useCallback( + (args) => { + const item = sidebarListItems[args.index]; + // Footer rows stay anchored while the main list previews a reorder. + if ( + item?.kind === "thread" ? item.section === "snoozed" : item?.marker === "snoozed-header" + ) { + return null; + } + return sidebarSortingStrategy(args); + }, + [sidebarListItems, sidebarSortingStrategy], + ); // Hidden and filtered threads keep their keys. Reserve those slots without // including the rows in the visible drop order or writing to them. const { pinnedKeysById, activeKeysById } = useMemo( @@ -4321,7 +4541,16 @@ export default function Sidebar() { <> 0 ? ( +