Skip to content

Commit 5886efc

Browse files
authored
Merge branch 'main' into claude/fix-c-input-double-submit
2 parents 5de19e2 + 971d645 commit 5886efc

3 files changed

Lines changed: 55 additions & 10 deletions

File tree

libs/chat/src/lib/a2ui/catalog/icon.component.spec.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
// SPDX-License-Identifier: MIT
22
import { describe, it, expect } from 'vitest';
3-
import { A2uiIconComponent } from './icon.component';
3+
import { A2uiIconComponent, toMaterialSymbolName } from './icon.component';
44

55
describe('A2uiIconComponent', () => {
66
// Display-only component: renders name() input as a <span>.
@@ -11,3 +11,24 @@ describe('A2uiIconComponent', () => {
1111
expect(A2uiIconComponent).toBeDefined();
1212
});
1313
});
14+
15+
describe('toMaterialSymbolName', () => {
16+
it('converts camelCase identifiers to snake_case ligatures', () => {
17+
expect(toMaterialSymbolName('accountCircle')).toBe('account_circle');
18+
expect(toMaterialSymbolName('shoppingCart')).toBe('shopping_cart');
19+
expect(toMaterialSymbolName('moreVert')).toBe('more_vert');
20+
expect(toMaterialSymbolName('visibilityOff')).toBe('visibility_off');
21+
expect(toMaterialSymbolName('arrowForward')).toBe('arrow_forward');
22+
});
23+
24+
it('passes single-word and already-snake_case names through unchanged', () => {
25+
expect(toMaterialSymbolName('check')).toBe('check');
26+
expect(toMaterialSymbolName('star')).toBe('star');
27+
expect(toMaterialSymbolName('trending_up')).toBe('trending_up');
28+
});
29+
30+
it('leaves non-identifier glyphs (emoji) untouched', () => {
31+
expect(toMaterialSymbolName('✓')).toBe('✓');
32+
expect(toMaterialSymbolName('⚠️')).toBe('⚠️');
33+
});
34+
});

libs/chat/src/lib/a2ui/catalog/icon.component.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,20 @@
22
import { Component, computed, input } from '@angular/core';
33
import type { Spec } from '@json-render/core';
44

5+
/**
6+
* Convert an icon identifier to its Material Symbols ligature form.
7+
*
8+
* Material Symbols ligatures are snake_case (`account_circle`, `trending_up`),
9+
* but A2UI catalogs commonly emit camelCase identifiers (`accountCircle`,
10+
* `shoppingCart`). Splitting on lower→upper boundaries and lowercasing maps
11+
* camelCase → the matching ligature. Already-snake_case names, single words,
12+
* and non-identifier glyphs (emoji) have no boundaries to split and pass
13+
* through unchanged. Unknown names still fall back to the browser default.
14+
*/
15+
export function toMaterialSymbolName(name: string): string {
16+
return name.replace(/([a-z0-9])([A-Z])/g, '$1_$2').toLowerCase();
17+
}
18+
519
@Component({
620
selector: 'a2ui-icon',
721
standalone: true,
@@ -12,7 +26,7 @@ import type { Spec } from '@json-render/core';
1226
[style.font-size]="size() ? size() + 'px' : '1.125rem'"
1327
[attr.aria-label]="name"
1428
role="img"
15-
>{{ name }}</span>
29+
>{{ glyphName() }}</span>
1630
}
1731
`,
1832
styles: [`
@@ -56,4 +70,7 @@ export class A2uiIconComponent {
5670
readonly spec = input<Spec | undefined>(undefined);
5771

5872
protected readonly effectiveName = computed(() => this.name() ?? this.icon());
73+
74+
/** The effective name as a Material Symbols ligature (camelCase → snake_case). */
75+
protected readonly glyphName = computed(() => toMaterialSymbolName(this.effectiveName()));
5976
}

libs/chat/src/lib/compositions/chat/chat.component.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -248,14 +248,21 @@ export function isPinned(
248248
/>
249249
}
250250
}
251-
<chat-message-actions
252-
chatMessageControls
253-
[content]="content"
254-
[disabled]="agent().isLoading()"
255-
(regenerate)="onRegenerate(i)"
256-
(rate)="onRate(message, $event)"
257-
(contentCopied)="onCopy(message, $event)"
258-
/>
251+
<!-- Only show message actions when there is copyable assistant
252+
text. Content-less messages (a bare tool call or a subagent
253+
delegation card) have nothing to copy/regenerate/rate, so the
254+
actions panel is pure whitespace there — suppress it to keep
255+
the stream compact. -->
256+
@if (content.trim()) {
257+
<chat-message-actions
258+
chatMessageControls
259+
[content]="content"
260+
[disabled]="agent().isLoading()"
261+
(regenerate)="onRegenerate(i)"
262+
(rate)="onRate(message, $event)"
263+
(contentCopied)="onCopy(message, $event)"
264+
/>
265+
}
259266
</chat-message>
260267
</ng-template>
261268

0 commit comments

Comments
 (0)