Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 110 additions & 2 deletions apps/web/src/components/ChatView.logic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ import {
type ServerProvider,
} from "@t3tools/contracts";
import { deriveProviderInstanceEntries, NO_PROVIDER_MODEL_SELECTION } from "../providerInstances";
import type { RightPanelSurface } from "../rightPanelStore";
import { selectThreadDiffPanelSelection, useDiffPanelStore } from "../diffPanelStore";
import {
selectActiveRightPanel,
selectThreadRightPanelState,
useRightPanelStore,
type RightPanelSurface,
} from "../rightPanelStore";
import {
CommandId,
EnvironmentId,
Expand All @@ -20,14 +26,15 @@ import {
ProviderInstanceId,
ThreadId,
RunId,
type ScopedThreadRef,
TurnItemId,
type OrchestrationV2ProjectedTurnItem,
type WorktreeSetupSnapshot,
} from "@t3tools/contracts";
import type { CodexArtifactTemplate } from "@t3tools/shared/codexArtifactTemplates";
import * as DateTime from "effect/DateTime";
import * as Option from "effect/Option";
import { afterEach, describe, expect, it, vi } from "vite-plus/test";
import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
import { Atom, AsyncResult } from "effect/reactivity";
import { appAtomRegistry } from "../rpc/atomRegistry";
import { environmentThreadDetails } from "../state/threads";
Expand All @@ -44,6 +51,7 @@ import {
resolveComposerProviderSelection,
resolveProactiveTurnDiffAction,
resolveDraftHeroState,
resolveExplicitDiffOpenOnActivate,
resolveWorktreeSetupProgress,
isPaintOnlyThreadTimeline,
peekHeldThreadTimeline,
Expand Down Expand Up @@ -1805,6 +1813,106 @@ describe("proactive completed diff guard", () => {
});
});

describe("Diff scope across right-panel tab switches", () => {
const threadRef: ScopedThreadRef = { environmentId, threadId };
const otherThreadRef: ScopedThreadRef = { environmentId, threadId: ThreadId.make("thread-2") };
let explicitDiffOpen: ScopedThreadRef | null = null;

const diffIsActive = (ref: ScopedThreadRef) =>
selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, ref) === "diff";
const diffScope = (ref: ScopedThreadRef) =>
selectThreadDiffPanelSelection(useDiffPanelStore.getState().byThreadKey, ref).kind;
const settle = (ref: ScopedThreadRef) => {
const explicit = explicitDiffOpen;
explicitDiffOpen = null;
if (diffIsActive(ref) && explicit !== ref) {
useDiffPanelStore.getState().selectGitScope(ref, "branch");
}
};
const clickTab = (ref: ScopedThreadRef, surfaceId: string) => {
const surface = selectThreadRightPanelState(
useRightPanelStore.getState().byThreadKey,
ref,
).surfaces.find((entry) => entry.id === surfaceId);
explicitDiffOpen = resolveExplicitDiffOpenOnActivate({
surfaceKind: surface!.kind,
diffOpen: diffIsActive(ref),
activeThreadRef: ref,
});
useRightPanelStore.getState().activateSurface(ref, surfaceId);
settle(ref);
};

beforeEach(() => {
explicitDiffOpen = null;
useRightPanelStore.setState({
byThreadKey: {},
userActionRevisionByThreadKey: {},
closeRevisionByThreadKey: {},
});
useDiffPanelStore.setState({ byThreadKey: {}, branchBaseRefByThreadKey: {} });
});

it("keeps Uncommitted when the user clicks back to the Diff tab", () => {
const panels = useRightPanelStore.getState();
panels.open(threadRef, "diff");
settle(threadRef);
useDiffPanelStore.getState().selectGitScope(threadRef, "unstaged");

panels.open(threadRef, "files");
settle(threadRef);
clickTab(threadRef, "diff");

expect(diffScope(threadRef)).toBe("unstaged");
});

it("still opens on Changes for generic openings", () => {
const panels = useRightPanelStore.getState();
const diffStore = useDiffPanelStore.getState();

panels.open(threadRef, "diff");
settle(threadRef);
expect(diffScope(threadRef)).toBe("branch");

diffStore.selectGitScope(threadRef, "unstaged");
panels.open(threadRef, "files");
settle(threadRef);
panels.closeSurface(threadRef, "files");
settle(threadRef);
expect(diffScope(threadRef)).toBe("branch");

diffStore.selectGitScope(threadRef, "unstaged");
panels.open(otherThreadRef, "diff");
diffStore.selectGitScope(otherThreadRef, "unstaged");
settle(otherThreadRef);
expect(diffScope(otherThreadRef)).toBe("branch");
});

it("marks only a click that brings the Diff tab on screen", () => {
expect(
resolveExplicitDiffOpenOnActivate({
surfaceKind: "diff",
diffOpen: false,
activeThreadRef: threadRef,
}),
).toBe(threadRef);
expect(
resolveExplicitDiffOpenOnActivate({
surfaceKind: "diff",
diffOpen: true,
activeThreadRef: threadRef,
}),
).toBeNull();
expect(
resolveExplicitDiffOpenOnActivate({
surfaceKind: "files",
diffOpen: false,
activeThreadRef: threadRef,
}),
).toBeNull();
});
});

describe("shouldRefocusComposerOnWindowFocus", () => {
function element(
tagName: string,
Expand Down
14 changes: 14 additions & 0 deletions apps/web/src/components/ChatView.logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,20 @@ export function resolveProactiveTurnDiffAction(input: {
return input.checkpoint.files.length >= 3 || changedLines >= 50 ? "open" : "ignore";
}

/**
* Clicking back to an existing Diff tab resumes the review in progress, so the
* generic-open reset to Changes must skip it. A diff that is already active is
* left unmarked: the reset never runs for it, and the marker would linger into
* the next generic open.
*/
export function resolveExplicitDiffOpenOnActivate(input: {
readonly surfaceKind: RightPanelSurface["kind"];
readonly diffOpen: boolean;
readonly activeThreadRef: ScopedThreadRef;
}): ScopedThreadRef | null {
return input.surfaceKind === "diff" && !input.diffOpen ? input.activeThreadRef : null;
}

export function codexArtifactTemplatePromptToAppend(
currentDraft: string,
template: CodexArtifactTemplate,
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
peekHeldThreadTimeline,
peekRememberedThreadTimeline,
rememberReadyThreadTimeline,
resolveExplicitDiffOpenOnActivate,
resolveThreadSwitchTimeline,
timelineHasEphemeralPreviewUrls,
recallCheckoutIsRepo,
Expand Down Expand Up @@ -5764,6 +5765,11 @@ export default function ChatView(props: ChatViewProps) {
const requestClosePanelTerminalStable = useStableCallback(requestClosePanelTerminal);
const activateRightPanelSurface = (surface: RightPanelSurface) => {
if (!activeThreadRef) return;
explicitDiffOpenRef.current = resolveExplicitDiffOpenOnActivate({
surfaceKind: surface.kind,
diffOpen,
activeThreadRef,
});
useRightPanelStore.getState().activateSurface(activeThreadRef, surface.id);
if (surface.kind === "preview" && surface.resourceId) {
setActivePreviewTab(activeThreadRef, surface.resourceId);
Expand Down
Loading