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
17 changes: 14 additions & 3 deletions apps/web/src/components/app-builder/AppBuilderChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -508,7 +508,14 @@ function SessionMessages({
export function AppBuilderChat({ organizationId }: AppBuilderChatProps) {
// Get state and manager from ProjectSession context
const { manager, state } = useProject();
const { isStreaming, isInterrupting, model: projectModel, sessions, pendingNewSession } = state;
const {
isStreaming,
isInterrupting,
model: projectModel,
sessions,
pendingNewSession,
isRecoveringSession,
} = state;

const messagesEndRef = useRef<HTMLDivElement>(null);
const scrollContainerRef = useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -743,15 +750,19 @@ export function AppBuilderChat({ organizationId }: AppBuilderChatProps) {
variant="ghost"
size="icon"
onClick={handleNewChatToggle}
disabled={isStreaming}
disabled={isStreaming || isRecoveringSession}
className={pendingNewSession ? 'text-primary bg-primary/10 h-8 w-8' : 'h-8 w-8'}
aria-label="New chat"
>
<SquarePen className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent side="bottom">
{pendingNewSession ? 'Cancel new chat' : 'New chat'}
{isRecoveringSession
? 'A new chat is required'
: pendingNewSession
? 'Cancel new chat'
: 'New chat'}
</TooltipContent>
</Tooltip>
<FeedbackDialog organizationId={organizationId} />
Expand Down
44 changes: 32 additions & 12 deletions apps/web/src/components/app-builder/ProjectManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,10 @@ export function createProjectManager(config: ProjectManagerConfig): ProjectManag
}

function getActiveSession(): AppBuilderSession | undefined {
const sessions = store.getState().sessions;
return sessions[sessions.length - 1];
if (!cloudAgentSessionId) return undefined;
return store
.getState()
.sessions.find(session => session.info.cloud_agent_session_id === cloudAgentSessionId);
}

function subscribeToSession(session: AppBuilderSession): void {
Expand All @@ -123,12 +125,16 @@ export function createProjectManager(config: ProjectManagerConfig): ProjectManag
const sessionInfos = proj.sessions;
if (sessionInfos.length === 0) return [];

const activeInfo =
sessionInfos.find(s => s.ended_at === null) ?? sessionInfos[sessionInfos.length - 1];
const activeInfo = project.session_id
? sessionInfos.find(s => s.cloud_agent_session_id === project.session_id)
: undefined;

const orderedSessionInfos = activeInfo

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

project.session_id is authoritative, but existing chat and feedback consumers treat the final array entry as active. Moving the canonical session to the end keeps those consumers aligned while older/orphaned sessions remain available as history.

? [...sessionInfos.filter(info => info.id !== activeInfo.id), activeInfo]
: sessionInfos;
const sessions: AppBuilderSession[] = [];

for (const info of sessionInfos) {
for (const info of orderedSessionInfos) {
const isActive = info.id === activeInfo?.id;

if (!isActive) {
Expand Down Expand Up @@ -199,6 +205,7 @@ export function createProjectManager(config: ProjectManagerConfig): ProjectManag
store.setState({
sessions: [...currentSessions, newSession],
isStreaming: true,
isRecoveringSession: false,
});
cloudAgentSessionId = newSessionId;

Expand Down Expand Up @@ -259,15 +266,22 @@ export function createProjectManager(config: ProjectManagerConfig): ProjectManag

// Determine if the active session needs initial streaming from the backend session info.
// `initiated` lives on ProjectSessionInfo (routing data), not on SessionDisplayInfo.
const activeProjectSessionInfo =
project.sessions.find(s => s.ended_at === null) ??
project.sessions[project.sessions.length - 1];
const activeProjectSessionInfo = project.session_id
? project.sessions.find(s => s.cloud_agent_session_id === project.session_id)
: undefined;

if (activeProjectSessionInfo?.initiated === false) {
if (activeProjectSessionInfo?.prepared === true && activeProjectSessionInfo.initiated === false) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This deliberately distinguishes a prepared-but-not-started session from a missing session. Only the former can be started in place; missing sessions must use the replacement-session flow.

pendingInitialStreamingStart = true;
} else if (cloudAgentSessionId) {
} else if (
cloudAgentSessionId &&
activeProjectSessionInfo &&
activeProjectSessionInfo.prepared !== false
) {
pendingReconnect = true;
} else {
if (cloudAgentSessionId) {
store.setState({ pendingNewSession: true, isRecoveringSession: true });
}
startPreviewPollingIfNeeded();
}

Expand Down Expand Up @@ -339,6 +353,7 @@ export function createProjectManager(config: ProjectManagerConfig): ProjectManag
}

const effectiveModel = model ?? store.getState().model;
const isRecoveringSession = store.getState().isRecoveringSession;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capture the mode before clearing pendingNewSession: a failed mandatory recovery must remain non-cancelable, while a failed user-requested New Chat must remain optional and cancellable.


store.setState({ pendingNewSession: false, isStreaming: true });

Expand Down Expand Up @@ -370,7 +385,11 @@ export function createProjectManager(config: ProjectManagerConfig): ProjectManag
.catch((err: Error) => {
if (destroyed) return;
logger.logError('Failed to start new session', err);
store.setState({ isStreaming: false });
store.setState({
pendingNewSession: true,
isRecoveringSession,
isStreaming: false,
});
});
}

Expand Down Expand Up @@ -423,11 +442,12 @@ export function createProjectManager(config: ProjectManagerConfig): ProjectManag
if (currentActive) {
currentActive.info.ended_at = new Date().toISOString();
}
store.setState({ pendingNewSession: true });
store.setState({ pendingNewSession: true, isRecoveringSession: false });
}

function cancelNewSession(): void {
if (destroyed) return;
if (store.getState().isRecoveringSession) return;
const currentActive = getActiveSession();
if (currentActive) {
currentActive.info.ended_at = null;
Expand Down
Loading