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",