From 2d69e97476c706002bb69c6b517e389e77fe022f Mon Sep 17 00:00:00 2001 From: jln <85513960+jln13x@users.noreply.github.com> Date: Wed, 22 Jul 2026 07:36:00 +0000 Subject: [PATCH] fix(web): restore projectless standalone chats --- .../web/src/components/ChatView.logic.test.ts | 23 +++++++++++++++++++ apps/web/src/components/ChatView.logic.ts | 8 +++++++ apps/web/src/components/ChatView.tsx | 11 +++++++-- .../chat/DraftHeroHeadline.test.tsx | 15 ++++++++++++ .../src/components/chat/DraftHeroHeadline.tsx | 22 ++++++++++++++++++ docs/personal-fork-changes.md | 2 ++ 6 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/components/chat/DraftHeroHeadline.test.tsx diff --git a/apps/web/src/components/ChatView.logic.test.ts b/apps/web/src/components/ChatView.logic.test.ts index 3746b014679..3151d6006c0 100644 --- a/apps/web/src/components/ChatView.logic.test.ts +++ b/apps/web/src/components/ChatView.logic.test.ts @@ -20,6 +20,7 @@ import { hasServerAcknowledgedLocalDispatch, reconcileMountedTerminalThreadIds, reconcileRetainedMountedThreadIds, + requiresDraftProjectSelection, resolveEffectiveServerThreadWorktreePath, resolveSendEnvMode, shouldWriteThreadErrorToCurrentServerThread, @@ -78,6 +79,28 @@ const readySession = { updatedAt: "2026-03-29T00:00:10.000Z", }; +describe("requiresDraftProjectSelection", () => { + it("keeps standalone local drafts available without a project", () => { + expect( + requiresDraftProjectSelection({ + isLocalDraftThread: true, + projectId: null, + hasActiveProject: false, + }), + ).toBe(false); + }); + + it("requires a project when a local project draft becomes orphaned", () => { + expect( + requiresDraftProjectSelection({ + isLocalDraftThread: true, + projectId, + hasActiveProject: false, + }), + ).toBe(true); + }); +}); + describe("buildThreadTurnInterruptInput", () => { it("targets the session's active running turn", () => { const activeTurnId = TurnId.make("turn-running"); diff --git a/apps/web/src/components/ChatView.logic.ts b/apps/web/src/components/ChatView.logic.ts index d1f01e3c6e9..cbded86d2a8 100644 --- a/apps/web/src/components/ChatView.logic.ts +++ b/apps/web/src/components/ChatView.logic.ts @@ -56,6 +56,14 @@ export function buildLocalDraftThread( }; } +export function requiresDraftProjectSelection(input: { + readonly isLocalDraftThread: boolean; + readonly projectId: ProjectId | null | undefined; + readonly hasActiveProject: boolean; +}): boolean { + return input.isLocalDraftThread && input.projectId !== null && !input.hasActiveProject; +} + export function shouldWriteThreadErrorToCurrentServerThread(input: { serverThread: | { diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 7f8161f357f..a3cdcc015d9 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -247,6 +247,7 @@ import { deriveLockedProvider, readFileAsDataUrl, reconcileMountedTerminalThreadIds, + requiresDraftProjectSelection, resolveEffectiveServerThreadWorktreePath, resolveSendEnvMode, revokeBlobPreviewUrl, @@ -1525,6 +1526,12 @@ function ChatViewContent(props: ChatViewProps) { ? scopeProjectRef(activeThread.environmentId, activeThread.projectId) : null; const activeProject = useProject(activeProjectRef); + const isStandaloneThread = activeThread?.projectId === null; + const projectSelectionRequired = requiresDraftProjectSelection({ + isLocalDraftThread, + projectId: activeThread?.projectId, + hasActiveProject: activeProject !== null, + }); const activeEnvironmentShell = useEnvironmentQuery( activeThread ? environmentShell.stateAtom(activeThread.environmentId) : null, ); @@ -4300,7 +4307,6 @@ function ChatViewContent(props: ChatViewProps) { } return; } - const isStandaloneThread = activeThread.projectId === null; if (!activeProject && !isStandaloneThread) { toastManager.add( stackedThreadToast({ @@ -5596,6 +5602,7 @@ function ChatViewContent(props: ChatViewProps) { @@ -5628,7 +5635,7 @@ function ChatViewContent(props: ChatViewProps) { isServerThread={isServerThread} isLocalDraftThread={isLocalDraftThread} forceExpandedOnMobile={forceExpandedMobileComposer && isDraftHeroState} - projectSelectionRequired={isLocalDraftThread && activeProject === null} + projectSelectionRequired={projectSelectionRequired} phase={phase} isConnecting={isConnecting} isSendBusy={isSendBusy} diff --git a/apps/web/src/components/chat/DraftHeroHeadline.test.tsx b/apps/web/src/components/chat/DraftHeroHeadline.test.tsx new file mode 100644 index 00000000000..12e7135b33a --- /dev/null +++ b/apps/web/src/components/chat/DraftHeroHeadline.test.tsx @@ -0,0 +1,15 @@ +import { renderToStaticMarkup } from "react-dom/server"; +import { describe, expect, it } from "vite-plus/test"; + +import { DraftHeroHeadline } from "./DraftHeroHeadline"; + +describe("DraftHeroHeadline", () => { + it("does not ask standalone chats to choose a project", () => { + const markup = renderToStaticMarkup( + , + ); + + expect(markup).toContain("What can I help you with?"); + expect(markup).not.toContain("Choose a project"); + }); +}); diff --git a/apps/web/src/components/chat/DraftHeroHeadline.tsx b/apps/web/src/components/chat/DraftHeroHeadline.tsx index 6a88fb8da19..ff116223cec 100644 --- a/apps/web/src/components/chat/DraftHeroHeadline.tsx +++ b/apps/web/src/components/chat/DraftHeroHeadline.tsx @@ -20,12 +20,34 @@ import { interface DraftHeroHeadlineProps { readonly activeProjectRef: ScopedProjectRef | null; readonly activeProjectTitle: string | null; + readonly isStandalone: boolean; } export function DraftHeroHeadline({ activeProjectRef, activeProjectTitle, + isStandalone, }: DraftHeroHeadlineProps) { + if (isStandalone) { + return ( +

+ What can I help you with? +

+ ); + } + + return ( + + ); +} + +function ProjectDraftHeroHeadline({ + activeProjectRef, + activeProjectTitle, +}: Omit) { const projects = useProjects(); const threads = useThreadShells(); const handleNewThread = useNewThreadHandler(); diff --git a/docs/personal-fork-changes.md b/docs/personal-fork-changes.md index cb2ad403e25..81d44c912ce 100644 --- a/docs/personal-fork-changes.md +++ b/docs/personal-fork-changes.md @@ -68,6 +68,8 @@ Upstream PRs integrated into the fork are listed in - Creating a standalone chat opens a local draft immediately, matching project-thread creation; the server thread is materialized atomically with the first message instead of blocking navigation on an empty-thread request. +- Standalone drafts remain writable in the shared new-thread composer and use projectless hero + copy; missing-project guards continue to apply only to orphaned project drafts. - Standalone chats participate in the same desktop completion sounds and macOS notifications as project threads. When agent-activity publishing is enabled, they also publish completion and attention states to connected mobile clients under the generic `Chats` activity group.