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. Load more
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) => {