4241 lines · 175.1 KB · TypeScript
RawDownload
import { releaseComposerDraftUploads } from "../lib/composerDraftUploads";import { autoAnimate } from "@formkit/auto-animate";import { useAtomValue } from "@effect/atom-react";import * as Schema from "effect/Schema";import {  DndContext,  PointerSensor,  closestCenter,  useSensor,  useSensors,  type DragEndEvent,} from "@dnd-kit/core";import {  SortableContext,  arrayMove,  useSortable,  verticalListSortingStrategy,} 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 {  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,  PlusIcon,  SearchIcon,  SettingsIcon,  SquarePenIcon,  TerminalIcon,  Undo2Icon,  XIcon,} from "lucide-react";import {  memo,  useCallback,  useEffect,  useMemo,  useReducer,  useRef,  useState,  type KeyboardEvent as ReactKeyboardEvent,  type MouseEvent as ReactMouseEvent,  type ReactNode,} from "react";import { useParams, useRouter } from "@tanstack/react-router"; import {  isAtomCommandInterrupted,  settlePromise,  squashAtomCommandFailure,} from "@t3tools/client-runtime/state/runtime";import { isElectron } from "../env";import {  resolveShortcutCommand,  shortcutLabelForCommand,  shouldShowThreadJumpHintsForModifiers,  threadJumpCommandForIndex,  threadJumpIndexFromCommand,  threadTraversalDirectionFromCommand,} from "../keybindings";import { useShortcutModifierState } from "../shortcutModifierState";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 { readLocalApi } from "../localApi";import { getProjectOrderKey, selectProjectGroupingSettings } from "../logicalProject";import {  buildSidebarProjectSnapshots,  type SidebarProjectSnapshot,} from "../sidebarProjectGrouping";import { legacyProjectCwdPreferenceKey, useUiStateStore } from "../uiStateStore";import { 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 {  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 { cn } from "~/lib/utils";import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon";import { buildThreadActionMenuItems } from "./threadActionMenu.logic";import {  filterSidebarV2VisibleThreads,  buildBulkTitleRegenerationContextMenuItem,  buildBulkUnpinContextMenuItem,  filterSidebarProjectScopeItems,  formatWorkingDurationLabel,  firstValidTimestampMs,  hasUnseenCompletion,  isSidebarNestedLinkClick,  isTrailingDoubleClick,  orderItemsByPreferredIds,  planPinnedReorder,  reduceSidebarProjectScopeMenuState,  resolveAdjacentThreadId,  resolveSidebarThreadStatus,  searchSidebarThreadsByTitle,  shouldCreateNewThreadInCurrentProject,  shouldRecedeSidebarThread,  resolveWorkingStartedAt,  sortLogicalProjectsForSidebar,  sortPinnedThreadsForSidebar,  sortSettledThreadsForSidebar,  sortThreadsForSidebar,  useRetainedValue,  useSidebarRowSubscriptionLease,} from "./Sidebar.logic";import { resolveLocalCheckoutBranchMismatch } from "./BranchToolbar.logic";import {  ThreadWorktreeIndicator,  nextThreadChangeRequestSnapshot,  prStatusIndicator,  resolveDisplayedThreadPr,  resolveDisplayedThreadPrProvider,  setThreadChangeRequestSnapshot,  settledPrHoverColorClass,  terminalStatusFromRunningIds,  threadChangeRequestSnapshotsAtom,  type ThreadChangeRequestSnapshot,  type TerminalStatusIndicator,} from "./ThreadStatusIndicators";import {  resolveSnoozePresets,  snoozeWakeDescription,  snoozeWakeLabel,  type SnoozePreset,} from "./Sidebar.snooze";import { ProjectFavicon } from "./ProjectFavicon";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,  ComboboxInput,  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 EMPTY_PROVIDER_ENTRIES: ReadonlyMap<string, ProviderInstanceEntry> = new Map(); 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="font-mono tabular-nums">      {formatWorkingDurationLabel(Date.now() - startedMs)}    </span>  );} function terminalProcessLabel(count: number): string {  return `${count} terminal ${count === 1 ? "process" : "processes"} running`;} function SidebarThreadTooltip({  thread,  projectTitle,  projectDisplayName,  projectCwd,  projectFaviconPath,  projectIcon,  environmentLabel,  environmentMachine,  providerEntry,  showInstanceBadge,  modelInstanceId,  modelLabel,  branchMismatch,  terminalStatus,  terminalProcessCount,}: {  thread: SidebarThreadSummary;  projectTitle: string | null;  projectDisplayName: string | null;  projectCwd: string | null;  projectFaviconPath: string | null;  projectIcon: ProjectIconOverride | 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;  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-none 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">              <ProjectFavicon                environmentId={thread.environmentId}                cwd={projectCwd ?? ""}                projectName={projectTitle ?? ""}                faviconPath={projectFaviconPath}                projectIcon={projectIcon}                className="size-3 shrink-0"              />              <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.runtime?.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.runtime?.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>      </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 pinned card's root <li>. Listeners go// on the whole card (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 card body already// carries its own button semantics.type SortablePinnedRowBag = Pick<  ReturnType<typeof useSortable>,  "listeners" | "setNodeRef" | "transform" | "transition" | "isDragging">; function SortablePinnedThreadRow(props: {  id: string;  children: (bag: SortablePinnedRowBag) => ReactNode;}) {  const { listeners, setNodeRef, transform, transition, isDragging } = useSortable({    id: props.id,  });  return props.children({ listeners, setNodeRef, transform, transition, isDragging });} // Unsent work shares one look: the new-thread draft rows and thread rows// with unsent composer text both use this tint and pen so they read alike.const draftSurfaceClassName = "bg-amber-400/[0.04] hover:bg-amber-400/[0.08]";const draftPenClassName = "size-3 shrink-0 text-amber-600 dark:text-amber-300/80"; // One unsent draft session the user has invested content in. Two lines,// nothing else: project name, then the typed prompt. All the draft's// settings (model, env mode, branch, worktree) still travel with it —

Lines 1–500 of 4241.