From 3906bf8bd54cd321e64b2eb653bc20524e2ee951 Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Wed, 2 Sep 2026 13:39:55 +0200 Subject: [PATCH] Fix create-with-AI minimize/resume regression from cancellable-call stub hasCancellableCall() was hardcoded to false (from an earlier commit that gutted the real async call tracking), which silently blocked minimizeCreateWithAi() from ever minimizing an in-progress AI flow creation into the floating banner, and blocked closeCreateWithAi() from prompting to cancel a running request. restoreSessionForFlow() also never resumed polling for a call that was still QUEUED/RUNNING when its snapshot was saved (e.g. after minimizing and reopening, or a page reload), so a resumed session lost live progress updates and could get stuck showing a stale phase. Restores real hasCancellableCall()/cancelActiveCall() backed by the submitMessage/getCall/cancelCall flow, resumes polling on restore for an active call, and refreshes the session from the backend otherwise. Adds getSession() across the assistant call service stack to support this. --- .../services/assistant/assistant-call.base.ts | 2 + .../services/assistant/assistant-call.fake.ts | 46 +++++++++++- src/app/services/assistant/assistant-call.ts | 6 ++ src/app/services/assistant/assistant.ts | 4 ++ .../shared/flow-assistant/flow-assistant.ts | 72 ++++++++++++++++--- 5 files changed, 117 insertions(+), 13 deletions(-) diff --git a/src/app/services/assistant/assistant-call.base.ts b/src/app/services/assistant/assistant-call.base.ts index ebdc9e5..f8f1044 100644 --- a/src/app/services/assistant/assistant-call.base.ts +++ b/src/app/services/assistant/assistant-call.base.ts @@ -17,6 +17,8 @@ export abstract class AssistantCallServiceBase { abstract createSession(request: AssistantSessionRequest): Observable; + abstract getSession(sessionId: string): Observable; + abstract submitMessage(sessionId: string, request: AssistantSessionMessageRequest): Observable; abstract getCall(callId: string): Observable; diff --git a/src/app/services/assistant/assistant-call.fake.ts b/src/app/services/assistant/assistant-call.fake.ts index d33a704..9caab9c 100644 --- a/src/app/services/assistant/assistant-call.fake.ts +++ b/src/app/services/assistant/assistant-call.fake.ts @@ -10,12 +10,13 @@ import { AssistantSessionState } from '@models/assistant'; import { FlowData } from '@models/flow'; -import { Observable, of } from 'rxjs'; +import { Observable, of, throwError } from 'rxjs'; import { AssistantCallServiceBase } from './assistant-call.base'; export class AssistantCallServiceFake extends AssistantCallServiceBase { private readonly models = ['llama3.1:8b', 'qwen2.5:7b', 'mistral:7b']; private readonly providers = ['InternalOllama', 'OpenAI']; + private readonly sessions = new Map(); private readonly calls = new Map(); override getConfig(): Observable { @@ -52,14 +53,46 @@ export class AssistantCallServiceFake extends AssistantCallServiceBase { lastValidationErrors: [], lastCallId: null }; + this.sessions.set(session.id, session); + return of(structuredClone(session)); + } + + override getSession(sessionId: string): Observable { + const session = this.sessions.get(sessionId); + if (!session) { + return throwError(() => new Error('Assistant session not found')); + } return of(structuredClone(session)); } override submitMessage(sessionId: string, request: AssistantSessionMessageRequest): Observable { - const intent = this.inferIntent(request.message, request.flow); - const result = this.buildActionResult(intent, request); + const session = this.sessions.get(sessionId); + const flow = request.flow ?? session?.currentFlow ?? undefined; + const intent = this.inferIntent(request.message, flow); + const result = this.buildActionResult(intent, { ...request, flow }); const callId = crypto.randomUUID(); this.calls.set(callId, { sessionId, intent, result }); + + if (session) { + session.messages = [ + ...session.messages, + { id: crypto.randomUUID(), role: 'user', content: request.message }, + { + id: crypto.randomUUID(), + role: 'assistant', + content: result.message, + warnings: result.warnings, + validationErrors: result.validationErrors + } + ]; + session.lastCallId = callId; + if (result.flow) { + session.currentFlow = result.flow; + session.currentDraftFlow = result.flow; + } + session.lastValidationErrors = result.validationErrors; + } + return of({ sessionId, callId }); } @@ -91,6 +124,13 @@ export class AssistantCallServiceFake extends AssistantCallServiceBase { override cancelCall(callId: string): Observable { const call = this.calls.get(callId); + const session = call ? this.sessions.get(call.sessionId) : undefined; + if (session) { + session.messages = [ + ...session.messages, + { id: crypto.randomUUID(), role: 'assistant', content: 'Assistant request cancelled.' } + ]; + } return of({ id: callId, sessionId: call?.sessionId ?? '', diff --git a/src/app/services/assistant/assistant-call.ts b/src/app/services/assistant/assistant-call.ts index 20f678b..76eff0f 100644 --- a/src/app/services/assistant/assistant-call.ts +++ b/src/app/services/assistant/assistant-call.ts @@ -45,6 +45,12 @@ export class AssistantCallService extends AssistantCallServiceBase { .pipe(map((raw) => mapAssistantSessionState(raw))); } + override getSession(sessionId: string): Observable { + return this.http + .get(`${environment.apiUrl}/assistant/sessions/${sessionId}`) + .pipe(map((raw) => mapAssistantSessionState(raw))); + } + override submitMessage(sessionId: string, request: AssistantSessionMessageRequest): Observable { return this.http .post(`${environment.apiUrl}/assistant/sessions/${sessionId}/messages`, request) diff --git a/src/app/services/assistant/assistant.ts b/src/app/services/assistant/assistant.ts index 37803da..cbc3ff3 100644 --- a/src/app/services/assistant/assistant.ts +++ b/src/app/services/assistant/assistant.ts @@ -28,6 +28,10 @@ export class AssistantService { return this.assistantCall.createSession(request); } + getSession(sessionId: string) { + return this.assistantCall.getSession(sessionId); + } + submitMessage(sessionId: string, request: AssistantSessionMessageRequest) { return this.assistantCall.submitMessage(sessionId, request); } diff --git a/src/app/shared/flow-assistant/flow-assistant.ts b/src/app/shared/flow-assistant/flow-assistant.ts index 85e87fa..f5691c9 100644 --- a/src/app/shared/flow-assistant/flow-assistant.ts +++ b/src/app/shared/flow-assistant/flow-assistant.ts @@ -427,12 +427,40 @@ export class FlowAssistant implements OnInit, OnDestroy { === this.canonicalAssistantErrorContent(FlowAssistant.STANDARD_ASSISTANT_ERROR); } - hasCancellableCall(): boolean { return false; } + hasCancellableCall(): boolean { + return this.isActiveCall(this.currentCall()); + } async cancelActiveCall(): Promise { + const call = this.currentCall(); + if (!this.isActiveCall(call)) { + this.requestPending.set(false); + this.createPromptSubmitted.set(false); + return true; + } + + this.stopPolling(); this.requestPending.set(false); - this.createPromptSubmitted.set(false); - return true; + + try { + const cancelledCall = await firstValueFrom(this.assistant.cancelCall(call.id).pipe(take(1))); + this.currentCall.set(cancelledCall); + this.createPromptSubmitted.set(false); + this.persistSnapshot(); + + const session = await firstValueFrom(this.assistant.getSession(cancelledCall.sessionId || call.sessionId).pipe(take(1))); + this.applySessionState(session, { syncDraftToEditor: false }); + this.currentCall.set(null); + this.persistSnapshot(); + return true; + } catch (err) { + console.error('Assistant call cancel failed', err); + this.currentCall.set(call); + this.beginPolling(call.id); + this.assistantErrorMessage.set('Unable to cancel the assistant request.'); + this.persistSnapshot(); + return false; + } } clearActiveSnapshot() { @@ -892,23 +920,43 @@ export class FlowAssistant implements OnInit, OnDestroy { this.assistantErrorMessage.set(snapshot.assistantErrorMessage); this.lastFailedPrompt.set(snapshot.lastFailedPrompt); this.lastSubmittedPrompt.set(snapshot.lastSubmittedPrompt); - this.createPromptSubmitted.set(false); + this.createPromptSubmitted.set(this.isCreateModal() && this.isActiveCall(snapshot.currentCall)); this.useDefaultConfiguration.set(snapshot.useDefaultConfiguration); this.selectedProvider.set(snapshot.selectedProvider); this.selectedModel.set(snapshot.selectedModel); this.phaseModels.set(snapshot.phaseModels); this.advancedModelsOpen.set(snapshot.advancedModelsOpen); - if (snapshot.sessionState) { - this.sessionState.set(snapshot.sessionState); - } else { - this.sessionState.set(null); - } + this.sessionState.set(snapshot.sessionState ?? null); - this.sessionLoading.set(false); if (!this.useDefaultConfiguration() && this.selectedProvider()) { void this.loadProviders(); void this.loadModels(this.selectedProvider()); } + + if (this.isActiveCall(snapshot.currentCall)) { + this.sessionLoading.set(false); + this.beginPolling(snapshot.currentCall!.id); + return; + } + + const sessionId = snapshot.sessionId ?? snapshot.sessionState?.id ?? null; + if (!sessionId) { + this.sessionLoading.set(false); + return; + } + + this.sessionLoading.set(true); + this.assistant.getSession(sessionId).pipe( + take(1), + finalize(() => this.sessionLoading.set(false)) + ).subscribe({ + next: (session) => { + this.applySessionState(session, { clearLocalMessages: false, syncDraftToEditor: false }); + }, + error: (err) => { + console.error('Assistant session refresh failed', err); + } + }); } private resolveFlowKey(flowId: string | null | undefined): string { @@ -990,6 +1038,10 @@ export class FlowAssistant implements OnInit, OnDestroy { } } + private isActiveCall(call: AssistantCallState | null): call is AssistantCallState { + return !!call && (call.status === 'QUEUED' || call.status === 'RUNNING'); + } + private hasMeaningfulFlow(flow: Flow | null): boolean { if (!flow) return false; const data = flow.data;