Skip to content
Open
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
62 changes: 22 additions & 40 deletions apps/web/src/components/app-builder/ProjectManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,10 @@ import type {
ProjectSessionInfo,
ProjectWithMessages,
SessionDisplayInfo,
WorkerVersion,
} from '@/lib/app-builder/types';
import type { Images } from '@/lib/images-schema';
import type { TRPCClient } from '@trpc/client';
import type { RootRouter } from '@/routers/root-router';
import type { CloudMessage } from '@/components/cloud-agent/types';
import type { StoredMessage } from '@/components/cloud-agent-next/types';
import type { UserMessage, TextPart } from '@/types/opencode.gen';
import { createLogger } from './project-manager/logging';
Expand Down Expand Up @@ -87,7 +85,8 @@ export function createProjectManager(config: ProjectManagerConfig): ProjectManag
}

function createStaticSession(info: ProjectSessionInfo): AppBuilderSession {
// Pass streaming config so ended sessions can load messages via WebSocket replay
// Pass streaming config so ended sessions can load messages on demand
// (v2: WebSocket replay, v1: getLegacySessionMessages tRPC query).
const streamingConfig = {
info: toDisplayInfo(info),
initialMessages: [] as never[],
Expand All @@ -98,7 +97,7 @@ export function createProjectManager(config: ProjectManagerConfig): ProjectManag
if (info.worker_version === 'v2') {
return createV2Session(streamingConfig);
}
return createV1Session({ ...streamingConfig, sessionPrepared: true });
return createV1Session(streamingConfig);
}

function getActiveSession(): AppBuilderSession | undefined {
Expand Down Expand Up @@ -154,8 +153,6 @@ export function createProjectManager(config: ProjectManagerConfig): ProjectManag
projectId,
organizationId,
trpcClient,
sessionPrepared: info.prepared,
onStreamComplete: () => startPreviewPollingIfNeeded(),
onSessionChanged: handleSessionChanged,
})
);
Expand All @@ -169,7 +166,7 @@ export function createProjectManager(config: ProjectManagerConfig): ProjectManag

function handleSessionChanged(
newSessionId: string,
workerVersion: WorkerVersion,
workerVersion: 'v2',
userMessage: { text: string; images?: Images }
): void {
logger.log('Session changed', { newSessionId, workerVersion });
Expand All @@ -187,27 +184,15 @@ export function createProjectManager(config: ProjectManagerConfig): ProjectManag
title: null,
};

const newSession =
workerVersion === 'v2'
? createV2Session({
info: newInfo,
initialMessages: [makeOptimisticV2UserMessage(newSessionId, userMessage.text)],
projectId,
organizationId,
trpcClient,
onStreamComplete: () => startPreviewPollingIfNeeded(),
onSessionChanged: handleSessionChanged,
})
: createV1Session({
info: newInfo,
initialMessages: [makeOptimisticV1UserMessage(userMessage.text, userMessage.images)],
projectId,
organizationId,
trpcClient,
sessionPrepared: true,
onStreamComplete: () => startPreviewPollingIfNeeded(),
onSessionChanged: handleSessionChanged,
});
const newSession = createV2Session({
info: newInfo,
initialMessages: [makeOptimisticV2UserMessage(newSessionId, userMessage)],
projectId,
organizationId,
trpcClient,
onStreamComplete: () => startPreviewPollingIfNeeded(),
onSessionChanged: handleSessionChanged,
});

subscribeToSession(newSession);

Expand All @@ -221,17 +206,14 @@ export function createProjectManager(config: ProjectManagerConfig): ProjectManag
newSession.connectToExistingSession(newSessionId);
}

function makeOptimisticV1UserMessage(text: string, images?: Images): CloudMessage {
return {
ts: Date.now(),
type: 'user',
text,
partial: false,
images,
};
}

function makeOptimisticV2UserMessage(sessionId: string, text: string): StoredMessage {
function makeOptimisticV2UserMessage(
sessionId: string,
userMessage: { text: string; images?: Images }
): StoredMessage {
// Image parts are intentionally omitted: the Images payload only contains R2
// paths/filenames (no public URLs), so emitting FileParts with empty URLs
// would render broken-image placeholders. WebSocket replay will populate the
// real FileParts once the session streams.
const messageId = `optimistic-${Date.now()}`;
const now = Date.now();
const info: UserMessage = {
Expand All @@ -247,7 +229,7 @@ export function createProjectManager(config: ProjectManagerConfig): ProjectManag
sessionID: sessionId,
messageID: messageId,
type: 'text',
text,
text: userMessage.text,
};
return { info, parts: [textPart] };
}
Expand Down
Loading