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
17 changes: 9 additions & 8 deletions apps/web/src/components/ChatView.browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2456,7 +2456,7 @@ describe("ChatView timeline estimator parity (full app)", () => {
}
});

it("keeps source control closed by default on wide draft thread routes before first send", async () => {
it("defaults source control open on wide draft thread routes before first send", async () => {
const draftId = DraftId.make("draft-source-control-before-start");
useComposerDraftStore.setState({
draftThreadsByThreadKey: {
Expand Down Expand Up @@ -2494,18 +2494,19 @@ describe("ChatView timeline estimator parity (full app)", () => {
"Unable to find source control toggle.",
);
expect(sourceControlToggle.disabled).toBe(false);
expect(sourceControlToggle.hasAttribute("data-pressed")).toBe(false);
await expect
.element(page.getByRole("heading", { name: "Source Control" }))
.not.toBeInTheDocument();
expect(sourceControlToggle.hasAttribute("data-pressed")).toBe(true);
expect(document.body.textContent).not.toContain("Explain this codebase");
expect(document.body.textContent).not.toContain("Review my uncommitted changes");
expect(document.body.textContent).not.toContain("Fix a bug");
await expect.element(page.getByRole("heading", { name: "Source Control" })).toBeVisible();

sourceControlToggle.click();

await vi.waitFor(
() => {
expect(mounted.router.state.location.search).toMatchObject({ sourceControl: "1" });
expect(sourceControlToggle.hasAttribute("data-pressed")).toBe(true);
expect(document.querySelector('h2[aria-label="Source Control"]')).not.toBeNull();
expect(mounted.router.state.location.search).toMatchObject({ sourceControl: "0" });
expect(sourceControlToggle.hasAttribute("data-pressed")).toBe(false);
expect(document.querySelector('h2[aria-label="Source Control"]')).toBeNull();
},
{ timeout: 8_000, interval: 16 },
);
Expand Down
21 changes: 3 additions & 18 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ import { isElectron } from "../env";
import { ensureLocalApi, readLocalApi } from "../localApi";
import {
closeRightPanelSearchParams,
isDraftSourceControlPanelOpen,
isSourceControlPanelOpen,
parseDiffRouteSearch,
preserveRightPanelSearchParamsForDraftNavigation,
Expand Down Expand Up @@ -1263,12 +1262,9 @@ export default function ChatView(props: ChatViewProps) {
composerInteractionMode ?? activeThread?.interactionMode ?? DEFAULT_INTERACTION_MODE;
const isLocalDraftThread = !isServerThread && localDraftThread !== undefined;
const canCheckoutPullRequestIntoThread = isLocalDraftThread;
// Mirror the owning route's panel default: server threads open on wide
// viewports, drafts stay closed until explicitly opened.
const sourceControlOpen =
routeKind === "server"
? isSourceControlPanelOpen(rawSearch, { defaultOpen: !shouldUseRightPanelSheet })
: isDraftSourceControlPanelOpen(rawSearch);
const sourceControlOpen = isSourceControlPanelOpen(rawSearch, {
defaultOpen: !shouldUseRightPanelSheet,
});
// The diff panel is a drill-in of source control, so the header toggle
// treats the right panel as one unit: it stays pressed while a diff is
// open and pressing it closes the whole panel.
Expand Down Expand Up @@ -1323,30 +1319,19 @@ export default function ChatView(props: ChatViewProps) {
// General Chat threads run in a hidden scratch workspace: source-control,
// scripts, and open-in affordances stay hidden even though a project exists.
const isGeneralChatThread = activeProject?.kind === "general-chat";
const insertDraftStarterPrompt = useCallback(
(text: string) => {
setComposerDraftPrompt(composerDraftTarget, text);
window.requestAnimationFrame(() => {
composerRef.current?.focusAtEnd();
});
},
[composerDraftTarget, composerRef, setComposerDraftPrompt],
);
const draftTimelineEmptyState = useMemo(
() =>
isLocalDraftThread && draftThread ? (
<DraftEmptyState
currentProjectRef={scopeProjectRef(draftThread.environmentId, draftThread.projectId)}
currentProjectName={activeProject?.name ?? null}
isGeneralChat={isGeneralChatThread}
onInsertPrompt={insertDraftStarterPrompt}
/>
) : undefined,
[
activeProject?.name,
draftThread?.environmentId,
draftThread?.projectId,
insertDraftStarterPrompt,
isGeneralChatThread,
isLocalDraftThread,
],
Expand Down
40 changes: 2 additions & 38 deletions apps/web/src/components/chat/DraftEmptyState.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,6 @@
import { scopedProjectKey, scopeProjectRef } from "@threadlines/client-runtime";
import type { ScopedProjectRef } from "@threadlines/contracts";
import {
BugIcon,
CheckIcon,
CompassIcon,
GitCompareArrowsIcon,
MessagesSquareIcon,
} from "lucide-react";
import { CheckIcon, MessagesSquareIcon } from "lucide-react";
import { useMemo } from "react";

import { usePrimaryEnvironmentId } from "../../environments/primary";
Expand All @@ -16,7 +10,6 @@ import { resolveGeneralChatsProjectRef } from "../../lib/generalChats";
import { selectGeneralChatsProjectAcrossEnvironments, useStore } from "../../store";
import { ProjectFavicon } from "../ProjectFavicon";
import { RecentThreadsList } from "../RecentThreadsList";
import { Button } from "../ui/button";
import { riseDelay, ThreadlinesFigure } from "../ThreadlinesFigure";
import {
Menu,
Expand All @@ -32,21 +25,12 @@ interface DraftEmptyStateProps {
currentProjectRef: ScopedProjectRef | null;
currentProjectName: string | null;
isGeneralChat: boolean;
onInsertPrompt?: ((text: string) => void) | undefined;
}

/** Starter prompts inserted into the composer for editing, not sent. */
const STARTER_PROMPTS = [
{ icon: CompassIcon, prompt: "Explain this codebase" },
{ icon: GitCompareArrowsIcon, prompt: "Review my uncommitted changes" },
{ icon: BugIcon, prompt: "Fix a bug" },
] as const;

export function DraftEmptyState({
currentProjectRef,
currentProjectName,
isGeneralChat,
onInsertPrompt,
}: DraftEmptyStateProps) {
const { handleNewThread, orderedProjects } = useHandleNewThread();
const activeEnvironmentId = useStore((state) => state.activeEnvironmentId);
Expand Down Expand Up @@ -131,28 +115,8 @@ export function DraftEmptyState({
?
</h2>

{!isGeneralChat && onInsertPrompt ? (
<div
className="no-thread-rise mt-5 flex flex-wrap items-center justify-center gap-1"
style={riseDelay("0.26s")}
>
{STARTER_PROMPTS.map(({ icon: Icon, prompt }) => (
<Button
className="text-muted-foreground hover:text-foreground"
key={prompt}
onClick={() => onInsertPrompt(prompt)}
size="sm"
variant="ghost"
>
<Icon />
{prompt}
</Button>
))}
</div>
) : null}

<RecentThreadsList
className="no-thread-rise mt-10 w-full [--no-thread-delay:0.4s]"
className="no-thread-rise mt-10 w-full [--no-thread-delay:0.26s]"
limit={5}
testId="draft-empty-recent-thread"
/>
Expand Down
10 changes: 0 additions & 10 deletions apps/web/src/diffRouteSearch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,16 +133,6 @@ export function isSourceControlPanelOpen(
return options.defaultOpen ?? true;
}

/**
* Drafts start with the source-control panel closed: a pristine thread has no
* turn activity to review, and the home/new-thread surface should read as a
* clean page. Only explicit `sourceControl=1` opens it; the wide-viewport
* default that applies to server threads does not.
*/
export function isDraftSourceControlPanelOpen(search: DiffRouteSearch): boolean {
return isSourceControlPanelOpen(search, { defaultOpen: false });
}

export function parseDiffRouteSearch(search: Record<string, unknown>): DiffRouteSearch {
const diff = isDiffOpenValue(search.diff) ? "1" : undefined;
const diffMode = diff && search.diffMode === "workingTree" ? "workingTree" : undefined;
Expand Down
6 changes: 4 additions & 2 deletions apps/web/src/routes/_chat.draft.$draftId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { ChatRightPanelInlineSidebar } from "../components/ChatRightPanelInlineS
import { useComposerDraftStore, DraftId } from "../composerDraftStore";
import {
closeRightPanelSearchParams,
isDraftSourceControlPanelOpen,
isSourceControlPanelOpen,
parseDiffRouteSearch,
stripRightPanelSearchParams,
} from "../diffRouteSearch";
Expand Down Expand Up @@ -83,7 +83,9 @@ function DraftChatThreadRouteView() {
}),
[draftSession?.promotedTo, serverThread, serverThreadHasTurnActivity, serverThreadRef],
);
const rawSourceControlOpen = isDraftSourceControlPanelOpen(search);
const rawSourceControlOpen = isSourceControlPanelOpen(search, {
defaultOpen: !shouldUseSourceControlSheet,
});
const draftProjectRef = draftSession
? scopeProjectRef(draftSession.environmentId, draftSession.projectId)
: null;
Expand Down
Loading