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
8 changes: 5 additions & 3 deletions apps/web/src/components/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1524,12 +1524,13 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve
}
};

const startThreadRename = (threadRef: ScopedThreadRef, title: string) => {
// The ref write stops the compiler memoizing this, and every row receives it.
const startThreadRename = useStableCallback((threadRef: ScopedThreadRef, title: string) => {
if (!readEnvironmentScope(threadRef.environmentId, AuthOrchestrationOperateScope)) return;
setRenamingThreadRef(threadRef);
setRenamingTitle(title);
renamingOriginalTitleRef.current = title;
};
});
const cancelThreadRename = () => setRenamingThreadRef(null);
const commitThreadRename = (threadRef: ScopedThreadRef, title: string) => {
const originalTitle = renamingOriginalTitleRef.current;
Expand Down Expand Up @@ -1559,6 +1560,7 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve
})();
};

const readOpensPullRequestsInRightPanel = useStableCallback(() => routeThreadRef !== null);
const handleThreadClick = (event: ReactMouseEvent, threadRef: ScopedThreadRef) => {
if (isSidebarNestedLinkClick(event.target)) return;
const isMac = isMacPlatform(navigator.platform);
Expand Down Expand Up @@ -3420,7 +3422,7 @@ export default function Sidebar({ routeReveal }: { routeReveal: SidebarRouteReve
routeRowThreadKey === threadKey ? revealActiveRow : undefined
}
isRouteThread={routeThreadKey === threadKey}
openPullRequestsInRightPanel={routeThreadRef !== null}
readOpensPullRequestsInRightPanel={readOpensPullRequestsInRightPanel}
jumpLabel={
showJumpHints ? (jumpLabelByKey.get(threadKey) ?? null) : null
}
Expand Down
7 changes: 4 additions & 3 deletions apps/web/src/components/sidebar/SidebarThreadRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -568,7 +568,8 @@ type SidebarThreadRowProps = {
// Unlike isActive, never true for the ancestor standing in for an open
// subagent: drafts and PR clicks care about which composer is on screen.
isRouteThread: boolean;
openPullRequestsInRightPanel: boolean;
// Read at click time; a prop would re-render every row on entering or leaving a thread route.
readOpensPullRequestsInRightPanel: () => boolean;
jumpLabel: string | null;
currentEnvironmentId: string | null;
environmentLabel: string | null;
Expand Down Expand Up @@ -660,7 +661,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP
onUnsettle,
onUnsnooze,
onUnpin,
openPullRequestsInRightPanel,
readOpensPullRequestsInRightPanel,
renamingTitle,
thread,
variant,
Expand Down Expand Up @@ -965,7 +966,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThreadRowP
const handlePrClick = (event: ReactMouseEvent<HTMLElement>, targetUrl?: string) => {
const url = targetUrl ?? pr?.url ?? currentLinkedPr?.url;
if (!url) return;
const showInRightPanel = targetUrl !== undefined || openPullRequestsInRightPanel;
const showInRightPanel = targetUrl !== undefined || readOpensPullRequestsInRightPanel();
const openedInRightPanel = openPrLink(event, url, showInRightPanel ? threadRef : undefined);
if (openedInRightPanel && showInRightPanel && !props.isRouteThread) {
onThreadActivate(threadRef);
Expand Down
42 changes: 31 additions & 11 deletions apps/web/test/appScenarios.renders.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// chat and settings routes, the sidebar and the right panel, through the same stores and
// atoms the app reads. See renderScenarios.renders.tsx for the counting rules.
import { createMemoryHistory } from "@tanstack/react-router";
import { scopeThreadRef } from "@t3tools/client-runtime/environment";
import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime/environment";
import type { ThreadId } from "@t3tools/contracts";
import { DEFAULT_CLIENT_SETTINGS } from "@t3tools/contracts/settings";
import type * as Context from "effect/Context";
Expand All @@ -14,13 +14,15 @@ import { commands, page, userEvent } from "vite-plus/test/browser";
import "~/index.css";

import { AppRoot } from "~/AppRoot";
import { DraftId, useComposerDraftStore } from "~/composerDraftStore";
import { __setClientSettingsForTests } from "~/hooks/useSettings";
import { useRightPanelStore } from "~/rightPanelStore";
import { getRouter, type AppRouter } from "~/router";
import { appAtomRegistry } from "~/rpc/atomRegistry";
import { vcsEnvironment } from "~/state/vcs";
import {
ENVIRONMENT_ID,
PROJECT_ID,
assistantItem,
emitTerminalMetadata,
emitThreadEvent,
Expand Down Expand Up @@ -120,6 +122,16 @@ async function openThread(threadId: ThreadId) {
return app;
}

const goToThread = (router: AppRouter, threadId: ThreadId) => async () => {
await act(() =>
router.navigate({
to: "/$environmentId/$threadId",
params: { environmentId: ENVIRONMENT_ID, threadId },
}),
);
await expect.element(lastAnswer(threadId)).toBeVisible();
};

async function measure(action: () => Promise<void>): Promise<RenderReport> {
await quiet();
startCounting();
Expand Down Expand Up @@ -269,21 +281,29 @@ it("app-switch-thread", async () => {
using _clock = frozenClock();
const { screen, router } = await openThread(A);
await using _app = screen;
const go = (threadId: ThreadId) => async () => {
await act(() =>
router.navigate({
to: "/$environmentId/$threadId",
params: { environmentId: ENVIRONMENT_ID, threadId },
}),
);
await expect.element(lastAnswer(threadId)).toBeVisible();
};
const windows = [await measure(go(B)), await measure(go(A))];
const windows = [await measure(goToThread(router, B)), await measure(goToThread(router, A))];
// Only the rows losing and gaining the active highlight change.
for (const window of windows) expect(window.components.SidebarThreadRow).toBe(2);
await report("app-switch-thread", windows);
});

// A row reads whether a thread is open only when its PR badge is clicked, so opening a thread
// from a draft changes only the row that gains the highlight.
it("app-open-thread-from-draft", async () => {
using _clock = frozenClock();
const drafts = useComposerDraftStore.getState();
const draftId = DraftId.make("draft-render");
drafts.setProjectDraftThreadId(scopeProjectRef(ENVIRONMENT_ID, PROJECT_ID), draftId);
using _draft = { [Symbol.dispose]: () => drafts.clearDraftThread(draftId) };
const { screen, router } = await mountApp(`/draft/${draftId}`);
await using _app = screen;
// The sidebar lists the project once the shell snapshot has arrived.
await expect.element(page.getByText("render", { exact: true }).first()).toBeVisible();
const counted = await measure(goToThread(router, A));
expect(counted.components.SidebarThreadRow).toBe(1);
await report("app-open-thread-from-draft", [counted], { commits: false });
});

// Through the diff.toggle shortcut, the same handler as the header's diff button.
const toggleDiff = (to: "open" | "close") => async () => {
await userEvent.keyboard("{Control>}d{/Control}");
Expand Down
Loading