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.
This commit is contained in:
Lucio Lelii 2026-09-02 13:39:55 +02:00
parent 38b01b2bee
commit 3906bf8bd5
5 changed files with 117 additions and 13 deletions

View File

@ -17,6 +17,8 @@ export abstract class AssistantCallServiceBase {
abstract createSession(request: AssistantSessionRequest): Observable<AssistantSessionState>;
abstract getSession(sessionId: string): Observable<AssistantSessionState>;
abstract submitMessage(sessionId: string, request: AssistantSessionMessageRequest): Observable<AssistantCallAccepted>;
abstract getCall(callId: string): Observable<AssistantCallState>;

View File

@ -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<string, AssistantSessionState>();
private readonly calls = new Map<string, { sessionId: string; intent: AssistantIntent; result: AssistantFlowActionResult }>();
override getConfig(): Observable<AssistantConfig> {
@ -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<AssistantSessionState> {
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<AssistantCallAccepted> {
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<AssistantCallState> {
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 ?? '',

View File

@ -45,6 +45,12 @@ export class AssistantCallService extends AssistantCallServiceBase {
.pipe(map((raw) => mapAssistantSessionState(raw)));
}
override getSession(sessionId: string): Observable<AssistantSessionState> {
return this.http
.get<unknown>(`${environment.apiUrl}/assistant/sessions/${sessionId}`)
.pipe(map((raw) => mapAssistantSessionState(raw)));
}
override submitMessage(sessionId: string, request: AssistantSessionMessageRequest): Observable<AssistantCallAccepted> {
return this.http
.post<unknown>(`${environment.apiUrl}/assistant/sessions/${sessionId}/messages`, request)

View File

@ -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);
}

View File

@ -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<boolean> {
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;