@nextclaw/ui 0.12.0 → 0.12.2
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 +57 -2
- package/dist/assets/ChannelsList-uKmkpD25.js +8 -0
- package/dist/assets/ChatPage-CslhBPfT.js +43 -0
- package/dist/assets/{DocBrowser-DxdSujSc.js → DocBrowser-C7-1sXqo.js} +1 -1
- package/dist/assets/DocBrowser-DQjtSsY3.js +1 -0
- package/dist/assets/{DocBrowserContext-CQ-8jMha.js → DocBrowserContext-DN5tjUoS.js} +1 -1
- package/dist/assets/{LogoBadge-D-KQIN4U.js → LogoBadge-DDS1sU_U.js} +1 -1
- package/dist/assets/MarketplacePage-BZQW70ti.js +1 -0
- package/dist/assets/{MarketplacePage-CRNvxtvx.js → MarketplacePage-DE0QjYVv.js} +1 -1
- package/dist/assets/{McpMarketplacePage-Cu7GmCcc.js → McpMarketplacePage-CeLvv1xy.js} +1 -1
- package/dist/assets/ModelConfig-D1JtGtQv.js +1 -0
- package/dist/assets/ProviderScopedModelInput-SAJH6nkC.js +1 -0
- package/dist/assets/{ProvidersList-BWbUb7-2.js → ProvidersList-1rKi3aQT.js} +1 -1
- package/dist/assets/{RemoteAccessPage-NsawrZb0.js → RemoteAccessPage-bIAKxDky.js} +1 -1
- package/dist/assets/RuntimeConfig-BTk9319O.js +1 -0
- package/dist/assets/SearchConfig-EjeszXbv.js +1 -0
- package/dist/assets/{SecretsConfig-CgDZOd3w.js → SecretsConfig-cnAXvREZ.js} +1 -1
- package/dist/assets/{SessionsConfig-Dd-KM7F7.js → SessionsConfig-BIXiDaK2.js} +1 -1
- package/dist/assets/{book-open-FnK2xCQd.js → book-open-DvWqOode.js} +1 -1
- package/dist/assets/chat-session-display-D4bYa0b8.js +1 -0
- package/dist/assets/{chunk-JZWAC4HX-B5l0hr_u.js → chunk-JZWAC4HX-CxfKRD7X.js} +1 -1
- package/dist/assets/{config-JKmXfZ3q.js → config-BeGwf2Ao.js} +1 -1
- package/dist/assets/{createLucideIcon-o1WWhwhd.js → createLucideIcon-C7MmdIX3.js} +1 -1
- package/dist/assets/{dist-C_moWYv7.js → dist-B6VMuIQN.js} +1 -1
- package/dist/assets/{dist-DazA6Wd_.js → dist-RWNFhxvR.js} +1 -1
- package/dist/assets/{external-link-BKje3SiD.js → external-link-U86Acd1t.js} +1 -1
- package/dist/assets/{hash-DfW4DT8O.js → hash-D-OVfV3Z.js} +1 -1
- package/dist/assets/i18n-hM3v-3YG.js +1 -0
- package/dist/assets/{index-BZaB1TqM.js → index-8XNPYwJu.js} +3 -3
- package/dist/assets/index-CpxuJa9o.css +1 -0
- package/dist/assets/{label-BzDWmdOe.js → label-CHJ1ATds.js} +1 -1
- package/dist/assets/loader-circle-C8cpaL0w.js +1 -0
- package/dist/assets/{logos-CTLlde_T.js → logos-U1_qDA3U.js} +1 -1
- package/dist/assets/{page-layout-BagR3t59.js → page-layout-Z1klaUFW.js} +1 -1
- package/dist/assets/plus-CrkO1kob.js +1 -0
- package/dist/assets/{popover-5DWhNfd4.js → popover-xWbqMnIN.js} +1 -1
- package/dist/assets/{react-C3yu5yge.js → react-3YE87-lE.js} +1 -1
- package/dist/assets/{refresh-ccw-BAJf-h7w.js → refresh-ccw-JQh1lwq-.js} +1 -1
- package/dist/assets/{save-aa6z4GJL.js → save-4VRlzkii.js} +1 -1
- package/dist/assets/search-EX-Papzl.js +1 -0
- package/dist/assets/{security-config-DRDxrApx.js → security-config-CGazBahs.js} +1 -1
- package/dist/assets/{select-BHJPiJWt.js → select-DF-AUoie.js} +1 -1
- package/dist/assets/skeleton-B0mmt1vo.js +1 -0
- package/dist/assets/{status-dot-DUwsTIdv.js → status-dot-Bq_8Ojvv.js} +1 -1
- package/dist/assets/{switch-B6nCfcOB.js → switch-D7JF_RZ-.js} +1 -1
- package/dist/assets/{tabs-custom-B57SMElx.js → tabs-custom-CLksZ2bO.js} +1 -1
- package/dist/assets/{trash-2-CrjYH5ok.js → trash-2-VV8jvziy.js} +1 -1
- package/dist/assets/{useConfirmDialog-DsxnXB1B.js → useConfirmDialog-D6HxybcM.js} +1 -1
- package/dist/assets/{useMutation-oTTWXgLG.js → useMutation-DBTWPbTg.js} +1 -1
- package/dist/assets/x-B4sxJkGY.js +1 -0
- package/dist/index.html +18 -18
- package/package.json +6 -6
- package/src/api/agents.ts +9 -1
- package/src/api/types.ts +25 -4
- package/src/components/agents/AgentDialogs.tsx +400 -0
- package/src/components/agents/AgentsPage.test.tsx +112 -1
- package/src/components/agents/AgentsPage.tsx +104 -112
- package/src/components/chat/ChatConversationPanel.test.tsx +31 -0
- package/src/components/chat/ChatConversationPanel.tsx +7 -6
- package/src/components/chat/ChatSidebar.test.tsx +41 -1
- package/src/components/chat/ChatWelcome.test.tsx +7 -2
- package/src/components/chat/ChatWelcome.tsx +38 -35
- package/src/components/chat/chat-page-runtime.test.ts +30 -0
- package/src/components/chat/chat-sidebar-session-item.tsx +6 -2
- package/src/components/chat/ncp/NcpChatPage.tsx +23 -1
- package/src/components/chat/ncp/ncp-session-adapter.ts +6 -1
- package/src/components/chat/useChatSessionTypeState.ts +1 -1
- package/src/components/common/ProviderScopedModelInput.tsx +149 -0
- package/src/components/config/ChannelForm.test.tsx +60 -0
- package/src/components/config/ChannelForm.tsx +52 -12
- package/src/components/config/ModelConfig.test.tsx +61 -0
- package/src/components/config/ModelConfig.tsx +15 -90
- package/src/components/config/RuntimeConfig.tsx +3 -24
- package/src/components/config/SearchConfig.test.tsx +150 -0
- package/src/components/config/SearchConfig.tsx +257 -71
- package/src/components/config/runtime-config-agent.utils.ts +5 -4
- package/src/hooks/agents/useAgents.ts +18 -1
- package/src/lib/i18n.agents.ts +21 -2
- package/src/lib/i18n.search.ts +37 -0
- package/src/lib/i18n.ts +6 -28
- package/dist/assets/ChannelsList-NKNKsf1J.js +0 -8
- package/dist/assets/ChatPage-p23OnnEI.js +0 -43
- package/dist/assets/DocBrowser-C8b2uPgL.js +0 -1
- package/dist/assets/MarketplacePage-GGkEXowp.js +0 -1
- package/dist/assets/ModelConfig-CEpx9fro.js +0 -1
- package/dist/assets/RuntimeConfig-BJHBsVTd.js +0 -1
- package/dist/assets/SearchConfig-BsaX_WYy.js +0 -1
- package/dist/assets/chat-session-display-BD_AN71I.js +0 -1
- package/dist/assets/i18n-BK1w-oBy.js +0 -1
- package/dist/assets/index-DaR9igPC.css +0 -1
- package/dist/assets/loader-circle-DdZPxBUz.js +0 -1
- package/dist/assets/plus-DP2PSCPO.js +0 -1
- package/dist/assets/provider-models-DJ29qHuA.js +0 -1
- package/dist/assets/search-pD6ZwQYF.js +0 -1
- package/dist/assets/skeleton-D6kCk9Y6.js +0 -1
- package/dist/assets/x-CTIQHUuD.js +0 -1
|
@@ -1,39 +1,29 @@
|
|
|
1
1
|
import { useMemo, useState } from 'react';
|
|
2
2
|
import { useNavigate } from 'react-router-dom';
|
|
3
|
-
import { useCreateAgent, useDeleteAgent, useAgents } from '@/hooks/agents/useAgents';
|
|
3
|
+
import { useCreateAgent, useDeleteAgent, useAgents, useUpdateAgent } from '@/hooks/agents/useAgents';
|
|
4
|
+
import { useConfig, useConfigMeta } from '@/hooks/useConfig';
|
|
5
|
+
import type { AgentProfileView } from '@/api/types';
|
|
6
|
+
import {
|
|
7
|
+
AgentCreateDialog,
|
|
8
|
+
AgentEditDialog,
|
|
9
|
+
type AgentCreateFormState,
|
|
10
|
+
type AgentEditFormState
|
|
11
|
+
} from '@/components/agents/AgentDialogs';
|
|
12
|
+
import {
|
|
13
|
+
buildSessionTypeOptions,
|
|
14
|
+
normalizeSessionType,
|
|
15
|
+
resolveSessionTypeLabel
|
|
16
|
+
} from '@/components/chat/useChatSessionTypeState';
|
|
4
17
|
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
5
18
|
import { AgentAvatar } from '@/components/common/AgentAvatar';
|
|
6
19
|
import { Button } from '@/components/ui/button';
|
|
7
20
|
import { Card, CardContent } from '@/components/ui/card';
|
|
8
|
-
import {
|
|
9
|
-
Dialog,
|
|
10
|
-
DialogContent,
|
|
11
|
-
DialogDescription,
|
|
12
|
-
DialogFooter,
|
|
13
|
-
DialogHeader,
|
|
14
|
-
DialogTitle
|
|
15
|
-
} from '@/components/ui/dialog';
|
|
16
|
-
import { Input } from '@/components/ui/input';
|
|
21
|
+
import { useNcpChatSessionTypes } from '@/hooks/use-ncp-chat-session-types';
|
|
17
22
|
import { PageLayout } from '@/components/layout/page-layout';
|
|
18
23
|
import { t } from '@/lib/i18n';
|
|
24
|
+
import { buildProviderModelCatalog } from '@/lib/provider-models';
|
|
19
25
|
import { cn } from '@/lib/utils';
|
|
20
|
-
import { Bot, House, MessageCircle, Plus, ShieldCheck, Sparkles, Trash2 } from 'lucide-react';
|
|
21
|
-
|
|
22
|
-
type CreateFormState = {
|
|
23
|
-
id: string;
|
|
24
|
-
displayName: string;
|
|
25
|
-
description: string;
|
|
26
|
-
avatar: string;
|
|
27
|
-
home: string;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const EMPTY_FORM: CreateFormState = {
|
|
31
|
-
id: '',
|
|
32
|
-
displayName: '',
|
|
33
|
-
description: '',
|
|
34
|
-
avatar: '',
|
|
35
|
-
home: ''
|
|
36
|
-
};
|
|
26
|
+
import { Bot, House, MessageCircle, Pencil, Plus, ShieldCheck, Sparkles, Trash2 } from 'lucide-react';
|
|
37
27
|
|
|
38
28
|
const CARD_TONES = [
|
|
39
29
|
{
|
|
@@ -63,10 +53,14 @@ function resolveAgentTone(index: number, builtIn: boolean) {
|
|
|
63
53
|
export function AgentsPage() {
|
|
64
54
|
const navigate = useNavigate();
|
|
65
55
|
const agentsQuery = useAgents();
|
|
56
|
+
const configQuery = useConfig();
|
|
57
|
+
const configMetaQuery = useConfigMeta();
|
|
58
|
+
const sessionTypesQuery = useNcpChatSessionTypes();
|
|
66
59
|
const createAgent = useCreateAgent();
|
|
60
|
+
const updateAgent = useUpdateAgent();
|
|
67
61
|
const deleteAgent = useDeleteAgent();
|
|
68
62
|
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
|
69
|
-
const [
|
|
63
|
+
const [editingAgent, setEditingAgent] = useState<AgentProfileView | null>(null);
|
|
70
64
|
const setSessionListSnapshot = useChatSessionListStore((state) => state.setSnapshot);
|
|
71
65
|
|
|
72
66
|
const agents = useMemo(() => agentsQuery.data?.agents ?? [], [agentsQuery.data?.agents]);
|
|
@@ -79,21 +73,56 @@ export function AgentsPage() {
|
|
|
79
73
|
),
|
|
80
74
|
[agents]
|
|
81
75
|
);
|
|
76
|
+
const providerCatalog = useMemo(
|
|
77
|
+
() => buildProviderModelCatalog({ config: configQuery.data, meta: configMetaQuery.data, onlyConfigured: true }),
|
|
78
|
+
[configMetaQuery.data, configQuery.data]
|
|
79
|
+
);
|
|
80
|
+
const runtimeOptions = useMemo(
|
|
81
|
+
() => buildSessionTypeOptions(sessionTypesQuery.data?.options ?? []),
|
|
82
|
+
[sessionTypesQuery.data?.options]
|
|
83
|
+
);
|
|
84
|
+
const defaultRuntime = useMemo(
|
|
85
|
+
() => normalizeSessionType(sessionTypesQuery.data?.defaultType ?? 'native'),
|
|
86
|
+
[sessionTypesQuery.data?.defaultType]
|
|
87
|
+
);
|
|
88
|
+
const defaultRuntimeLabel = useMemo(
|
|
89
|
+
() => runtimeOptions.find((option) => option.value === defaultRuntime)?.label ?? resolveSessionTypeLabel(defaultRuntime),
|
|
90
|
+
[defaultRuntime, runtimeOptions]
|
|
91
|
+
);
|
|
82
92
|
|
|
83
|
-
const handleCreate = async () => {
|
|
93
|
+
const handleCreate = async (form: AgentCreateFormState) => {
|
|
84
94
|
await createAgent.mutateAsync({
|
|
85
95
|
data: {
|
|
86
96
|
id: form.id,
|
|
87
97
|
...(form.displayName.trim() ? { displayName: form.displayName.trim() } : {}),
|
|
88
98
|
...(form.description.trim() ? { description: form.description.trim() } : {}),
|
|
89
99
|
...(form.avatar.trim() ? { avatar: form.avatar.trim() } : {}),
|
|
90
|
-
...(form.home.trim() ? { home: form.home.trim() } : {})
|
|
100
|
+
...(form.home.trim() ? { home: form.home.trim() } : {}),
|
|
101
|
+
...(form.model.trim() ? { model: form.model.trim() } : {}),
|
|
102
|
+
...(form.runtime.trim() ? { runtime: form.runtime.trim() } : {})
|
|
91
103
|
}
|
|
92
104
|
});
|
|
93
|
-
setForm(EMPTY_FORM);
|
|
94
105
|
setIsCreateDialogOpen(false);
|
|
95
106
|
};
|
|
96
107
|
|
|
108
|
+
const handleStartEdit = (agent: AgentProfileView) => {
|
|
109
|
+
setEditingAgent(agent);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const handleUpdate = async (agentId: string, form: AgentEditFormState) => {
|
|
113
|
+
await updateAgent.mutateAsync({
|
|
114
|
+
agentId,
|
|
115
|
+
data: {
|
|
116
|
+
displayName: form.displayName,
|
|
117
|
+
description: form.description,
|
|
118
|
+
avatar: form.avatar,
|
|
119
|
+
model: form.model,
|
|
120
|
+
...(form.runtime.trim() ? { runtime: form.runtime.trim() } : { runtime: "" })
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
setEditingAgent(null);
|
|
124
|
+
};
|
|
125
|
+
|
|
97
126
|
const startChatWithAgent = (agentId: string) => {
|
|
98
127
|
setSessionListSnapshot({
|
|
99
128
|
selectedAgentId: agentId,
|
|
@@ -178,6 +207,11 @@ export function AgentsPage() {
|
|
|
178
207
|
) : (
|
|
179
208
|
sortedAgents.map((agent, index) => {
|
|
180
209
|
const tone = resolveAgentTone(index, Boolean(agent.builtIn));
|
|
210
|
+
const runtimeValue = agent.runtime?.trim() || agent.engine?.trim() || '';
|
|
211
|
+
const runtimeLabel = runtimeValue
|
|
212
|
+
? runtimeOptions.find((option) => option.value === normalizeSessionType(runtimeValue))?.label ??
|
|
213
|
+
resolveSessionTypeLabel(runtimeValue)
|
|
214
|
+
: defaultRuntimeLabel;
|
|
181
215
|
return (
|
|
182
216
|
<Card
|
|
183
217
|
key={agent.id}
|
|
@@ -223,6 +257,16 @@ export function AgentsPage() {
|
|
|
223
257
|
</p>
|
|
224
258
|
|
|
225
259
|
<div className="mt-auto flex flex-col gap-4">
|
|
260
|
+
<div>
|
|
261
|
+
<div className="flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-[#94a3b8]">
|
|
262
|
+
<Sparkles className="h-3.5 w-3.5" />
|
|
263
|
+
{t('agentsCardRuntimeLabel')}
|
|
264
|
+
</div>
|
|
265
|
+
<div className="mt-1.5 text-sm leading-6 text-[#475569]">
|
|
266
|
+
{runtimeLabel}
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
226
270
|
<div className="border-t border-gray-100 pt-3">
|
|
227
271
|
<div className="flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-[#94a3b8]">
|
|
228
272
|
<House className="h-3.5 w-3.5" />
|
|
@@ -242,6 +286,16 @@ export function AgentsPage() {
|
|
|
242
286
|
<MessageCircle className="mr-2 h-4 w-4" />
|
|
243
287
|
{t('agentsCardStartChat')}
|
|
244
288
|
</Button>
|
|
289
|
+
<Button
|
|
290
|
+
type="button"
|
|
291
|
+
variant="ghost"
|
|
292
|
+
className="h-8 rounded-xl px-3 text-xs text-[#7b8794] hover:bg-[#f3f4f6] hover:text-[#475569]"
|
|
293
|
+
onClick={() => handleStartEdit(agent)}
|
|
294
|
+
disabled={updateAgent.isPending}
|
|
295
|
+
>
|
|
296
|
+
<Pencil className="mr-1.5 h-3.5 w-3.5" />
|
|
297
|
+
{t('agentsEditAction')}
|
|
298
|
+
</Button>
|
|
245
299
|
{!agent.builtIn ? (
|
|
246
300
|
<Button
|
|
247
301
|
type="button"
|
|
@@ -263,91 +317,29 @@ export function AgentsPage() {
|
|
|
263
317
|
)}
|
|
264
318
|
</div>
|
|
265
319
|
|
|
266
|
-
<
|
|
320
|
+
<AgentCreateDialog
|
|
267
321
|
open={isCreateDialogOpen}
|
|
322
|
+
pending={createAgent.isPending}
|
|
323
|
+
providerCatalog={providerCatalog}
|
|
324
|
+
runtimeOptions={runtimeOptions}
|
|
325
|
+
defaultRuntime={defaultRuntime}
|
|
326
|
+
onOpenChange={setIsCreateDialogOpen}
|
|
327
|
+
onSubmit={handleCreate}
|
|
328
|
+
/>
|
|
329
|
+
|
|
330
|
+
<AgentEditDialog
|
|
331
|
+
agent={editingAgent}
|
|
332
|
+
pending={updateAgent.isPending}
|
|
333
|
+
providerCatalog={providerCatalog}
|
|
334
|
+
runtimeOptions={runtimeOptions}
|
|
335
|
+
defaultRuntime={defaultRuntime}
|
|
268
336
|
onOpenChange={(open) => {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
setForm(EMPTY_FORM);
|
|
337
|
+
if (!open && !updateAgent.isPending) {
|
|
338
|
+
setEditingAgent(null);
|
|
272
339
|
}
|
|
273
340
|
}}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
<div className="border-b border-[#f0e2c8] px-6 py-6">
|
|
277
|
-
<DialogHeader className="text-left">
|
|
278
|
-
<DialogTitle>{t('agentsCreateDialogTitle')}</DialogTitle>
|
|
279
|
-
<DialogDescription>{t('agentsCreateDialogDescription')}</DialogDescription>
|
|
280
|
-
</DialogHeader>
|
|
281
|
-
</div>
|
|
282
|
-
<div className="space-y-4 px-6 py-6">
|
|
283
|
-
<div className="grid gap-4 md:grid-cols-2">
|
|
284
|
-
<Input
|
|
285
|
-
value={form.id}
|
|
286
|
-
onChange={(event) =>
|
|
287
|
-
setForm((prev) => ({ ...prev, id: event.target.value }))
|
|
288
|
-
}
|
|
289
|
-
placeholder={t('agentsFormIdPlaceholder')}
|
|
290
|
-
/>
|
|
291
|
-
<Input
|
|
292
|
-
value={form.displayName}
|
|
293
|
-
onChange={(event) =>
|
|
294
|
-
setForm((prev) => ({
|
|
295
|
-
...prev,
|
|
296
|
-
displayName: event.target.value
|
|
297
|
-
}))
|
|
298
|
-
}
|
|
299
|
-
placeholder={t('agentsFormNamePlaceholder')}
|
|
300
|
-
/>
|
|
301
|
-
<Input
|
|
302
|
-
value={form.description}
|
|
303
|
-
onChange={(event) =>
|
|
304
|
-
setForm((prev) => ({
|
|
305
|
-
...prev,
|
|
306
|
-
description: event.target.value
|
|
307
|
-
}))
|
|
308
|
-
}
|
|
309
|
-
placeholder={t('agentsFormDescriptionPlaceholder')}
|
|
310
|
-
/>
|
|
311
|
-
<Input
|
|
312
|
-
value={form.avatar}
|
|
313
|
-
onChange={(event) =>
|
|
314
|
-
setForm((prev) => ({ ...prev, avatar: event.target.value }))
|
|
315
|
-
}
|
|
316
|
-
placeholder={t('agentsFormAvatarPlaceholder')}
|
|
317
|
-
/>
|
|
318
|
-
<Input
|
|
319
|
-
value={form.home}
|
|
320
|
-
onChange={(event) =>
|
|
321
|
-
setForm((prev) => ({ ...prev, home: event.target.value }))
|
|
322
|
-
}
|
|
323
|
-
placeholder={t('agentsFormHomePlaceholder')}
|
|
324
|
-
/>
|
|
325
|
-
</div>
|
|
326
|
-
<div className="rounded-2xl border border-[#efe3ca] bg-[#fff9ef] px-4 py-3 text-xs leading-6 text-[#7a6246]">
|
|
327
|
-
{t('agentsCreateDialogHint')}
|
|
328
|
-
</div>
|
|
329
|
-
</div>
|
|
330
|
-
<DialogFooter className="border-t border-[#f1e7d4] px-6 py-5">
|
|
331
|
-
<Button
|
|
332
|
-
type="button"
|
|
333
|
-
variant="ghost"
|
|
334
|
-
onClick={() => setIsCreateDialogOpen(false)}
|
|
335
|
-
disabled={createAgent.isPending}
|
|
336
|
-
>
|
|
337
|
-
{t('cancel')}
|
|
338
|
-
</Button>
|
|
339
|
-
<Button
|
|
340
|
-
type="button"
|
|
341
|
-
className="rounded-2xl bg-[#1f5c4d] px-5 text-white hover:bg-[#184d40]"
|
|
342
|
-
onClick={() => void handleCreate()}
|
|
343
|
-
disabled={createAgent.isPending || form.id.trim().length === 0}
|
|
344
|
-
>
|
|
345
|
-
<Plus className="mr-2 h-4 w-4" />
|
|
346
|
-
{t('agentsCreateAction')}
|
|
347
|
-
</Button>
|
|
348
|
-
</DialogFooter>
|
|
349
|
-
</DialogContent>
|
|
350
|
-
</Dialog>
|
|
341
|
+
onSubmit={handleUpdate}
|
|
342
|
+
/>
|
|
351
343
|
</PageLayout>
|
|
352
344
|
);
|
|
353
345
|
}
|
|
@@ -104,6 +104,8 @@ describe('ChatConversationPanel', () => {
|
|
|
104
104
|
sessionTypeLabel: 'Codex',
|
|
105
105
|
sessionKey: 'draft-session-1',
|
|
106
106
|
sessionDisplayName: undefined,
|
|
107
|
+
agentId: null,
|
|
108
|
+
agentDisplayName: null,
|
|
107
109
|
sessionProjectRoot: null,
|
|
108
110
|
sessionProjectName: null,
|
|
109
111
|
canDeleteSession: false,
|
|
@@ -146,6 +148,35 @@ describe('ChatConversationPanel', () => {
|
|
|
146
148
|
expect(screen.getByText('project-alpha')).toBeTruthy();
|
|
147
149
|
expect(screen.getByLabelText('More actions')).toBeTruthy();
|
|
148
150
|
});
|
|
151
|
+
|
|
152
|
+
it('does not show a header agent marker for the main agent', () => {
|
|
153
|
+
useChatThreadStore.setState({
|
|
154
|
+
snapshot: {
|
|
155
|
+
...useChatThreadStore.getState().snapshot,
|
|
156
|
+
agentId: 'main',
|
|
157
|
+
agentDisplayName: 'Main',
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
render(<ChatConversationPanel />);
|
|
162
|
+
|
|
163
|
+
expect(screen.queryByTestId('agent-avatar')).toBeNull();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('shows only a lightweight avatar marker for a specialist agent', () => {
|
|
167
|
+
useChatThreadStore.setState({
|
|
168
|
+
snapshot: {
|
|
169
|
+
...useChatThreadStore.getState().snapshot,
|
|
170
|
+
agentId: 'engineer',
|
|
171
|
+
agentDisplayName: 'Engineer',
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
render(<ChatConversationPanel />);
|
|
176
|
+
|
|
177
|
+
expect(screen.getByTestId('agent-avatar').textContent).toBe('engineer');
|
|
178
|
+
expect(screen.queryByText('Engineer')).toBeNull();
|
|
179
|
+
});
|
|
149
180
|
});
|
|
150
181
|
|
|
151
182
|
describe('ChatChildSessionPanel', () => {
|
|
@@ -63,6 +63,10 @@ export function ChatConversationPanel() {
|
|
|
63
63
|
snapshot.sessionDisplayName ||
|
|
64
64
|
(snapshot.canDeleteSession && snapshot.sessionKey ? snapshot.sessionKey : null) ||
|
|
65
65
|
t("chatSidebarNewTask");
|
|
66
|
+
const normalizedAgentId = snapshot.agentId?.trim() ?? "";
|
|
67
|
+
const shouldShowHeaderAgentAvatar =
|
|
68
|
+
normalizedAgentId.length > 0 &&
|
|
69
|
+
normalizedAgentId.toLowerCase() !== "main";
|
|
66
70
|
|
|
67
71
|
const showWelcome =
|
|
68
72
|
!snapshot.canDeleteSession &&
|
|
@@ -119,17 +123,14 @@ export function ChatConversationPanel() {
|
|
|
119
123
|
)}
|
|
120
124
|
>
|
|
121
125
|
<div className="min-w-0 flex-1 flex items-center gap-2">
|
|
122
|
-
{
|
|
123
|
-
<div className="inline-flex
|
|
126
|
+
{shouldShowHeaderAgentAvatar ? (
|
|
127
|
+
<div className="inline-flex shrink-0 items-center">
|
|
124
128
|
<AgentAvatar
|
|
125
|
-
agentId={
|
|
129
|
+
agentId={normalizedAgentId}
|
|
126
130
|
displayName={snapshot.agentDisplayName}
|
|
127
131
|
avatarUrl={snapshot.agentAvatarUrl}
|
|
128
132
|
className="h-5 w-5"
|
|
129
133
|
/>
|
|
130
|
-
<span className="max-w-[120px] truncate text-xs font-medium text-gray-700">
|
|
131
|
-
{snapshot.agentDisplayName?.trim() || snapshot.agentId}
|
|
132
|
-
</span>
|
|
133
134
|
</div>
|
|
134
135
|
) : null}
|
|
135
136
|
<span className="text-sm font-medium text-gray-700 truncate">
|
|
@@ -12,6 +12,7 @@ const mocks = vi.hoisted(() => ({
|
|
|
12
12
|
selectSession: vi.fn(),
|
|
13
13
|
docOpen: vi.fn(),
|
|
14
14
|
updateNcpSession: vi.fn(),
|
|
15
|
+
agents: [] as Array<{ id: string; displayName?: string; avatarUrl?: string | null }>,
|
|
15
16
|
sessionItems: [] as NcpSessionListItemView[],
|
|
16
17
|
isLoading: false
|
|
17
18
|
}));
|
|
@@ -74,7 +75,7 @@ vi.mock('@/components/common/StatusBadge', () => ({
|
|
|
74
75
|
vi.mock('@/hooks/agents/useAgents', () => ({
|
|
75
76
|
useAgents: () => ({
|
|
76
77
|
data: {
|
|
77
|
-
agents:
|
|
78
|
+
agents: mocks.agents
|
|
78
79
|
}
|
|
79
80
|
})
|
|
80
81
|
}));
|
|
@@ -106,6 +107,7 @@ describe('ChatSidebar', () => {
|
|
|
106
107
|
mocks.docOpen.mockReset();
|
|
107
108
|
mocks.updateNcpSession.mockReset();
|
|
108
109
|
mocks.updateNcpSession.mockResolvedValue({});
|
|
110
|
+
mocks.agents = [];
|
|
109
111
|
mocks.sessionItems = [];
|
|
110
112
|
mocks.isLoading = false;
|
|
111
113
|
|
|
@@ -254,6 +256,44 @@ describe('ChatSidebar', () => {
|
|
|
254
256
|
expect(screen.queryByText('Native')).toBeNull();
|
|
255
257
|
});
|
|
256
258
|
|
|
259
|
+
it('hides the sidebar agent avatar for the main agent but keeps specialist avatars', () => {
|
|
260
|
+
mocks.agents = [
|
|
261
|
+
{ id: 'main', displayName: 'Main' },
|
|
262
|
+
{ id: 'engineer', displayName: 'Engineer' }
|
|
263
|
+
];
|
|
264
|
+
mocks.sessionItems = [
|
|
265
|
+
createSessionItem({
|
|
266
|
+
key: 'session:main-1',
|
|
267
|
+
createdAt: '2026-03-19T09:00:00.000Z',
|
|
268
|
+
updatedAt: '2026-03-19T09:05:00.000Z',
|
|
269
|
+
label: 'Main Task',
|
|
270
|
+
sessionType: 'native',
|
|
271
|
+
sessionTypeMutable: false,
|
|
272
|
+
messageCount: 1,
|
|
273
|
+
agentId: 'main'
|
|
274
|
+
}),
|
|
275
|
+
createSessionItem({
|
|
276
|
+
key: 'session:engineer-1',
|
|
277
|
+
createdAt: '2026-03-19T10:00:00.000Z',
|
|
278
|
+
updatedAt: '2026-03-19T10:05:00.000Z',
|
|
279
|
+
label: 'Engineer Task',
|
|
280
|
+
sessionType: 'native',
|
|
281
|
+
sessionTypeMutable: false,
|
|
282
|
+
messageCount: 1,
|
|
283
|
+
agentId: 'engineer'
|
|
284
|
+
})
|
|
285
|
+
];
|
|
286
|
+
|
|
287
|
+
render(
|
|
288
|
+
<MemoryRouter>
|
|
289
|
+
<ChatSidebar />
|
|
290
|
+
</MemoryRouter>
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
expect(screen.queryByLabelText('Main')).toBeNull();
|
|
294
|
+
expect(screen.getByLabelText('Engineer')).not.toBeNull();
|
|
295
|
+
});
|
|
296
|
+
|
|
257
297
|
it('edits the session label inline and saves through the ncp session api by default', async () => {
|
|
258
298
|
mocks.sessionItems = [
|
|
259
299
|
createSessionItem({
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import { fireEvent } from '@testing-library/react';
|
|
2
3
|
import { describe, expect, it, vi } from 'vitest';
|
|
3
4
|
import { ChatWelcome } from '@/components/chat/ChatWelcome';
|
|
4
5
|
|
|
5
6
|
describe('ChatWelcome', () => {
|
|
6
|
-
it('renders draft agent
|
|
7
|
+
it('renders a lightweight draft agent select and allows switching', () => {
|
|
7
8
|
const onCreateSession = vi.fn();
|
|
8
9
|
const onSelectAgent = vi.fn();
|
|
9
10
|
|
|
@@ -19,7 +20,11 @@ describe('ChatWelcome', () => {
|
|
|
19
20
|
/>
|
|
20
21
|
);
|
|
21
22
|
|
|
23
|
+
const trigger = screen.getByRole('combobox', { name: 'Draft agent' });
|
|
24
|
+
fireEvent.keyDown(trigger, { key: 'ArrowDown' });
|
|
22
25
|
fireEvent.click(screen.getByText('Engineer'));
|
|
26
|
+
|
|
23
27
|
expect(onSelectAgent).toHaveBeenCalledWith('engineer');
|
|
28
|
+
expect(screen.queryByText('Current Agent:')).toBeNull();
|
|
24
29
|
});
|
|
25
30
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentProfileView } from '@/api/types';
|
|
2
2
|
import { AgentAvatar } from '@/components/common/AgentAvatar';
|
|
3
|
+
import { Select, SelectContent, SelectItem, SelectTrigger } from '@/components/ui/select';
|
|
3
4
|
import { t } from '@/lib/i18n';
|
|
4
5
|
import { Bot, BrainCircuit, AlarmClock, MessageCircle } from 'lucide-react';
|
|
5
6
|
|
|
@@ -43,43 +44,45 @@ export function ChatWelcome({ onCreateSession, agents, selectedAgentId, onSelect
|
|
|
43
44
|
<h2 className="text-xl font-semibold text-gray-900 mb-2">{t('chatWelcomeTitle')}</h2>
|
|
44
45
|
<p className="text-sm text-gray-500 mb-8">{t('chatWelcomeSubtitle')}</p>
|
|
45
46
|
|
|
46
|
-
<div className="mb-
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
'inline-flex items-center gap-2 rounded-full border px-3 py-2 text-left transition-colors',
|
|
59
|
-
active
|
|
60
|
-
? 'border-gray-900 bg-gray-900 text-white'
|
|
61
|
-
: 'border-gray-200 bg-white text-gray-700 hover:bg-gray-50'
|
|
62
|
-
].join(' ')}
|
|
63
|
-
>
|
|
47
|
+
<div className="mb-6 flex items-center justify-center gap-2.5">
|
|
48
|
+
<span className="text-[13px] font-medium text-gray-500">
|
|
49
|
+
{t('chatDraftAgentTitle')}
|
|
50
|
+
</span>
|
|
51
|
+
<Select value={selectedAgentId} onValueChange={onSelectAgent}>
|
|
52
|
+
<SelectTrigger
|
|
53
|
+
aria-label={t('chatDraftAgentTitle')}
|
|
54
|
+
className="h-auto w-auto gap-1 rounded-full border-0 bg-transparent px-1.5 py-1 text-gray-500 shadow-none hover:bg-white/70 hover:text-gray-800 focus:ring-0"
|
|
55
|
+
>
|
|
56
|
+
<span className="sr-only">{t('chatDraftAgentTitle')}</span>
|
|
57
|
+
<div className="flex items-center gap-1.5">
|
|
58
|
+
{selectedAgent ? (
|
|
64
59
|
<AgentAvatar
|
|
65
|
-
agentId={
|
|
66
|
-
displayName={
|
|
67
|
-
avatarUrl={
|
|
68
|
-
className="h-
|
|
60
|
+
agentId={selectedAgent.id}
|
|
61
|
+
displayName={selectedAgent.displayName}
|
|
62
|
+
avatarUrl={selectedAgent.avatarUrl}
|
|
63
|
+
className="h-7 w-7 shrink-0"
|
|
69
64
|
/>
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
65
|
+
) : null}
|
|
66
|
+
</div>
|
|
67
|
+
</SelectTrigger>
|
|
68
|
+
<SelectContent className="rounded-xl border-gray-200/80 shadow-lg">
|
|
69
|
+
{agents.map((agent) => (
|
|
70
|
+
<SelectItem key={agent.id} value={agent.id} className="rounded-lg pr-10">
|
|
71
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
72
|
+
<AgentAvatar
|
|
73
|
+
agentId={agent.id}
|
|
74
|
+
displayName={agent.displayName}
|
|
75
|
+
avatarUrl={agent.avatarUrl}
|
|
76
|
+
className="h-5 w-5 shrink-0"
|
|
77
|
+
/>
|
|
78
|
+
<span className="truncate text-sm font-medium text-gray-700">
|
|
79
|
+
{agent.displayName?.trim() || agent.id}
|
|
80
|
+
</span>
|
|
81
|
+
</div>
|
|
82
|
+
</SelectItem>
|
|
83
|
+
))}
|
|
84
|
+
</SelectContent>
|
|
85
|
+
</Select>
|
|
83
86
|
</div>
|
|
84
87
|
|
|
85
88
|
{/* Capability cards */}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
resolveSelectedModelValue,
|
|
7
7
|
resolveSelectedThinkingLevelValue
|
|
8
8
|
} from '@/components/chat/chat-session-preference-governance';
|
|
9
|
+
import { shouldRefreshDraftSessionId } from '@/components/chat/ncp/NcpChatPage';
|
|
9
10
|
|
|
10
11
|
const modelOptions = [
|
|
11
12
|
{
|
|
@@ -154,6 +155,35 @@ describe('resolveSelectedModelValue', () => {
|
|
|
154
155
|
});
|
|
155
156
|
});
|
|
156
157
|
|
|
158
|
+
describe('shouldRefreshDraftSessionId', () => {
|
|
159
|
+
it('does not replace the initial draft session id on first mount', () => {
|
|
160
|
+
expect(
|
|
161
|
+
shouldRefreshDraftSessionId({
|
|
162
|
+
previousSelectedSessionKey: undefined,
|
|
163
|
+
nextSelectedSessionKey: null
|
|
164
|
+
})
|
|
165
|
+
).toBe(false);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('replaces the draft session id after leaving an existing session', () => {
|
|
169
|
+
expect(
|
|
170
|
+
shouldRefreshDraftSessionId({
|
|
171
|
+
previousSelectedSessionKey: 'session-1',
|
|
172
|
+
nextSelectedSessionKey: null
|
|
173
|
+
})
|
|
174
|
+
).toBe(true);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('does not replace the draft session id while staying on the same session', () => {
|
|
178
|
+
expect(
|
|
179
|
+
shouldRefreshDraftSessionId({
|
|
180
|
+
previousSelectedSessionKey: 'session-1',
|
|
181
|
+
nextSelectedSessionKey: 'session-1'
|
|
182
|
+
})
|
|
183
|
+
).toBe(false);
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
157
187
|
describe('resolveRecentSessionPreferredModel', () => {
|
|
158
188
|
it('returns the most recent preferred model from the same runtime', () => {
|
|
159
189
|
const sessions = [
|
|
@@ -50,6 +50,10 @@ export function ChatSidebarSessionItem(props: ChatSidebarSessionItemProps) {
|
|
|
50
50
|
} = props;
|
|
51
51
|
|
|
52
52
|
const iconTone = active ? 'text-gray-700' : 'text-gray-500';
|
|
53
|
+
const normalizedAgentId = agentId?.trim() ?? '';
|
|
54
|
+
const shouldShowAgentAvatar = Boolean(
|
|
55
|
+
normalizedAgentId && normalizedAgentId.toLowerCase() !== 'main',
|
|
56
|
+
);
|
|
53
57
|
|
|
54
58
|
return (
|
|
55
59
|
<div
|
|
@@ -112,9 +116,9 @@ export function ChatSidebarSessionItem(props: ChatSidebarSessionItemProps) {
|
|
|
112
116
|
<button type="button" onClick={onSelect} className="w-full text-left">
|
|
113
117
|
<div className="grid grid-cols-[minmax(0,1fr)_0.875rem] items-center gap-1.5 pr-8">
|
|
114
118
|
<span className="flex min-w-0 items-center gap-1.5">
|
|
115
|
-
{
|
|
119
|
+
{shouldShowAgentAvatar ? (
|
|
116
120
|
<AgentAvatar
|
|
117
|
-
agentId={
|
|
121
|
+
agentId={normalizedAgentId}
|
|
118
122
|
displayName={agentLabel}
|
|
119
123
|
avatarUrl={agentAvatarUrl}
|
|
120
124
|
className="h-5 w-5 shrink-0"
|
|
@@ -57,6 +57,7 @@ export function buildNcpSendMetadata(payload: {
|
|
|
57
57
|
}
|
|
58
58
|
if (payload.sessionType?.trim()) {
|
|
59
59
|
metadata.session_type = payload.sessionType.trim();
|
|
60
|
+
metadata.runtime = payload.sessionType.trim();
|
|
60
61
|
}
|
|
61
62
|
if (payload.agentId?.trim()) {
|
|
62
63
|
metadata.agent_id = payload.agentId.trim();
|
|
@@ -78,6 +79,17 @@ export function buildNcpSendMetadata(payload: {
|
|
|
78
79
|
return metadata;
|
|
79
80
|
}
|
|
80
81
|
|
|
82
|
+
export function shouldRefreshDraftSessionId(params: {
|
|
83
|
+
previousSelectedSessionKey: string | null | undefined;
|
|
84
|
+
nextSelectedSessionKey: string | null;
|
|
85
|
+
}): boolean {
|
|
86
|
+
return (
|
|
87
|
+
params.nextSelectedSessionKey === null &&
|
|
88
|
+
params.previousSelectedSessionKey !== undefined &&
|
|
89
|
+
params.previousSelectedSessionKey !== null
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
81
93
|
export function NcpChatPage({ view }: ChatPageProps) {
|
|
82
94
|
const [presenter] = useState(() => new NcpChatPresenter());
|
|
83
95
|
const [draftSessionId, setDraftSessionId] = useState(() =>
|
|
@@ -111,6 +123,9 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
111
123
|
}>();
|
|
112
124
|
const threadRef = useRef<HTMLDivElement | null>(null);
|
|
113
125
|
const selectedSessionKeyRef = useRef<string | null>(selectedSessionKey);
|
|
126
|
+
const previousSelectedSessionKeyRef = useRef<string | null | undefined>(
|
|
127
|
+
undefined,
|
|
128
|
+
);
|
|
114
129
|
const routeSessionKey = useMemo(
|
|
115
130
|
() => parseSessionKeyFromRoute(routeSessionIdParam),
|
|
116
131
|
[routeSessionIdParam],
|
|
@@ -153,11 +168,18 @@ export function NcpChatPage({ view }: ChatPageProps) {
|
|
|
153
168
|
}, [draftSessionId, presenter]);
|
|
154
169
|
|
|
155
170
|
useEffect(() => {
|
|
156
|
-
if (
|
|
171
|
+
if (
|
|
172
|
+
shouldRefreshDraftSessionId({
|
|
173
|
+
previousSelectedSessionKey:
|
|
174
|
+
previousSelectedSessionKeyRef.current,
|
|
175
|
+
nextSelectedSessionKey: selectedSessionKey,
|
|
176
|
+
})
|
|
177
|
+
) {
|
|
157
178
|
const nextDraftSessionId = createNcpSessionId();
|
|
158
179
|
setDraftSessionId(nextDraftSessionId);
|
|
159
180
|
presenter.setDraftSessionId(nextDraftSessionId);
|
|
160
181
|
}
|
|
182
|
+
previousSelectedSessionKeyRef.current = selectedSessionKey;
|
|
161
183
|
}, [presenter, selectedSessionKey]);
|
|
162
184
|
|
|
163
185
|
const effectiveSessionProjectRoot = hasSessionProjectRootOverride
|