@nextclaw/ui 0.12.14 → 0.12.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/dist/assets/{api-C51456xV.js → api-BIg--UMJ.js} +1 -1
- package/dist/assets/{app-manager-provider-D_cKqqRG.js → app-manager-provider-BfKiVYea.js} +1 -1
- package/dist/assets/{app-navigation.config-Dve1W20Y.js → app-navigation.config-xIjCAn-R.js} +1 -1
- package/dist/assets/{book-open-B4mOKdz8.js → book-open-CUd69I2f.js} +1 -1
- package/dist/assets/{channels-list-page-WJ7d4zMI.js → channels-list-page-5wQy-UW7.js} +1 -1
- package/dist/assets/chat-CXrERmQ1.js +60 -0
- package/dist/assets/{chat-page-DLFTPfmu.js → chat-page-CTLX0KsB.js} +1 -1
- package/dist/assets/{chunk-JZWAC4HX-ptDyT_1C.js → chunk-JZWAC4HX-THqEFwu9.js} +1 -1
- package/dist/assets/{config-split-page-B3PRA_AV.js → config-split-page-UJSTBsEU.js} +1 -1
- package/dist/assets/{createLucideIcon-C_GFKVuW.js → createLucideIcon-DVAlgDOi.js} +1 -1
- package/dist/assets/{desktop-update-config-CzGi43xw.js → desktop-update-config-B0vQ5JID.js} +1 -1
- package/dist/assets/{dialog-BHcaU6NE.js → dialog-t7OAmObC.js} +1 -1
- package/dist/assets/{dist-DtBFqZ6_.js → dist-DWPNydLC.js} +1 -1
- package/dist/assets/doc-browser-BIggpN8Z.js +1 -0
- package/dist/assets/{doc-browser-CwgI7ipB.js → doc-browser-DznuT-CU.js} +1 -1
- package/dist/assets/{doc-browser-context-Dib9sS83.js → doc-browser-context-zXaTjrpA.js} +1 -1
- package/dist/assets/{doc-browser-CoKIUCJj.js → doc-browser-t96ibd-b.js} +1 -1
- package/dist/assets/{es2015-BlNhrQUG.js → es2015-BRVsmfFO.js} +1 -1
- package/dist/assets/{external-link-DP2IJ7AM.js → external-link-D1Xqff6i.js} +1 -1
- package/dist/assets/{folder-BPwc278w.js → folder-B_fuaX3x.js} +1 -1
- package/dist/assets/{hash-CvcvtMBq.js → hash-BRvv_UUq.js} +1 -1
- package/dist/assets/i18n-CF_jgT_-.js +1 -0
- package/dist/assets/index-DuHqJ8wn.css +1 -0
- package/dist/assets/{index-CAYF44Dz.js → index-mw77my39.js} +2 -2
- package/dist/assets/{key-round-BQXmPSxD.js → key-round-DFVNXZcD.js} +1 -1
- package/dist/assets/loader-circle-DEz3bHGb.js +1 -0
- package/dist/assets/{logo-badge-uB4SwANR.js → logo-badge-CRVKkIl9.js} +1 -1
- package/dist/assets/{logos-BcELLmYh.js → logos-BVCi_7_I.js} +1 -1
- package/dist/assets/marketplace-page-DVmk8dZk.js +1 -0
- package/dist/assets/{marketplace-page-DiqqX25V.js → marketplace-page-apq5LpYx.js} +1 -1
- package/dist/assets/mcp-marketplace-page-CskrJuKU.js +1 -0
- package/dist/assets/{mcp-marketplace-page-C_akqPwv.js → mcp-marketplace-page-DiqTAdRJ.js} +1 -1
- package/dist/assets/message-square-CLhDWybk.js +1 -0
- package/dist/assets/{model-config-B0L43HTL.js → model-config-cth12uRn.js} +1 -1
- package/dist/assets/{notice-card-C9PFAR67.js → notice-card-CXY09tsa.js} +1 -1
- package/dist/assets/play-CnnPm8ca.js +1 -0
- package/dist/assets/plus-CdYMdiws.js +1 -0
- package/dist/assets/{popover-B8msg2FQ.js → popover-CbQxrchk.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-DeAo2Y65.js → provider-scoped-model-input-CZEB2m98.js} +1 -1
- package/dist/assets/{providers-list-5_VShcn7.js → providers-list-Dsj2BYPm.js} +1 -1
- package/dist/assets/{refresh-ccw-CeG203yU.js → refresh-ccw-C-ytTHiq.js} +1 -1
- package/dist/assets/remote-DMMC2PSo.js +1 -0
- package/dist/assets/{rotate-cw-F7aThvYj.js → rotate-cw-ClSrRUa0.js} +1 -1
- package/dist/assets/{runtime-config-page-B-y_0HIS.js → runtime-config-page-JpAUjHTI.js} +1 -1
- package/dist/assets/{save-7ztImRj7.js → save-KxhpE3Zr.js} +1 -1
- package/dist/assets/{search-DZSNKEGp.js → search-Bz3Q64sr.js} +1 -1
- package/dist/assets/{search-config-DJTm9Fno.js → search-config-C_xRBv_i.js} +1 -1
- package/dist/assets/{secrets-config-DKFeFii1.js → secrets-config-BOL024Fj.js} +1 -1
- package/dist/assets/{select-DRDejPLk.js → select-tRTLG4FK.js} +1 -1
- package/dist/assets/{sessions-config-page-CZGqS32n.js → sessions-config-page-CLEEGNYL.js} +1 -1
- package/dist/assets/{setting-row-BcF6eTW0.js → setting-row-DMDgBCC7.js} +1 -1
- package/dist/assets/{settings-DjvNMJde.js → settings-Cto6z-Ij.js} +1 -1
- package/dist/assets/skeleton-BwfJfVK3.js +1 -0
- package/dist/assets/{sparkles-CyDTgTM4.js → sparkles-xZ74eW0P.js} +1 -1
- package/dist/assets/{status-dot-aQU9Mia4.js → status-dot-Bobpfutv.js} +1 -1
- package/dist/assets/{tabs-custom-C4P7g4vR.js → tabs-custom-C3Mf-NLb.js} +1 -1
- package/dist/assets/{tag-chip-CVIqyMv7.js → tag-chip-BZ14i5b1.js} +1 -1
- package/dist/assets/{theme-provider-dHqcWU-j.js → theme-provider-D6Cgm6i-.js} +1 -1
- package/dist/assets/{tooltip-C6VPreZ7.js → tooltip-7lsLGcL9.js} +1 -1
- package/dist/assets/{trash-2-C1cdqL6V.js → trash-2-WFSNa5oj.js} +1 -1
- package/dist/assets/{use-config-DFja1sda.js → use-config-CXu7dFzw.js} +1 -1
- package/dist/assets/{use-confirm-dialog-DvIbSUX3.js → use-confirm-dialog-Df1SozKw.js} +1 -1
- package/dist/assets/{use-infinite-scroll-loader-D8h0k-iL.js → use-infinite-scroll-loader-BogRuzgz.js} +1 -1
- package/dist/assets/{use-viewport-layout-D-pjxsyz.js → use-viewport-layout-CWlW5b-T.js} +1 -1
- package/dist/assets/x-BvS2y4e_.js +1 -0
- package/dist/index.html +39 -39
- package/package.json +4 -4
- package/src/features/chat/components/chat-sidebar-session-item.tsx +22 -24
- package/src/features/chat/components/conversation/chat-input-bar.container.tsx +5 -1
- package/src/features/chat/managers/ncp-chat-input.manager.test.ts +29 -0
- package/src/features/chat/utils/chat-input-bar.utils.test.ts +26 -0
- package/src/features/chat/utils/chat-input-toolbar.utils.ts +19 -18
- package/src/features/chat/utils/ncp-chat-input-availability.utils.test.ts +35 -3
- package/src/features/chat/utils/ncp-chat-input-availability.utils.ts +4 -6
- package/src/index.css +97 -5
- package/src/shared/components/common/agent-avatar.test.tsx +33 -0
- package/src/shared/components/common/agent-avatar.tsx +6 -9
- package/src/shared/lib/i18n/chat.ts +1 -0
- package/dist/assets/chat-BxA-mw53.js +0 -58
- package/dist/assets/doc-browser-DYKpRqe-.js +0 -1
- package/dist/assets/i18n-BnNAQpVM.js +0 -1
- package/dist/assets/index-mRmSAB-e.css +0 -1
- package/dist/assets/loader-circle-C6gg2m2a.js +0 -1
- package/dist/assets/marketplace-page-0sEdt5sA.js +0 -1
- package/dist/assets/mcp-marketplace-page-B8vmu9xe.js +0 -1
- package/dist/assets/message-square-CLVODA23.js +0 -1
- package/dist/assets/play-DeNVUA5C.js +0 -1
- package/dist/assets/plus-BptLViq1.js +0 -1
- package/dist/assets/remote-pzp4oLcL.js +0 -1
- package/dist/assets/skeleton-5Mg6vZHN.js +0 -1
- package/dist/assets/x-BjMO7v8q.js +0 -1
package/dist/index.html
CHANGED
|
@@ -78,45 +78,45 @@
|
|
|
78
78
|
})();
|
|
79
79
|
</script>
|
|
80
80
|
<title>NextClaw</title>
|
|
81
|
-
<script type="module" crossorigin src="/assets/index-
|
|
82
|
-
<link rel="modulepreload" crossorigin href="/assets/i18n-
|
|
83
|
-
<link rel="modulepreload" crossorigin href="/assets/api-
|
|
84
|
-
<link rel="modulepreload" crossorigin href="/assets/es2015-
|
|
85
|
-
<link rel="modulepreload" crossorigin href="/assets/createLucideIcon-
|
|
86
|
-
<link rel="modulepreload" crossorigin href="/assets/select-
|
|
87
|
-
<link rel="modulepreload" crossorigin href="/assets/dist-
|
|
88
|
-
<link rel="modulepreload" crossorigin href="/assets/x-
|
|
89
|
-
<link rel="modulepreload" crossorigin href="/assets/dialog-
|
|
90
|
-
<link rel="modulepreload" crossorigin href="/assets/popover-
|
|
91
|
-
<link rel="modulepreload" crossorigin href="/assets/tooltip-
|
|
92
|
-
<link rel="modulepreload" crossorigin href="/assets/chunk-JZWAC4HX-
|
|
93
|
-
<link rel="modulepreload" crossorigin href="/assets/use-config-
|
|
94
|
-
<link rel="modulepreload" crossorigin href="/assets/theme-provider-
|
|
95
|
-
<link rel="modulepreload" crossorigin href="/assets/search-
|
|
96
|
-
<link rel="modulepreload" crossorigin href="/assets/book-open-
|
|
97
|
-
<link rel="modulepreload" crossorigin href="/assets/external-link-
|
|
98
|
-
<link rel="modulepreload" crossorigin href="/assets/folder-
|
|
99
|
-
<link rel="modulepreload" crossorigin href="/assets/logos-
|
|
100
|
-
<link rel="modulepreload" crossorigin href="/assets/loader-circle-
|
|
101
|
-
<link rel="modulepreload" crossorigin href="/assets/plus-
|
|
102
|
-
<link rel="modulepreload" crossorigin href="/assets/refresh-ccw-
|
|
103
|
-
<link rel="modulepreload" crossorigin href="/assets/settings-
|
|
104
|
-
<link rel="modulepreload" crossorigin href="/assets/sparkles-
|
|
105
|
-
<link rel="modulepreload" crossorigin href="/assets/trash-2-
|
|
106
|
-
<link rel="modulepreload" crossorigin href="/assets/doc-browser-context-
|
|
107
|
-
<link rel="modulepreload" crossorigin href="/assets/doc-browser-
|
|
108
|
-
<link rel="modulepreload" crossorigin href="/assets/doc-browser-
|
|
109
|
-
<link rel="modulepreload" crossorigin href="/assets/use-viewport-layout-
|
|
110
|
-
<link rel="modulepreload" crossorigin href="/assets/logo-badge-
|
|
111
|
-
<link rel="modulepreload" crossorigin href="/assets/skeleton-
|
|
112
|
-
<link rel="modulepreload" crossorigin href="/assets/chat-
|
|
113
|
-
<link rel="modulepreload" crossorigin href="/assets/key-round-
|
|
114
|
-
<link rel="modulepreload" crossorigin href="/assets/message-square-
|
|
115
|
-
<link rel="modulepreload" crossorigin href="/assets/app-navigation.config-
|
|
116
|
-
<link rel="modulepreload" crossorigin href="/assets/notice-card-
|
|
117
|
-
<link rel="modulepreload" crossorigin href="/assets/status-dot-
|
|
118
|
-
<link rel="modulepreload" crossorigin href="/assets/app-manager-provider-
|
|
119
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
81
|
+
<script type="module" crossorigin src="/assets/index-mw77my39.js"></script>
|
|
82
|
+
<link rel="modulepreload" crossorigin href="/assets/i18n-CF_jgT_-.js">
|
|
83
|
+
<link rel="modulepreload" crossorigin href="/assets/api-BIg--UMJ.js">
|
|
84
|
+
<link rel="modulepreload" crossorigin href="/assets/es2015-BRVsmfFO.js">
|
|
85
|
+
<link rel="modulepreload" crossorigin href="/assets/createLucideIcon-DVAlgDOi.js">
|
|
86
|
+
<link rel="modulepreload" crossorigin href="/assets/select-tRTLG4FK.js">
|
|
87
|
+
<link rel="modulepreload" crossorigin href="/assets/dist-DWPNydLC.js">
|
|
88
|
+
<link rel="modulepreload" crossorigin href="/assets/x-BvS2y4e_.js">
|
|
89
|
+
<link rel="modulepreload" crossorigin href="/assets/dialog-t7OAmObC.js">
|
|
90
|
+
<link rel="modulepreload" crossorigin href="/assets/popover-CbQxrchk.js">
|
|
91
|
+
<link rel="modulepreload" crossorigin href="/assets/tooltip-7lsLGcL9.js">
|
|
92
|
+
<link rel="modulepreload" crossorigin href="/assets/chunk-JZWAC4HX-THqEFwu9.js">
|
|
93
|
+
<link rel="modulepreload" crossorigin href="/assets/use-config-CXu7dFzw.js">
|
|
94
|
+
<link rel="modulepreload" crossorigin href="/assets/theme-provider-D6Cgm6i-.js">
|
|
95
|
+
<link rel="modulepreload" crossorigin href="/assets/search-Bz3Q64sr.js">
|
|
96
|
+
<link rel="modulepreload" crossorigin href="/assets/book-open-CUd69I2f.js">
|
|
97
|
+
<link rel="modulepreload" crossorigin href="/assets/external-link-D1Xqff6i.js">
|
|
98
|
+
<link rel="modulepreload" crossorigin href="/assets/folder-B_fuaX3x.js">
|
|
99
|
+
<link rel="modulepreload" crossorigin href="/assets/logos-BVCi_7_I.js">
|
|
100
|
+
<link rel="modulepreload" crossorigin href="/assets/loader-circle-DEz3bHGb.js">
|
|
101
|
+
<link rel="modulepreload" crossorigin href="/assets/plus-CdYMdiws.js">
|
|
102
|
+
<link rel="modulepreload" crossorigin href="/assets/refresh-ccw-C-ytTHiq.js">
|
|
103
|
+
<link rel="modulepreload" crossorigin href="/assets/settings-Cto6z-Ij.js">
|
|
104
|
+
<link rel="modulepreload" crossorigin href="/assets/sparkles-xZ74eW0P.js">
|
|
105
|
+
<link rel="modulepreload" crossorigin href="/assets/trash-2-WFSNa5oj.js">
|
|
106
|
+
<link rel="modulepreload" crossorigin href="/assets/doc-browser-context-zXaTjrpA.js">
|
|
107
|
+
<link rel="modulepreload" crossorigin href="/assets/doc-browser-t96ibd-b.js">
|
|
108
|
+
<link rel="modulepreload" crossorigin href="/assets/doc-browser-DznuT-CU.js">
|
|
109
|
+
<link rel="modulepreload" crossorigin href="/assets/use-viewport-layout-CWlW5b-T.js">
|
|
110
|
+
<link rel="modulepreload" crossorigin href="/assets/logo-badge-CRVKkIl9.js">
|
|
111
|
+
<link rel="modulepreload" crossorigin href="/assets/skeleton-BwfJfVK3.js">
|
|
112
|
+
<link rel="modulepreload" crossorigin href="/assets/chat-CXrERmQ1.js">
|
|
113
|
+
<link rel="modulepreload" crossorigin href="/assets/key-round-DFVNXZcD.js">
|
|
114
|
+
<link rel="modulepreload" crossorigin href="/assets/message-square-CLhDWybk.js">
|
|
115
|
+
<link rel="modulepreload" crossorigin href="/assets/app-navigation.config-xIjCAn-R.js">
|
|
116
|
+
<link rel="modulepreload" crossorigin href="/assets/notice-card-CXY09tsa.js">
|
|
117
|
+
<link rel="modulepreload" crossorigin href="/assets/status-dot-Bobpfutv.js">
|
|
118
|
+
<link rel="modulepreload" crossorigin href="/assets/app-manager-provider-BfKiVYea.js">
|
|
119
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DuHqJ8wn.css">
|
|
120
120
|
</head>
|
|
121
121
|
|
|
122
122
|
<body>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextclaw/ui",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.16",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -28,11 +28,11 @@
|
|
|
28
28
|
"tailwind-merge": "^2.5.4",
|
|
29
29
|
"zod": "^3.23.8",
|
|
30
30
|
"zustand": "^5.0.2",
|
|
31
|
-
"@nextclaw/agent-chat": "0.
|
|
32
|
-
"@nextclaw/agent-chat-ui": "0.3.9",
|
|
31
|
+
"@nextclaw/agent-chat-ui": "0.3.11",
|
|
33
32
|
"@nextclaw/ncp-http-agent-client": "0.3.17",
|
|
34
33
|
"@nextclaw/ncp-react": "0.4.25",
|
|
35
|
-
"@nextclaw/ncp": "0.5.5"
|
|
34
|
+
"@nextclaw/ncp": "0.5.5",
|
|
35
|
+
"@nextclaw/agent-chat": "0.1.10"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
38
|
"@testing-library/react": "^16.3.0",
|
|
@@ -7,7 +7,7 @@ import { Input } from '@/shared/components/ui/input';
|
|
|
7
7
|
import { type SessionContextView } from '@/features/chat/utils/session-context.utils';
|
|
8
8
|
import type { SessionRunStatus } from '@/features/chat/types/session-run-status.types';
|
|
9
9
|
import { cn } from '@/shared/lib/utils';
|
|
10
|
-
import {
|
|
10
|
+
import { formatDateShort, t } from '@/shared/lib/i18n';
|
|
11
11
|
import { Check, GitBranch, Pencil, X } from 'lucide-react';
|
|
12
12
|
|
|
13
13
|
type ChatSidebarSessionItemProps = {
|
|
@@ -115,20 +115,16 @@ function ChatSidebarSessionDisplayView({
|
|
|
115
115
|
onOpenChildSessions,
|
|
116
116
|
onStartEditing
|
|
117
117
|
}: ChatSidebarSessionDisplayViewProps) {
|
|
118
|
-
const
|
|
119
|
-
const normalizedAgentId = agentId?.trim() ?? '';
|
|
120
|
-
const shouldShowAgentAvatar = Boolean(
|
|
121
|
-
normalizedAgentId && normalizedAgentId.toLowerCase() !== 'main',
|
|
122
|
-
);
|
|
118
|
+
const trailingControlsClassName = childSessionCount > 0 && onOpenChildSessions ? 'pr-16' : 'pr-8';
|
|
123
119
|
|
|
124
120
|
return (
|
|
125
121
|
<div className="group/session relative">
|
|
126
122
|
<button type="button" onClick={onSelect} className="w-full text-left">
|
|
127
|
-
<div className=
|
|
123
|
+
<div className={cn('grid grid-cols-[minmax(0,1fr)_auto] items-start gap-2', trailingControlsClassName)}>
|
|
128
124
|
<span className="flex min-w-0 items-center gap-1.5">
|
|
129
|
-
{
|
|
125
|
+
{agentId?.trim() && agentId.trim().toLowerCase() !== 'main' ? (
|
|
130
126
|
<AgentAvatar
|
|
131
|
-
agentId={
|
|
127
|
+
agentId={agentId}
|
|
132
128
|
displayName={agentLabel}
|
|
133
129
|
avatarUrl={agentAvatarUrl}
|
|
134
130
|
className="h-5 w-5 shrink-0"
|
|
@@ -149,26 +145,28 @@ function ChatSidebarSessionDisplayView({
|
|
|
149
145
|
) : null}
|
|
150
146
|
{context.icon ? (
|
|
151
147
|
<span className="inline-flex h-[1.125rem] w-[1.125rem] shrink-0 items-center justify-center">
|
|
152
|
-
<SessionContextIconNode icon={context.icon} className={
|
|
148
|
+
<SessionContextIconNode icon={context.icon} className={active ? 'text-gray-700' : 'text-gray-500'} />
|
|
153
149
|
</span>
|
|
154
150
|
) : null}
|
|
155
151
|
</span>
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
<
|
|
159
|
-
aria-label={t('chatSessionUnread')}
|
|
160
|
-
className="h-2 w-2 rounded-full bg-primary"
|
|
161
|
-
/>
|
|
162
|
-
) : null}
|
|
163
|
-
<span className="inline-flex h-3.5 w-3.5 items-center justify-center">
|
|
164
|
-
{runStatus ? <SessionRunBadge status={runStatus} /> : null}
|
|
152
|
+
{runStatus ? (
|
|
153
|
+
<span className="inline-flex shrink-0 items-center justify-end gap-1.5 pt-0.5">
|
|
154
|
+
<SessionRunBadge status={runStatus} />
|
|
165
155
|
</span>
|
|
166
|
-
|
|
156
|
+
) : null}
|
|
167
157
|
</div>
|
|
168
|
-
<div className="mt-
|
|
169
|
-
<span>
|
|
170
|
-
{agentLabel?.trim() ? `${agentLabel} · ` : ''}{session.messageCount}
|
|
158
|
+
<div className="mt-1 flex items-center gap-2 text-[11px] text-gray-400">
|
|
159
|
+
<span className="min-w-0 truncate">
|
|
160
|
+
{agentLabel?.trim() ? `${agentLabel} · ` : ''}{session.messageCount}
|
|
171
161
|
</span>
|
|
162
|
+
{showUnreadDot ? (
|
|
163
|
+
<span
|
|
164
|
+
aria-label={t('chatSessionUnread')}
|
|
165
|
+
className="ml-auto h-2 w-2 shrink-0 rounded-full bg-primary"
|
|
166
|
+
/>
|
|
167
|
+
) : (
|
|
168
|
+
<span className="ml-auto shrink-0">{formatDateShort(session.updatedAt)}</span>
|
|
169
|
+
)}
|
|
172
170
|
</div>
|
|
173
171
|
</button>
|
|
174
172
|
{childSessionCount > 0 && onOpenChildSessions ? (
|
|
@@ -235,7 +233,7 @@ export function ChatSidebarSessionItem({
|
|
|
235
233
|
return (
|
|
236
234
|
<div
|
|
237
235
|
className={cn(
|
|
238
|
-
'w-full rounded-xl px-3 py-2 text-left transition-all text-[13px]',
|
|
236
|
+
'w-full rounded-xl px-3 py-2.5 text-left transition-all text-[13px]',
|
|
239
237
|
active
|
|
240
238
|
? 'bg-gray-200 text-gray-900 font-semibold shadow-sm'
|
|
241
239
|
: 'text-gray-700 hover:bg-gray-200/60 hover:text-gray-900'
|
|
@@ -6,6 +6,7 @@ import type { SessionSkillEntryView } from '@/shared/lib/api';
|
|
|
6
6
|
import { buildChatSlashItems, buildModelStateHint, buildModelToolbarSelect, buildSkillPickerModel, buildThinkingToolbarSelect, type ChatModelRecord, type ChatSkillRecord, type ChatThinkingLevel } from '@/features/chat/utils/chat-input-bar.utils';
|
|
7
7
|
import { usePresenter } from '@/features/chat/components/providers/chat-presenter.provider';
|
|
8
8
|
import { useI18n } from '@/app/components/i18n-provider';
|
|
9
|
+
import { useViewportLayout } from '@/app/hooks/use-viewport-layout';
|
|
9
10
|
import { useChatInputStore } from '@/features/chat/stores/chat-input.store';
|
|
10
11
|
import { chatRecentModelsManager, CHAT_RECENT_MODELS_MIN_OPTIONS } from '@/features/chat/managers/chat-recent-models.manager';
|
|
11
12
|
import { chatRecentSkillsManager, CHAT_RECENT_SKILLS_MIN_OPTIONS } from '@/features/chat/managers/chat-recent-skills.manager';
|
|
@@ -203,6 +204,7 @@ function buildSkillPicker(params: { allSkillsLabel: string; presenter: ChatPrese
|
|
|
203
204
|
export function ChatInputBarContainer() {
|
|
204
205
|
const presenter = usePresenter();
|
|
205
206
|
const { language } = useI18n();
|
|
207
|
+
const { isMobile } = useViewportLayout();
|
|
206
208
|
const snapshot = useChatInputStore((state) => state.snapshot);
|
|
207
209
|
const runtimeAvailability = useChatRuntimeAvailability();
|
|
208
210
|
const [slashQuery, setSlashQuery] = useState<string | null>(null);
|
|
@@ -215,7 +217,9 @@ export function ChatInputBarContainer() {
|
|
|
215
217
|
const isModelOptionsEmpty = isNcpChatModelOptionsEmpty(snapshot);
|
|
216
218
|
const inputDisabled = isNcpChatComposerDisabled(snapshot);
|
|
217
219
|
const attachmentSupported = typeof presenter.chatInputManager.addAttachments === 'function';
|
|
218
|
-
const textareaPlaceholder = isModelOptionsEmpty
|
|
220
|
+
const textareaPlaceholder = isModelOptionsEmpty
|
|
221
|
+
? t('chatModelNoOptions')
|
|
222
|
+
: t(isMobile ? 'chatInputPlaceholderCompact' : 'chatInputPlaceholder');
|
|
219
223
|
const slashItems = useMemo(
|
|
220
224
|
() => buildChatSlashItems(skillRecords, slashQuery ?? '', labels.slashTexts, recentSkillValues),
|
|
221
225
|
[labels.slashTexts, recentSkillValues, skillRecords, slashQuery]
|
|
@@ -146,4 +146,33 @@ describe('NcpChatInputManager', () => {
|
|
|
146
146
|
expect(streamActionsManager.sendMessage).not.toHaveBeenCalled();
|
|
147
147
|
expect(sessionListManager.promoteRootDraftSessionRoute).not.toHaveBeenCalled();
|
|
148
148
|
});
|
|
149
|
+
|
|
150
|
+
it('still attempts to send when provider metadata is stale or the session type is marked unavailable', async () => {
|
|
151
|
+
useChatInputStore.setState({
|
|
152
|
+
snapshot: {
|
|
153
|
+
...useChatInputStore.getState().snapshot,
|
|
154
|
+
isProviderStateResolved: true,
|
|
155
|
+
modelOptions: [],
|
|
156
|
+
sessionTypeUnavailable: true,
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
const streamActionsManager = {
|
|
160
|
+
sendMessage: vi.fn().mockResolvedValue(undefined),
|
|
161
|
+
stopCurrentRun: vi.fn().mockResolvedValue(undefined),
|
|
162
|
+
} as unknown as ConstructorParameters<typeof NcpChatInputManager>[1];
|
|
163
|
+
const sessionListManager = {
|
|
164
|
+
ensureDraftSession: vi.fn(() => 'draft-session'),
|
|
165
|
+
promoteRootDraftSessionRoute: vi.fn(),
|
|
166
|
+
} as unknown as ConstructorParameters<typeof NcpChatInputManager>[2];
|
|
167
|
+
const manager = new NcpChatInputManager(
|
|
168
|
+
{} as ConstructorParameters<typeof NcpChatInputManager>[0],
|
|
169
|
+
streamActionsManager,
|
|
170
|
+
sessionListManager,
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
await manager.send();
|
|
174
|
+
|
|
175
|
+
expect(streamActionsManager.sendMessage).toHaveBeenCalledTimes(1);
|
|
176
|
+
expect(sessionListManager.promoteRootDraftSessionRoute).toHaveBeenCalledWith('current-route-session');
|
|
177
|
+
});
|
|
149
178
|
});
|
|
@@ -219,6 +219,32 @@ describe('buildModelToolbarSelect', () => {
|
|
|
219
219
|
});
|
|
220
220
|
});
|
|
221
221
|
|
|
222
|
+
it('keeps the full provider/model label in shared state while exposing a compact mobile label', () => {
|
|
223
|
+
const select = buildModelToolbarSelect({
|
|
224
|
+
modelOptions: [
|
|
225
|
+
{
|
|
226
|
+
value: 'anthropic/claude-sonnet-4-very-long-name',
|
|
227
|
+
modelLabel: 'claude-sonnet-4-very-long-name',
|
|
228
|
+
providerLabel: 'Anthropic'
|
|
229
|
+
}
|
|
230
|
+
],
|
|
231
|
+
recentModelValues: [],
|
|
232
|
+
selectedModel: 'anthropic/claude-sonnet-4-very-long-name',
|
|
233
|
+
isModelOptionsLoading: false,
|
|
234
|
+
hasModelOptions: true,
|
|
235
|
+
onValueChange: vi.fn(),
|
|
236
|
+
texts: {
|
|
237
|
+
modelSelectPlaceholder: 'Select model',
|
|
238
|
+
modelNoOptionsLabel: 'No models',
|
|
239
|
+
recentModelsLabel: 'Recent',
|
|
240
|
+
allModelsLabel: 'All models'
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
expect(select.selectedLabel).toBe('Anthropic/claude-sonnet-4-very-long-name');
|
|
245
|
+
expect(select.options[0]?.label).toBe('Anthropic/claude-sonnet-4-very-long-name');
|
|
246
|
+
});
|
|
247
|
+
|
|
222
248
|
it('groups recent models ahead of the remaining catalog', () => {
|
|
223
249
|
const select = buildModelToolbarSelect({
|
|
224
250
|
modelOptions: [
|
|
@@ -52,7 +52,9 @@ export function buildModelStateHint(params: {
|
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
export function buildModelToolbarSelect(
|
|
55
|
+
export function buildModelToolbarSelect({
|
|
56
|
+
modelOptions, recentModelValues, selectedModel, isModelOptionsLoading, hasModelOptions, onValueChange, texts,
|
|
57
|
+
}: {
|
|
56
58
|
modelOptions: ChatModelRecord[];
|
|
57
59
|
recentModelValues?: string[];
|
|
58
60
|
selectedModel: string;
|
|
@@ -67,22 +69,21 @@ export function buildModelToolbarSelect(params: {
|
|
|
67
69
|
| "allModelsLabel"
|
|
68
70
|
>;
|
|
69
71
|
}): ChatToolbarSelect {
|
|
70
|
-
const selectedModelOption =
|
|
71
|
-
(option) => option.value ===
|
|
72
|
+
const selectedModelOption = modelOptions.find(
|
|
73
|
+
(option) => option.value === selectedModel,
|
|
72
74
|
);
|
|
73
|
-
const
|
|
74
|
-
const
|
|
75
|
-
const resolvedValue = params.hasModelOptions
|
|
75
|
+
const resolvedModelOption = selectedModelOption ?? modelOptions[0];
|
|
76
|
+
const resolvedValue = hasModelOptions
|
|
76
77
|
? resolvedModelOption?.value
|
|
77
78
|
: undefined;
|
|
78
|
-
const recentValueSet = new Set(
|
|
79
|
+
const recentValueSet = new Set(recentModelValues ?? []);
|
|
79
80
|
const modelOptionMap = new Map(
|
|
80
|
-
|
|
81
|
+
modelOptions.map((option) => [option.value, option] as const),
|
|
81
82
|
);
|
|
82
|
-
const recentOptions = (
|
|
83
|
+
const recentOptions = (recentModelValues ?? [])
|
|
83
84
|
.map((value) => modelOptionMap.get(value))
|
|
84
85
|
.filter((option): option is ChatModelRecord => Boolean(option));
|
|
85
|
-
const remainingOptions =
|
|
86
|
+
const remainingOptions = modelOptions.filter(
|
|
86
87
|
(option) => !recentValueSet.has(option.value),
|
|
87
88
|
);
|
|
88
89
|
const optionGroups =
|
|
@@ -90,7 +91,7 @@ export function buildModelToolbarSelect(params: {
|
|
|
90
91
|
? [
|
|
91
92
|
{
|
|
92
93
|
key: "recent-models",
|
|
93
|
-
label:
|
|
94
|
+
label: texts.recentModelsLabel,
|
|
94
95
|
options: recentOptions.map((option) => ({
|
|
95
96
|
value: option.value,
|
|
96
97
|
label: formatModelOptionLabel(option),
|
|
@@ -98,7 +99,7 @@ export function buildModelToolbarSelect(params: {
|
|
|
98
99
|
},
|
|
99
100
|
{
|
|
100
101
|
key: "all-models",
|
|
101
|
-
label:
|
|
102
|
+
label: texts.allModelsLabel,
|
|
102
103
|
options: remainingOptions.map((option) => ({
|
|
103
104
|
value: option.value,
|
|
104
105
|
label: formatModelOptionLabel(option),
|
|
@@ -110,20 +111,20 @@ export function buildModelToolbarSelect(params: {
|
|
|
110
111
|
return {
|
|
111
112
|
key: "model",
|
|
112
113
|
value: resolvedValue,
|
|
113
|
-
placeholder:
|
|
114
|
+
placeholder: texts.modelSelectPlaceholder,
|
|
114
115
|
selectedLabel: resolvedModelOption
|
|
115
116
|
? formatModelOptionLabel(resolvedModelOption)
|
|
116
117
|
: undefined,
|
|
117
118
|
icon: "sparkles",
|
|
118
|
-
options:
|
|
119
|
+
options: modelOptions.map((option) => ({
|
|
119
120
|
value: option.value,
|
|
120
121
|
label: formatModelOptionLabel(option),
|
|
121
122
|
})),
|
|
122
123
|
groups: optionGroups,
|
|
123
|
-
disabled: !
|
|
124
|
-
loading:
|
|
125
|
-
emptyLabel:
|
|
126
|
-
onValueChange
|
|
124
|
+
disabled: !hasModelOptions,
|
|
125
|
+
loading: isModelOptionsLoading,
|
|
126
|
+
emptyLabel: texts.modelNoOptionsLabel,
|
|
127
|
+
onValueChange,
|
|
127
128
|
};
|
|
128
129
|
}
|
|
129
130
|
|
|
@@ -41,7 +41,7 @@ function createSnapshot(
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
describe('ncp-chat-input-availability.utils', () => {
|
|
44
|
-
it('keeps the composer editable during cold start while
|
|
44
|
+
it('keeps the composer editable during cold start while runtime blocking still prevents send', () => {
|
|
45
45
|
const snapshot = createSnapshot({
|
|
46
46
|
isProviderStateResolved: false,
|
|
47
47
|
modelOptions: [],
|
|
@@ -60,6 +60,22 @@ describe('ncp-chat-input-availability.utils', () => {
|
|
|
60
60
|
).toBe(true);
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
+
it('does not block send only because model options have not loaded yet', () => {
|
|
64
|
+
const snapshot = createSnapshot({
|
|
65
|
+
isProviderStateResolved: false,
|
|
66
|
+
modelOptions: [],
|
|
67
|
+
sessionTypeUnavailable: false,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
expect(
|
|
71
|
+
isNcpChatSendDisabled({
|
|
72
|
+
snapshot,
|
|
73
|
+
hasSendableDraft: true,
|
|
74
|
+
isRuntimeBlocked: false,
|
|
75
|
+
})
|
|
76
|
+
).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
|
|
63
79
|
it('marks model options as empty only after provider state resolves', () => {
|
|
64
80
|
const loadingSnapshot = createSnapshot({
|
|
65
81
|
isProviderStateResolved: false,
|
|
@@ -74,19 +90,35 @@ describe('ncp-chat-input-availability.utils', () => {
|
|
|
74
90
|
expect(isNcpChatModelOptionsEmpty(emptySnapshot)).toBe(true);
|
|
75
91
|
});
|
|
76
92
|
|
|
77
|
-
it('
|
|
93
|
+
it('keeps editing and sending available when the selected session type reports unavailable', () => {
|
|
78
94
|
const snapshot = createSnapshot({
|
|
79
95
|
isProviderStateResolved: true,
|
|
80
96
|
sessionTypeUnavailable: true,
|
|
81
97
|
});
|
|
82
98
|
|
|
83
|
-
expect(isNcpChatComposerDisabled(snapshot)).toBe(
|
|
99
|
+
expect(isNcpChatComposerDisabled(snapshot)).toBe(false);
|
|
84
100
|
expect(
|
|
85
101
|
isNcpChatSendDisabled({
|
|
86
102
|
snapshot,
|
|
87
103
|
hasSendableDraft: true,
|
|
88
104
|
isRuntimeBlocked: false,
|
|
89
105
|
})
|
|
106
|
+
).toBe(false);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('blocks send when there is no sendable draft', () => {
|
|
110
|
+
const snapshot = createSnapshot({
|
|
111
|
+
isProviderStateResolved: true,
|
|
112
|
+
modelOptions: [],
|
|
113
|
+
sessionTypeUnavailable: true,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
expect(
|
|
117
|
+
isNcpChatSendDisabled({
|
|
118
|
+
snapshot,
|
|
119
|
+
hasSendableDraft: false,
|
|
120
|
+
isRuntimeBlocked: false,
|
|
121
|
+
})
|
|
90
122
|
).toBe(true);
|
|
91
123
|
});
|
|
92
124
|
});
|
|
@@ -24,9 +24,9 @@ export function isNcpChatModelOptionsEmpty(
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export function isNcpChatComposerDisabled(
|
|
27
|
-
|
|
27
|
+
_snapshot: NcpChatInputAvailabilitySnapshot
|
|
28
28
|
): boolean {
|
|
29
|
-
return
|
|
29
|
+
return false;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export function isNcpChatSendDisabled(params: {
|
|
@@ -34,12 +34,10 @@ export function isNcpChatSendDisabled(params: {
|
|
|
34
34
|
snapshot: NcpChatInputAvailabilitySnapshot;
|
|
35
35
|
isRuntimeBlocked: boolean;
|
|
36
36
|
}): boolean {
|
|
37
|
-
const { hasSendableDraft, isRuntimeBlocked
|
|
37
|
+
const { hasSendableDraft, isRuntimeBlocked } = params;
|
|
38
38
|
|
|
39
39
|
return (
|
|
40
40
|
isRuntimeBlocked ||
|
|
41
|
-
!hasSendableDraft
|
|
42
|
-
!hasNcpChatModelOptions(snapshot) ||
|
|
43
|
-
snapshot.sessionTypeUnavailable
|
|
41
|
+
!hasSendableDraft
|
|
44
42
|
);
|
|
45
43
|
}
|
package/src/index.css
CHANGED
|
@@ -220,6 +220,16 @@
|
|
|
220
220
|
--md-code-toolbar-bg: rgba(15, 23, 42, 0.56);
|
|
221
221
|
--md-code-toolbar-border: rgba(148, 163, 184, 0.3);
|
|
222
222
|
--md-code-text: #e5e7eb;
|
|
223
|
+
--md-code-muted: #9ca3af;
|
|
224
|
+
--md-code-keyword: #c084fc;
|
|
225
|
+
--md-code-title: #93c5fd;
|
|
226
|
+
--md-code-string: #86efac;
|
|
227
|
+
--md-code-number: #fbbf24;
|
|
228
|
+
--md-code-comment: #94a3b8;
|
|
229
|
+
--md-code-attr: #67e8f9;
|
|
230
|
+
--md-code-meta: #f0abfc;
|
|
231
|
+
--md-code-deletion: #fca5a5;
|
|
232
|
+
--md-code-addition: #86efac;
|
|
223
233
|
--md-table-head-bg: rgba(148, 163, 184, 0.14);
|
|
224
234
|
|
|
225
235
|
color: var(--md-text);
|
|
@@ -241,6 +251,16 @@
|
|
|
241
251
|
--md-code-toolbar-bg: rgba(2, 6, 23, 0.88);
|
|
242
252
|
--md-code-toolbar-border: rgba(191, 219, 254, 0.26);
|
|
243
253
|
--md-code-text: #e2e8f0;
|
|
254
|
+
--md-code-muted: #a5b4fc;
|
|
255
|
+
--md-code-keyword: #d8b4fe;
|
|
256
|
+
--md-code-title: #bfdbfe;
|
|
257
|
+
--md-code-string: #bbf7d0;
|
|
258
|
+
--md-code-number: #fde68a;
|
|
259
|
+
--md-code-comment: #cbd5e1;
|
|
260
|
+
--md-code-attr: #a5f3fc;
|
|
261
|
+
--md-code-meta: #f5d0fe;
|
|
262
|
+
--md-code-deletion: #fecaca;
|
|
263
|
+
--md-code-addition: #bbf7d0;
|
|
244
264
|
--md-table-head-bg: rgba(191, 219, 254, 0.16);
|
|
245
265
|
}
|
|
246
266
|
|
|
@@ -394,9 +414,12 @@
|
|
|
394
414
|
.chat-codeblock {
|
|
395
415
|
margin: 0.2rem 0 0.1rem;
|
|
396
416
|
border: 1px solid var(--md-code-toolbar-border);
|
|
397
|
-
border-radius: 0.
|
|
417
|
+
border-radius: 0.5rem;
|
|
398
418
|
overflow: hidden;
|
|
399
|
-
background:
|
|
419
|
+
background:
|
|
420
|
+
linear-gradient(180deg, rgba(255, 255, 255, 0.045), rgba(255, 255, 255, 0)),
|
|
421
|
+
var(--md-code-surface);
|
|
422
|
+
box-shadow: 0 12px 34px rgba(15, 23, 42, 0.13);
|
|
400
423
|
}
|
|
401
424
|
|
|
402
425
|
.chat-codeblock-toolbar {
|
|
@@ -426,7 +449,7 @@
|
|
|
426
449
|
font-weight: 600;
|
|
427
450
|
color: var(--md-code-text);
|
|
428
451
|
border: 1px solid var(--md-code-toolbar-border);
|
|
429
|
-
border-radius: 0.
|
|
452
|
+
border-radius: 0.42rem;
|
|
430
453
|
padding: 0.18rem 0.36rem;
|
|
431
454
|
background: transparent;
|
|
432
455
|
cursor: pointer;
|
|
@@ -439,17 +462,86 @@
|
|
|
439
462
|
|
|
440
463
|
.chat-codeblock pre {
|
|
441
464
|
margin: 0;
|
|
442
|
-
padding: 0.
|
|
465
|
+
padding: 0.8rem 0.9rem 0.86rem;
|
|
443
466
|
overflow-x: auto;
|
|
444
467
|
background: transparent;
|
|
468
|
+
scrollbar-color: rgba(148, 163, 184, 0.48) transparent;
|
|
469
|
+
scrollbar-width: thin;
|
|
445
470
|
}
|
|
446
471
|
|
|
447
472
|
.chat-codeblock code {
|
|
448
473
|
display: block;
|
|
449
474
|
min-width: max-content;
|
|
450
475
|
white-space: pre;
|
|
451
|
-
line-height: 1.
|
|
476
|
+
line-height: 1.62;
|
|
452
477
|
color: var(--md-code-text);
|
|
478
|
+
font-size: 0.79rem;
|
|
479
|
+
font-variant-ligatures: none;
|
|
480
|
+
tab-size: 2;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
.chat-codeblock .hljs-keyword,
|
|
484
|
+
.chat-codeblock .hljs-built_in,
|
|
485
|
+
.chat-codeblock .hljs-type,
|
|
486
|
+
.chat-codeblock .hljs-literal,
|
|
487
|
+
.chat-codeblock .hljs-selector-tag {
|
|
488
|
+
color: var(--md-code-keyword);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
.chat-codeblock .hljs-title,
|
|
492
|
+
.chat-codeblock .hljs-title.function_,
|
|
493
|
+
.chat-codeblock .hljs-title.class_,
|
|
494
|
+
.chat-codeblock .hljs-section,
|
|
495
|
+
.chat-codeblock .hljs-selector-id {
|
|
496
|
+
color: var(--md-code-title);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
.chat-codeblock .hljs-string,
|
|
500
|
+
.chat-codeblock .hljs-regexp,
|
|
501
|
+
.chat-codeblock .hljs-symbol,
|
|
502
|
+
.chat-codeblock .hljs-bullet {
|
|
503
|
+
color: var(--md-code-string);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
.chat-codeblock .hljs-number,
|
|
507
|
+
.chat-codeblock .hljs-variable,
|
|
508
|
+
.chat-codeblock .hljs-template-variable,
|
|
509
|
+
.chat-codeblock .hljs-params {
|
|
510
|
+
color: var(--md-code-number);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
.chat-codeblock .hljs-comment,
|
|
514
|
+
.chat-codeblock .hljs-quote {
|
|
515
|
+
color: var(--md-code-comment);
|
|
516
|
+
font-style: italic;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.chat-codeblock .hljs-attr,
|
|
520
|
+
.chat-codeblock .hljs-attribute,
|
|
521
|
+
.chat-codeblock .hljs-property,
|
|
522
|
+
.chat-codeblock .hljs-name,
|
|
523
|
+
.chat-codeblock .hljs-selector-class {
|
|
524
|
+
color: var(--md-code-attr);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
.chat-codeblock .hljs-meta,
|
|
528
|
+
.chat-codeblock .hljs-doctag,
|
|
529
|
+
.chat-codeblock .hljs-tag {
|
|
530
|
+
color: var(--md-code-meta);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
.chat-codeblock .hljs-subst,
|
|
534
|
+
.chat-codeblock .hljs-punctuation,
|
|
535
|
+
.chat-codeblock .hljs-operator {
|
|
536
|
+
color: var(--md-code-muted);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
.chat-codeblock .hljs-deletion {
|
|
540
|
+
color: var(--md-code-deletion);
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
.chat-codeblock .hljs-addition {
|
|
544
|
+
color: var(--md-code-addition);
|
|
453
545
|
}
|
|
454
546
|
|
|
455
547
|
.chat-table-wrap {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { AgentAvatar } from './agent-avatar';
|
|
4
|
+
|
|
5
|
+
describe('AgentAvatar', () => {
|
|
6
|
+
it('uses the configured image for the main agent before fallback', () => {
|
|
7
|
+
render(
|
|
8
|
+
<AgentAvatar
|
|
9
|
+
agentId="main"
|
|
10
|
+
displayName="Main"
|
|
11
|
+
avatarUrl="https://example.com/main-avatar.png"
|
|
12
|
+
/>,
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const avatar = screen.getByRole('img', { name: 'Main' });
|
|
16
|
+
expect(avatar.getAttribute('src')).toBe('https://example.com/main-avatar.png');
|
|
17
|
+
expect(avatar.querySelector('svg')).toBeNull();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('uses the bot icon for the main agent fallback', () => {
|
|
21
|
+
render(<AgentAvatar agentId="main" displayName="Main" />);
|
|
22
|
+
|
|
23
|
+
const avatar = screen.getByLabelText('Main');
|
|
24
|
+
expect(avatar.querySelector('svg')).toBeTruthy();
|
|
25
|
+
expect(avatar.textContent).not.toContain('M');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('keeps letter fallback avatars for specialist agents', () => {
|
|
29
|
+
render(<AgentAvatar agentId="engineer" displayName="Engineer" />);
|
|
30
|
+
|
|
31
|
+
expect(screen.getByLabelText('Engineer').textContent).toBe('E');
|
|
32
|
+
});
|
|
33
|
+
});
|