Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions apps/website/content/docs/chat/api/api-docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -1677,6 +1677,12 @@
"description": "",
"optional": false
},
{
"name": "showModelPicker",
"type": "InputSignal<boolean>",
"description": "When `false`, hide the auto-rendered model picker even when\n`modelOptions` is non-empty. Useful in cramped surfaces (popup,\nsidebar) where the picker crowds the input. Defaults to `true`.\nHas no effect when consumers project their own\n`<chat-select chatInputModelSelect>` via content projection.",
"optional": false
},
{
"name": "showWelcome",
"type": "Signal<boolean>",
Expand Down Expand Up @@ -2796,6 +2802,12 @@
"description": "Keyboard shortcut (single key) that toggles the popup with cmd (mac)\nor ctrl (other). Set to `null` to disable. Default: 'k' — matches the\nwidely-used cmd/ctrl+K convention.",
"optional": false
},
{
"name": "showModelPicker",
"type": "InputSignal<boolean>",
"description": "Forwarded to the inner `<chat>`. When `false`, hides the\nauto-rendered model picker even with non-empty `modelOptions`.\nUse this in narrow surfaces (the chat-sidebar panel is 28rem\nwide; chat-popup is 24rem) where the picker crowds the input.\nDefaults to `true`.",
"optional": false
},
{
"name": "views",
"type": "InputSignal<Readonly<Record<string, Type<unknown> | RenderViewEntry>> | undefined>",
Expand Down Expand Up @@ -3297,6 +3309,12 @@
"description": "Two-way bound current model value.",
"optional": false
},
{
"name": "showModelPicker",
"type": "InputSignal<boolean>",
"description": "Forwarded to the inner `<chat>`. When `false`, hides the\nauto-rendered model picker even with non-empty `modelOptions`.\nUse this in narrow surfaces (the chat-sidebar panel is 28rem\nwide; chat-popup is 24rem) where the picker crowds the input.\nDefaults to `true`.",
"optional": false
},
{
"name": "views",
"type": "InputSignal<Readonly<Record<string, Type<unknown> | RenderViewEntry>> | undefined>",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { WelcomeSuggestionsComponent } from './welcome-suggestions.component';
[views]="catalog"
[modelOptions]="shell.modelOptions()"
[selectedModel]="shell.model()"
[showModelPicker]="false"
(selectedModelChange)="shell.onModelChange($event)"
(replayRequested)="shell.onTimelineReplay($event)"
(forkRequested)="shell.onTimelineFork($event)"
Expand Down
2 changes: 2 additions & 0 deletions examples/chat/angular/src/app/modes/sidebar-mode.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,14 @@ import { WelcomeSuggestionsComponent } from './welcome-suggestions.component';
[views]="catalog"
[modelOptions]="shell.modelOptions()"
[selectedModel]="shell.model()"
[showModelPicker]="false"
[open]="true"
[pushContent]="true"
(selectedModelChange)="shell.onModelChange($event)"
(replayRequested)="shell.onTimelineReplay($event)"
(forkRequested)="shell.onTimelineFork($event)"
>
<span chatSidebarPanelTitle>{{ shell.currentThreadTitle() }}</span>
<div class="sidebar-mode__background">
<p class="sidebar-mode__hint">
Use the launcher (right edge) to dismiss or re-open the chat panel.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ import { FEATURED_SUGGESTIONS, MORE_SUGGESTIONS } from './welcome-suggestions';
gap: 12px;
}
.welcome-suggestions__featured {
flex: 1 1 0;
min-width: 0;
max-width: 380px;
overflow: hidden;
}
Expand Down Expand Up @@ -80,6 +82,9 @@ import { FEATURED_SUGGESTIONS, MORE_SUGGESTIONS } from './welcome-suggestions';
max-width: 480px;
width: max-content;
}
.welcome-suggestions__row > chat-select {
flex: 0 0 auto;
}
`,
],
})
Expand Down
10 changes: 10 additions & 0 deletions examples/chat/angular/src/app/shell/demo-shell.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,16 @@ export class DemoShell {
/** Persisted thread id (null on first run). Reactive so reload reconnects to the same thread. */
protected readonly threadIdSignal = signal<string | null>(this.persistence.read('threadId') ?? null);

/** Title of the currently-selected thread, or 'New chat' if none. The
* Python graph writes thread.metadata.title from the first user message
* via _maybe_write_thread_title; threadsSvc surfaces it via threads(). */
readonly currentThreadTitle = computed(() => {
const id = this.threadIdSignal();
if (!id) return 'New chat';
const thread = this.threadsSvc.threads().find((t) => t.id === id);
return thread?.title ?? 'New chat';
});

protected readonly selectedProjectId = signal<string | null>(
this.persistence.read('selectedProjectId') ?? null,
);
Expand Down
2 changes: 1 addition & 1 deletion libs/a2ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ngaf/a2ui",
"version": "0.0.41",
"version": "0.0.42",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion libs/ag-ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ngaf/ag-ui",
"version": "0.0.41",
"version": "0.0.42",
"peerDependencies": {
"@ngaf/chat": "*",
"@ngaf/licensing": "*",
Expand Down
2 changes: 1 addition & 1 deletion libs/chat/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ngaf/chat",
"version": "0.0.41",
"version": "0.0.42",
"exports": {
"./chat.css": "./chat.css",
"./themes/default-dark.css": "./themes/default-dark.css",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand All @@ -89,6 +90,14 @@ export class ChatPopupComponent {
/** Forwarded to the inner <chat>. When non-empty, a model picker pill
* renders in the chat-input chrome. */
readonly modelOptions = input<readonly ChatSelectOption[]>([]);
/**
* Forwarded to the inner `<chat>`. 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<boolean>(true);
/** Two-way bound current model value. */
readonly selectedModel = model<string>('');
readonly open = model(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -79,13 +99,19 @@ import { CHAT_HOST_TOKENS, ensureChatRootStyles } from '../../styles/chat-tokens
<chat-launcher-button (clicked)="toggle()" />
</div>
<aside class="chat-sidebar__panel" [attr.data-open]="open() ? 'true' : 'false'" role="complementary" [attr.aria-hidden]="open() ? 'false' : 'true'">
<button type="button" class="chat-sidebar__close" (click)="closeWindow()" aria-label="Close chat">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
<div class="chat-sidebar__panel-header">
<div class="chat-sidebar__panel-title">
<ng-content select="[chatSidebarPanelTitle]" />
</div>
<button type="button" class="chat-sidebar__close" (click)="closeWindow()" aria-label="Close chat">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
</button>
</div>
<chat
[agent]="agent()"
[views]="views()"
[modelOptions]="modelOptions()"
[showModelPicker]="showModelPicker()"
[selectedModel]="selectedModel()"
(selectedModelChange)="selectedModel.set($event)"
(replayRequested)="replayRequested.emit($event)"
Expand All @@ -106,6 +132,14 @@ export class ChatSidebarComponent {
/** Forwarded to the inner <chat>. When non-empty, a model picker pill
* renders in the chat-input chrome. */
readonly modelOptions = input<readonly ChatSelectOption[]>([]);
/**
* Forwarded to the inner `<chat>`. 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<boolean>(true);
/** Two-way bound current model value. */
readonly selectedModel = model<string>('');
readonly open = model(false);
Expand Down
19 changes: 19 additions & 0 deletions libs/chat/src/lib/compositions/chat/chat.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
12 changes: 10 additions & 2 deletions libs/chat/src/lib/compositions/chat/chat.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ export function isPinned(
@if (showWelcome()) {
<chat-welcome>
<chat-input chatWelcomeInput [agent]="agent()" [submitOnEnter]="true" placeholder="Type a message...">
@if (modelOptions().length > 0) {
@if (showModelPicker() && modelOptions().length > 0) {
<chat-select
chatInputModelSelect
[options]="modelOptions()"
Expand Down Expand Up @@ -273,7 +273,7 @@ export function isPinned(
}
<chat-error [agent]="agent()" />
<chat-input [agent]="agent()" [submitOnEnter]="true" placeholder="Type a message..." (submitted)="onUserSubmitted()">
@if (modelOptions().length > 0) {
@if (showModelPicker() && modelOptions().length > 0) {
<chat-select
chatInputModelSelect
[options]="modelOptions()"
Expand Down Expand Up @@ -310,6 +310,14 @@ export class ChatComponent {
* empty and project a `<chat-select chatInputModelSelect>` themselves.
*/
readonly modelOptions = input<readonly ChatSelectOption[]>([]);
/**
* 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
* `<chat-select chatInputModelSelect>` via content projection.
*/
readonly showModelPicker = input<boolean>(true);
readonly selectedModel = model<string>('');
readonly modelPickerPlaceholder = input<string>('Choose a model');

Expand Down
2 changes: 1 addition & 1 deletion libs/langgraph/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ngaf/langgraph",
"version": "0.0.41",
"version": "0.0.42",
"peerDependencies": {
"@ngaf/chat": "*",
"@ngaf/licensing": "*",
Expand Down
2 changes: 1 addition & 1 deletion libs/licensing/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ngaf/licensing",
"version": "0.0.41",
"version": "0.0.42",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion libs/render/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ngaf/render",
"version": "0.0.41",
"version": "0.0.42",
"peerDependencies": {
"@angular/core": "^20.0.0 || ^21.0.0",
"@angular/common": "^20.0.0 || ^21.0.0",
Expand Down
2 changes: 1 addition & 1 deletion libs/telemetry/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ngaf/telemetry",
"version": "0.0.41",
"version": "0.0.42",
"license": "MIT",
"publishConfig": {
"access": "public"
Expand Down