diff --git a/apps/server/src/orchestration/projector.ts b/apps/server/src/orchestration/projector.ts index deb8a6d44d..2a666420cb 100644 --- a/apps/server/src/orchestration/projector.ts +++ b/apps/server/src/orchestration/projector.ts @@ -575,15 +575,16 @@ export function projectEvent( return nextBase; } + const effectiveTurnCount = Math.max(1, payload.turnCount); const checkpoints = thread.checkpoints - .filter((entry) => entry.checkpointTurnCount <= payload.turnCount) + .filter((entry) => entry.checkpointTurnCount <= effectiveTurnCount) .toSorted((left, right) => left.checkpointTurnCount - right.checkpointTurnCount) .slice(-MAX_THREAD_CHECKPOINTS); const retainedTurnIds = new Set(checkpoints.map((checkpoint) => checkpoint.turnId)); const messages = retainThreadMessagesAfterRevert( thread.messages, retainedTurnIds, - payload.turnCount, + effectiveTurnCount, ).slice(-MAX_THREAD_MESSAGES); const proposedPlans = retainThreadProposedPlansAfterRevert( thread.proposedPlans, diff --git a/apps/web/src/components/ChatView.tsx b/apps/web/src/components/ChatView.tsx index 40cd1b4210..51eb08b241 100644 --- a/apps/web/src/components/ChatView.tsx +++ b/apps/web/src/components/ChatView.tsx @@ -74,6 +74,7 @@ import { } from "../pendingUserInput"; import { selectProjectsAcrossEnvironments, + selectThreadByRef, selectThreadsAcrossEnvironments, useStore, } from "../store"; @@ -2379,6 +2380,17 @@ export default function ChatView(props: ChatViewProps) { turnCount, createdAt: new Date().toISOString(), }); + + const threadRef = scopeThreadRef(activeThread.environmentId, activeThread.id); + const updatedThread = selectThreadByRef(useStore.getState(), threadRef); + if (updatedThread && promptRef.current.trim().length === 0) { + const lastUserMessage = [...updatedThread.messages] + .reverse() + .find((m) => m.role === "user"); + if (lastUserMessage) { + setComposerDraftPrompt(composerDraftTarget, lastUserMessage.text); + } + } } catch (err) { setThreadError( activeThread.id, @@ -2389,11 +2401,13 @@ export default function ChatView(props: ChatViewProps) { }, [ activeThread, + composerDraftTarget, environmentId, isConnecting, isRevertingCheckpoint, isSendBusy, phase, + setComposerDraftPrompt, setThreadError, ], ); diff --git a/apps/web/src/store.test.ts b/apps/web/src/store.test.ts index 99ddf4ca09..bc853c4ae4 100644 --- a/apps/web/src/store.test.ts +++ b/apps/web/src/store.test.ts @@ -1015,4 +1015,84 @@ describe("incremental orchestration updates", () => { }); expect(threadsOf(next)[0]?.latestTurn?.sourceProposedPlan).toBeUndefined(); }); + + it("retains first turn messages when reverting single-turn conversation (turnCount=0)", () => { + const state = makeState( + makeThread({ + messages: [ + { + id: MessageId.make("user-1"), + role: "user", + text: "hello", + turnId: TurnId.make("turn-1"), + createdAt: "2026-02-27T00:00:00.000Z", + completedAt: "2026-02-27T00:00:00.000Z", + streaming: false, + }, + { + id: MessageId.make("assistant-1"), + role: "assistant", + text: "hi there", + turnId: TurnId.make("turn-1"), + createdAt: "2026-02-27T00:00:01.000Z", + completedAt: "2026-02-27T00:00:01.000Z", + streaming: false, + }, + ], + proposedPlans: [ + { + id: "plan-1", + turnId: TurnId.make("turn-1"), + planMarkdown: "plan 1", + implementedAt: null, + implementationThreadId: null, + createdAt: "2026-02-27T00:00:00.000Z", + updatedAt: "2026-02-27T00:00:00.000Z", + }, + ], + activities: [ + { + id: EventId.make("activity-1"), + tone: "info", + kind: "step", + summary: "step one", + payload: {}, + turnId: TurnId.make("turn-1"), + createdAt: "2026-02-27T00:00:00.000Z", + }, + ], + turnDiffSummaries: [ + { + turnId: TurnId.make("turn-1"), + completedAt: "2026-02-27T00:00:01.000Z", + status: "ready", + checkpointTurnCount: 1, + checkpointRef: CheckpointRef.make("ref-1"), + files: [], + }, + ], + }), + ); + + const next = applyOrchestrationEvent( + state, + makeEvent("thread.reverted", { + threadId: ThreadId.make("thread-1"), + turnCount: 0, + }), + localEnvironmentId, + ); + + expect(threadsOf(next)[0]?.messages.map((message) => message.id)).toEqual([ + "user-1", + "assistant-1", + ]); + expect(threadsOf(next)[0]?.proposedPlans.map((plan) => plan.id)).toEqual(["plan-1"]); + expect(threadsOf(next)[0]?.activities.map((activity) => activity.id)).toEqual([ + EventId.make("activity-1"), + ]); + expect(threadsOf(next)[0]?.turnDiffSummaries.map((summary) => summary.turnId)).toEqual([ + TurnId.make("turn-1"), + ]); + }); }); diff --git a/apps/web/src/store.ts b/apps/web/src/store.ts index e3012a8c8b..bcf61ad6f5 100644 --- a/apps/web/src/store.ts +++ b/apps/web/src/store.ts @@ -1562,11 +1562,12 @@ function applyEnvironmentOrchestrationEvent( case "thread.reverted": return updateThreadState(state, event.payload.threadId, (thread) => { + const effectiveTurnCount = Math.max(1, event.payload.turnCount); const turnDiffSummaries = thread.turnDiffSummaries .filter( (entry) => entry.checkpointTurnCount !== undefined && - entry.checkpointTurnCount <= event.payload.turnCount, + entry.checkpointTurnCount <= effectiveTurnCount, ) .toSorted( (left, right) => @@ -1578,7 +1579,7 @@ function applyEnvironmentOrchestrationEvent( const messages = retainThreadMessagesAfterRevert( thread.messages, retainedTurnIds, - event.payload.turnCount, + effectiveTurnCount, ).slice(-MAX_THREAD_MESSAGES); const proposedPlans = retainThreadProposedPlansAfterRevert( thread.proposedPlans,