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
62 changes: 61 additions & 1 deletion apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ import { environmentCatalog } from "../connection/catalog";
import { selectThreadTerminalUiState, useTerminalUiStateStore } from "../terminalUiStateStore";
import { useKnownTerminalSessions, useThreadRunningTerminalIds } from "../state/terminalSessions";
import { projectEnvironment } from "../state/projects";
import { linkedPullRequestDetailAtom } from "../state/pullRequests";
import { useEnvironmentQuery } from "../state/query";
import {
environmentServerConfigsAtom,
Expand Down Expand Up @@ -317,6 +318,7 @@ import {
} from "./chat/ThreadErrorBanner";
import {
resolveDisplayedThreadPr,
threadPullRequestRefreshSource,
threadChangeRequestSnapshotsAtom,
useLinkedThreadPullRequest,
} from "./ThreadStatusIndicators";
Expand Down Expand Up @@ -1745,14 +1747,16 @@ function ChatViewContent(props: ChatViewProps) {
// the tab is found again whether or not that surface was opened with an environment on it.
const activePullRequestSurfaceId =
activeRightPanelSurface?.kind === "pull-request" ? activeRightPanelSurface.id : undefined;
const handlePullRequestTabStatusChange = useCallback(
const updatePullRequestTabStatusFromPanel = useCallback(
(status: PullRequestTabStatus) => {
const id = activePullRequestSurfaceId;
if (id === undefined) return;
setPullRequestTabStatuses((current) => updatePullRequestTabStatus(current, id, status));
},
[activePullRequestSurfaceId],
);
const refreshVcsStatus = useAtomCommand(vcsEnvironment.refreshStatus, { reportFailure: false });
const sidebarPrRefreshKeyRef = useRef<string | null>(null);
const activeFileSurface =
activeRightPanelSurface?.kind === "file" ? activeRightPanelSurface : null;
const activePreviewState = useThreadPreviewState(activeThreadRef);
Expand Down Expand Up @@ -4594,6 +4598,62 @@ function ChatViewContent(props: ChatViewProps) {
linkedPullRequest: linkedThreadPullRequest,
linkedPullRequestStatus,
});
const handlePullRequestTabStatusChange = useCallback(
(status: PullRequestTabStatus) => {
updatePullRequestTabStatusFromPanel(status);
const source = threadPullRequestRefreshSource({
panel: status,
thread: {
repository: threadRepository,
number: linkedThreadPullRequest?.number ?? activeThreadPr?.number ?? null,
state: activeThreadPr?.state ?? null,
linked: linkedThreadPullRequest !== null,
},
});
if (source === null) {
sidebarPrRefreshKeyRef.current = null;
return;
}
const refreshKey = `${activeThreadKey}:${source}:${status.repository}#${status.number}:${status.state}`;
if (sidebarPrRefreshKeyRef.current === refreshKey) return;
sidebarPrRefreshKeyRef.current = refreshKey;

if (source === "linked-detail" && activeThreadRef && linkedThreadPullRequest) {
appAtomRegistry.refresh(
linkedPullRequestDetailAtom({
environmentId: activeThreadRef.environmentId,
input: {
projectId: linkedThreadPullRequest.projectId,
repository: linkedThreadPullRequest.repository,
number: linkedThreadPullRequest.number,
},
}),
Comment thread
t3-code[bot] marked this conversation as resolved.
);
return;
Comment thread
cursor[bot] marked this conversation as resolved.
}
if (source === "vcs" && activeThreadRef && gitCwd !== null) {
void refreshVcsStatus({
environmentId: activeThreadRef.environmentId,
input: { cwd: gitCwd },
}).then(() => {
if (sidebarPrRefreshKeyRef.current === refreshKey) {
sidebarPrRefreshKeyRef.current = null;
}
});
}
},
[
activeThreadKey,
activeThreadPr?.number,
activeThreadPr?.state,
activeThreadRef,
gitCwd,
linkedThreadPullRequest,
refreshVcsStatus,
threadRepository,
updatePullRequestTabStatusFromPanel,
],
);
const activeThreadReferenceCopyTarget = useMemo(
() =>
activeThreadId === null || !isServerThread
Expand Down
56 changes: 56 additions & 0 deletions apps/web/src/components/ThreadStatusIndicators.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
resolveDisplayedThreadPrProvider,
resolveThreadPr,
settledPrHoverColorClass,
threadPullRequestRefreshSource,
threadChangeRequestSnapshotsAtom,
type ThreadChangeRequestSnapshot,
} from "./ThreadStatusIndicators";
Expand Down Expand Up @@ -56,6 +57,61 @@ function snapshotFor(
return { branch, pr, sourceControlProvider };
}

describe("threadPullRequestRefreshSource", () => {
const panel = { repository: "pingdotgg/t3code", number: 42, state: "merged" as const };

it("refreshes the VCS stream when the open panel is newer than an inferred sidebar PR", () => {
expect(
threadPullRequestRefreshSource({
panel,
thread: { repository: "pingdotgg/t3code", number: 42, state: "open", linked: false },
}),
).toBe("vcs");
});

it("refreshes linked detail when the open panel is newer than a linked sidebar PR", () => {
expect(
threadPullRequestRefreshSource({
panel,
thread: { repository: "pingdotgg/t3code", number: 42, state: "open", linked: true },
}),
).toBe("linked-detail");
});

it("refreshes when the sidebar has not resolved state yet", () => {
expect(
threadPullRequestRefreshSource({
panel,
thread: { repository: "pingdotgg/t3code", number: 42, state: null, linked: false },
}),
).toBe("vcs");
});

it("does nothing once sidebar state matches or the panel shows another PR", () => {
expect(
threadPullRequestRefreshSource({
panel,
thread: { repository: "pingdotgg/t3code", number: 42, state: "merged", linked: false },
}),
).toBeNull();
expect(
threadPullRequestRefreshSource({
panel,
thread: { repository: "pingdotgg/t3code", number: 41, state: "open", linked: false },
}),
).toBeNull();
});

it("matches repository identity without case sensitivity", () => {
expect(
threadPullRequestRefreshSource({
panel: { ...panel, repository: "PingDotGG/T3Code" },
thread: { repository: "pingdotgg/t3code", number: 42, state: "open", linked: false },
}),
).toBe("vcs");
});
});

describe("resolveThreadPr", () => {
it("keeps local-checkout PR indicators scoped to the stored thread branch", () => {
expect(
Expand Down
26 changes: 26 additions & 0 deletions apps/web/src/components/ThreadStatusIndicators.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,32 @@ export interface TerminalStatusIndicator {

export type ThreadPr = VcsStatusResult["pr"];

export type ThreadPullRequestRefreshSource = "linked-detail" | "vcs";

/** Refresh only when the panel has newer state for this thread's own pull request. */
export function threadPullRequestRefreshSource(input: {
readonly panel: {
readonly repository: string;
readonly number: number;
readonly state: NonNullable<ThreadPr>["state"];
};
readonly thread: {
readonly repository: string | null;
readonly number: number | null;
readonly state: NonNullable<ThreadPr>["state"] | null;
readonly linked: boolean;
};
}): ThreadPullRequestRefreshSource | null {
if (
input.thread.repository?.toLowerCase() !== input.panel.repository.toLowerCase() ||
input.thread.number !== input.panel.number ||
input.thread.state === input.panel.state
) {
return null;
}
Comment thread
cursor[bot] marked this conversation as resolved.
return input.thread.linked ? "linked-detail" : "vcs";
}

export interface LinkedThreadPullRequestStatus {
readonly pr: NonNullable<ThreadPr>;
readonly sourceControlProvider: NonNullable<VcsStatusResult["sourceControlProvider"]>;
Expand Down
Loading