diff --git a/libs/a2ui/package.json b/libs/a2ui/package.json
index d47134a0a..736f660f4 100644
--- a/libs/a2ui/package.json
+++ b/libs/a2ui/package.json
@@ -1,6 +1,6 @@
{
"name": "@ngaf/a2ui",
- "version": "0.0.24",
+ "version": "0.0.25",
"license": "MIT",
"repository": {
"type": "git",
diff --git a/libs/ag-ui/package.json b/libs/ag-ui/package.json
index dd44dde0a..16e1c623c 100644
--- a/libs/ag-ui/package.json
+++ b/libs/ag-ui/package.json
@@ -1,6 +1,6 @@
{
"name": "@ngaf/ag-ui",
- "version": "0.0.24",
+ "version": "0.0.25",
"peerDependencies": {
"@ngaf/chat": "*",
"@ngaf/licensing": "*",
diff --git a/libs/chat/package.json b/libs/chat/package.json
index 6d0670ef4..09e3c4fe8 100644
--- a/libs/chat/package.json
+++ b/libs/chat/package.json
@@ -1,6 +1,6 @@
{
"name": "@ngaf/chat",
- "version": "0.0.24",
+ "version": "0.0.25",
"exports": {
".": {
"types": "./index.d.ts",
diff --git a/libs/chat/src/lib/compositions/chat/chat.component.ts b/libs/chat/src/lib/compositions/chat/chat.component.ts
index 02a8499b8..4132174f0 100644
--- a/libs/chat/src/lib/compositions/chat/chat.component.ts
+++ b/libs/chat/src/lib/compositions/chat/chat.component.ts
@@ -139,6 +139,7 @@ import type { ChatRenderEvent } from './chat-render-event';
@let classified = classifyMessage(content, message);
`,
+ template: `
+ @if (failed()) {
+
+ 🖼️
+ @if (node().alt) {
+ {{ node().alt }}
+ } @else {
+ image unavailable
+ }
+
+ } @else {
+
+ }
+ `,
})
export class MarkdownImageComponent {
readonly node = input.required();
+ protected readonly failed = signal(false);
}
diff --git a/libs/chat/src/lib/primitives/chat-input/chat-input.component.spec.ts b/libs/chat/src/lib/primitives/chat-input/chat-input.component.spec.ts
index 303b0b36f..5ba51e2af 100644
--- a/libs/chat/src/lib/primitives/chat-input/chat-input.component.spec.ts
+++ b/libs/chat/src/lib/primitives/chat-input/chat-input.component.spec.ts
@@ -121,4 +121,18 @@ describe('ChatInputComponent', () => {
const controls = (fixture.nativeElement as HTMLElement).querySelector('.chat-input__controls');
expect(controls).not.toBeNull();
});
+
+ it('auto-resizes textarea height when messageText changes — bug #198 regression', () => {
+ // Live Chrome smoke caught: rows="1" textarea did not grow with
+ // multi-line input. clientHeight stayed at 24px while scrollHeight
+ // grew to 72px+, hiding lines past the first. Fix: an effect() sets
+ // el.style.height = scrollHeight (capped at 200px) on every change.
+ const textarea = (fixture.nativeElement as HTMLElement).querySelector('textarea') as HTMLTextAreaElement;
+ expect(textarea).not.toBeNull();
+ fixture.componentInstance.messageText.set('line one\nline two\nline three');
+ fixture.detectChanges();
+ // The effect sets el.style.height; jsdom layout produces a value (
+ // possibly '0px' due to no real layout, but the property is set).
+ expect(textarea.style.height).not.toBe('');
+ });
});
diff --git a/libs/chat/src/lib/primitives/chat-input/chat-input.component.ts b/libs/chat/src/lib/primitives/chat-input/chat-input.component.ts
index b3ef9bb0d..04fab273c 100644
--- a/libs/chat/src/lib/primitives/chat-input/chat-input.component.ts
+++ b/libs/chat/src/lib/primitives/chat-input/chat-input.component.ts
@@ -3,6 +3,7 @@
import {
Component,
computed,
+ effect,
input,
output,
signal,
@@ -116,6 +117,30 @@ export class ChatInputComponent {
private readonly textareaEl = viewChild>('textareaEl');
+ /** Maximum auto-grow height in pixels. Caps at ~8 lines; beyond that, scroll. */
+ private static readonly MAX_AUTO_HEIGHT_PX = 200;
+
+ /**
+ * Auto-resize the textarea to fit its content as the user types or pastes
+ * multi-line text. Caps at MAX_AUTO_HEIGHT_PX; beyond that the textarea
+ * scrolls. Without this, multi-line input is hidden behind the rows="1"
+ * fixed height (caught by live browser smoke).
+ */
+ constructor() {
+ effect(() => {
+ const text = this.messageText();
+ const el = this.textareaEl()?.nativeElement;
+ if (!el) return;
+ // Reset to allow scrollHeight to shrink when content shortens.
+ el.style.height = 'auto';
+ const next = Math.min(el.scrollHeight, ChatInputComponent.MAX_AUTO_HEIGHT_PX);
+ el.style.height = `${next}px`;
+ el.style.overflowY = el.scrollHeight > ChatInputComponent.MAX_AUTO_HEIGHT_PX ? 'auto' : 'hidden';
+ // Reference text so the effect re-runs on every change.
+ void text;
+ });
+ }
+
focusTextarea(): void {
this.textareaEl()?.nativeElement.focus();
}
diff --git a/libs/chat/src/lib/primitives/chat-message-actions/chat-message-actions.component.ts b/libs/chat/src/lib/primitives/chat-message-actions/chat-message-actions.component.ts
index 3cee9cd4a..ee7b65333 100644
--- a/libs/chat/src/lib/primitives/chat-message-actions/chat-message-actions.component.ts
+++ b/libs/chat/src/lib/primitives/chat-message-actions/chat-message-actions.component.ts
@@ -57,6 +57,7 @@ import { CHAT_MESSAGE_ACTIONS_STYLES } from '../../styles/chat-message-actions.s
type="button"
class="chat-message-actions__btn"
[class.is-active]="rating() === 'up'"
+ [attr.aria-pressed]="rating() === 'up'"
aria-label="Thumbs up"
title="Good response"
(click)="onRate('up')"
@@ -70,6 +71,7 @@ import { CHAT_MESSAGE_ACTIONS_STYLES } from '../../styles/chat-message-actions.s
type="button"
class="chat-message-actions__btn"
[class.is-active]="rating() === 'down'"
+ [attr.aria-pressed]="rating() === 'down'"
aria-label="Thumbs down"
title="Poor response"
(click)="onRate('down')"
@@ -99,25 +101,38 @@ export class ChatMessageActionsComponent {
protected async onCopy(): Promise {
const text = this.content();
if (!text) return;
- try {
- const win = this.document.defaultView;
- if (win?.navigator?.clipboard?.writeText) {
+ let succeeded = false;
+ const win = this.document.defaultView;
+ // Prefer Async Clipboard API; fall back to execCommand if it rejects
+ // (e.g. permissions, non-secure context, document-not-focused). The
+ // prior impl gated the fallback only on API absence, so a rejecting
+ // API silently failed with no user feedback.
+ if (win?.navigator?.clipboard?.writeText) {
+ try {
await win.navigator.clipboard.writeText(text);
- } else {
+ succeeded = true;
+ } catch {
+ // Async API failed — fall through to legacy path below.
+ }
+ }
+ if (!succeeded) {
+ try {
const ta = this.document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
this.document.body.appendChild(ta);
ta.select();
- this.document.execCommand?.('copy');
+ succeeded = !!this.document.execCommand?.('copy');
ta.remove();
+ } catch {
+ // Both paths failed — leave copied state unchanged.
}
+ }
+ if (succeeded) {
this.copied.set(true);
this.contentCopied.emit(text);
setTimeout(() => this.copied.set(false), 2000);
- } catch {
- // Silent fail — clipboard may be blocked by permissions.
}
}
diff --git a/libs/chat/src/lib/primitives/chat-select/chat-select.component.spec.ts b/libs/chat/src/lib/primitives/chat-select/chat-select.component.spec.ts
index b7c933268..2f017e29f 100644
--- a/libs/chat/src/lib/primitives/chat-select/chat-select.component.spec.ts
+++ b/libs/chat/src/lib/primitives/chat-select/chat-select.component.spec.ts
@@ -97,6 +97,21 @@ describe('ChatSelectComponent', () => {
expect(host.querySelector('.chat-select__menu')).toBeNull();
});
+ it('closes the menu on Escape when focus is still on the trigger — bug #198 regression', () => {
+ // Live Chrome smoke caught: clicking the trigger to open the menu leaves
+ // focus on the trigger (not the menu). Pressing Escape there used to be
+ // ignored — only Escape inside the menu was handled. Fix: handle Escape
+ // in onTriggerKeydown when the menu is open.
+ const trigger = host.querySelector('.chat-select__trigger')!;
+ trigger.click();
+ fixture.detectChanges();
+ expect(host.querySelector('.chat-select__menu')).not.toBeNull();
+ const evt = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true });
+ trigger.dispatchEvent(evt);
+ fixture.detectChanges();
+ expect(host.querySelector('.chat-select__menu')).toBeNull();
+ });
+
it('disables the trigger when [disabled]=true', () => {
setSignalInput(fixture, 'disabled', true);
fixture.detectChanges();
diff --git a/libs/chat/src/lib/primitives/chat-select/chat-select.component.ts b/libs/chat/src/lib/primitives/chat-select/chat-select.component.ts
index 80114ae9b..e012f0068 100644
--- a/libs/chat/src/lib/primitives/chat-select/chat-select.component.ts
+++ b/libs/chat/src/lib/primitives/chat-select/chat-select.component.ts
@@ -139,6 +139,15 @@ export class ChatSelectComponent {
protected onTriggerKeydown(e: KeyboardEvent): void {
if (this.disabled()) return;
+ // Escape closes an open menu when focus is still on the trigger
+ // (e.g. user clicked to open, then pressed Escape without arrowing
+ // into the menu). Caught by live browser smoke — without this, click
+ // + Escape leaves the menu open until the user clicks outside.
+ if (e.key === 'Escape' && this.open()) {
+ e.preventDefault();
+ this.open.set(false);
+ return;
+ }
if (e.key === 'Enter' || e.key === ' ' || e.key === 'ArrowDown') {
e.preventDefault();
this.open.set(true);
diff --git a/libs/chat/src/lib/styles/chat-markdown.styles.ts b/libs/chat/src/lib/styles/chat-markdown.styles.ts
index db75d5416..ec72ff877 100644
--- a/libs/chat/src/lib/styles/chat-markdown.styles.ts
+++ b/libs/chat/src/lib/styles/chat-markdown.styles.ts
@@ -110,10 +110,19 @@ export const CHAT_MARKDOWN_STYLES = `
vertical-align: top;
}
chat-streaming-md th { font-weight: 600; }
- /* Component-rendered table: make wrapper elements layout-transparent */
- chat-streaming-md chat-md-table { display: contents; }
+ /* Component-rendered table: chat-md-table becomes a horizontally-scrollable
+ wrapper for the inner ; row/cell elements stay layout-transparent
+ so the browser's table layout takes over. Without this overflow wrapper,
+ wide tables push their parent container past the viewport horizontally. */
+ chat-streaming-md chat-md-table {
+ display: block;
+ overflow-x: auto;
+ max-width: 100%;
+ margin: 0 0 0.75rem;
+ }
chat-streaming-md chat-md-table-row { display: contents; }
chat-streaming-md chat-md-table-cell { display: contents; }
+ chat-streaming-md chat-md-table > table { margin: 0; }
/* Task-list items: checkbox + first paragraph render inline; subsequent
blocks (sub-lists, multi-paragraph items) flow normally below. */
chat-streaming-md li.chat-md-list-item--task {
@@ -143,4 +152,21 @@ export const CHAT_MARKDOWN_STYLES = `
/* Media */
chat-streaming-md img { max-width: 100%; height: auto; border-radius: 6px; }
+ /* Broken-image fallback: muted pill showing alt text + icon. Triggered
+ when
fires (error). Caught by live browser smoke — prior impl
+ showed only the browser's broken-image icon with no readable alt. */
+ chat-streaming-md .chat-md-image--broken {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.4rem;
+ padding: 0.25rem 0.5rem;
+ background: var(--ngaf-chat-surface-alt);
+ border: 1px dashed var(--ngaf-chat-separator);
+ border-radius: 6px;
+ font-size: 0.9em;
+ color: var(--ngaf-chat-text-muted, currentColor);
+ opacity: 0.85;
+ }
+ chat-streaming-md .chat-md-image__icon { font-size: 1em; line-height: 1; }
+ chat-streaming-md .chat-md-image__alt { font-style: italic; }
`;
diff --git a/libs/cockpit-docs/package.json b/libs/cockpit-docs/package.json
index bd79fe6c5..3178e2f81 100644
--- a/libs/cockpit-docs/package.json
+++ b/libs/cockpit-docs/package.json
@@ -1,6 +1,6 @@
{
"name": "@ngaf/cockpit-docs",
- "version": "0.0.24",
+ "version": "0.0.25",
"license": "MIT",
"repository": {
"type": "git",
diff --git a/libs/cockpit-registry/package.json b/libs/cockpit-registry/package.json
index 09c65328c..12a43f0c9 100644
--- a/libs/cockpit-registry/package.json
+++ b/libs/cockpit-registry/package.json
@@ -1,6 +1,6 @@
{
"name": "@ngaf/cockpit-registry",
- "version": "0.0.24",
+ "version": "0.0.25",
"license": "MIT",
"repository": {
"type": "git",
diff --git a/libs/cockpit-shell/package.json b/libs/cockpit-shell/package.json
index a2e588291..1b5bab0fa 100644
--- a/libs/cockpit-shell/package.json
+++ b/libs/cockpit-shell/package.json
@@ -1,6 +1,6 @@
{
"name": "@ngaf/cockpit-shell",
- "version": "0.0.24",
+ "version": "0.0.25",
"license": "MIT",
"repository": {
"type": "git",
diff --git a/libs/cockpit-testing/package.json b/libs/cockpit-testing/package.json
index e1ef6b44d..ebc4fe005 100644
--- a/libs/cockpit-testing/package.json
+++ b/libs/cockpit-testing/package.json
@@ -1,6 +1,6 @@
{
"name": "@ngaf/cockpit-testing",
- "version": "0.0.24",
+ "version": "0.0.25",
"license": "MIT",
"repository": {
"type": "git",
diff --git a/libs/cockpit-ui/package.json b/libs/cockpit-ui/package.json
index 3e46e4b6a..fd3c3cfd8 100644
--- a/libs/cockpit-ui/package.json
+++ b/libs/cockpit-ui/package.json
@@ -1,6 +1,6 @@
{
"name": "@ngaf/cockpit-ui",
- "version": "0.0.24",
+ "version": "0.0.25",
"license": "MIT",
"repository": {
"type": "git",
diff --git a/libs/db/package.json b/libs/db/package.json
index a14f756e5..be991ef5c 100644
--- a/libs/db/package.json
+++ b/libs/db/package.json
@@ -1,6 +1,6 @@
{
"name": "@ngaf/db",
- "version": "0.0.24",
+ "version": "0.0.25",
"license": "MIT",
"repository": {
"type": "git",
diff --git a/libs/design-tokens/package.json b/libs/design-tokens/package.json
index 5adea4849..e62f66286 100644
--- a/libs/design-tokens/package.json
+++ b/libs/design-tokens/package.json
@@ -1,6 +1,6 @@
{
"name": "@ngaf/design-tokens",
- "version": "0.0.24",
+ "version": "0.0.25",
"license": "MIT",
"repository": {
"type": "git",
diff --git a/libs/example-layouts/package.json b/libs/example-layouts/package.json
index 4bf750cda..5bb7c4e78 100644
--- a/libs/example-layouts/package.json
+++ b/libs/example-layouts/package.json
@@ -1,6 +1,6 @@
{
"name": "@ngaf/example-layouts",
- "version": "0.0.24",
+ "version": "0.0.25",
"peerDependencies": {
"@angular/core": "^20.0.0 || ^21.0.0",
"@angular/common": "^20.0.0 || ^21.0.0"
diff --git a/libs/langgraph/package.json b/libs/langgraph/package.json
index baf5ea4cf..d6a8ee432 100644
--- a/libs/langgraph/package.json
+++ b/libs/langgraph/package.json
@@ -1,6 +1,6 @@
{
"name": "@ngaf/langgraph",
- "version": "0.0.24",
+ "version": "0.0.25",
"peerDependencies": {
"@ngaf/chat": "*",
"@ngaf/licensing": "*",
diff --git a/libs/licensing/package.json b/libs/licensing/package.json
index b8f803e83..3225d435e 100644
--- a/libs/licensing/package.json
+++ b/libs/licensing/package.json
@@ -1,6 +1,6 @@
{
"name": "@ngaf/licensing",
- "version": "0.0.24",
+ "version": "0.0.25",
"license": "MIT",
"repository": {
"type": "git",
diff --git a/libs/partial-json/package.json b/libs/partial-json/package.json
index a3285d050..912ac296b 100644
--- a/libs/partial-json/package.json
+++ b/libs/partial-json/package.json
@@ -1,6 +1,6 @@
{
"name": "@ngaf/partial-json",
- "version": "0.0.24",
+ "version": "0.0.25",
"deprecated": "Replaced by @cacheplane/partial-json. No further versions will be published from this package.",
"license": "MIT",
"repository": {
diff --git a/libs/render/package.json b/libs/render/package.json
index 08f2fbcb7..c6f96abcc 100644
--- a/libs/render/package.json
+++ b/libs/render/package.json
@@ -1,6 +1,6 @@
{
"name": "@ngaf/render",
- "version": "0.0.24",
+ "version": "0.0.25",
"peerDependencies": {
"@angular/core": "^20.0.0 || ^21.0.0",
"@angular/common": "^20.0.0 || ^21.0.0",
diff --git a/libs/ui-react/package.json b/libs/ui-react/package.json
index 3667e703e..89cfe3893 100644
--- a/libs/ui-react/package.json
+++ b/libs/ui-react/package.json
@@ -1,6 +1,6 @@
{
"name": "@ngaf/ui-react",
- "version": "0.0.24",
+ "version": "0.0.25",
"license": "MIT",
"repository": {
"type": "git",