From 892d00fcff5af579dc5adde859f9e78182c34673 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 19 May 2026 09:24:18 -0700 Subject: [PATCH 1/6] feat(chat): add [showModelPicker] input to suppress auto picker in cramped surfaces --- .../compositions/chat/chat.component.spec.ts | 19 +++++++++++++++++++ .../lib/compositions/chat/chat.component.ts | 12 ++++++++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/libs/chat/src/lib/compositions/chat/chat.component.spec.ts b/libs/chat/src/lib/compositions/chat/chat.component.spec.ts index 8e5d73cfb..4629cd4d8 100644 --- a/libs/chat/src/lib/compositions/chat/chat.component.spec.ts +++ b/libs/chat/src/lib/compositions/chat/chat.component.spec.ts @@ -216,6 +216,25 @@ describe('ChatComponent welcome branch', () => { expect(c.showWelcome()).toBe(false); }); }); + + it('hides the auto-rendered model picker when [showModelPicker] is false', () => { + TestBed.configureTestingModule({}); + const injector = TestBed.inject(Injector); + runInInjectionContext(injector, () => { + const c = new ChatComponent(); + setSignalInput(c.agent, mockAgent({ messages: [] })); + setSignalInput(c.modelOptions, [{ value: 'gpt-5', label: 'gpt-5' }]); + // Default: showModelPicker is true, so the auto-picker guard passes + expect(c.showModelPicker()).toBe(true); + expect(c.modelOptions().length).toBeGreaterThan(0); + // When showModelPicker is false, the guard suppresses the picker + setSignalInput(c.showModelPicker, false); + expect(c.showModelPicker()).toBe(false); + // The template guard `showModelPicker() && modelOptions().length > 0` + // evaluates to false, so the auto-rendered picker is hidden. + expect(c.showModelPicker() && c.modelOptions().length > 0).toBe(false); + }); + }); }); describe('ChatComponent — left-flash regression', () => { diff --git a/libs/chat/src/lib/compositions/chat/chat.component.ts b/libs/chat/src/lib/compositions/chat/chat.component.ts index 692e43fed..8ecd6c807 100644 --- a/libs/chat/src/lib/compositions/chat/chat.component.ts +++ b/libs/chat/src/lib/compositions/chat/chat.component.ts @@ -150,7 +150,7 @@ export function isPinned( @if (showWelcome()) { - @if (modelOptions().length > 0) { + @if (showModelPicker() && modelOptions().length > 0) { - @if (modelOptions().length > 0) { + @if (showModelPicker() && modelOptions().length > 0) { ` themselves. */ readonly modelOptions = input([]); + /** + * When `false`, hide the auto-rendered model picker even when + * `modelOptions` is non-empty. Useful in cramped surfaces (popup, + * sidebar) where the picker crowds the input. Defaults to `true`. + * Has no effect when consumers project their own + * `` via content projection. + */ + readonly showModelPicker = input(true); readonly selectedModel = model(''); readonly modelPickerPlaceholder = input('Choose a model'); From 9c18ec1582f913797cbd325fb2e05537b8726995 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 19 May 2026 09:25:24 -0700 Subject: [PATCH 2/6] feat(chat): forward [showModelPicker] from chat-sidebar + chat-popup --- .../lib/compositions/chat-popup/chat-popup.component.ts | 9 +++++++++ .../compositions/chat-sidebar/chat-sidebar.component.ts | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/libs/chat/src/lib/compositions/chat-popup/chat-popup.component.ts b/libs/chat/src/lib/compositions/chat-popup/chat-popup.component.ts index 5424f11ab..c6817d5eb 100644 --- a/libs/chat/src/lib/compositions/chat-popup/chat-popup.component.ts +++ b/libs/chat/src/lib/compositions/chat-popup/chat-popup.component.ts @@ -69,6 +69,7 @@ import { CHAT_HOST_TOKENS, ensureChatRootStyles } from '../../styles/chat-tokens [agent]="agent()" [views]="views()" [modelOptions]="modelOptions()" + [showModelPicker]="showModelPicker()" [selectedModel]="selectedModel()" (selectedModelChange)="selectedModel.set($event)" (replayRequested)="replayRequested.emit($event)" @@ -89,6 +90,14 @@ export class ChatPopupComponent { /** Forwarded to the inner . When non-empty, a model picker pill * renders in the chat-input chrome. */ readonly modelOptions = input([]); + /** + * Forwarded to the inner ``. When `false`, hides the + * auto-rendered model picker even with non-empty `modelOptions`. + * Use this in narrow surfaces (the chat-sidebar panel is 28rem + * wide; chat-popup is 24rem) where the picker crowds the input. + * Defaults to `true`. + */ + readonly showModelPicker = input(true); /** Two-way bound current model value. */ readonly selectedModel = model(''); readonly open = model(false); diff --git a/libs/chat/src/lib/compositions/chat-sidebar/chat-sidebar.component.ts b/libs/chat/src/lib/compositions/chat-sidebar/chat-sidebar.component.ts index f650ac56b..c04c88231 100644 --- a/libs/chat/src/lib/compositions/chat-sidebar/chat-sidebar.component.ts +++ b/libs/chat/src/lib/compositions/chat-sidebar/chat-sidebar.component.ts @@ -86,6 +86,7 @@ import { CHAT_HOST_TOKENS, ensureChatRootStyles } from '../../styles/chat-tokens [agent]="agent()" [views]="views()" [modelOptions]="modelOptions()" + [showModelPicker]="showModelPicker()" [selectedModel]="selectedModel()" (selectedModelChange)="selectedModel.set($event)" (replayRequested)="replayRequested.emit($event)" @@ -106,6 +107,14 @@ export class ChatSidebarComponent { /** Forwarded to the inner . When non-empty, a model picker pill * renders in the chat-input chrome. */ readonly modelOptions = input([]); + /** + * Forwarded to the inner ``. When `false`, hides the + * auto-rendered model picker even with non-empty `modelOptions`. + * Use this in narrow surfaces (the chat-sidebar panel is 28rem + * wide; chat-popup is 24rem) where the picker crowds the input. + * Defaults to `true`. + */ + readonly showModelPicker = input(true); /** Two-way bound current model value. */ readonly selectedModel = model(''); readonly open = model(false); From ecf733e5f21b6f7106fd6125ac525bf8068a3994 Mon Sep 17 00:00:00 2001 From: Brian Love Date: Tue, 19 May 2026 09:26:17 -0700 Subject: [PATCH 3/6] feat(chat): chat-sidebar fixed panel header with title slot + relocated close button --- .../chat-sidebar/chat-sidebar.component.ts | 35 ++++++++++++++++--- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/libs/chat/src/lib/compositions/chat-sidebar/chat-sidebar.component.ts b/libs/chat/src/lib/compositions/chat-sidebar/chat-sidebar.component.ts index c04c88231..0d782265e 100644 --- a/libs/chat/src/lib/compositions/chat-sidebar/chat-sidebar.component.ts +++ b/libs/chat/src/lib/compositions/chat-sidebar/chat-sidebar.component.ts @@ -51,12 +51,32 @@ import { CHAT_HOST_TOKENS, ensureChatRootStyles } from '../../styles/chat-tokens @media (max-width: 640px) { .chat-sidebar__panel { width: 100vw; } } + .chat-sidebar__panel-header { + flex: 0 0 auto; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 8px 12px; + border-bottom: 1px solid var(--ngaf-chat-separator); + min-height: 48px; + } + .chat-sidebar__panel-title { + min-width: 0; + flex: 1 1 auto; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + color: var(--ngaf-chat-text); + font-weight: 500; + font-size: var(--ngaf-chat-font-size-sm); + } .chat-sidebar__close { - position: absolute; top: 8px; right: 8px; + flex: 0 0 auto; width: 32px; height: 32px; background: transparent; border: 0; cursor: pointer; color: var(--ngaf-chat-text-muted); - border-radius: 50%; z-index: 1; + border-radius: 50%; display: flex; align-items: center; justify-content: center; @@ -79,9 +99,14 @@ import { CHAT_HOST_TOKENS, ensureChatRootStyles } from '../../styles/chat-tokens