From 9e42ca154a8dafb74a61b6b27c888d65a2d0b939 Mon Sep 17 00:00:00 2001 From: Lucio Lelii Date: Mon, 7 Sep 2026 12:30:03 +0200 Subject: [PATCH] Fold the assistant's panels away, and name the default it will use Quick prompts and the assistant model panel both start collapsed now: the prompt box is what the copilot is opened for, and everything else is a detour until asked for. A saved session still reopens with whatever was last expanded, the same as the other collapsible sections. The assistant model panel gains the toggle it was already written for - modelPickerOpen and toggleModelPicker existed, were persisted in the session snapshot, and were never wired to the template, so the panel could not be folded at all. Ticking Use default configuration used to leave the panel empty: it said a default applied without saying which. The backend has been publishing defaultProvider, defaultModel and defaultPhaseModels alongside the retriever URLs all along, so nothing new was needed there - only the showing. The effective selection also appears in the section header, so the collapsed panel still answers what the run will use, and the per-phase lines list only the phases actually overridden rather than three rows repeating the main model. Co-Authored-By: Claude Opus 5 (1M context) --- .../shared/flow-assistant/flow-assistant.css | 14 ++ .../shared/flow-assistant/flow-assistant.html | 27 ++++ .../flow-assistant/flow-assistant.spec.ts | 142 ++++++++++++++++++ .../shared/flow-assistant/flow-assistant.ts | 36 ++++- 4 files changed, 217 insertions(+), 2 deletions(-) diff --git a/src/app/shared/flow-assistant/flow-assistant.css b/src/app/shared/flow-assistant/flow-assistant.css index 128485d..bac0a8c 100644 --- a/src/app/shared/flow-assistant/flow-assistant.css +++ b/src/app/shared/flow-assistant/flow-assistant.css @@ -242,6 +242,20 @@ gap: 8px; } +/* What the backend will use when nothing is chosen here. */ +.assistant-defaults { + display: grid; + gap: 2px; + padding: 8px 10px; + border: 1px solid #dbe4ee; + border-radius: 10px; + background: #f8fafc; +} + +.assistant-defaults .assistant-meta { + margin: 0; +} + .assistant-add-credential { justify-self: start; } diff --git a/src/app/shared/flow-assistant/flow-assistant.html b/src/app/shared/flow-assistant/flow-assistant.html index 4ad4fc1..a01163c 100644 --- a/src/app/shared/flow-assistant/flow-assistant.html +++ b/src/app/shared/flow-assistant/flow-assistant.html @@ -43,10 +43,23 @@

Assistant model

+ + @if (effectiveModelSummary(); as summary) { +

{{ summary }}

+ } @else {

Provider credentials are managed by the backend.

+ }
+
+ @if (modelPickerOpen()) { The LLM configuration is locked for this session.

} + @if (useDefaultConfiguration()) { +
+ @if (defaultConfigurationSummary(); as summary) { +

{{ summary }}

+ } @else { +

The backend has not published a default provider and model.

+ } + @for (phase of defaultPhaseModelSummary(); track phase.label) { +

{{ phase.label }}: {{ phase.model }}

+ } +
+ } + @if (!useDefaultConfiguration()) {
@@ -188,6 +214,7 @@
} } + } @if (assistantErrorMessage()) { diff --git a/src/app/shared/flow-assistant/flow-assistant.spec.ts b/src/app/shared/flow-assistant/flow-assistant.spec.ts index 3977870..aa0c57f 100644 --- a/src/app/shared/flow-assistant/flow-assistant.spec.ts +++ b/src/app/shared/flow-assistant/flow-assistant.spec.ts @@ -259,3 +259,145 @@ describe('FlowAssistant credentials', () => { expect(provider.options).toEqual([{ label: 'Gemini', value: 'Gemini' }]); }); }); + +describe('FlowAssistant quick prompts', () => { + function build() { + const assistant = { + getConfig: vi.fn(() => of({ + defaultProvider: '', + defaultModel: '', + availableProvidersRetrieverUrl: '/retriever/LLM/providers', + availableModelsRetrieverUrl: '/retriever/LLM/models?provider={provider}', + defaultPhaseModels: {}, + providerCatalogUrl: '/llm/providers' + })), + listProviders: vi.fn(() => of([])), + listModels: vi.fn(() => of([])), + areModelsOpen: vi.fn(() => of(false)), + listProviderCatalog: vi.fn(() => of([])) + }; + + TestBed.configureTestingModule({ + providers: [ + { provide: AssistantService, useValue: assistant }, + { provide: NodeSettingsDialogService, useValue: { open: vi.fn((_input: unknown) => Promise.resolve(null)) } }, + { provide: VaultService, useValue: { listSecrets: vi.fn(() => of([])) } }, + { + provide: EditorStateHolder, + useValue: { currentFlow: vi.fn(() => null), activeFlowData: vi.fn(() => null) } + }, + { provide: Authorization, useValue: { currentUser: vi.fn(() => null) } }, + { + provide: AssistantSessionStore, + useValue: { + flowKey: vi.fn(() => 'flow-1'), + getSnapshot: vi.fn(() => null), + setSnapshot: vi.fn(), + clearSnapshot: vi.fn(), + cloneSnapshot: vi.fn((snapshot: unknown) => snapshot) + } + } + ] + }); + + return TestBed.createComponent(FlowAssistant).componentInstance as any; + } + + afterEach(() => TestBed.resetTestingModule()); + + it('starts collapsed, like the other collapsible sections', () => { + expect(build().quickPromptsOpen()).toBe(false); + }); + + it('starts with the assistant model panel collapsed too', () => { + // The signal and its toggle already existed; the template never used them, so the panel was + // permanently expanded and there was no way to fold it away. + expect(build().modelPickerOpen()).toBe(false); + }); + + it('stays collapsed when a flow with no saved session is opened', async () => { + const component = build(); + component.quickPromptsOpen.set(true); + + await component.restoreSessionForFlow('flow-2'); + + expect(component.quickPromptsOpen()).toBe(false); + }); +}); + +describe('FlowAssistant default configuration', () => { + function build(config: Record) { + const assistant = { + getConfig: vi.fn(() => of(config)), + listProviders: vi.fn(() => of([])), + listModels: vi.fn(() => of([])), + areModelsOpen: vi.fn(() => of(false)), + listProviderCatalog: vi.fn(() => of([])) + }; + + TestBed.configureTestingModule({ + providers: [ + { provide: AssistantService, useValue: assistant }, + { provide: NodeSettingsDialogService, useValue: { open: vi.fn((_input: unknown) => Promise.resolve(null)) } }, + { provide: VaultService, useValue: { listSecrets: vi.fn(() => of([])) } }, + { + provide: EditorStateHolder, + useValue: { currentFlow: vi.fn(() => null), activeFlowData: vi.fn(() => null) } + }, + { provide: Authorization, useValue: { currentUser: vi.fn(() => null) } }, + { + provide: AssistantSessionStore, + useValue: { + flowKey: vi.fn(() => 'flow-1'), + getSnapshot: vi.fn(() => null), + setSnapshot: vi.fn(), + clearSnapshot: vi.fn(), + cloneSnapshot: vi.fn((snapshot: unknown) => snapshot) + } + } + ] + }); + + const component = TestBed.createComponent(FlowAssistant).componentInstance as any; + component.assistantConfig.set(config); + return component; + } + + afterEach(() => TestBed.resetTestingModule()); + + it('names the backend default while Use default configuration is on', () => { + // Ticking the box said a default applied without ever saying which one. + const component = build({ defaultProvider: 'InternalOllama', defaultModel: 'llama3.2:3b', defaultPhaseModels: {} }); + + expect(component.useDefaultConfiguration()).toBe(true); + expect(component.defaultConfigurationSummary()).toBe('InternalOllama \u00b7 llama3.2:3b'); + expect(component.effectiveModelSummary()).toBe('Default: InternalOllama \u00b7 llama3.2:3b'); + }); + + it('lists only the phases the backend actually overrides', () => { + const component = build({ + defaultProvider: 'InternalOllama', + defaultModel: 'llama3.2:3b', + defaultPhaseModels: { planningModel: 'qwen2.5:7b', jsonModel: ' ', repairModel: undefined } + }); + + expect(component.defaultPhaseModelSummary()).toEqual([{ label: 'Planning', model: 'qwen2.5:7b' }]); + }); + + it('says nothing rather than something empty when no default was published', () => { + const component = build({ defaultProvider: '', defaultModel: '', defaultPhaseModels: {} }); + + expect(component.defaultConfigurationSummary()).toBeNull(); + expect(component.effectiveModelSummary()).toBeNull(); + }); + + it('summarises the chosen provider and model once the default is turned off', () => { + const component = build({ defaultProvider: 'InternalOllama', defaultModel: 'llama3.2:3b', defaultPhaseModels: {} }); + + component.useDefaultConfiguration.set(false); + component.selectedProvider.set('Gemini'); + component.selectedModel.set('gemini-2.5-flash'); + + expect(component.effectiveModelSummary()).toBe('Gemini \u00b7 gemini-2.5-flash'); + }); +}); diff --git a/src/app/shared/flow-assistant/flow-assistant.ts b/src/app/shared/flow-assistant/flow-assistant.ts index f73af95..8b86298 100644 --- a/src/app/shared/flow-assistant/flow-assistant.ts +++ b/src/app/shared/flow-assistant/flow-assistant.ts @@ -84,7 +84,8 @@ export class FlowAssistant implements OnInit, OnDestroy { readonly credentialSaving = signal(false); readonly advancedModelsOpen = signal(false); readonly modelPickerOpen = signal(false); - readonly quickPromptsOpen = signal(true); + /** Closed on arrival, like the other collapsible sections: the prompt box is what you came for. */ + readonly quickPromptsOpen = signal(false); readonly assistantErrorMessage = signal(null); readonly lastFailedPrompt = signal(null); readonly lastSubmittedPrompt = signal(''); @@ -205,6 +206,37 @@ export class FlowAssistant implements OnInit, OnDestroy { readonly currentFlow = this.editorState.currentFlow; readonly currentDraft = computed(() => this.sessionState()?.currentDraftFlow ?? null); readonly configurationLocked = computed(() => !!this.sessionState()?.id); + /** + * What the backend says it will use when nothing is chosen here. It has always been in the + * config payload alongside the retriever URLs; the panel just never showed it, so ticking Use + * default configuration told you a default applied without ever saying which. + */ + readonly defaultConfigurationSummary = computed(() => { + const config = this.assistantConfig(); + const parts = [config?.defaultProvider?.trim(), config?.defaultModel?.trim()].filter((part) => !!part); + return parts.length ? parts.join(' \u00b7 ') : null; + }); + + /** The phases the backend overrides, if any. Silence here means they all use the main model. */ + readonly defaultPhaseModelSummary = computed(() => { + const phases = this.assistantConfig()?.defaultPhaseModels; + return [ + { label: 'Planning', model: phases?.planningModel?.trim() }, + { label: 'JSON', model: phases?.jsonModel?.trim() }, + { label: 'Repair', model: phases?.repairModel?.trim() } + ].filter((entry): entry is { label: string; model: string } => !!entry.model); + }); + + /** What the run will use, readable with the panel collapsed. */ + readonly effectiveModelSummary = computed(() => { + if (this.useDefaultConfiguration()) { + const summary = this.defaultConfigurationSummary(); + return summary ? `Default: ${summary}` : null; + } + const parts = [this.selectedProvider().trim(), this.selectedModel().trim()].filter((part) => !!part); + return parts.length ? parts.join(' \u00b7 ') : null; + }); + readonly providerNeedsCredential = computed(() => !!this.selectedProvider().trim() && !this.isInternalProvider(this.selectedProvider()) ); @@ -892,7 +924,7 @@ export class FlowAssistant implements OnInit, OnDestroy { if (!snapshot) { this.prompt.set(''); this.modelPickerOpen.set(false); - this.quickPromptsOpen.set(true); + this.quickPromptsOpen.set(false); this.sessionState.set(null); this.localMessages.set([]); this.assistantErrorMessage.set(null);