Z ZPI 文档 v0.3.8
介绍页

源码

packages/ui/src/preview/preview-lifecycle.ts

typescript84 行查看原文
import type { FileAction, RunStatus } from "../types.ts";
import {
  type AssistantPreviewCard,
  type AssistantPreviewCardFileStatService,
  buildAssistantPreviewCardsFromReferences,
  extractAssistantFileReferences,
} from "./assistant-preview-cards.ts";
import { resolveValidatedAssistantPreviewCards } from "./assistant-preview-validation.ts";
export interface PreviewInput {
  sessionId: string;
  runId: string;
  text: string;
  cwd: string;
  home?: string;
}
export interface PreviewServices {
  load(input: PreviewInput): Promise<AssistantPreviewCard[]>;
  openFile(sessionId: string, path: string): Promise<void>;
  openWebsite(sessionId: string, url: string): Promise<void>;
  openExternal(sessionId: string, url: string, localPath?: string): Promise<void>;
  fileAction(sessionId: string, path: string, action: FileAction): Promise<void>;
}
export function previewInputKey(input: PreviewInput): string {
  return JSON.stringify([input.sessionId, input.runId, input.cwd, input.home ?? "", input.text]);
}
export function createPreviewLoader(services: {
  checkFilesExist(
    params: { paths: string[] },
    sessionId: string,
  ): ReturnType<AssistantPreviewCardFileStatService["checkFilesExist"]>;
  getChangedPaths(sessionId: string, runId: string): Promise<string[]>;
}) {
  // Coalesce in-flight projections. File existence can change while a conversation
  // is unmounted; mounted rows own their settled snapshots and remounts revalidate.
  const requests = new Map<string, Promise<AssistantPreviewCard[]>>();
  return {
    clear(sessionId?: string) {
      for (const key of requests.keys())
        if (!sessionId || (JSON.parse(key) as string[])[0] === sessionId) requests.delete(key);
    },
    load(input: PreviewInput) {
      const key = previewInputKey(input);
      const cached = requests.get(key);
      if (cached) return cached;
      const request = (async () => {
        const references = extractAssistantFileReferences(input.text, input.cwd, { homePath: input.home });
        const needsChanges = references.some((ref) => ref.kind === "markdown" || ref.kind === "html");
        const changedFilePaths = needsChanges
          ? await services.getChangedPaths(input.sessionId, input.runId).catch(() => [])
          : [];
        const candidates = buildAssistantPreviewCardsFromReferences(input.text, input.cwd, references, {
          changedFilePaths,
          homePath: input.home,
        });
        return resolveValidatedAssistantPreviewCards(candidates, {
          checkFilesExist: (params) => services.checkFilesExist(params, input.sessionId),
        }).catch(() => []);
      })().finally(() => {
        if (requests.get(key) === request) requests.delete(key);
      });
      requests.set(key, request);
      return request;
    },
  };
}
export interface PreviewAutoOpenState {
  scope: string;
  armed: boolean;
}
export function advancePreviewAutoOpen(
  previous: PreviewAutoOpenState,
  scope: string,
  status: RunStatus | undefined,
  runId?: string,
): { state: PreviewAutoOpenState; target?: string | null } {
  const changed = previous.scope !== scope;
  const current = changed ? { scope, armed: false } : previous;
  if (status === "running") return { state: { scope, armed: true }, target: null };
  if (status !== "completed")
    return { state: { scope, armed: false }, ...(changed || current.armed ? { target: null } : {}) };
  if (!current.armed || !runId) return { state: current, ...(changed ? { target: null } : {}) };
  return { state: { scope, armed: false }, target: `${scope}:${runId}` };
}