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
4 changes: 3 additions & 1 deletion apps/web/src/components/BranchToolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,9 @@ export const BranchToolbar = memo(function BranchToolbar({
);
const serverThreadSelector = useMemo(() => createThreadSelectorByRef(threadRef), [threadRef]);
const serverThread = useStore(serverThreadSelector);
const draftThread = useComposerDraftStore((store) => store.getDraftThreadByRef(threadRef));
const draftThread = useComposerDraftStore((store) =>
draftId ? store.getDraftSession(draftId) : store.getDraftThreadByRef(threadRef),
);
const activeProjectRef = serverThread
? scopeProjectRef(serverThread.environmentId, serverThread.projectId)
: draftThread
Expand Down
4 changes: 3 additions & 1 deletion apps/web/src/components/BranchToolbarBranchSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,9 @@ export function BranchToolbarBranchSelector({
const serverThread = useStore(serverThreadSelector);
const serverSession = serverThread?.session ?? null;
const setThreadBranchAction = useStore((store) => store.setThreadBranch);
const draftThread = useComposerDraftStore((store) => store.getDraftThreadByRef(threadRef));
const draftThread = useComposerDraftStore((store) =>
draftId ? store.getDraftSession(draftId) : store.getDraftThreadByRef(threadRef),
);
const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext);

const activeProjectRef = serverThread
Expand Down
116 changes: 115 additions & 1 deletion apps/web/src/components/ChatView.browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1327,6 +1327,7 @@ async function mountChatView(options: {
snapshot: OrchestrationReadModel;
configureFixture?: (fixture: TestFixture) => void;
resolveRpc?: (body: NormalizedWsRpcRequestBody) => unknown | undefined;
initialPath?: string;
}): Promise<MountedChatView> {
fixture = buildFixture(options.snapshot);
options.configureFixture?.(fixture);
Expand All @@ -1346,7 +1347,7 @@ async function mountChatView(options: {

const router = getRouter(
createMemoryHistory({
initialEntries: [`/${LOCAL_ENVIRONMENT_ID}/${THREAD_ID}`],
initialEntries: [options.initialPath ?? `/${LOCAL_ENVIRONMENT_ID}/${THREAD_ID}`],
}),
);

Expand Down Expand Up @@ -2512,6 +2513,119 @@ describe("ChatView timeline estimator parity (full app)", () => {
}
});

it("uses the active draft route session when changing the base branch", async () => {
const staleDraftId = draftIdFromPath("/draft/draft-stale-branch-session");
const activeDraftId = draftIdFromPath("/draft/draft-active-branch-session");

useComposerDraftStore.setState({
draftThreadsByThreadKey: {
[staleDraftId]: {
threadId: THREAD_ID,
environmentId: LOCAL_ENVIRONMENT_ID,
projectId: PROJECT_ID,
logicalProjectKey: `${PROJECT_DRAFT_KEY}:stale`,
createdAt: NOW_ISO,
runtimeMode: "full-access",
interactionMode: "default",
branch: "main",
worktreePath: null,
envMode: "worktree",
},
[activeDraftId]: {
threadId: THREAD_ID,
environmentId: LOCAL_ENVIRONMENT_ID,
projectId: PROJECT_ID,
logicalProjectKey: PROJECT_DRAFT_KEY,
createdAt: NOW_ISO,
runtimeMode: "full-access",
interactionMode: "default",
branch: "main",
worktreePath: null,
envMode: "worktree",
},
},
logicalProjectDraftThreadKeyByLogicalProjectKey: {
[`${PROJECT_DRAFT_KEY}:stale`]: staleDraftId,
[PROJECT_DRAFT_KEY]: activeDraftId,
},
});

const mounted = await mountChatView({
viewport: DEFAULT_VIEWPORT,
snapshot: createDraftOnlySnapshot(),
initialPath: `/draft/${activeDraftId}`,
resolveRpc: (body) => {
if (body._tag === WS_METHODS.gitListBranches) {
return {
isRepo: true,
hasOriginRemote: true,
nextCursor: null,
totalCount: 2,
branches: [
{
name: "main",
current: true,
isDefault: true,
worktreePath: null,
},
{
name: "release/next",
current: false,
isDefault: false,
worktreePath: null,
},
],
};
}
return undefined;
},
});

try {
const branchButton = await waitForElement(
() =>
Array.from(document.querySelectorAll("button")).find(
(button) => button.textContent?.trim() === "From main",
) as HTMLButtonElement | null,
'Unable to find branch selector button with "From main".',
);
branchButton.click();

const branchOption = await waitForElement(
() =>
Array.from(document.querySelectorAll("span")).find(
(element) => element.textContent?.trim() === "release/next",
) as HTMLSpanElement | null,
'Unable to find the "release/next" branch option.',
);
branchOption.click();

await vi.waitFor(
() => {
expect(useComposerDraftStore.getState().getDraftSession(activeDraftId)?.branch).toBe(
"release/next",
);
expect(useComposerDraftStore.getState().getDraftSession(staleDraftId)?.branch).toBe(
"main",
);
},
{ timeout: 8_000, interval: 16 },
);

await vi.waitFor(
() => {
const updatedButton = Array.from(document.querySelectorAll("button")).find((button) =>
button.textContent?.trim().includes("From release/next"),
);
expect(updatedButton).toBeTruthy();
},
{ timeout: 8_000, interval: 16 },
);
} finally {
await mounted.cleanup();
}
});

it("surrounds selected plain text and preserves the inner selection for repeated wrapping", async () => {
const mounted = await mountChatView({
viewport: DEFAULT_VIEWPORT,
Expand Down
1 change: 1 addition & 0 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3289,6 +3289,7 @@ export default function ChatView(props: ChatViewProps) {
<ChatHeader
activeThreadEnvironmentId={activeThread.environmentId}
activeThreadId={activeThread.id}
{...(routeKind === "draft" && draftId ? { draftId } : {})}
activeThreadTitle={activeThread.title}
activeProjectName={activeProject?.name}
isGitRepo={isGitRepo}
Expand Down
33 changes: 33 additions & 0 deletions apps/web/src/components/GitActionsControl.browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -426,4 +426,37 @@ describe("GitActionsControl thread-scoped progress toast", () => {
host.remove();
}
});

it("does not overwrite a selected base branch while a new worktree draft is being configured", async () => {
hasServerThreadRef.current = false;
activeDraftThreadRef.current = {
threadId: SHARED_THREAD_ID,
environmentId: ENVIRONMENT_A,
branch: "feature/base-branch",
worktreePath: null,
envMode: "worktree",
};

const host = document.createElement("div");
document.body.append(host);
const screen = await render(
<GitActionsControl
gitCwd={GIT_CWD}
activeThreadRef={scopeThreadRef(ENVIRONMENT_A, SHARED_THREAD_ID)}
/>,
{
container: host,
},
);

try {
await Promise.resolve();

expect(setDraftThreadContextSpy).not.toHaveBeenCalled();
expect(setThreadBranchSpy).not.toHaveBeenCalled();
} finally {
await screen.unmount();
host.remove();
}
});
});
28 changes: 22 additions & 6 deletions apps/web/src/components/GitActionsControl.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import {
import { refreshGitStatus, useGitStatus } from "~/lib/gitStatusState";
import { newCommandId, randomUUID } from "~/lib/utils";
import { resolvePathLinkTarget } from "~/terminal-links";
import { useComposerDraftStore } from "~/composerDraftStore";
import { type DraftId, useComposerDraftStore } from "~/composerDraftStore";
import { readEnvironmentApi } from "~/environmentApi";
import { readLocalApi } from "~/localApi";
import { useStore } from "~/store";
Expand All @@ -58,6 +58,7 @@ import { createThreadSelectorByRef } from "~/storeSelectors";
interface GitActionsControlProps {
gitCwd: string | null;
activeThreadRef: ScopedThreadRef | null;
draftId?: DraftId;
}

interface PendingDefaultBranchAction {
Expand Down Expand Up @@ -209,7 +210,11 @@ function GitQuickActionIcon({ quickAction }: { quickAction: GitQuickAction }) {
return <InfoIcon className={iconClassName} />;
}

export default function GitActionsControl({ gitCwd, activeThreadRef }: GitActionsControlProps) {
export default function GitActionsControl({
gitCwd,
activeThreadRef,
draftId,
}: GitActionsControlProps) {
const activeEnvironmentId = activeThreadRef?.environmentId ?? null;
const threadToastData = useMemo(
() => (activeThreadRef ? { threadRef: activeThreadRef } : undefined),
Expand All @@ -221,7 +226,11 @@ export default function GitActionsControl({ gitCwd, activeThreadRef }: GitAction
);
const activeServerThread = useStore(activeServerThreadSelector);
const activeDraftThread = useComposerDraftStore((store) =>
activeThreadRef ? store.getDraftThreadByRef(activeThreadRef) : null,
draftId
? store.getDraftSession(draftId)
: activeThreadRef
? store.getDraftThreadByRef(activeThreadRef)
: null,
);
const setDraftThreadContext = useComposerDraftStore((store) => store.setDraftThreadContext);
const setThreadBranch = useStore((store) => store.setThreadBranch);
Expand Down Expand Up @@ -282,7 +291,7 @@ export default function GitActionsControl({ gitCwd, activeThreadRef }: GitAction
return;
}

setDraftThreadContext(activeThreadRef, {
setDraftThreadContext(draftId ?? activeThreadRef, {
branch,
worktreePath: activeDraftThread.worktreePath,
});
Expand All @@ -291,6 +300,7 @@ export default function GitActionsControl({ gitCwd, activeThreadRef }: GitAction
activeDraftThread,
activeServerThread,
activeThreadRef,
draftId,
setDraftThreadContext,
setThreadBranch,
],
Expand Down Expand Up @@ -344,14 +354,18 @@ export default function GitActionsControl({ gitCwd, activeThreadRef }: GitAction
const isPullRunning =
useIsMutating({ mutationKey: gitMutationKeys.pull(activeEnvironmentId, gitCwd) }) > 0;
const isGitActionRunning = isRunStackedActionRunning || isPullRunning;
const isSelectingWorktreeBase =
!activeServerThread &&
activeDraftThread?.envMode === "worktree" &&
activeDraftThread.worktreePath === null;

useEffect(() => {
if (isGitActionRunning) {
if (isGitActionRunning || isSelectingWorktreeBase) {
return;
}

const branchUpdate = resolveLiveThreadBranchUpdate({
threadBranch: activeServerThread?.branch ?? null,
threadBranch: activeServerThread?.branch ?? activeDraftThread?.branch ?? null,
gitStatus: gitStatusForActions,
});
if (!branchUpdate) {
Expand All @@ -361,8 +375,10 @@ export default function GitActionsControl({ gitCwd, activeThreadRef }: GitAction
persistThreadBranchSync(branchUpdate.branch);
}, [
activeServerThread?.branch,
activeDraftThread?.branch,
gitStatusForActions,
isGitActionRunning,
isSelectingWorktreeBase,
persistThreadBranchSync,
]);

Expand Down
4 changes: 4 additions & 0 deletions apps/web/src/components/chat/ChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { scopeThreadRef } from "@t3tools/client-runtime";
import { memo } from "react";
import GitActionsControl from "../GitActionsControl";
import { type DraftId } from "~/composerDraftStore";
import { DiffIcon, TerminalSquareIcon } from "lucide-react";
import { Badge } from "../ui/badge";
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
Expand All @@ -19,6 +20,7 @@ import { OpenInPicker } from "./OpenInPicker";
interface ChatHeaderProps {
activeThreadEnvironmentId: EnvironmentId;
activeThreadId: ThreadId;
draftId?: DraftId;
activeThreadTitle: string;
activeProjectName: string | undefined;
isGitRepo: boolean;
Expand All @@ -44,6 +46,7 @@ interface ChatHeaderProps {
export const ChatHeader = memo(function ChatHeader({
activeThreadEnvironmentId,
activeThreadId,
draftId,
activeThreadTitle,
activeProjectName,
isGitRepo,
Expand Down Expand Up @@ -109,6 +112,7 @@ export const ChatHeader = memo(function ChatHeader({
<GitActionsControl
gitCwd={gitCwd}
activeThreadRef={scopeThreadRef(activeThreadEnvironmentId, activeThreadId)}
{...(draftId ? { draftId } : {})}
/>
)}
<Tooltip>
Expand Down
Loading