4931 lines · 199.8 KB · TypeScript
RawDownload
import { useSupportsMultiplePullRequests } from "~/hooks/useSupportsMultiplePullRequests";import { resolveThreadCurrentPullRequestLink } from "@t3tools/shared/threadPullRequests";import { useAtomValue } from "@effect/atom-react";import * as Schema from "effect/Schema";import {  DndContext,  useSensor,  useSensors,  type DragEndEvent,  type DragOverEvent,  type DragStartEvent,  type Modifier,} from "@dnd-kit/core";import { SortableContext, useSortable } from "@dnd-kit/sortable";import { restrictToFirstScrollableAncestor, restrictToVerticalAxis } from "@dnd-kit/modifiers";import { CSS } from "@dnd-kit/utilities";import {  canSnooze,  effectiveSnoozed,  threadWokeAt,} from "@t3tools/client-runtime/state/thread-settled";import { resolveSettledThreadTimestamp } from "@t3tools/client-runtime/state/thread-sort";import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/models";import {  parseScopedThreadKey,  scopeProjectRef,  scopeThreadRef,  scopedThreadKey,} from "@t3tools/client-runtime/environment";import {  resolveEnvironmentMachineKind,  type EnvironmentMachineKind,  type ProjectIconOverride,  type ScopedThreadRef,  type ThreadId,} from "@t3tools/contracts";import type { TimestampFormat } from "@t3tools/contracts/settings";import {  AlarmClockIcon,  AlarmClockOffIcon,  CheckIcon,  ChevronDownIcon,  CircleAlertIcon,  CircleCheckIcon,  CircleDashedIcon,  ClockIcon,  FolderIcon,  FolderPlusIcon,  GitBranchIcon,  PinIcon,  PinOffIcon,  PlusIcon,  SearchIcon,  SettingsIcon,  SquarePenIcon,  TerminalIcon,  Undo2Icon,  XIcon,} from "lucide-react";import {  memo,  useCallback,  useEffect,  useLayoutEffect,  useMemo,  useReducer,  useRef,  useState,  type KeyboardEvent as ReactKeyboardEvent,  type MouseEvent as ReactMouseEvent,  type ReactNode,} from "react";import { useParams, useRouter } from "@tanstack/react-router"; import { useRightPanelStore } from "../rightPanelStore";import {  isAtomCommandInterrupted,  settlePromise,  squashAtomCommandFailure,  type AtomCommandResult,} from "@t3tools/client-runtime/state/runtime";import { isElectron } from "../env";import {  resolveShortcutCommand,  shortcutLabelForCommand,  shouldShowThreadJumpHintsForModifiers,  threadJumpCommandForIndex,  threadJumpIndexFromCommand,  threadTraversalDirectionFromCommand,} from "../keybindings";import { useShortcutModifierState } from "../shortcutModifierState";import { useTerminalFocus } from "../hooks/useTerminalFocus";import { isTerminalFocused } from "../lib/terminalFocus";import { isModelPickerOpen } from "../modelPickerVisibility";import { selectThreadTerminalUiState, useTerminalUiStateStore } from "../terminalUiStateStore";import { isMacPlatform } from "~/lib/utils";import { useOpenPrLink } from "../lib/openPullRequestLink";import { releaseComposerDraftUploads } from "../lib/composerDraftUploads";import { readLocalApi } from "../localApi";import {  isSameSidebarThreadRef,  useSidebarPendingFileDropStore,} from "../sidebarPendingFileDropStore";import { getProjectOrderKey, selectProjectGroupingSettings } from "../logicalProject";import {  buildSidebarProjectSnapshots,  type SidebarProjectSnapshot,} from "../sidebarProjectGrouping";import { legacyProjectCwdPreferenceKey, useUiStateStore } from "../uiStateStore";import {  getThreadKeysToDeselectAfterDelete,  useThreadSelectionStore,} from "../threadSelectionStore";import { useThreadActions } from "../hooks/useThreadActions";import { useHandleNewThread } from "../hooks/useHandleNewThread";import { isCommandPaletteOpen, openCommandPalette } from "../commandPaletteBus";import { startNewThreadFromContext } from "../lib/chatThreadActions";import { useClientSettings } from "../hooks/useSettings";import { useCopyToClipboard } from "../hooks/useCopyToClipboard";import { useLocalStorage } from "../hooks/useLocalStorage";import { useNowMinute } from "../hooks/useNowMinute";import { useEnvironments, usePrimaryEnvironmentId } from "../state/environments";import {  readThreadShell,  useAllEnvironmentProjectSnapshotsReady,  useProjects,  useThreadShells,} from "../state/entities";import { environmentServerConfigsAtom, primaryServerKeybindingsAtom } from "../state/server";import { vcsEnvironment } from "../state/vcs";import { threadEnvironment } from "../state/threads";import { useEnvironmentQuery } from "../state/query";import { useAtomCommand } from "../state/use-atom-command";import {  buildThreadRouteParams,  resolveActiveThreadRouteRef,  resolveThreadRouteTarget,} from "../threadRoutes";import { formatRelativeTimeLabel, parseTimestampDate } from "../timestampFormat";import type { SidebarThreadSummary } from "../types";import type { EnvironmentProject } from "@t3tools/client-runtime/state/shell";import { cn } from "~/lib/utils";import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon";import { buildThreadActionMenuItems } from "./threadActionMenu.logic";import {  animateSidebarLayoutChanges,  applySidebarThreadDrop,  buildBulkTitleRegenerationContextMenuItem,  buildBulkUnpinContextMenuItem,  deleteSelectedThreadEntries,  filterSidebarProjectScopeItems,  formatWorkingDurationLabel,  firstValidTimestampMs,  hasUnseenCompletion,  isSidebarNestedLinkClick,  isTrailingDoubleClick,  orderItemsByPreferredIds,  planSidebarThreadDrop,  reduceSidebarProjectScopeMenuState,  resolveAdjacentThreadId,  resolveSidebarDropTarget,  resolveSidebarDropVerb,  type SidebarDropVerb,  resolveSidebarThreadStatus,  searchSidebarThreads,  shouldCreateNewThreadInCurrentProject,  shouldRecedeSidebarThread,  resolveWorkingStartedAt,  sidebarListItemId,  sidebarMarkerId,  sortLogicalProjectsForSidebar,  sortPinnedThreadsForSidebar,  sortSettledThreadsForSidebar,  sortThreadsForSidebar,  useRetainedValue,  useSidebarRowSubscriptionLease,  useThreadJumpHintVisibility,  type SidebarListItem,  type SidebarListMarker,  type SidebarSection,} from "./Sidebar.logic";import { resolveLocalCheckoutBranchMismatch } from "./BranchToolbar.logic";import {  createSidebarCollisionDetection,  createSidebarSortingStrategy,  restrictBelowSidebarLabel,} from "./Sidebar.drag";import { SidebarDragLifecycle, SidebarPointerSensor } from "./Sidebar.pointer";import { createSidebarListMotion } from "./Sidebar.motion";import {  ThreadPullRequestBadgeControl,  ThreadPullRequestsMiniList,  ThreadWorktreeIndicator,  prStatusIndicator,  resolveThreadPullRequestBadge,  terminalStatusFromRunningIds,  type TerminalStatusIndicator,  useLinkedThreadPullRequest,} from "./ThreadStatusIndicators";import {  resolveSnoozePresets,  snoozeWakeDescription,  snoozeWakeLabel,  type SnoozePreset,} from "./Sidebar.snooze";import { ProjectFavicon, type ProjectFaviconProject } from "./ProjectFavicon";import { makeWorkspaceFileDropHandlers } from "./chat/workspaceFileDrop";import { ProviderInstanceIcon } from "./chat/ProviderInstanceIcon";import { getTriggerDisplayModelLabel } from "./chat/providerIconUtils";import {  deriveProviderEntriesByEnvironment,  shouldShowInstanceBadge,  type ProviderInstanceEntry,} from "../providerInstances";import { useThreadRunningTerminalIds } from "../state/terminalSessions";import { stackedThreadToast, toastManager } from "./ui/toast";import { Button } from "./ui/button";import { Input } from "./ui/input";import {  Combobox,  ComboboxEmpty,  ComboboxSearchInput,  ComboboxItem,  ComboboxList,  ComboboxPopup,  ComboboxTrigger,  useComboboxFilter,} from "./ui/combobox";import { SidebarContent, SidebarGroup, SidebarMenuButton, useSidebar } from "./ui/sidebar";import { SidebarChromeFooter, SidebarChromeHeader } from "./sidebar/SidebarChrome";import { Popover, PopoverPopup, PopoverTrigger } from "./ui/popover";import { Tooltip, TooltipPopup, TooltipProvider, TooltipTrigger } from "./ui/tooltip";import {  composerDraftHasUserContent,  DraftId,  useComposerDraftStore,  useThreadHasUnsentDraft,  type ComposerThreadDraftState,  type DraftSessionState,} from "../composerDraftStore"; // Settled-tail paging: recent history is the common lookup; the deep tail// stays behind an explicit Show more.const SETTLED_TAIL_INITIAL_COUNT = 10;const SETTLED_TAIL_PAGE_COUNT = 25;// Fresh keys deliberately reset both shelves to collapsed for existing users.const SETTLED_SHELF_EXPANDED_KEY = "t3code:sidebar:settled-expanded";const SNOOZED_SHELF_EXPANDED_KEY = "t3code:sidebar:snoozed-expanded"; function compactSidebarTimeLabel(label: string): string {  if (label === "just now") return "now";  return label.endsWith(" ago") ? label.slice(0, -4) : label;} function threadTimeLabel(thread: SidebarThreadSummary): string {  const timestamp = thread.latestUserMessageAt ?? thread.updatedAt;  return compactSidebarTimeLabel(formatRelativeTimeLabel(timestamp));} // Settled rows read "how long ago did this wrap up", matching their sort// key: both go through resolveSettledThreadTimestamp so label and order can't// disagree.function settledTimeLabel(thread: SidebarThreadSummary): string {  const timestamp = resolveSettledThreadTimestamp(thread);  return timestamp === null ? "" : compactSidebarTimeLabel(formatRelativeTimeLabel(timestamp));} // Floats at the row's right edge, vertically centered, while the jump// modifier is held. An overlay pill instead of an inline slot: the hint// must neither displace the status/time label (holding ⌘ used to blank// out "Working") nor shift any layout when it appears. pointer-events-none// so it never swallows clicks meant for the settle/un-settle buttons it// can overlap.function JumpHintBadge(props: { label: string }) {  return (    <span      aria-hidden      className="pointer-events-none absolute right-1.5 top-1/2 z-10 inline-flex h-5 -translate-y-1/2 items-center rounded-full border border-border/80 bg-background/95 px-1.5 font-mono text-[10px] font-medium tracking-tight text-foreground shadow-sm"    >      {props.label}    </span>  );} // Self-ticking so only this span re-renders each second, not the whole row.function WorkingDuration(props: { startedAt: string | null }) {  const startedMs = props.startedAt !== null ? Date.parse(props.startedAt) : Number.NaN;  const [, setTick] = useState(0);  useEffect(() => {    if (Number.isNaN(startedMs)) return;    const id = window.setInterval(() => setTick((tick) => tick + 1), 1_000);    return () => window.clearInterval(id);  }, [startedMs]);  if (Number.isNaN(startedMs)) return null;  return <span className="tabular-nums">{formatWorkingDurationLabel(Date.now() - startedMs)}</span>;} const EMPTY_PROVIDER_ENTRIES: ReadonlyMap<string, ProviderInstanceEntry> = new Map();// Collapsed shelves share one empty list so a route change alone does not// give the sidebar list a new identity.const EMPTY_THREADS: readonly EnvironmentThreadShell[] = []; function terminalProcessLabel(count: number): string {  return `${count} terminal ${count === 1 ? "process" : "processes"} running`;} function SidebarThreadTooltip({  thread,  project,  projectDisplayName,  environmentLabel,  environmentMachine,  providerEntry,  showInstanceBadge,  modelInstanceId,  modelLabel,  branchMismatch,  terminalStatus,  terminalProcessCount,}: {  thread: SidebarThreadSummary;  project: ProjectFaviconProject | null;  projectDisplayName: string | null;  environmentLabel: string | null;  environmentMachine: EnvironmentMachineKind;  providerEntry: ProviderInstanceEntry | null;  showInstanceBadge: boolean;  modelInstanceId: string;  modelLabel: string;  branchMismatch: {    threadBranch: string;    currentBranch: string;  } | null;  terminalStatus: TerminalStatusIndicator | null;  terminalProcessCount: number;}) {  const driverKind = providerEntry?.driverKind ?? null;  const supportsMultiplePullRequests = useSupportsMultiplePullRequests(thread.environmentId);  return (    <TooltipPopup      side="right"      align="start"      sideOffset={4}      variant="glass"      className="max-w-80 text-left whitespace-normal [&_[data-slot=tooltip-viewport]]:p-0"    >      <div className="flex min-w-0 max-w-80 flex-col gap-2 p-[var(--floating-content-inset)]">        <div className="min-w-0 truncate text-xs leading-tight font-medium text-foreground">          {thread.title}        </div>        <div className="grid gap-1.5 pl-0.5 text-xs text-muted-foreground">          {projectDisplayName ? (            <div className="flex min-w-0 items-center gap-2">              {project ? <ProjectFavicon project={project} className="size-3 shrink-0" /> : null}              <div className="min-w-0 truncate text-foreground/75">{projectDisplayName}</div>            </div>          ) : null}          {environmentLabel ? (            <div className="flex min-w-0 items-center gap-2">              <EnvironmentMachineIcon                kind={environmentMachine}                className="size-3 shrink-0 stroke-muted-foreground"              />              <div className="min-w-0 truncate text-foreground/75">{environmentLabel}</div>            </div>          ) : null}          {thread.branch ? (            <div className="flex min-w-0 items-center gap-2">              <GitBranchIcon className="size-3 shrink-0 stroke-muted-foreground" />              <div className="min-w-0 truncate text-foreground/75">{thread.branch}</div>            </div>          ) : null}          {branchMismatch ? (            <div className="flex min-w-0 items-start gap-2 text-warning">              <CircleAlertIcon aria-hidden className="mt-0.5 size-3 shrink-0 stroke-current" />              <div className="min-w-0 flex-1 wrap-break-word leading-5">                You're currently checked out on another branch.              </div>            </div>          ) : null}          {driverKind ? (            <div className="flex min-w-0 items-center gap-2">              <ProviderInstanceIcon                driverKind={driverKind}                displayName={                  providerEntry?.displayName ?? thread.session?.providerName ?? modelInstanceId                }                accentColor={providerEntry?.accentColor}                // Initials would swallow a size-3 glyph: accent dot, name in label.                showBadge={showInstanceBadge && providerEntry?.accentColor !== undefined}                badgeContent="none"                badgeClassName="h-2 min-w-2 px-0"                iconClassName="size-3 shrink-0 grayscale opacity-60"              />              <div className="min-w-0 truncate text-foreground/75">                {showInstanceBadge && providerEntry                  ? `${modelLabel} · ${providerEntry.displayName}`                  : modelLabel}              </div>            </div>          ) : null}          {terminalStatus ? (            <div className="flex min-w-0 items-center gap-2">              <TerminalIcon                aria-hidden                className={cn("size-3 shrink-0", terminalStatus.colorClass)}              />              <div className="min-w-0 truncate text-foreground/75">                {terminalProcessLabel(terminalProcessCount)}              </div>            </div>          ) : null}          {thread.session?.lastError ? (            <div className="flex min-w-0 items-center gap-2 text-red-600 dark:text-red-400">              <CircleAlertIcon className="size-3 shrink-0 stroke-current" />              <div className="min-w-0 truncate">Error occurred</div>            </div>          ) : null}        </div>        {supportsMultiplePullRequests && thread.pullRequests.length > 0 ? (          <div className="border-t border-border/60 pt-2 pl-0.5 text-xs text-muted-foreground">            <ThreadPullRequestsMiniList pullRequests={thread.pullRequests} />          </div>        ) : null}      </div>    </TooltipPopup>  );} /** * Hover entry point for snooze: a clock button opening the preset menu. * Controlled by the row (which also uses the open state to pin its hover * actions while the menu is up). */function SnoozePopoverButton(props: {  open: boolean;  onOpenChange: (open: boolean) => void;  onSnooze: (preset: SnoozePreset) => void;  timestampFormat: TimestampFormat;}) {  const { open, onOpenChange, onSnooze, timestampFormat } = props;  // Presets resolve at open time so "In 1 hour" is relative to the click,  // not to when the row mounted.  const presets = useMemo(    () => (open ? resolveSnoozePresets(new Date(), timestampFormat) : []),    [open, timestampFormat],  );  return (    <Popover open={open} onOpenChange={onOpenChange}>      <Tooltip>        <TooltipTrigger          render={            <PopoverTrigger              render={                <button                  type="button"                  aria-label="Snooze thread"                  onClick={(event) => event.stopPropagation()}                  onDoubleClick={(event) => event.stopPropagation()}                  className="inline-flex h-full cursor-pointer items-center gap-0.5 rounded-md bg-transparent px-1.5 text-xs text-muted-foreground hover:text-foreground"                />              }            />          }        >          <ClockIcon className="size-3" />        </TooltipTrigger>        <TooltipPopup>Snooze thread</TooltipPopup>      </Tooltip>      <PopoverPopup side="bottom" align="end" className="w-56" viewportClassName="p-1">        {presets.map((preset) => (          <button            key={preset.id}            type="button"            onClick={(event) => {              event.stopPropagation();              onOpenChange(false);              onSnooze(preset);            }}            className="flex w-full cursor-pointer items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs text-foreground/90 hover:bg-accent hover:text-foreground"          >            <span className="flex-1">{preset.label}</span>            <span className="font-mono text-[10px] text-muted-foreground/60 tabular-nums">              {preset.whenLabel}            </span>          </button>        ))}      </PopoverPopup>    </Popover>  );} // Subset of useSortable applied to a thread row's root <li>. Listeners go// on the whole row (no dedicated handle): the pointer sensor's distance// constraint keeps plain clicks working, and we skip dnd-kit's aria// attributes since there is no keyboard sensor and the row body already// carries its own button semantics.type SortableThreadRowBag = Pick<  ReturnType<typeof useSortable>,  "listeners" | "setNodeRef" | "transform" | "transition" | "isDragging"

Lines 1–500 of 4931.