Skip to content

Commit 2c7e5c3

Browse files
fix(app-builder): route sessions through cloud-agent-next (#3021)
refactor(app-builder): migrate to cloud-agent-next v2 and retire v1 streaming - Default app_builder_project_sessions.worker_version to 'v2' and remove the PostHog feature-flag gate so every new project runs on cloud-agent-next. - Drop the v1 cloud-agent branches from the send/start/interrupt service paths; legacy v1 sessions are now read-only and the backend always upgrades to a fresh v2 session when the user sends a message. - Remove the prepareLegacySession tRPC endpoint and WebSocket streaming coordinator on the client. Ended v1 sessions load their R2 history lazily via a new getLegacySessionMessages tRPC query (fired from the ExpandableSessionBlock via loadMessages), so getProject no longer fans out N R2 reads on every project load. - Simplify v1 session plumbing: v1 messages.ts keeps only the helpers the upgrade path uses; streaming.ts destructures cloudAgentSessionId and drops the partial-message bookkeeping that was only meaningful while v1 had a live WebSocket. - Restore preview polling on v2 stream completion: active v2 sessions built from existing project data were missing the onStreamComplete callback, so follow-up messages on an established v2 session would not restart preview polling after streaming transitioned to idle. - Drop redundant workerVersion parameter from onSessionChanged since App Builder is always on cloud-agent-next now. - Fix dev app-builder tunnel script to write APP_BUILDER_URL to apps/web/.env.development.local, which is what Next.js dev actually reads (the repo-root file is ignored when vercel env pull has created the apps/web copy as a real file instead of a symlink). Co-authored-by: Evgeny Shurakov <eshurakov@users.noreply.github.com>
1 parent f6591d3 commit 2c7e5c3

20 files changed

Lines changed: 19115 additions & 2984 deletions

File tree

apps/web/src/components/app-builder/ProjectManager.ts

Lines changed: 23 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,10 @@ import type {
1010
ProjectSessionInfo,
1111
ProjectWithMessages,
1212
SessionDisplayInfo,
13-
WorkerVersion,
1413
} from '@/lib/app-builder/types';
1514
import type { Images } from '@/lib/images-schema';
1615
import type { TRPCClient } from '@trpc/client';
1716
import type { RootRouter } from '@/routers/root-router';
18-
import type { CloudMessage } from '@/components/cloud-agent/types';
1917
import type { StoredMessage } from '@/components/cloud-agent-next/types';
2018
import type { UserMessage, TextPart } from '@/types/opencode.gen';
2119
import { createLogger } from './project-manager/logging';
@@ -87,7 +85,8 @@ export function createProjectManager(config: ProjectManagerConfig): ProjectManag
8785
}
8886

8987
function createStaticSession(info: ProjectSessionInfo): AppBuilderSession {
90-
// Pass streaming config so ended sessions can load messages via WebSocket replay
88+
// Pass streaming config so ended sessions can load messages on demand
89+
// (v2: WebSocket replay, v1: getLegacySessionMessages tRPC query).
9190
const streamingConfig = {
9291
info: toDisplayInfo(info),
9392
initialMessages: [] as never[],
@@ -98,7 +97,7 @@ export function createProjectManager(config: ProjectManagerConfig): ProjectManag
9897
if (info.worker_version === 'v2') {
9998
return createV2Session(streamingConfig);
10099
}
101-
return createV1Session({ ...streamingConfig, sessionPrepared: true });
100+
return createV1Session(streamingConfig);
102101
}
103102

104103
function getActiveSession(): AppBuilderSession | undefined {
@@ -154,8 +153,6 @@ export function createProjectManager(config: ProjectManagerConfig): ProjectManag
154153
projectId,
155154
organizationId,
156155
trpcClient,
157-
sessionPrepared: info.prepared,
158-
onStreamComplete: () => startPreviewPollingIfNeeded(),
159156
onSessionChanged: handleSessionChanged,
160157
})
161158
);
@@ -169,10 +166,9 @@ export function createProjectManager(config: ProjectManagerConfig): ProjectManag
169166

170167
function handleSessionChanged(
171168
newSessionId: string,
172-
workerVersion: WorkerVersion,
173169
userMessage: { text: string; images?: Images }
174170
): void {
175-
logger.log('Session changed', { newSessionId, workerVersion });
171+
logger.log('Session changed', { newSessionId });
176172

177173
const currentActive = getActiveSession();
178174
if (currentActive) {
@@ -187,27 +183,15 @@ export function createProjectManager(config: ProjectManagerConfig): ProjectManag
187183
title: null,
188184
};
189185

190-
const newSession =
191-
workerVersion === 'v2'
192-
? createV2Session({
193-
info: newInfo,
194-
initialMessages: [makeOptimisticV2UserMessage(newSessionId, userMessage.text)],
195-
projectId,
196-
organizationId,
197-
trpcClient,
198-
onStreamComplete: () => startPreviewPollingIfNeeded(),
199-
onSessionChanged: handleSessionChanged,
200-
})
201-
: createV1Session({
202-
info: newInfo,
203-
initialMessages: [makeOptimisticV1UserMessage(userMessage.text, userMessage.images)],
204-
projectId,
205-
organizationId,
206-
trpcClient,
207-
sessionPrepared: true,
208-
onStreamComplete: () => startPreviewPollingIfNeeded(),
209-
onSessionChanged: handleSessionChanged,
210-
});
186+
const newSession = createV2Session({
187+
info: newInfo,
188+
initialMessages: [makeOptimisticV2UserMessage(newSessionId, userMessage)],
189+
projectId,
190+
organizationId,
191+
trpcClient,
192+
onStreamComplete: () => startPreviewPollingIfNeeded(),
193+
onSessionChanged: handleSessionChanged,
194+
});
211195

212196
subscribeToSession(newSession);
213197

@@ -221,17 +205,14 @@ export function createProjectManager(config: ProjectManagerConfig): ProjectManag
221205
newSession.connectToExistingSession(newSessionId);
222206
}
223207

224-
function makeOptimisticV1UserMessage(text: string, images?: Images): CloudMessage {
225-
return {
226-
ts: Date.now(),
227-
type: 'user',
228-
text,
229-
partial: false,
230-
images,
231-
};
232-
}
233-
234-
function makeOptimisticV2UserMessage(sessionId: string, text: string): StoredMessage {
208+
function makeOptimisticV2UserMessage(
209+
sessionId: string,
210+
userMessage: { text: string; images?: Images }
211+
): StoredMessage {
212+
// Image parts are intentionally omitted: the Images payload only contains R2
213+
// paths/filenames (no public URLs), so emitting FileParts with empty URLs
214+
// would render broken-image placeholders. WebSocket replay will populate the
215+
// real FileParts once the session streams.
235216
const messageId = `optimistic-${Date.now()}`;
236217
const now = Date.now();
237218
const info: UserMessage = {
@@ -247,7 +228,7 @@ export function createProjectManager(config: ProjectManagerConfig): ProjectManag
247228
sessionID: sessionId,
248229
messageID: messageId,
249230
type: 'text',
250-
text,
231+
text: userMessage.text,
251232
};
252233
return { info, parts: [textPart] };
253234
}
@@ -381,7 +362,7 @@ export function createProjectManager(config: ProjectManagerConfig): ProjectManag
381362
void mutationPromise
382363
.then(result => {
383364
if (destroyed) return;
384-
handleSessionChanged(result.cloudAgentSessionId, result.workerVersion, {
365+
handleSessionChanged(result.cloudAgentSessionId, {
385366
text: message,
386367
images,
387368
});

0 commit comments

Comments
 (0)