@nextclaw/ui 0.12.1 → 0.12.3
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 +44 -0
- package/dist/assets/ChannelsList-DZWam3Ob.js +8 -0
- package/dist/assets/ChatPage-YBL7iJ1X.js +43 -0
- package/dist/assets/{DocBrowser-DjcghYGO.js → DocBrowser-C7-1sXqo.js} +1 -1
- package/dist/assets/DocBrowser-DQjtSsY3.js +1 -0
- package/dist/assets/{DocBrowserContext-CLlq7rZQ.js → DocBrowserContext-DN5tjUoS.js} +1 -1
- package/dist/assets/{LogoBadge-D_dOy5U3.js → LogoBadge-DDS1sU_U.js} +1 -1
- package/dist/assets/{MarketplacePage-BlIeNn3x.js → MarketplacePage-2tWWgwAb.js} +1 -1
- package/dist/assets/MarketplacePage-BorWJftJ.js +1 -0
- package/dist/assets/{McpMarketplacePage-mz2_IX1O.js → McpMarketplacePage-N-fB4HID.js} +1 -1
- package/dist/assets/ModelConfig-DvsBTUiE.js +1 -0
- package/dist/assets/ProviderScopedModelInput-D9woCARc.js +1 -0
- package/dist/assets/{ProvidersList-B0RCb_Vg.js → ProvidersList-D-qPGgC4.js} +1 -1
- package/dist/assets/{RemoteAccessPage-CcfQjLtx.js → RemoteAccessPage-COnjm8_x.js} +1 -1
- package/dist/assets/RuntimeConfig-BHpqcaHm.js +1 -0
- package/dist/assets/SearchConfig-DIT6M65Q.js +1 -0
- package/dist/assets/{SecretsConfig-DbiS3txa.js → SecretsConfig-Cefg1LFJ.js} +1 -1
- package/dist/assets/{SessionsConfig-CbIOcAp8.js → SessionsConfig-BZnmVTIu.js} +1 -1
- package/dist/assets/{book-open-BLxSL7Dk.js → book-open-DvWqOode.js} +1 -1
- package/dist/assets/chat-session-display-D4bYa0b8.js +1 -0
- package/dist/assets/{chunk-JZWAC4HX-Bp0t5xoO.js → chunk-JZWAC4HX-CxfKRD7X.js} +1 -1
- package/dist/assets/{config-C96FWufn.js → config-BeGwf2Ao.js} +1 -1
- package/dist/assets/{createLucideIcon-B_U7Nq4F.js → createLucideIcon-C7MmdIX3.js} +1 -1
- package/dist/assets/{dist-D9pHzW9z.js → dist-B6VMuIQN.js} +1 -1
- package/dist/assets/{dist-BFY-GyT4.js → dist-RWNFhxvR.js} +1 -1
- package/dist/assets/{external-link-BydIQTIH.js → external-link-U86Acd1t.js} +1 -1
- package/dist/assets/{hash-Djdf0x1C.js → hash-D-OVfV3Z.js} +1 -1
- package/dist/assets/i18n-hM3v-3YG.js +1 -0
- package/dist/assets/index-CpxuJa9o.css +1 -0
- package/dist/assets/{index-DqSv8Azv.js → index-DHmCjcxq.js} +3 -3
- package/dist/assets/{label-Bvv4Mrea.js → label-CHJ1ATds.js} +1 -1
- package/dist/assets/loader-circle-C8cpaL0w.js +1 -0
- package/dist/assets/{logos-CGJJRI5_.js → logos-U1_qDA3U.js} +1 -1
- package/dist/assets/{page-layout-6Nm4Cnvr.js → page-layout-Z1klaUFW.js} +1 -1
- package/dist/assets/plus-CrkO1kob.js +1 -0
- package/dist/assets/{popover-b9rSYI6X.js → popover-xWbqMnIN.js} +1 -1
- package/dist/assets/{react-CDZz_StC.js → react-3YE87-lE.js} +1 -1
- package/dist/assets/{refresh-ccw-BvSSnnCw.js → refresh-ccw-JQh1lwq-.js} +1 -1
- package/dist/assets/{save-CAf0_-b9.js → save-4VRlzkii.js} +1 -1
- package/dist/assets/search-EX-Papzl.js +1 -0
- package/dist/assets/{security-config-DF66-l25.js → security-config-DEgOD4VX.js} +1 -1
- package/dist/assets/{select-CEIMqc0H.js → select-DF-AUoie.js} +1 -1
- package/dist/assets/skeleton-B0mmt1vo.js +1 -0
- package/dist/assets/{status-dot-CmQI5Qq2.js → status-dot-Bq_8Ojvv.js} +1 -1
- package/dist/assets/{switch-B7SxDXyR.js → switch-D7JF_RZ-.js} +1 -1
- package/dist/assets/{tabs-custom-Dxt6EJJW.js → tabs-custom-CLksZ2bO.js} +1 -1
- package/dist/assets/{trash-2-BnQ1PDTw.js → trash-2-VV8jvziy.js} +1 -1
- package/dist/assets/{useConfirmDialog-B-vMOmhG.js → useConfirmDialog-CuQqiPx7.js} +1 -1
- package/dist/assets/{useMutation-Bi39Z9_J.js → useMutation-DBTWPbTg.js} +1 -1
- package/dist/assets/x-B4sxJkGY.js +1 -0
- package/dist/index.html +18 -18
- package/package.json +4 -4
- package/src/api/agents.ts +9 -1
- package/src/api/types.ts +25 -1
- package/src/components/agents/AgentDialogs.tsx +400 -0
- package/src/components/agents/AgentsPage.test.tsx +148 -1
- package/src/components/agents/AgentsPage.tsx +114 -115
- package/src/components/chat/ChatConversationPanel.test.tsx +69 -3
- package/src/components/chat/ChatConversationPanel.tsx +24 -3
- package/src/components/chat/managers/chat-session-list.manager.test.ts +34 -3
- package/src/components/chat/managers/chat-session-list.manager.ts +17 -4
- package/src/components/chat/ncp/NcpChatPage.tsx +1 -0
- package/src/components/chat/ncp/ncp-chat.presenter.ts +5 -1
- package/src/components/chat/ncp/ncp-session-adapter.ts +6 -1
- package/src/components/chat/stores/chat-session-list.store.ts +6 -1
- package/src/components/chat/useChatSessionTypeState.ts +10 -2
- 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 +2 -2
- 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 +19 -0
- package/src/lib/i18n.search.ts +37 -0
- package/src/lib/i18n.ts +6 -26
- package/dist/assets/ChannelsList-DekMP4a3.js +0 -8
- package/dist/assets/ChatPage-Dgw4vlDt.js +0 -43
- package/dist/assets/DocBrowser-CExjX5is.js +0 -1
- package/dist/assets/MarketplacePage-DGfzg1LG.js +0 -1
- package/dist/assets/ModelConfig-C_49_a9v.js +0 -1
- package/dist/assets/RuntimeConfig-DBWzwoY-.js +0 -1
- package/dist/assets/SearchConfig-jSdwlH4b.js +0 -1
- package/dist/assets/chat-session-display-8yW6-mtm.js +0 -1
- package/dist/assets/i18n-DAekxt_G.js +0 -1
- package/dist/assets/index-CHEgQIiO.css +0 -1
- package/dist/assets/loader-circle-CGXXikVG.js +0 -1
- package/dist/assets/plus-CrW9BJDy.js +0 -1
- package/dist/assets/provider-models-IJDA940D.js +0 -1
- package/dist/assets/search-DgoXxocn.js +0 -1
- package/dist/assets/skeleton-BiPUQkOD.js +0 -1
- package/dist/assets/x-PBSiWt3l.js +0 -1
|
@@ -1,39 +1,31 @@
|
|
|
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
|
+
resolveAgentRuntimeSessionType,
|
|
16
|
+
resolveSessionTypeLabel
|
|
17
|
+
} from '@/components/chat/useChatSessionTypeState';
|
|
18
|
+
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
4
19
|
import { useChatSessionListStore } from '@/components/chat/stores/chat-session-list.store';
|
|
5
20
|
import { AgentAvatar } from '@/components/common/AgentAvatar';
|
|
6
21
|
import { Button } from '@/components/ui/button';
|
|
7
22
|
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';
|
|
23
|
+
import { useNcpChatSessionTypes } from '@/hooks/use-ncp-chat-session-types';
|
|
17
24
|
import { PageLayout } from '@/components/layout/page-layout';
|
|
18
25
|
import { t } from '@/lib/i18n';
|
|
26
|
+
import { buildProviderModelCatalog } from '@/lib/provider-models';
|
|
19
27
|
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
|
-
};
|
|
28
|
+
import { Bot, House, MessageCircle, Pencil, Plus, ShieldCheck, Sparkles, Trash2 } from 'lucide-react';
|
|
37
29
|
|
|
38
30
|
const CARD_TONES = [
|
|
39
31
|
{
|
|
@@ -63,10 +55,14 @@ function resolveAgentTone(index: number, builtIn: boolean) {
|
|
|
63
55
|
export function AgentsPage() {
|
|
64
56
|
const navigate = useNavigate();
|
|
65
57
|
const agentsQuery = useAgents();
|
|
58
|
+
const configQuery = useConfig();
|
|
59
|
+
const configMetaQuery = useConfigMeta();
|
|
60
|
+
const sessionTypesQuery = useNcpChatSessionTypes();
|
|
66
61
|
const createAgent = useCreateAgent();
|
|
62
|
+
const updateAgent = useUpdateAgent();
|
|
67
63
|
const deleteAgent = useDeleteAgent();
|
|
68
64
|
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
|
69
|
-
const [
|
|
65
|
+
const [editingAgent, setEditingAgent] = useState<AgentProfileView | null>(null);
|
|
70
66
|
const setSessionListSnapshot = useChatSessionListStore((state) => state.setSnapshot);
|
|
71
67
|
|
|
72
68
|
const agents = useMemo(() => agentsQuery.data?.agents ?? [], [agentsQuery.data?.agents]);
|
|
@@ -79,26 +75,66 @@ export function AgentsPage() {
|
|
|
79
75
|
),
|
|
80
76
|
[agents]
|
|
81
77
|
);
|
|
78
|
+
const providerCatalog = useMemo(
|
|
79
|
+
() => buildProviderModelCatalog({ config: configQuery.data, meta: configMetaQuery.data, onlyConfigured: true }),
|
|
80
|
+
[configMetaQuery.data, configQuery.data]
|
|
81
|
+
);
|
|
82
|
+
const runtimeOptions = useMemo(
|
|
83
|
+
() => buildSessionTypeOptions(sessionTypesQuery.data?.options ?? []),
|
|
84
|
+
[sessionTypesQuery.data?.options]
|
|
85
|
+
);
|
|
86
|
+
const defaultRuntime = useMemo(
|
|
87
|
+
() => normalizeSessionType(sessionTypesQuery.data?.defaultType ?? 'native'),
|
|
88
|
+
[sessionTypesQuery.data?.defaultType]
|
|
89
|
+
);
|
|
90
|
+
const defaultRuntimeLabel = useMemo(
|
|
91
|
+
() => runtimeOptions.find((option) => option.value === defaultRuntime)?.label ?? resolveSessionTypeLabel(defaultRuntime),
|
|
92
|
+
[defaultRuntime, runtimeOptions]
|
|
93
|
+
);
|
|
82
94
|
|
|
83
|
-
const handleCreate = async () => {
|
|
95
|
+
const handleCreate = async (form: AgentCreateFormState) => {
|
|
84
96
|
await createAgent.mutateAsync({
|
|
85
97
|
data: {
|
|
86
98
|
id: form.id,
|
|
87
99
|
...(form.displayName.trim() ? { displayName: form.displayName.trim() } : {}),
|
|
88
100
|
...(form.description.trim() ? { description: form.description.trim() } : {}),
|
|
89
101
|
...(form.avatar.trim() ? { avatar: form.avatar.trim() } : {}),
|
|
90
|
-
...(form.home.trim() ? { home: form.home.trim() } : {})
|
|
102
|
+
...(form.home.trim() ? { home: form.home.trim() } : {}),
|
|
103
|
+
...(form.model.trim() ? { model: form.model.trim() } : {}),
|
|
104
|
+
...(form.runtime.trim() ? { runtime: form.runtime.trim() } : {})
|
|
91
105
|
}
|
|
92
106
|
});
|
|
93
|
-
setForm(EMPTY_FORM);
|
|
94
107
|
setIsCreateDialogOpen(false);
|
|
95
108
|
};
|
|
96
109
|
|
|
97
|
-
const
|
|
110
|
+
const handleStartEdit = (agent: AgentProfileView) => {
|
|
111
|
+
setEditingAgent(agent);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const handleUpdate = async (agentId: string, form: AgentEditFormState) => {
|
|
115
|
+
await updateAgent.mutateAsync({
|
|
116
|
+
agentId,
|
|
117
|
+
data: {
|
|
118
|
+
displayName: form.displayName,
|
|
119
|
+
description: form.description,
|
|
120
|
+
avatar: form.avatar,
|
|
121
|
+
model: form.model,
|
|
122
|
+
...(form.runtime.trim() ? { runtime: form.runtime.trim() } : { runtime: "" })
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
setEditingAgent(null);
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const startChatWithAgent = (agent: AgentProfileView) => {
|
|
98
129
|
setSessionListSnapshot({
|
|
99
|
-
selectedAgentId:
|
|
130
|
+
selectedAgentId: agent.id,
|
|
100
131
|
selectedSessionKey: null
|
|
101
132
|
});
|
|
133
|
+
useChatInputStore.getState().setSnapshot({
|
|
134
|
+
pendingSessionType: resolveAgentRuntimeSessionType(agent, defaultRuntime),
|
|
135
|
+
pendingProjectRoot: null,
|
|
136
|
+
pendingProjectRootSessionKey: null
|
|
137
|
+
});
|
|
102
138
|
navigate('/chat');
|
|
103
139
|
};
|
|
104
140
|
|
|
@@ -178,6 +214,11 @@ export function AgentsPage() {
|
|
|
178
214
|
) : (
|
|
179
215
|
sortedAgents.map((agent, index) => {
|
|
180
216
|
const tone = resolveAgentTone(index, Boolean(agent.builtIn));
|
|
217
|
+
const runtimeValue = agent.runtime?.trim() || agent.engine?.trim() || '';
|
|
218
|
+
const runtimeLabel = runtimeValue
|
|
219
|
+
? runtimeOptions.find((option) => option.value === normalizeSessionType(runtimeValue))?.label ??
|
|
220
|
+
resolveSessionTypeLabel(runtimeValue)
|
|
221
|
+
: defaultRuntimeLabel;
|
|
181
222
|
return (
|
|
182
223
|
<Card
|
|
183
224
|
key={agent.id}
|
|
@@ -223,6 +264,16 @@ export function AgentsPage() {
|
|
|
223
264
|
</p>
|
|
224
265
|
|
|
225
266
|
<div className="mt-auto flex flex-col gap-4">
|
|
267
|
+
<div>
|
|
268
|
+
<div className="flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-[#94a3b8]">
|
|
269
|
+
<Sparkles className="h-3.5 w-3.5" />
|
|
270
|
+
{t('agentsCardRuntimeLabel')}
|
|
271
|
+
</div>
|
|
272
|
+
<div className="mt-1.5 text-sm leading-6 text-[#475569]">
|
|
273
|
+
{runtimeLabel}
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
226
277
|
<div className="border-t border-gray-100 pt-3">
|
|
227
278
|
<div className="flex items-center gap-2 text-[11px] font-semibold uppercase tracking-[0.18em] text-[#94a3b8]">
|
|
228
279
|
<House className="h-3.5 w-3.5" />
|
|
@@ -237,11 +288,21 @@ export function AgentsPage() {
|
|
|
237
288
|
<Button
|
|
238
289
|
type="button"
|
|
239
290
|
className="h-9 rounded-xl bg-[#1f5c4d] px-4 text-white hover:bg-[#184d40]"
|
|
240
|
-
onClick={() => startChatWithAgent(agent
|
|
291
|
+
onClick={() => startChatWithAgent(agent)}
|
|
241
292
|
>
|
|
242
293
|
<MessageCircle className="mr-2 h-4 w-4" />
|
|
243
294
|
{t('agentsCardStartChat')}
|
|
244
295
|
</Button>
|
|
296
|
+
<Button
|
|
297
|
+
type="button"
|
|
298
|
+
variant="ghost"
|
|
299
|
+
className="h-8 rounded-xl px-3 text-xs text-[#7b8794] hover:bg-[#f3f4f6] hover:text-[#475569]"
|
|
300
|
+
onClick={() => handleStartEdit(agent)}
|
|
301
|
+
disabled={updateAgent.isPending}
|
|
302
|
+
>
|
|
303
|
+
<Pencil className="mr-1.5 h-3.5 w-3.5" />
|
|
304
|
+
{t('agentsEditAction')}
|
|
305
|
+
</Button>
|
|
245
306
|
{!agent.builtIn ? (
|
|
246
307
|
<Button
|
|
247
308
|
type="button"
|
|
@@ -263,91 +324,29 @@ export function AgentsPage() {
|
|
|
263
324
|
)}
|
|
264
325
|
</div>
|
|
265
326
|
|
|
266
|
-
<
|
|
327
|
+
<AgentCreateDialog
|
|
267
328
|
open={isCreateDialogOpen}
|
|
329
|
+
pending={createAgent.isPending}
|
|
330
|
+
providerCatalog={providerCatalog}
|
|
331
|
+
runtimeOptions={runtimeOptions}
|
|
332
|
+
defaultRuntime={defaultRuntime}
|
|
333
|
+
onOpenChange={setIsCreateDialogOpen}
|
|
334
|
+
onSubmit={handleCreate}
|
|
335
|
+
/>
|
|
336
|
+
|
|
337
|
+
<AgentEditDialog
|
|
338
|
+
agent={editingAgent}
|
|
339
|
+
pending={updateAgent.isPending}
|
|
340
|
+
providerCatalog={providerCatalog}
|
|
341
|
+
runtimeOptions={runtimeOptions}
|
|
342
|
+
defaultRuntime={defaultRuntime}
|
|
268
343
|
onOpenChange={(open) => {
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
setForm(EMPTY_FORM);
|
|
344
|
+
if (!open && !updateAgent.isPending) {
|
|
345
|
+
setEditingAgent(null);
|
|
272
346
|
}
|
|
273
347
|
}}
|
|
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>
|
|
348
|
+
onSubmit={handleUpdate}
|
|
349
|
+
/>
|
|
351
350
|
</PageLayout>
|
|
352
351
|
);
|
|
353
352
|
}
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { render, screen } from '@testing-library/react';
|
|
2
|
+
import userEvent from '@testing-library/user-event';
|
|
2
3
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
4
|
import { ChatChildSessionPanel } from '@/components/chat/chat-child-session-panel';
|
|
4
5
|
import { ChatConversationPanel } from '@/components/chat/ChatConversationPanel';
|
|
6
|
+
import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
5
7
|
import { useChatThreadStore } from '@/components/chat/stores/chat-thread.store';
|
|
6
8
|
|
|
7
9
|
const mocks = vi.hoisted(() => ({
|
|
8
10
|
deleteSession: vi.fn(),
|
|
9
11
|
goToProviders: vi.fn(),
|
|
12
|
+
createSession: vi.fn(),
|
|
13
|
+
setSelectedAgentId: vi.fn(),
|
|
14
|
+
setPendingSessionType: vi.fn(),
|
|
10
15
|
resolvedChildTabs: [
|
|
11
16
|
{
|
|
12
17
|
sessionKey: 'child-session-1',
|
|
@@ -39,7 +44,22 @@ vi.mock('@/components/chat/containers/chat-message-list.container', () => ({
|
|
|
39
44
|
}));
|
|
40
45
|
|
|
41
46
|
vi.mock('@/components/chat/ChatWelcome', () => ({
|
|
42
|
-
ChatWelcome: (
|
|
47
|
+
ChatWelcome: ({
|
|
48
|
+
onCreateSession,
|
|
49
|
+
onSelectAgent
|
|
50
|
+
}: {
|
|
51
|
+
onCreateSession: () => void;
|
|
52
|
+
onSelectAgent: (agentId: string) => void;
|
|
53
|
+
}) => (
|
|
54
|
+
<div data-testid="chat-welcome">
|
|
55
|
+
<button type="button" onClick={onCreateSession}>
|
|
56
|
+
create draft session
|
|
57
|
+
</button>
|
|
58
|
+
<button type="button" onClick={() => onSelectAgent('engineer')}>
|
|
59
|
+
switch draft agent
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
62
|
+
)
|
|
43
63
|
}));
|
|
44
64
|
|
|
45
65
|
vi.mock('@/components/chat/presenter/chat-presenter-context', () => ({
|
|
@@ -47,14 +67,18 @@ vi.mock('@/components/chat/presenter/chat-presenter-context', () => ({
|
|
|
47
67
|
chatThreadManager: {
|
|
48
68
|
deleteSession: mocks.deleteSession,
|
|
49
69
|
goToProviders: mocks.goToProviders,
|
|
50
|
-
createSession: vi.fn(),
|
|
51
70
|
openSessionFromToolAction: vi.fn(),
|
|
52
71
|
selectChildSessionDetail: vi.fn(),
|
|
53
72
|
closeChildSessionDetail: vi.fn(),
|
|
54
73
|
goToParentSession: vi.fn(),
|
|
55
74
|
},
|
|
56
75
|
chatSessionListManager: {
|
|
57
|
-
selectSession: vi.fn()
|
|
76
|
+
selectSession: vi.fn(),
|
|
77
|
+
createSession: mocks.createSession,
|
|
78
|
+
setSelectedAgentId: mocks.setSelectedAgentId
|
|
79
|
+
},
|
|
80
|
+
chatInputManager: {
|
|
81
|
+
setPendingSessionType: mocks.setPendingSessionType
|
|
58
82
|
}
|
|
59
83
|
})
|
|
60
84
|
}));
|
|
@@ -96,6 +120,15 @@ describe('ChatConversationPanel', () => {
|
|
|
96
120
|
beforeEach(() => {
|
|
97
121
|
mocks.deleteSession.mockReset();
|
|
98
122
|
mocks.goToProviders.mockReset();
|
|
123
|
+
mocks.createSession.mockReset();
|
|
124
|
+
mocks.setSelectedAgentId.mockReset();
|
|
125
|
+
mocks.setPendingSessionType.mockReset();
|
|
126
|
+
useChatInputStore.setState({
|
|
127
|
+
snapshot: {
|
|
128
|
+
...useChatInputStore.getState().snapshot,
|
|
129
|
+
defaultSessionType: 'native'
|
|
130
|
+
}
|
|
131
|
+
});
|
|
99
132
|
useChatThreadStore.setState({
|
|
100
133
|
snapshot: {
|
|
101
134
|
...useChatThreadStore.getState().snapshot,
|
|
@@ -116,6 +149,10 @@ describe('ChatConversationPanel', () => {
|
|
|
116
149
|
isAwaitingAssistantOutput: false,
|
|
117
150
|
parentSessionKey: null,
|
|
118
151
|
parentSessionLabel: null,
|
|
152
|
+
availableAgents: [
|
|
153
|
+
{ id: 'main', displayName: 'Main', runtime: 'native' },
|
|
154
|
+
{ id: 'engineer', displayName: 'Engineer', runtime: 'codex' }
|
|
155
|
+
],
|
|
119
156
|
childSessionTabs: [],
|
|
120
157
|
activeChildSessionKey: null,
|
|
121
158
|
}
|
|
@@ -177,6 +214,35 @@ describe('ChatConversationPanel', () => {
|
|
|
177
214
|
expect(screen.getByTestId('agent-avatar').textContent).toBe('engineer');
|
|
178
215
|
expect(screen.queryByText('Engineer')).toBeNull();
|
|
179
216
|
});
|
|
217
|
+
|
|
218
|
+
it('creates a draft session with the selected draft agent runtime', async () => {
|
|
219
|
+
const user = userEvent.setup();
|
|
220
|
+
|
|
221
|
+
useChatThreadStore.setState({
|
|
222
|
+
snapshot: {
|
|
223
|
+
...useChatThreadStore.getState().snapshot,
|
|
224
|
+
agentId: 'engineer',
|
|
225
|
+
agentDisplayName: 'Engineer'
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
render(<ChatConversationPanel />);
|
|
230
|
+
|
|
231
|
+
await user.click(screen.getByRole('button', { name: 'create draft session' }));
|
|
232
|
+
|
|
233
|
+
expect(mocks.createSession).toHaveBeenCalledWith('codex');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('syncs the pending session type when switching the draft agent', async () => {
|
|
237
|
+
const user = userEvent.setup();
|
|
238
|
+
|
|
239
|
+
render(<ChatConversationPanel />);
|
|
240
|
+
|
|
241
|
+
await user.click(screen.getByRole('button', { name: 'switch draft agent' }));
|
|
242
|
+
|
|
243
|
+
expect(mocks.setSelectedAgentId).toHaveBeenCalledWith('engineer');
|
|
244
|
+
expect(mocks.setPendingSessionType).toHaveBeenCalledWith('codex');
|
|
245
|
+
});
|
|
180
246
|
});
|
|
181
247
|
|
|
182
248
|
describe('ChatChildSessionPanel', () => {
|
|
@@ -11,7 +11,9 @@ import { AgentAvatar } from "@/components/common/AgentAvatar";
|
|
|
11
11
|
import { usePresenter } from "@/components/chat/presenter/chat-presenter-context";
|
|
12
12
|
import { ChatSessionHeaderActions } from "@/components/chat/session-header/chat-session-header-actions";
|
|
13
13
|
import { ChatSessionProjectBadge } from "@/components/chat/session-header/chat-session-project-badge";
|
|
14
|
+
import { useChatInputStore } from "@/components/chat/stores/chat-input.store";
|
|
14
15
|
import { useChatThreadStore } from "@/components/chat/stores/chat-thread.store";
|
|
16
|
+
import { resolveAgentRuntimeSessionType } from "@/components/chat/useChatSessionTypeState";
|
|
15
17
|
import { t } from "@/lib/i18n";
|
|
16
18
|
import { cn } from "@/lib/utils";
|
|
17
19
|
|
|
@@ -45,6 +47,9 @@ function ChatConversationSkeleton() {
|
|
|
45
47
|
|
|
46
48
|
export function ChatConversationPanel() {
|
|
47
49
|
const presenter = usePresenter();
|
|
50
|
+
const defaultSessionType = useChatInputStore(
|
|
51
|
+
(state) => state.snapshot.defaultSessionType,
|
|
52
|
+
);
|
|
48
53
|
const snapshot = useChatThreadStore((state) => state.snapshot);
|
|
49
54
|
const fallbackThreadRef = useRef<HTMLDivElement | null>(null);
|
|
50
55
|
const threadRef = snapshot.threadRef ?? fallbackThreadRef;
|
|
@@ -80,6 +85,22 @@ export function ChatConversationPanel() {
|
|
|
80
85
|
snapshot.messages.length === 0 &&
|
|
81
86
|
!snapshot.isSending &&
|
|
82
87
|
!snapshot.isAwaitingAssistantOutput;
|
|
88
|
+
const availableAgents = snapshot.availableAgents ?? [];
|
|
89
|
+
const resolveDraftAgent = (agentId: string) =>
|
|
90
|
+
availableAgents.find((agent) => agent.id === agentId) ?? null;
|
|
91
|
+
const createDraftSessionForAgent = () => {
|
|
92
|
+
const sessionType = resolveAgentRuntimeSessionType(
|
|
93
|
+
resolveDraftAgent(snapshot.agentId ?? "main"),
|
|
94
|
+
defaultSessionType,
|
|
95
|
+
);
|
|
96
|
+
presenter.chatSessionListManager.createSession(sessionType);
|
|
97
|
+
};
|
|
98
|
+
const selectDraftAgent = (agentId: string) => {
|
|
99
|
+
presenter.chatSessionListManager.setSelectedAgentId(agentId);
|
|
100
|
+
presenter.chatInputManager.setPendingSessionType(
|
|
101
|
+
resolveAgentRuntimeSessionType(resolveDraftAgent(agentId), defaultSessionType),
|
|
102
|
+
);
|
|
103
|
+
};
|
|
83
104
|
|
|
84
105
|
const { onScroll: handleScroll } = useStickyBottomScroll({
|
|
85
106
|
scrollRef: threadRef,
|
|
@@ -192,10 +213,10 @@ export function ChatConversationPanel() {
|
|
|
192
213
|
>
|
|
193
214
|
{showWelcome ? (
|
|
194
215
|
<ChatWelcome
|
|
195
|
-
onCreateSession={
|
|
196
|
-
agents={
|
|
216
|
+
onCreateSession={createDraftSessionForAgent}
|
|
217
|
+
agents={availableAgents}
|
|
197
218
|
selectedAgentId={snapshot.agentId ?? "main"}
|
|
198
|
-
onSelectAgent={
|
|
219
|
+
onSelectAgent={selectDraftAgent}
|
|
199
220
|
/>
|
|
200
221
|
) : hideEmptyHint ? (
|
|
201
222
|
<div className="h-full" />
|
|
@@ -9,13 +9,16 @@ describe('ChatSessionListManager', () => {
|
|
|
9
9
|
snapshot: {
|
|
10
10
|
...useChatInputStore.getState().snapshot,
|
|
11
11
|
defaultSessionType: 'native',
|
|
12
|
-
pendingSessionType: 'native'
|
|
12
|
+
pendingSessionType: 'native',
|
|
13
|
+
pendingProjectRoot: null,
|
|
14
|
+
pendingProjectRootSessionKey: null
|
|
13
15
|
}
|
|
14
16
|
});
|
|
15
17
|
useChatSessionListStore.setState({
|
|
16
18
|
snapshot: {
|
|
17
19
|
...useChatSessionListStore.getState().snapshot,
|
|
18
|
-
selectedSessionKey: 'session-1'
|
|
20
|
+
selectedSessionKey: 'session-1',
|
|
21
|
+
listMode: 'time-first'
|
|
19
22
|
}
|
|
20
23
|
});
|
|
21
24
|
});
|
|
@@ -28,13 +31,30 @@ describe('ChatSessionListManager', () => {
|
|
|
28
31
|
resetStreamState: vi.fn()
|
|
29
32
|
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[1];
|
|
30
33
|
|
|
31
|
-
const manager = new ChatSessionListManager(uiManager, streamActionsManager);
|
|
34
|
+
const manager = new ChatSessionListManager(uiManager, streamActionsManager, () => 'draft-session-1');
|
|
32
35
|
manager.createSession('codex');
|
|
33
36
|
|
|
34
37
|
expect(streamActionsManager.resetStreamState).toHaveBeenCalledTimes(1);
|
|
35
38
|
expect(uiManager.goToChatRoot).toHaveBeenCalledTimes(1);
|
|
36
39
|
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBe('session-1');
|
|
37
40
|
expect(useChatInputStore.getState().snapshot.pendingSessionType).toBe('codex');
|
|
41
|
+
expect(useChatInputStore.getState().snapshot.pendingProjectRoot).toBeNull();
|
|
42
|
+
expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('hydrates the draft project root when creating a session inside a project group', () => {
|
|
46
|
+
const uiManager = {
|
|
47
|
+
goToChatRoot: vi.fn()
|
|
48
|
+
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[0];
|
|
49
|
+
const streamActionsManager = {
|
|
50
|
+
resetStreamState: vi.fn()
|
|
51
|
+
} as unknown as ConstructorParameters<typeof ChatSessionListManager>[1];
|
|
52
|
+
|
|
53
|
+
const manager = new ChatSessionListManager(uiManager, streamActionsManager, () => 'draft-session-9');
|
|
54
|
+
manager.createSession('native', '/tmp/project-alpha');
|
|
55
|
+
|
|
56
|
+
expect(useChatInputStore.getState().snapshot.pendingProjectRoot).toBe('/tmp/project-alpha');
|
|
57
|
+
expect(useChatInputStore.getState().snapshot.pendingProjectRootSessionKey).toBe('draft-session-9');
|
|
38
58
|
});
|
|
39
59
|
|
|
40
60
|
it('delegates existing-session selection to routing without eagerly mutating the selected session state', () => {
|
|
@@ -51,4 +71,15 @@ describe('ChatSessionListManager', () => {
|
|
|
51
71
|
expect(uiManager.goToSession).toHaveBeenCalledWith('session-2');
|
|
52
72
|
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBe('session-1');
|
|
53
73
|
});
|
|
74
|
+
|
|
75
|
+
it('updates the sidebar list mode without touching other session list state', () => {
|
|
76
|
+
const uiManager = {} as ConstructorParameters<typeof ChatSessionListManager>[0];
|
|
77
|
+
const streamActionsManager = {} as ConstructorParameters<typeof ChatSessionListManager>[1];
|
|
78
|
+
|
|
79
|
+
const manager = new ChatSessionListManager(uiManager, streamActionsManager);
|
|
80
|
+
manager.setListMode('project-first');
|
|
81
|
+
|
|
82
|
+
expect(useChatSessionListStore.getState().snapshot.listMode).toBe('project-first');
|
|
83
|
+
expect(useChatSessionListStore.getState().snapshot.selectedSessionKey).toBe('session-1');
|
|
84
|
+
});
|
|
54
85
|
});
|
|
@@ -3,11 +3,13 @@ import { useChatInputStore } from '@/components/chat/stores/chat-input.store';
|
|
|
3
3
|
import type { ChatUiManager } from '@/components/chat/managers/chat-ui.manager';
|
|
4
4
|
import type { SetStateAction } from 'react';
|
|
5
5
|
import type { ChatStreamActionsManager } from '@/components/chat/managers/chat-stream-actions.manager';
|
|
6
|
+
import { normalizeSessionProjectRootValue } from '@/lib/session-project/session-project.utils';
|
|
6
7
|
|
|
7
8
|
export class ChatSessionListManager {
|
|
8
9
|
constructor(
|
|
9
10
|
private uiManager: ChatUiManager,
|
|
10
|
-
private streamActionsManager: ChatStreamActionsManager
|
|
11
|
+
private streamActionsManager: ChatStreamActionsManager,
|
|
12
|
+
private getDraftSessionId: () => string = () => ''
|
|
11
13
|
) {}
|
|
12
14
|
|
|
13
15
|
private resolveUpdateValue = <T>(prev: T, next: SetStateAction<T>): T => {
|
|
@@ -35,7 +37,16 @@ export class ChatSessionListManager {
|
|
|
35
37
|
useChatSessionListStore.getState().setSnapshot({ selectedSessionKey: value });
|
|
36
38
|
};
|
|
37
39
|
|
|
38
|
-
|
|
40
|
+
setListMode = (next: SetStateAction<'time-first' | 'project-first'>) => {
|
|
41
|
+
const prev = useChatSessionListStore.getState().snapshot.listMode;
|
|
42
|
+
const value = this.resolveUpdateValue(prev, next);
|
|
43
|
+
if (value === prev) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
useChatSessionListStore.getState().setSnapshot({ listMode: value });
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
createSession = (sessionType?: string, projectRoot?: string | null) => {
|
|
39
50
|
const { snapshot } = useChatInputStore.getState();
|
|
40
51
|
const { defaultSessionType: configuredDefaultSessionType } = snapshot;
|
|
41
52
|
const defaultSessionType = configuredDefaultSessionType || 'native';
|
|
@@ -43,11 +54,13 @@ export class ChatSessionListManager {
|
|
|
43
54
|
typeof sessionType === 'string' && sessionType.trim().length > 0
|
|
44
55
|
? sessionType.trim()
|
|
45
56
|
: defaultSessionType;
|
|
57
|
+
const normalizedProjectRoot = normalizeSessionProjectRootValue(projectRoot);
|
|
58
|
+
const draftSessionId = normalizedProjectRoot ? this.getDraftSessionId() : null;
|
|
46
59
|
this.streamActionsManager.resetStreamState();
|
|
47
60
|
useChatInputStore.getState().setSnapshot({
|
|
48
61
|
pendingSessionType: nextSessionType,
|
|
49
|
-
pendingProjectRoot:
|
|
50
|
-
pendingProjectRootSessionKey:
|
|
62
|
+
pendingProjectRoot: normalizedProjectRoot,
|
|
63
|
+
pendingProjectRootSessionKey: draftSessionId
|
|
51
64
|
});
|
|
52
65
|
this.uiManager.goToChatRoot();
|
|
53
66
|
};
|
|
@@ -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();
|
|
@@ -7,7 +7,11 @@ import { NcpChatThreadManager } from '@/components/chat/ncp/ncp-chat-thread.mana
|
|
|
7
7
|
export class NcpChatPresenter {
|
|
8
8
|
chatUiManager = new ChatUiManager();
|
|
9
9
|
chatStreamActionsManager = new ChatStreamActionsManager();
|
|
10
|
-
chatSessionListManager = new ChatSessionListManager(
|
|
10
|
+
chatSessionListManager = new ChatSessionListManager(
|
|
11
|
+
this.chatUiManager,
|
|
12
|
+
this.chatStreamActionsManager,
|
|
13
|
+
() => this.getDraftSessionId()
|
|
14
|
+
);
|
|
11
15
|
chatInputManager = new NcpChatInputManager(
|
|
12
16
|
this.chatUiManager,
|
|
13
17
|
this.chatStreamActionsManager,
|
|
@@ -85,7 +85,12 @@ function readNcpSessionType(summary: NcpSessionSummaryView): string {
|
|
|
85
85
|
if (!metadata) {
|
|
86
86
|
return 'native';
|
|
87
87
|
}
|
|
88
|
-
return
|
|
88
|
+
return (
|
|
89
|
+
readOptionalString(metadata.runtime) ??
|
|
90
|
+
readOptionalString(metadata.session_type) ??
|
|
91
|
+
readOptionalString(metadata.sessionType) ??
|
|
92
|
+
'native'
|
|
93
|
+
);
|
|
89
94
|
}
|
|
90
95
|
|
|
91
96
|
function readNcpParentSessionId(summary: NcpSessionSummaryView): string | null {
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
import { create } from 'zustand';
|
|
2
|
+
|
|
3
|
+
export type ChatSessionListMode = 'time-first' | 'project-first';
|
|
4
|
+
|
|
2
5
|
export type ChatSessionListSnapshot = {
|
|
3
6
|
selectedSessionKey: string | null;
|
|
4
7
|
selectedAgentId: string;
|
|
5
8
|
query: string;
|
|
9
|
+
listMode: ChatSessionListMode;
|
|
6
10
|
};
|
|
7
11
|
|
|
8
12
|
type ChatSessionListStore = {
|
|
@@ -13,7 +17,8 @@ type ChatSessionListStore = {
|
|
|
13
17
|
const initialSnapshot: ChatSessionListSnapshot = {
|
|
14
18
|
selectedSessionKey: null,
|
|
15
19
|
selectedAgentId: 'main',
|
|
16
|
-
query: ''
|
|
20
|
+
query: '',
|
|
21
|
+
listMode: 'time-first'
|
|
17
22
|
};
|
|
18
23
|
|
|
19
24
|
export const useChatSessionListStore = create<ChatSessionListStore>((set) => ({
|