2205 lines · 69.7 KB · TypeScript
RawDownload
import {  ANTIGRAVITY_DEFAULT_MODEL,  EnvironmentId,  MessageId,  ProjectId,  ProviderDriverKind,  ProviderInstanceId,  type ServerProvider,  ThreadId,  TurnId,} from "@t3tools/contracts";import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test"; import type { Thread, ThreadShell, TurnDiffSummary } from "../types";import { deriveProviderInstanceEntries, NO_PROVIDER_MODEL_SELECTION } from "../providerInstances";import type { CodexArtifactTemplate } from "@t3tools/client-runtime/codex-artifact-templates";import { scopeThreadRef } from "@t3tools/client-runtime/environment";import {  type RightPanelSurface,  pullRequestSurface,  selectActiveRightPanelSurface,  useRightPanelStore,} from "../rightPanelStore";import {  selectThreadPreviewMiniPlayer,  usePreviewMiniPlayerStore,} from "../previewMiniPlayerStore";import {  MAX_HIDDEN_MOUNTED_PREVIEW_THREADS,  MAX_HIDDEN_MOUNTED_TERMINAL_THREADS,  agentControlledBrowserCloseConfirmation,  branchMismatchKey,  buildExpiredTerminalContextToastCopy,  buildLoadingThreadFromShell,  buildRunningThreadTurnInterruptInput,  buildThreadTurnInterruptInput,  createLocalDispatchSnapshot,  deriveComposerSendState,  deriveLockedProvider,  dismissBranchMismatchForSession,  ENVIRONMENT_RECONNECT_WARNING_GRACE_MS,  getAntigravitySendBlockReason,  getStartedThreadModelChangeBlockReason,  hasEnvironmentReconnectWarningGraceElapsed,  hasServerAcknowledgedLocalDispatch,  shouldRefocusComposerOnWindowFocus,  isBranchMismatchDismissedForSession,  reconcileMountedTerminalThreadIds,  reconcileRetainedMountedThreadIds,  recallCheckoutIsRepo,  rememberCheckoutIsRepo,  resolveBackgroundDraftWorkspaceOptions,  resolveComposerInteractionMode,  resolveComposerProviderSelection,  resolveDraftPromotionNavigationTarget,  observeProactivePanelUserChoice,  resolveProactiveTurnDiffAction,  resolveThreadMetadataUpdateForNextTurn,  resolveSendEnvMode,  threadShellHasStarted,  resolveDraftHeroState,  isPaintOnlyThreadTimeline,  peekHeldThreadTimeline,  peekRememberedThreadTimeline,  rememberReadyThreadTimeline,  resetHeldThreadTimeline,  resolveThreadSwitchTimeline,  threadKeysShareEnvironment,  timelineHasEphemeralPreviewUrls,  scheduleEnvironmentReconnectWarning,  startNewThreadForProject,  codexArtifactTemplatePromptToAppend,  shouldDockDraftHeroForSubmission,  shouldReleaseTimelineAnchorForToolActivity,  shouldOpenProactivePullRequest,  shouldRetargetThreadPullRequestPanel,  shouldOpenProactiveTurnDiff,  shouldRenderPreviewMiniPlayer,  shouldShowBranchMismatchBanner,  shouldShowPlanFollowUpPrompt,  shouldWriteThreadErrorToCurrentServerThread,  toolGroupConsumesUpwardNavigation,} from "./ChatView.logic"; describe("agent browser close confirmation", () => {  const surfaces = [    { id: "browser:one", kind: "preview", resourceId: "tab-1" },    { id: "browser:two", kind: "preview", resourceId: "tab-2" },    { id: "diff", kind: "diff" },  ] satisfies RightPanelSurface[];   it("only warns for browsers under active agent control", () => {    expect(      agentControlledBrowserCloseConfirmation(surfaces, {        "tab-1": { controller: "none" },        "tab-2": { controller: "human" },      }),    ).toBeNull();     expect(      agentControlledBrowserCloseConfirmation([surfaces[0]!], {        "tab-1": { controller: "agent" },      }),    ).toBe(      [        "Close browser while the agent is using it?",        "The agent is actively controlling this browser. Closing it may interrupt the current browser action.",      ].join("\n"),    );  });   it("counts every agent-controlled browser in a bulk close", () => {    expect(      agentControlledBrowserCloseConfirmation(surfaces, {        "tab-1": { controller: "agent" },        "tab-2": { controller: "agent" },      }),    ).toContain("Close 2 browsers");  });}); describe("floating browser preview", () => {  it("keeps agent preview intent when a user selects its browser tab and then switches away", () => {    useRightPanelStore.setState({ byThreadKey: {}, userActionRevisionByThreadKey: {} });    usePreviewMiniPlayerStore.setState({ byThreadKey: {} });    const ref = scopeThreadRef(EnvironmentId.make("env-1"), ThreadId.make("thread-1"));    const panels = useRightPanelStore.getState();    const revision = panels.getUserActionRevision(ref);    usePreviewMiniPlayerStore.getState().open(ref, "agent-tab");    panels.reconcileBrowserSurfaces(ref, ["agent-tab"]);    const intent = selectThreadPreviewMiniPlayer(      usePreviewMiniPlayerStore.getState().byThreadKey,      ref,    );    const isFloating = () =>      shouldRenderPreviewMiniPlayer(        selectThreadPreviewMiniPlayer(usePreviewMiniPlayerStore.getState().byThreadKey, ref)          ?.tabId ?? null,        selectActiveRightPanelSurface(useRightPanelStore.getState().byThreadKey, ref),      );     panels.openProactive(ref, { id: "diff", kind: "diff" }, revision);    expect(isFloating()).toBe(true);    panels.activateSurface(ref, "browser:agent-tab");    expect(isFloating()).toBe(false);    expect(panels.openProactive(ref, { id: "diff", kind: "diff" }, revision)).toBe(false);    panels.open(ref, "diff");    expect(isFloating()).toBe(true);    expect(      selectThreadPreviewMiniPlayer(usePreviewMiniPlayerStore.getState().byThreadKey, ref),    ).toBe(intent);  });   it("only hides the duplicate while the same browser is rendered in the panel", () => {    expect(shouldRenderPreviewMiniPlayer(null, null)).toBe(false);    expect(      shouldRenderPreviewMiniPlayer("tab-1", {        id: "browser:one",        kind: "preview",        resourceId: "tab-1",      }),    ).toBe(false);    expect(      shouldRenderPreviewMiniPlayer("tab-1", {        id: "browser:two",        kind: "preview",        resourceId: "tab-2",      }),    ).toBe(true);    expect(shouldRenderPreviewMiniPlayer("tab-1", { id: "diff", kind: "diff" })).toBe(true);  });}); describe("proactive panels", () => {  it("keeps a manual PR selection made after following a replacement while loading", () => {    useRightPanelStore.setState({ byThreadKey: {}, userActionRevisionByThreadKey: {} });    const ref = scopeThreadRef(EnvironmentId.make("env-1"), ThreadId.make("thread-1"));    const panels = useRightPanelStore.getState();    const oldPr = pullRequestSurface({      projectId: "project-1",      repository: "owner/repo",      number: 1,    });    const replacement = pullRequestSurface({ ...oldPr, number: 2 });    const turnId = TurnId.make("turn-1");    panels.openPullRequest(ref, oldPr);    const loading = observeProactivePanelUserChoice(null, {      threadKey: "env-1:thread-1",      runningTurnId: turnId,      userActionRevision: panels.getUserActionRevision(ref),    });    expect(panels.openProactive(ref, replacement, loading.userActionRevision)).toBe(true);     panels.activateSurface(ref, oldPr.id);    const loaded = observeProactivePanelUserChoice(loading, {      threadKey: loading.threadKey,      runningTurnId: turnId,      userActionRevision: panels.getUserActionRevision(ref),    });    expect(panels.openProactive(ref, replacement, loaded.userActionRevision)).toBe(false);    expect(selectActiveRightPanelSurface(useRightPanelStore.getState().byThreadKey, ref)).toEqual(      oldPr,    );    expect(shouldOpenProactivePullRequest(loaded.targetKey, "owner/repo:2")).toBe(true);    expect(      shouldOpenProactiveTurnDiff({        previousRunningTurnId: loaded.runningTurnId,        runningTurnId: null,        settledTurnId: turnId,        turnCompleted: true,      }),    ).toBe(true);    expect(panels.openProactive(ref, { id: "diff", kind: "diff" }, loaded.userActionRevision)).toBe(      false,    );  });   it.each(["idle", "loading", "observed"] as const)(    "captures a new turn's choice once with initial state %s",    (initialState) => {      useRightPanelStore.setState({ byThreadKey: {}, userActionRevisionByThreadKey: {} });      const ref = scopeThreadRef(EnvironmentId.make("env-1"), ThreadId.make("thread-1"));      const panels = useRightPanelStore.getState();      const firstTurn = TurnId.make("turn-1");      const nextTurn = TurnId.make("turn-2");      const initial = observeProactivePanelUserChoice(null, {        threadKey: "env-1:thread-1",        runningTurnId: initialState === "idle" ? null : firstTurn,        userActionRevision: panels.getUserActionRevision(ref),      });      panels.openFile(ref, "src/first.ts");      const loadingNextTurn = observeProactivePanelUserChoice(        {          ...initial,          ...(initialState === "observed" ? { runningTurnId: firstTurn, targetKey: null } : {}),        },        {          threadKey: initial.threadKey,          runningTurnId: nextTurn,          userActionRevision: panels.getUserActionRevision(ref),        },      );      expect(        panels.openProactive(ref, { id: "diff", kind: "diff" }, loadingNextTurn.userActionRevision),      ).toBe(true);       panels.openFile(ref, "src/second.ts");      const loaded = observeProactivePanelUserChoice(loadingNextTurn, {        threadKey: initial.threadKey,        runningTurnId: nextTurn,        userActionRevision: panels.getUserActionRevision(ref),      });      expect(        panels.openProactive(ref, { id: "diff", kind: "diff" }, loaded.userActionRevision),      ).toBe(false);      expect(        selectActiveRightPanelSurface(useRightPanelStore.getState().byThreadKey, ref)?.id,      ).toBe("file:src/second.ts");    },  );   it("opens an existing pull request on entry and follows newly observed links", () => {    expect(shouldOpenProactivePullRequest(undefined, "project:repo:42")).toBe(true);    expect(shouldOpenProactivePullRequest(undefined, null)).toBe(false);    expect(shouldOpenProactivePullRequest(null, "project:repo:42")).toBe(true);    expect(shouldOpenProactivePullRequest("project:repo:42", "project:repo:42")).toBe(false);    expect(shouldOpenProactivePullRequest("project:repo:42", null)).toBe(false);  });   it("follows a changed server PR link without replacing an unrelated open panel", () => {    const previous = {      projectId: ProjectId.make("project-1"),      repository: "pingdotgg/t3code",      number: 42,      url: "https://github.com/pingdotgg/t3code/pull/42",    };    const current = {      ...previous,      number: 43,      url: "https://github.com/pingdotgg/t3code/pull/43",    };    const surface = {      id: "pull-request:previous",      kind: "pull-request",      projectId: previous.projectId,      repository: "PingDotGG/T3Code",      number: previous.number,    } satisfies RightPanelSurface;     expect(shouldRetargetThreadPullRequestPanel(previous, current, surface)).toBe(true);    expect(shouldRetargetThreadPullRequestPanel(previous, previous, surface)).toBe(false);    expect(shouldRetargetThreadPullRequestPanel(previous, null, surface)).toBe(false);    expect(      shouldRetargetThreadPullRequestPanel(previous, current, { ...surface, number: 99 }),    ).toBe(false);    expect(      shouldRetargetThreadPullRequestPanel(previous, current, {        ...surface,        projectId: "another-project",      }),    ).toBe(false);  });   it("opens a completed diff on entry or when the observed running turn settles", () => {    const turnId = TurnId.make("turn-1");    expect(      shouldOpenProactiveTurnDiff({        previousRunningTurnId: undefined,        runningTurnId: null,        settledTurnId: turnId,        turnCompleted: true,      }),    ).toBe(true);    expect(      shouldOpenProactiveTurnDiff({        previousRunningTurnId: turnId,        runningTurnId: null,        settledTurnId: turnId,        turnCompleted: true,      }),    ).toBe(true);    expect(      shouldOpenProactiveTurnDiff({        previousRunningTurnId: turnId,        runningTurnId: TurnId.make("turn-2"),        settledTurnId: turnId,        turnCompleted: true,      }),    ).toBe(false);    expect(      shouldOpenProactiveTurnDiff({        previousRunningTurnId: turnId,        runningTurnId: null,        settledTurnId: turnId,        turnCompleted: false,      }),    ).toBe(false);  });   it("opens a completed turn diff only for changed files", () => {    const changedCheckpoint = {      status: "ready",      files: [{ path: "src/app.ts", kind: "modified", additions: 1, deletions: 0 }],    } satisfies Pick<TurnDiffSummary, "status" | "files">;    const unchangedCheckpoint = {      status: "ready",      files: [],    } satisfies Pick<TurnDiffSummary, "status" | "files">;     expect(      resolveProactiveTurnDiffAction({        checkpoint: changedCheckpoint,        isGitRepo: true,      }),    ).toBe("open");    expect(      resolveProactiveTurnDiffAction({        checkpoint: unchangedCheckpoint,        isGitRepo: true,      }),    ).toBe("ignore");  });   it("waits for definitive checkpoint and repository state", () => {    const missingCheckpoint = {      status: "missing",      files: [],    } satisfies Pick<TurnDiffSummary, "status" | "files">;    const changedCheckpoint = {      status: "ready",      files: [{ path: "src/app.ts", kind: "modified", additions: 1, deletions: 0 }],    } satisfies Pick<TurnDiffSummary, "status" | "files">;     expect(      resolveProactiveTurnDiffAction({        checkpoint: undefined,        isGitRepo: true,      }),    ).toBe("defer");    expect(      resolveProactiveTurnDiffAction({        checkpoint: missingCheckpoint,        isGitRepo: true,      }),    ).toBe("defer");    expect(      resolveProactiveTurnDiffAction({        checkpoint: changedCheckpoint,        isGitRepo: undefined,      }),    ).toBe("defer");  });}); describe("toolGroupConsumesUpwardNavigation", () => {  class ScrollElement extends EventTarget {    scrollTop = 0;    scrollHeight = 100;    clientHeight = 100;    overflowY = "visible";     constructor(      readonly parentElement: ScrollElement | null = null,      readonly isToolGroup = false,    ) {      super();    }     closest(selector: string): ScrollElement | null {      if (selector !== "[data-tool-group-scroll]") return null;      return this.isToolGroup ? this : (this.parentElement?.closest(selector) ?? null);    }  }   beforeEach(() => {    vi.stubGlobal("Element", ScrollElement);    vi.stubGlobal("getComputedStyle", (element: ScrollElement) => ({      overflowY: element.overflowY,    }));  });  afterEach(() => vi.unstubAllGlobals());   it("releases upward navigation when an overflowing group is at the top", () => {    const group = Object.assign(new ScrollElement(null, true), {      overflowY: "auto",      scrollHeight: 300,    });     expect(toolGroupConsumesUpwardNavigation(new ScrollElement(group))).toBe(false);  });   it.each([    { overflowY: "auto", scrollTop: 1 },    { overflowY: "auto", scrollTop: 0.25 },    { overflowY: "scroll", scrollTop: 80 },  ])("consumes upward navigation within a scrolled group: %j", (scroll) => {    const group = Object.assign(new ScrollElement(null, true), {      scrollHeight: 300,      ...scroll,    });     expect(toolGroupConsumesUpwardNavigation(group)).toBe(true);  });   it.each([100, 300])(    "consumes scrolling in a nested result with a group content height of %i",    (scrollHeight) => {      const group = Object.assign(new ScrollElement(null, true), {        overflowY: "auto",        scrollHeight,      });      const result = Object.assign(new ScrollElement(group), {        overflowY: "auto",        scrollHeight: 300,        scrollTop: 0.25,      });       expect(toolGroupConsumesUpwardNavigation(new ScrollElement(result))).toBe(true);    },  );   it("releases upward navigation when the group and nested result are both at the top", () => {    const group = Object.assign(new ScrollElement(null, true), {      overflowY: "auto",      scrollHeight: 300,    });    const result = Object.assign(new ScrollElement(group), {      overflowY: "scroll",      scrollHeight: 300,    });     expect(toolGroupConsumesUpwardNavigation(new ScrollElement(result))).toBe(false);  });   it("ignores targets outside a tool group and non-element targets", () => {    const outside = Object.assign(new ScrollElement(), {      overflowY: "auto",      scrollHeight: 300,      scrollTop: 40,    });     expect(toolGroupConsumesUpwardNavigation(outside)).toBe(false);    expect(toolGroupConsumesUpwardNavigation(new EventTarget())).toBe(false);    expect(toolGroupConsumesUpwardNavigation(null)).toBe(false);  });   it("does not consume scrolling from an ancestor beyond the tool group", () => {    const timeline = Object.assign(new ScrollElement(), {      overflowY: "auto",      scrollHeight: 300,      scrollTop: 40,    });    const group = new ScrollElement(timeline, true);     expect(toolGroupConsumesUpwardNavigation(new ScrollElement(group))).toBe(false);  });   it.each(["hidden", "clip", "visible"])(    "ignores a non-scrollable child with overflow-y %s",    (overflowY) => {

Lines 1–500 of 2205.