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) {