3148 lines · 107.1 KB · TypeScript
RawDownload
import { usePullRequestLinking } from "~/hooks/usePullRequestLinking";import { useAtomValue } from "@effect/atom-react";import {  CheckIcon,  ChevronRightIcon,  CopyIcon,  FileSpreadsheetIcon,  FileTextIcon,  GlobeIcon,  ImageIcon,  InfoIcon,  LightbulbIcon,  MailIcon,  Maximize2Icon,  MessageSquareIcon,  MessageSquareWarningIcon,  Minimize2Icon,  OctagonAlertIcon,  PresentationIcon,  SparklesIcon,  TriangleAlertIcon,  WrapTextIcon,  type LucideIcon,} from "lucide-react";import type {  AssetResource,  EnvironmentId,  ScopedThreadRef,  ServerProviderSkill,  ThreadPullRequestKey,} from "@t3tools/contracts";import { faviconUrlForOrigin } from "@t3tools/shared/favicon";import {  isAtomCommandInterrupted,  squashAtomCommandFailure,  type AtomCommandResult,} from "@t3tools/client-runtime/state/runtime";import {  codexArtifactTemplatePresentationLabel,  type CodexArtifactTemplate,  type CodexArtifactTemplateKind,} from "@t3tools/client-runtime/codex-artifact-templates";import {  classifyMarkdownImageSource,  markdownImageSourceFragment,} from "@t3tools/client-runtime/markdown-images";import { inlineCodeFilePathCandidate } from "@t3tools/client-runtime/markdown-links";import { mediaFileReference, mediaUrlReference } from "@t3tools/client-runtime/media-reference";import { mediaKindFromPath, mediaMimeTypeFromExtension } from "@t3tools/shared/filePreview";import * as Cause from "effect/Cause";import { AsyncResult } from "effect/unstable/reactivity";import React, {  Children,  Suspense,  type CSSProperties,  type ComponentProps,  type ClipboardEvent as ReactClipboardEvent,  type KeyboardEvent as ReactKeyboardEvent,  type MouseEvent as ReactMouseEvent,  isValidElement,  use,  useCallback,  memo,  useEffect,  useMemo,  useRef,  useState,  type ReactNode,} from "react";import type { Components, Options as ReactMarkdownOptions } from "react-markdown";import ReactMarkdown from "react-markdown";import { defaultUrlTransform } from "react-markdown";import rehypeRaw from "rehype-raw";import rehypeSanitize, { defaultSchema } from "rehype-sanitize";import remarkBreaks from "remark-breaks";import { parseAssistantCitationHref } from "@t3tools/shared/assistantCitations";import { AssistantCitationChip } from "./chat/AssistantCitationChip";import remarkGfm from "remark-gfm";import { remarkGithubAlerts } from "../markdown-github-alerts";import {  artifactTemplateFromHastProperties,  CODEX_ARTIFACT_TEMPLATE_HAST_PROPERTIES,  remarkCodexDirectives,  renderCodexFileCitationsAsMarkdown,} from "@t3tools/client-runtime/codex-markdown-directives";import { renderSkillInlineMarkdownChildren } from "./chat/SkillInlineText";import {  resolveMarkdownMediaPreview,  type ExpandedImagePreview,} from "./chat/ExpandedImagePreview";import { ExpandedImageDialog } from "./chat/ExpandedImageDialog";import { markdownImageGallery, markdownImageItems } from "./chat/markdownImageGallery";import { MediaVideoPlayer } from "./media/MediaVideoPlayer";import { MediaActions, type MediaActionSource } from "./media/MediaActions";import { resolveProtocolRelativeMediaUrl } from "./media/mediaContent";import { CHAT_FILE_TAG_CHIP_CLASS_NAME, FileTagChipContent } from "./chat/FileTagChip";import { PierreEntryIcon } from "./chat/PierreEntryIcon";import {  revealInFileExplorerLabelForKind,  revealInFileExplorerLabelForOs,} from "./preview/fileExplorerLabel";import {  resolveExternalWebLinkHost,  showExternalLinkContextMenu,} from "./chat/externalLinkContextMenu";import { hasSpecificPierreIconForFileName, syntheticFileNameForLanguageId } from "../pierre-icons";import { Tooltip, TooltipPopup, TooltipTrigger } from "./ui/tooltip";import { Button } from "./ui/button";import { Collapsible, CollapsiblePanel, CollapsibleTrigger } from "./ui/collapsible";import { ScrollArea } from "./ui/scroll-area";import { Menu, MenuItem, MenuPopup, MenuTrigger } from "./ui/menu";import { stackedThreadToast, toastManager } from "./ui/toast";import { recordVisitForThread } from "../browserHistoryStore";import {  PreferredEditorEnvironmentRequiredError,  useOpenInPreferredEditor,  usePreferredEditor,} from "../editorPreferences";import { openInEditorMenuLabel } from "../editorLabels";import { resolveDiffThemeName, type DiffThemeName } from "../lib/diffRendering";import { fnv1a32 } from "../lib/diffRendering";import { LRUCache } from "../lib/lruCache";import { getSyntaxHighlighterPromise } from "../lib/syntaxHighlighting";import { GitHubIcon } from "./Icons";import { RenderErrorBoundary } from "./RenderErrorBoundary";import { useTheme } from "../hooks/useTheme";import { getClientSettings, useClientSettings } from "../hooks/useSettings";import {  chatMarkdownClipboardPayload,  serializeTableElementToCsv,  serializeTableElementToMarkdown,} from "../markdown-clipboard";import { remarkNormalizeListItemIndentation } from "../markdown-list-indentation";import {  extractMarkdownLinkHrefs,  isWindowsDrivePathHref,  normalizeMarkdownLinkDestination,  resolveInlineCodeFileLinkMeta,  resolveMarkdownFileLinkMeta,  rewriteMarkdownFileUriHref,  shouldOpenMarkdownFileLinkInBrowserByDefault,  shouldOpenMarkdownFileLinkInEditor,  type MarkdownFileLinkMeta,} from "../markdown-links";import { readLocalApi } from "../localApi";import { useAssetUrlRefresh, useAssetUrlState } from "../assets/assetUrls";import { cn } from "../lib/utils";import { useRemoteOpenResolution, type RemoteOpenMode } from "../remoteOpen";import { useRightPanelStore } from "../rightPanelStore";import { readThreadShell, useProjects } from "../state/entities";import { serverEnvironment } from "../state/server";import { shellEnvironment } from "../state/shell";import { assetEnvironment } from "../state/assets";import { usePreparedConnection } from "../state/session";import { previewEnvironment } from "../state/preview";import { useAtomCommand } from "../state/use-atom-command";import { useAtomQueryRunner } from "../state/use-atom-query-runner";import { projectEnvironment } from "../state/projects";import {  claimWorkspaceBasenameLookup,  needsWorkspaceBasenameLookup,  pickWorkspaceBasenameMatch,  WORKSPACE_BASENAME_LOOKUP_LIMIT,} from "../workspaceBasenameLookup";import {  findProjectForChangeRequest,  parseChangeRequestUrl,  pullRequestCandidateUrlFromReferenceAutolink,  useOpenChangeRequestLink,} from "~/lib/openPullRequestLink";import { useOpenLink } from "../browser/useOpenLink";import { writeTextToClipboard } from "../hooks/useCopyToClipboard";import { isPreviewSupportedInRuntime } from "../previewStateStore";import { isAbsolutePath, resolvePathLinkTarget } from "../terminal-links";import {  isBrowserPreviewFile,  openFileInPreview,  openUrlInPreview,  BrowserPreviewUnavailableError,  BrowserSettingsReadError,} from "../browser/openFileInPreview";import { resolveLinkTarget } from "../browser/browserLinkTarget";import { PullRequestLinkPreview } from "./pullRequest/PullRequestLinkPreview"; interface ChatMarkdownProps {  text: string;  cwd: string | undefined;  threadRef?: ScopedThreadRef | undefined;  /** Panel that receives pull request links, including the standalone PR view. */  pullRequestPanelRef?: ScopedThreadRef | undefined;  /** Environment that owns non-thread markdown, such as a pull request panel. */  environmentId?: EnvironmentId | undefined;  onTaskListChange?: ((input: { markerOffset: number; checked: boolean }) => void) | undefined;  isStreaming?: boolean;  skills?: ReadonlyArray<Pick<ServerProviderSkill, "name" | "displayName">>;  className?: string;  /** Treat single newlines as hard breaks — chat-style user input. */  lineBreaks?: boolean;  /** Parse sanitized raw HTML instead of displaying its source text. */  parseRawHtml?: boolean;  /** Append a prompt that invokes a newly created artifact-template skill. */  onUseArtifactTemplate?: ((template: CodexArtifactTemplate) => void) | undefined;  /** Directory that anchors relative links and images; defaults to `cwd`. Set      to the file's own directory when rendering a markdown file. */  imageBaseDir?: string | undefined;  onImageExpand?: ((preview: ExpandedImagePreview) => void) | undefined;  extraRemarkPlugins?: NonNullable<ReactMarkdownOptions["remarkPlugins"]>;} export function canUseMarkdownFileShellActions(  environmentId: EnvironmentId | null,  remoteOpenMode: RemoteOpenMode,  isRemoteOpenResolved: boolean,): boolean {  return environmentId !== null && isRemoteOpenResolved && remoteOpenMode === "local-exec";} export function hasMarkdownFilePrimaryAction(input: {  canOpenInEditor: boolean;  canOpenInBrowser: boolean;  canOpenInPanel: boolean;  canOpenMedia?: boolean;}): boolean {  return (    input.canOpenInEditor ||    input.canOpenInBrowser ||    input.canOpenInPanel ||    input.canOpenMedia === true  );} export function shouldUseMarkdownFileBrowserPrimaryAction(input: {  iconPath: string;  canOpenInEditor: boolean;  canOpenInBrowser: boolean;  canOpenInPanel: boolean;}): boolean {  return (    input.canOpenInBrowser &&    (shouldOpenMarkdownFileLinkInBrowserByDefault(input.iconPath) ||      (!input.canOpenInEditor && !input.canOpenInPanel))  );} const EMPTY_MARKDOWN_SKILLS: ReadonlyArray<Pick<ServerProviderSkill, "name" | "displayName">> = [];const EMPTY_REMARK_PLUGINS: NonNullable<ReactMarkdownOptions["remarkPlugins"]> = []; const ARTIFACT_TEMPLATE_ICON_BY_KIND = {  document: FileTextIcon,  presentation: PresentationIcon,  spreadsheet: FileSpreadsheetIcon,  site: GlobeIcon,  "google-docs": FileTextIcon,  "google-slides": PresentationIcon,  "google-sheets": FileSpreadsheetIcon,  image: ImageIcon,  email: MailIcon,  slack: MessageSquareIcon,} satisfies Record<CodexArtifactTemplateKind, LucideIcon>; function CodexArtifactTemplateCard(props: {  readonly template: CodexArtifactTemplate;  readonly onUse?: ((template: CodexArtifactTemplate) => void) | undefined;}) {  const Icon = ARTIFACT_TEMPLATE_ICON_BY_KIND[props.template.artifactKind];  const presentationLabel = codexArtifactTemplatePresentationLabel(props.template.artifactKind);   return (    <div      role="group"      aria-label={`${props.template.displayName} template`}      className="chat-markdown-artifact-template my-[0.65rem] flex w-full min-w-0 items-center gap-3 rounded-xl border border-border/70 bg-card/60 px-3 py-2.5 text-foreground shadow-xs"      data-artifact-kind={props.template.artifactKind}      data-markdown-copy={`${props.template.displayName} (${presentationLabel})\n\n`}      data-skill-name={props.template.skillName}    >      <div className="flex min-w-0 flex-1 items-center gap-3">        <span className="relative flex size-9 shrink-0 items-center justify-center rounded-lg border border-border/70 bg-background text-muted-foreground shadow-xs">          <Icon aria-hidden className="size-5" />          <span className="absolute -right-1 -bottom-1 flex size-4 items-center justify-center rounded-full border border-background bg-fuchsia-500 text-white shadow-xs">            <SparklesIcon aria-hidden className="size-2.5" />          </span>        </span>        <span className="min-w-0">          <span className="block truncate text-sm font-medium text-foreground">            {props.template.displayName}          </span>          <span className="block text-xs text-muted-foreground">{presentationLabel}</span>        </span>      </div>      {props.onUse ? (        <Button          type="button"          size="sm"          variant="outline"          className="shrink-0"          onClick={() => props.onUse?.(props.template)}        >          Use template        </Button>      ) : null}    </div>  );} const CODE_FENCE_LANGUAGE_REGEX = /(?:^|\s)language-([^\s]+)/;const WINDOWS_DRIVE_PATH_REGEX = /^[A-Za-z]:[\\/]/;const MAX_HIGHLIGHT_CACHE_ENTRIES = 500;const MAX_HIGHLIGHT_CACHE_MEMORY_BYTES = 50 * 1024 * 1024; interface MarkdownActionFailureContext {  readonly operation: string;  readonly target?: string;  readonly format?: "markdown" | "csv";  readonly language?: string;  readonly fenceTitle?: string;  readonly copyTarget?: string;} function reportMarkdownActionFailure(context: MarkdownActionFailureContext, cause: unknown): void {  console.error("[chat-markdown] action failed", context, cause);} const highlightedCodeCache = new LRUCache<string>(  MAX_HIGHLIGHT_CACHE_ENTRIES,  MAX_HIGHLIGHT_CACHE_MEMORY_BYTES,); function findTaskListMarkerOffset(markdown: string, listItemStart: number): number | null {  const firstLineEnd = markdown.indexOf("\n", listItemStart);  const firstLine = markdown.slice(    listItemStart,    firstLineEnd === -1 ? markdown.length : firstLineEnd,  );  const match = firstLine.match(/^(?:\s*(?:[-+*]|\d+[.)])\s+)(\[[ xX]\])/);  if (!match?.[1]) return null;  return listItemStart + firstLine.indexOf(match[1]);} /** * The default `1.25rem` marker gutter (`.chat-markdown ol`) fits one-character * markers. Wider markers can extend past it and get clipped by a collapsed * message's overflow. Widen the gutter to fit the widest marker, including a * negative marker's minus sign. */function orderedListGutterStyle(  itemCount: number,  start: unknown,): { "--list-gutter": string } | undefined {  const parsedStart = Number.parseInt(String(start ?? 1), 10);  const firstNumber = Number.isNaN(parsedStart) ? 1 : parsedStart;  const lastNumber = firstNumber + Math.max(itemCount - 1, 0);  const markerWidth = Math.max(String(firstNumber).length, String(lastNumber).length);  if (markerWidth <= 1) return undefined;  return { "--list-gutter": `${markerWidth + 1}ch` };} type MarkdownImageHastNode = {  type?: string;  tagName?: string;  properties?: Record<string, unknown>;  children?: MarkdownImageHastNode[];}; function meaningfulHastChildren(node: MarkdownImageHastNode): MarkdownImageHastNode[] {  return (node.children ?? []).filter(    (child) => !(child.type === "text" && (child as { value?: string }).value?.trim() === ""),  );} /** * An image that is the only content of its block (optionally wrapped in a * link) is almost always a screenshot or figure, so it gets a reserved slot * while it loads. Images mixed with text or other images — badge rows, icons * in a sentence — stay inline at their natural size, since a placeholder taller * than the image would move the page more than the image itself does. *//** Containers whose sole child image reads as a figure rather than part of a sentence. */const STANDALONE_IMAGE_BLOCKS = new Set([  "p",  "div",  "li",  "td",  "th",  "figure",  "center",  "blockquote",]); function soleImageDescendant(node: MarkdownImageHastNode): MarkdownImageHastNode | undefined {  const children = meaningfulHastChildren(node);  if (children.length !== 1) return undefined;  const only = children[0];  if (only?.type !== "element") return undefined;  if (only.tagName === "img") return only;  // A link, emphasis, or similar inline wrapper around the image still counts  // as long as nothing else shares the block.  return only.tagName === "a" || only.tagName === "strong" || only.tagName === "em"    ? soleImageDescendant(only)    : undefined;} function markStandaloneImages(node: MarkdownImageHastNode) {  // A raw `<img>` on its own line reaches the root without a paragraph.  if (node.type === "root" || (node.tagName && STANDALONE_IMAGE_BLOCKS.has(node.tagName))) {    const image = soleImageDescendant(node);    if (image) image.properties = { ...image.properties, dataStandalone: true };  }  node.children?.forEach((child) => {    if (child.type === "element") markStandaloneImages(child);  });} /** Carries authored image source metadata through the sanitizer to the image renderer. */function rehypePreserveImageSourceMeta() {  return (tree: MarkdownImageHastNode) => {    const visit = (node: MarkdownImageHastNode) => {      const src = node.properties?.src;      const title = node.properties?.title;      if (node.type === "element" && node.tagName === "img") {        node.properties = {          ...node.properties,          ...(typeof src === "string" && isWindowsDrivePathHref(src) ? { dataLocalSrc: src } : {}),          ...(typeof title === "string" ? { dataMarkdownTitle: title } : {}),        };      }      node.children?.forEach(visit);    };     visit(tree);    markStandaloneImages(tree);  };} const CHAT_MARKDOWN_SANITIZE_SCHEMA = {  ...defaultSchema,  attributes: {    ...defaultSchema.attributes,    "*": (defaultSchema.attributes?.["*"] ?? []).filter((attribute) => attribute !== "title"),    code: [...(defaultSchema.attributes?.code ?? []), "dataCodeMeta", "dataInlineCode"],    blockquote: [...(defaultSchema.attributes?.blockquote ?? []), "dataAlert"],    div: [...(defaultSchema.attributes?.div ?? []), ...CODEX_ARTIFACT_TEMPLATE_HAST_PROPERTIES],    a: [...(defaultSchema.attributes?.a ?? []), "dataPullRequestAutolink"],    img: [      ...(defaultSchema.attributes?.img ?? []),      "dataLocalSrc",      "dataMarkdownTitle",      "dataStandalone",    ],  },  protocols: {    ...defaultSchema.protocols,    href: [...(defaultSchema.protocols?.href ?? []), "file", "t3-citation"],    src: [...(defaultSchema.protocols?.src ?? []), "file"],  },} satisfies Parameters<typeof rehypeSanitize>[0]; const CHAT_MARKDOWN_REMARK_PLUGINS = [  remarkGfm,  remarkGithubAlerts,  remarkNormalizeListItemIndentation,  remarkCodexDirectives,  remarkPreserveCodeMeta,  remarkNormalizeLinksAndTagInlineCode,] satisfies NonNullable<ReactMarkdownOptions["remarkPlugins"]>; const CHAT_MARKDOWN_REMARK_PLUGINS_WITH_BREAKS = [  remarkGfm,  remarkGithubAlerts,  remarkNormalizeListItemIndentation,  remarkCodexDirectives,  remarkBreaks,  remarkPreserveCodeMeta,  remarkNormalizeLinksAndTagInlineCode,] satisfies NonNullable<ReactMarkdownOptions["remarkPlugins"]>; const CHAT_MARKDOWN_REHYPE_PLUGINS = [  rehypeRaw,  rehypePreserveImageSourceMeta,  [rehypeSanitize, CHAT_MARKDOWN_SANITIZE_SCHEMA],] satisfies NonNullable<ReactMarkdownOptions["rehypePlugins"]>; /** GitHub's own five alert kinds, in its colors: the glyph names the urgency, the title says it. */const GITHUB_ALERT_PRESENTATIONS: Record<  string,  { label: string; Icon: typeof InfoIcon; borderClassName: string; titleClassName: string }> = {  note: {    label: "Note",    Icon: InfoIcon,    borderClassName: "border-blue-500/70",    titleClassName: "text-blue-600 dark:text-blue-400",  },  tip: {    label: "Tip",    Icon: LightbulbIcon,    borderClassName: "border-emerald-500/70",    titleClassName: "text-emerald-600 dark:text-emerald-400",  },  important: {

Lines 1–500 of 3148.