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
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.24",
"version": "0.0.25",
"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.24",
"version": "0.0.25",
"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.24",
"version": "0.0.25",
"exports": {
".": {
"types": "./index.d.ts",
Expand Down
1 change: 1 addition & 0 deletions libs/chat/src/lib/compositions/chat/chat.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ import type { ChatRenderEvent } from './chat-render-event';
@let classified = classifyMessage(content, message);
<chat-message
[role]="'assistant'"
[message]="message"
[prevRole]="prevRole(i)"
[streaming]="agent().isLoading() && i === agent().messages().length - 1"
[current]="i === agent().messages().length - 1"
Expand Down
26 changes: 24 additions & 2 deletions libs/chat/src/lib/markdown/views/markdown-image.component.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,36 @@
// libs/chat/src/lib/markdown/views/markdown-image.component.ts
// SPDX-License-Identifier: MIT
import { Component, ChangeDetectionStrategy, input } from '@angular/core';
import { ChangeDetectionStrategy, Component, input, signal } from '@angular/core';
import type { MarkdownImageNode } from '@cacheplane/partial-markdown';

@Component({
selector: 'chat-md-image',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<img [src]="node().url" [alt]="node().alt" [attr.title]="node().title || null" />`,
template: `
@if (failed()) {
<span class="chat-md-image chat-md-image--broken"
role="img"
[attr.aria-label]="node().alt || node().url"
[attr.title]="node().title || node().url || null">
<span class="chat-md-image__icon" aria-hidden="true">🖼️</span>
@if (node().alt) {
<span class="chat-md-image__alt">{{ node().alt }}</span>
} @else {
<span class="chat-md-image__alt">image unavailable</span>
}
</span>
} @else {
<img
[src]="node().url"
[alt]="node().alt"
[attr.title]="node().title || null"
(error)="failed.set(true)"
/>
}
`,
})
export class MarkdownImageComponent {
readonly node = input.required<MarkdownImageNode>();
protected readonly failed = signal(false);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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('');
});
});
25 changes: 25 additions & 0 deletions libs/chat/src/lib/primitives/chat-input/chat-input.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import {
Component,
computed,
effect,
input,
output,
signal,
Expand Down Expand Up @@ -116,6 +117,30 @@ export class ChatInputComponent {

private readonly textareaEl = viewChild<ElementRef<HTMLTextAreaElement>>('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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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')"
Expand All @@ -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')"
Expand Down Expand Up @@ -99,25 +101,38 @@ export class ChatMessageActionsComponent {
protected async onCopy(): Promise<void> {
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.
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLButtonElement>('.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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
30 changes: 28 additions & 2 deletions libs/chat/src/lib/styles/chat-markdown.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <table>; 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 {
Expand Down Expand Up @@ -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 <img> 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; }
`;
2 changes: 1 addition & 1 deletion libs/cockpit-docs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ngaf/cockpit-docs",
"version": "0.0.24",
"version": "0.0.25",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion libs/cockpit-registry/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ngaf/cockpit-registry",
"version": "0.0.24",
"version": "0.0.25",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion libs/cockpit-shell/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ngaf/cockpit-shell",
"version": "0.0.24",
"version": "0.0.25",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion libs/cockpit-testing/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ngaf/cockpit-testing",
"version": "0.0.24",
"version": "0.0.25",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion libs/cockpit-ui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ngaf/cockpit-ui",
"version": "0.0.24",
"version": "0.0.25",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion libs/db/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ngaf/db",
"version": "0.0.24",
"version": "0.0.25",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion libs/design-tokens/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ngaf/design-tokens",
"version": "0.0.24",
"version": "0.0.25",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion libs/example-layouts/package.json
Original file line number Diff line number Diff line change
@@ -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"
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.24",
"version": "0.0.25",
"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.24",
"version": "0.0.25",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
2 changes: 1 addition & 1 deletion libs/partial-json/package.json
Original file line number Diff line number Diff line change
@@ -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": {
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.24",
"version": "0.0.25",
"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/ui-react/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ngaf/ui-react",
"version": "0.0.24",
"version": "0.0.25",
"license": "MIT",
"repository": {
"type": "git",
Expand Down
Loading