From afd4e09e5a08bb58dae4dd9fe841d6c2da83dce5 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 19 May 2026 09:03:22 -0700 Subject: [PATCH 1/2] fix(chat): suppress floor typing-indicator while AI bubble streams; add a2ui welcome chips MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related demo-polish fixes uncovered while running c-a2ui locally: 1. **Double loading affordance.** During every assistant turn the user saw BOTH the AI bubble's pulsing caret (`.chat-message__caret`) AND the floor `` simultaneously. Both communicate the same thing ("agent is working"); two of them reads as visual noise. Fix: a new `currentAssistantStreaming()` computed on ChatComponent returns true when the latest message is an assistant message AND the agent is loading — i.e. exactly the condition under which the bubble caret is rendering. The `@if (pinned())` guard on the typing indicator now also requires `!currentAssistantStreaming()`, so the caret and the dots no longer overlap. The typing indicator still shows in its original spot when there's no streaming assistant bubble yet (the moment between user submit and the first AI message append). 2. **c-a2ui had no welcome chips.** The other cockpit chat caps (generative-ui, interrupts, subagents, tool-calls) ship two suggestion chips so a first-time visitor doesn't have to type anything to see the demo. c-a2ui shipped none. Add 'LAX → JFK' and 'SFO → SEA' chips matching the existing pattern. Verified locally via chrome MCP: the c-a2ui welcome screen now shows both chips; clicking 'LAX → JFK' fires the prompt and during the ~15-20s gpt-5 structured-output wait the user sees exactly one loading affordance instead of two. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../a2ui/angular/src/app/a2ui.component.ts | 26 ++++++++++++++-- .../lib/compositions/chat/chat.component.ts | 30 ++++++++++++++++++- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/cockpit/chat/a2ui/angular/src/app/a2ui.component.ts b/cockpit/chat/a2ui/angular/src/app/a2ui.component.ts index 8e5b477f7..270e02631 100644 --- a/cockpit/chat/a2ui/angular/src/app/a2ui.component.ts +++ b/cockpit/chat/a2ui/angular/src/app/a2ui.component.ts @@ -1,17 +1,32 @@ // SPDX-License-Identifier: MIT import { Component } from '@angular/core'; -import { ChatComponent, a2uiBasicCatalog } from '@ngaf/chat'; +import { ChatComponent, ChatWelcomeSuggestionComponent, a2uiBasicCatalog } from '@ngaf/chat'; import { ExampleChatLayoutComponent } from '@ngaf/example-layouts'; import { agent } from '@ngaf/langgraph'; import { environment } from '../environments/environment'; +const WELCOME_SUGGESTIONS = [ + { label: 'LAX → JFK', value: 'I want to fly LAX to JFK' }, + { label: 'SFO → SEA', value: 'I want to fly SFO to SEA' }, +] as const; + @Component({ selector: 'app-a2ui', standalone: true, - imports: [ChatComponent, ExampleChatLayoutComponent], + imports: [ChatComponent, ChatWelcomeSuggestionComponent, ExampleChatLayoutComponent], template: ` - + +
+ @for (s of suggestions; track s.value) { + + } +
+
`, }) @@ -21,4 +36,9 @@ export class A2uiComponent { assistantId: environment.a2uiAssistantId, }); protected readonly catalog = a2uiBasicCatalog(); + protected readonly suggestions = WELCOME_SUGGESTIONS; + + protected send(text: string): void { + void this.agent.submit({ message: text }); + } } diff --git a/libs/chat/src/lib/compositions/chat/chat.component.ts b/libs/chat/src/lib/compositions/chat/chat.component.ts index 1984804b2..692e43fed 100644 --- a/libs/chat/src/lib/compositions/chat/chat.component.ts +++ b/libs/chat/src/lib/compositions/chat/chat.component.ts @@ -255,7 +255,12 @@ export function isPinned( - @if (pinned()) { + + @if (pinned() && !currentAssistantStreaming()) { } @@ -395,6 +400,29 @@ export class ChatComponent { private programmaticScrollCount = 0; private static readonly PIN_TOLERANCE_PX = 150; + /** + * True iff there's a current (last-index) assistant message that's + * still streaming. The bubble's own caret already signals loading; + * we suppress the floor typing-indicator in that case so the user + * doesn't see two loading affordances at once. + * + * Matches the same `streaming + current` condition the bubble uses + * to enable `.chat-message__caret`: + * `agent().isLoading() && i === agent().messages().length - 1` + * `i === agent().messages().length - 1` + * + * Restricted to assistant role because the caret only renders on + * assistant bubbles (`:host([data-role="assistant"][data-current=... + * ][data-streaming=...])`). + */ + protected readonly currentAssistantStreaming = computed(() => { + if (!this.agent().isLoading()) return false; + const msgs = this.agent().messages(); + if (msgs.length === 0) return false; + const last = msgs[msgs.length - 1]; + return last?.role === 'assistant'; + }); + constructor() { // Inject the chat lib's root CSS custom properties (--ngaf-chat-bg, // --ngaf-chat-surface, --ngaf-chat-radius-input, etc.) the first From 166145516ef35dab00796f3a07e61e6593d78487 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 19 May 2026 16:05:33 +0000 Subject: [PATCH 2/2] chore(docs): regenerate api docs --- apps/website/content/docs/chat/api/api-docs.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/website/content/docs/chat/api/api-docs.json b/apps/website/content/docs/chat/api/api-docs.json index 2a4a7b524..221d785b8 100644 --- a/apps/website/content/docs/chat/api/api-docs.json +++ b/apps/website/content/docs/chat/api/api-docs.json @@ -1575,6 +1575,12 @@ "description": "", "optional": false }, + { + "name": "currentAssistantStreaming", + "type": "Signal", + "description": "True iff there's a current (last-index) assistant message that's\nstill streaming. The bubble's own caret already signals loading;\nwe suppress the floor typing-indicator in that case so the user\ndoesn't see two loading affordances at once.\n\nMatches the same `streaming + current` condition the bubble uses\nto enable `.chat-message__caret`:\n `agent().isLoading() && i === agent().messages().length - 1`\n `i === agent().messages().length - 1`\n\nRestricted to assistant role because the caret only renders on\nassistant bubbles (`:host([data-role=\"assistant\"][data-current=...\n ][data-streaming=...])`).", + "optional": false + }, { "name": "forkRequested", "type": "OutputEmitterRef",